Создавайте гибкие и высокопроизводительные системы моддинга
В постоянно развивающемся мире разработки игр моддинг стал мощным способом улучшить игровой процесс и вдохнуть новую жизнь в существующие игры. Это позволяет создателям и игрокам воплощать в жизнь свои уникальные идеи, обогащая игровую экосистему свежим контентом и безграничными возможностями. Как разработчики, мы всегда ищем инновационные способы создания и поддержки систем моддинга, которые были бы одновременно эффективными и удобными для пользователя. Сегодня мы отправляемся в увлекательное путешествие, чтобы исследовать синергию между двумя замечательными технологиями, Lua и Rust, погружаясь в мир систем моддинга.
Lua, легкий и универсальный язык сценариев, долгое время был популярным выбором среди разработчиков игр для реализации поддержки моддинга. Его гибкость и простота использования делают его идеальным кандидатом для расширения функциональности игры и создания пользовательского контента. С другой стороны, Rust, язык системного программирования, ориентированный на безопасность и производительность, быстро набрал обороты благодаря своим мощным функциям и впечатляющим возможностям. Объединив лучшее из обоих миров, мы решили создать надежную, но доступную систему моддинга, которая открывает двери для бесконечного творчества.
Присоединяйтесь к нам, пока мы экспериментируем с уникальными возможностями Lua и Rust, делясь своими знаниями, проблемами и победами на этом пути. Являетесь ли вы опытным разработчиком или только начинаете свой путь в разработке игр, это приключение обещает стать захватывающим исследованием систем моддинга и потенциала Lua и Rust.
Делаем грузовой проект и интегрируем Lua
Давайте отправимся в это путешествие, создав новый проект Cargo и настроив простой сценарий Lua.
Сначала создайте новый проект Cargo и перейдите в каталог проекта:
cargo new modding-example && cd modding-example
Затем добавьте крейт rlua
в свой проект:
cargo add rlua
Теперь давайте создадим файл main.rs
со следующим кодом для выполнения Lua-скрипта:
// File: src/main.rs use rlua::{Lua, Result}; use std::fs; fn exec_lua_code() -> Result<()> { let lua_code = fs::read_to_string("game/main.lua").expect("Unable to read the Lua script"); let lua = Lua::new(); lua.context(|lua_ctx| { lua_ctx.load(&lua_code).exec()?; Ok(()) }) } fn main() -> Result<()> { exec_lua_code() }
Если мы попытаемся запустить это, мы заметим, что скрипт не может быть выполнен, потому что файл game/main.lua
еще не существует. Создадим необходимую директорию и файл:
mkdir game touch game/main.lua
Теперь давайте добавим изюминку в наш Lua-скрипт, распечатав некоторую информацию:
-- File: game/main.lua print(_VERSION) print("🌙 Lua is working!")
С этой настройкой вы должны увидеть версию Lua и сообщение «Lua работает!» сообщение, выводимое на консоль при запуске программы на Rust.
Делаем Stdout более интересным
Наши тексты выглядят просто, и мы не можем отличить вывод lua от вывода ржавчины. Давайте изменим это, используя цвета из популярного ящика colored
.
Во-первых, давайте добавим в наш проект ящик colored
:
cargo add colored
Теперь давайте повторно реализуем оператор Lua print
с нашим собственным.
-- File: game/main.lua function print(...) local args = {...} for _, arg in ipairs(args) do if type(arg) == "table" then __rust_bindings_print(tostring(arg)) elseif type(arg) == "string" then __rust_bindings_print(arg) else __rust_bindings_print(tostring(arg)) end end end -- rest of game/main.lua
Теперь нам нужно передать __rust_bindings_print
контексту lua, чтобы он вызывал наш код rust
. Мы также собираемся создать функцию log
для нашей среды выполнения rust
:
// File: src/main.rs use rlua::{Lua, Result}; use std::fs; mod logger; /* rest of main.rs */
И теперь мы должны создать этот файл:
touch src/logger.rs
Наконец, мы можем реализовать раскрашенный вывод:
// File: src/logger.rs use colored::*; use rlua::{Result, Value}; use std::io::Write; pub fn lua_print<'lua>(lua_ctx: rlua::Context<'lua>, value: Value<'lua>) -> Result<()> { let mut str = String::from("nil"); if let Some(lua_str) = lua_ctx.coerce_string(value)? { str = lua_str.to_str()?.to_string(); } match writeln!(std::io::stdout(), "[{}] {}", "lua".cyan(), str) { Ok(_) => Ok(()), Err(e) => Err(rlua::Error::external(e)), } } pub fn log(message: &str) { println!("[{}] {}", "rust".red(), message); }
Теперь мы можем использовать эти функции в нашем src/main.rs
// File: src/main.rs use logger::{log, lua_print}; // new line // rest of use statements // ... fn exec_lua_code() -> Result<()> { let lua_code = fs::read_to_string("game/main.lua").expect("Unable to read the Lua script"); let lua = Lua::new(); lua.context(|lua_ctx| { log("🔧 Loading Lua bindings"); lua_ctx .globals() .set("__rust_bindings_print", lua_ctx.create_function(lua_print)?)?; lua_ctx.load(&lua_code).exec()?; Ok(()) }) } // ...
Наш цветной вывод теперь работает! И теперь мы можем легко различать наши среды выполнения rust
и lua
.
Делаем структуру модов
Давайте на секунду задумаемся о том, как будут выглядеть наши моды.
На ум приходит следующая структура:
/game |--/mods |--/base |----mod.json |----mod.lua |--/dlc |----mod.json |----mod.lua |--main.lua
Файл .json
будет иметь следующую схему:
{ "name": "base", "version": "0.0.1", "description": "A mod for the base game.", "author": "Stefan Kupresak" }
Загрузка модов в Rust
Подобно тому, как мы создали модуль logger
, теперь мы собираемся создать модуль ржавчины mods
:
touch src/mods.rs
И добавляем ссылку на него в наш main.rs
// File: src/main.rs mod mods;
Для этого модуля нам также потребуется добавить несколько новых грузовых зависимостей, которые помогут нам проанализировать файлы манифеста мода json
:
cargo add serde_json
А для serde
мы собираемся вручную добавить его в Cargo.toml
.
[dependecies] ... serde = { version = "*", features = ["derive"] }
Теперь мы можем реализовать модуль mods.rs
:
// File: src/mods.rs use std::fs; use rlua::{MetaMethod, UserData, UserDataMethods}; use serde::{Deserialize, Serialize}; #[derive(Serialize, Deserialize, Clone)] pub struct Mod { pub name: String, pub version: String, pub description: String, pub author: String, } /** * UserData is used from rlua so * we can pass the Vec<Mod> to our * Lua context **/ impl UserData for Mod {} /** * This helps us later to convert * the mods struct to a lua table **/ pub fn items_to_lua_table<'lua>(lua_ctx: &Context<'lua>, items: Vec<Mod>) -> Result<Table<'lua>> { let table = lua_ctx.create_table()?; for (i, item) in items.iter().enumerate() { // lua tables start from index 1 :) // see: https://www.tutorialspoint.com/why-do-lua-arrays-tables-start-at-1-instead-of-0 table.set(i + 1, item.clone())?; } Ok(table) } fn list_mods_root() -> Vec<String> { let mut mods = vec![]; for entry in fs::read_dir("game/mods").expect("Unable to read the mods directory") { let entry = entry.expect("Unable to read the mods directory"); let path = entry.path(); if path.is_dir() { let mod_json_path = path.join("mod.json"); if mod_json_path.exists() { mods.push(mod_json_path.to_str().unwrap().to_string()); } } } mods } pub fn load() -> Vec<Mod> { let mod_paths = list_mods_root(); let mut mods = vec![]; for mod_json_path in mod_paths { let mod_json = fs::read_to_string(mod_json_path).expect("Unable to read the mod.json file"); let mod_json = serde_json::from_str(&mod_json).expect("Unable to parse the mod.json file"); mods.push(mod_json); } mods }
Захватывающий! Теперь мы можем загрузить моды из нашего файла main.rs
.
// File src/main.rs fn main() -> Result<()> { let mods = mods::load(); log(format!("Loaded {} mods", mods.len()).as_str()); exec_lua_code(); }
Теперь у нас должен быть следующий вывод:
[rust] Loaded 2 mods [rust] 🔧 Loading Lua bindings [lua] Lua 5.4 [lua] 🌙 Lua is working!
Имейте в виду, что наш каталог game/
выглядит так:
Передача нашего вектора модов в Lua
Все идет нормально. Наконец, нам нужно передать наш вектор в lua
и начать строить «каркас» для загрузки этих модов.
Во-первых, давайте передадим наши моды нашей функции exec_lua_code
:
// File src/main.rs fn exec_lua_code(mods: Vec<Mod>) -> Result<()> { /* function body */ } fn main() -> Result<()> { let mods = mods::load(); /* ... */ exec_lua_code(mods) }
Теперь мы можем передать нашу структуру в контекст Lua:
// File src/main.rs fn exec_lua_code(mods: Vec<Mod>) -> Result<()> { /* ... */ lua.context(|lua_ctx| { /* ... */ let mods_table = mods::items_to_lua_table(&lua_ctx, mods)?; lua_ctx.globals().set("mods", mods_table)?; /* ... */ }) }
Мы можем убедиться, что это работает, выполнив следующие действия в нашем main.lua
print("number of mods -> " .. #mods)
Пользовательские данные и Lua
Однако есть одна странная вещь: если мы попытаемся получить доступ к конкретному моду, мы получим эту конструкцию userdata
.
print("first mod: ", mods[1]) -- userdata 0x5587a65d49e8
Это специальная конструкция в lua, указывающая на блок необработанной памяти. Что интересно, мы не можем получить к ней доступ как к таблице, как вы ожидаете:
print("first mod name: ", mods[1].name) -- throws an error
Это происходит потому, что lua
не знает, как индексировать эту структуру данных. Чтобы сделать его индексируемым, мы должны реализовать функцию __index
в его метатаблице.
rlua
добавляет удобный способ сделать это. Реализуя трейт UserData
, мы можем расширить эти пользовательские данные и добавить этот метаметод, который позволит нам индексировать эти данные в Lua:
// File: src/mods.rs impl UserData for Mod { fn add_methods<'lua, M: UserDataMethods<'lua, Self>>(methods: &mut M) { methods.add_meta_method(MetaMethod::Index, |_, modd: &Mod, key: String| { match key.as_str() { "name" => Ok(modd.name.clone()), "version" => Ok(modd.version.clone()), "description" => Ok(modd.description.clone()), "author" => Ok(modd.author.clone()), _ => Err(rlua::Error::external(format!("Unknown key: {}", key))), } }) } }
Теперь, если мы попробуем ту же самую строку еще раз, она просто сработает!
print("first mod name: ", mods[1].name) [lua] first mod name: [lua] base
Создание небольшого игрового фреймворка Lua
Когда часть ржавчины завершена, теперь мы можем расширить сценарий Lua и создать несколько файлов mod.lua
, чтобы поэкспериментировать с нашей новой системой.
В этой системе я решил иметь один файл записи mod.lua
для каждого мода и сделать его модулем.
-- File: game/main.lua -- rest of main.lua file Game = {} function Game:new() o = {} self.__index = self return setmetatable(o, self) end function Game:load() -- Starts loading the game print("⚡ Loading game") -- Load the game mods print("🚧 Loading " .. #mods .. " mod(s)") for _, mod in ipairs(mods) do print("✅ Loading mod " .. mod.name) local f, err = loadfile("game/mods/" .. mod.name .. "/mod.lua") if f then m = f() if m and m.init then m:init(mod) else print("❌ Mod " .. mod.name .. " does not have an init function") end else print("❌ Mod " .. mod.name .. " failed to load: " .. err) end end end game = Game:new() game:load()
Имейте в виду, что этот пример работает со следующим файлом game/mods/base/mod.lua
-- File: game/mods/base/mod.lua mod = {} function mod:init(mod) print("🕹️ Initializing mod " .. mod.name) end return mod
Будет производить:
➜ cargo run Finished dev [unoptimized + debuginfo] target(s) in 0.01s Running `target/debug/modding-example` [rust] 🔧 Loading Lua bindings [rust] 🚀 executing lua script [lua] ⚡ Loading game [lua] 🚧 Loading 2 mod(s) [lua] ✅ Loading mod base [lua] 🕹️ Initializing mod base [lua] ✅ Loading mod dlc [lua] ❌ Mod dlc does not have an init function
Краткое содержание
В этой статье мы исследуем мощную комбинацию Lua и Rust для создания гибкой и высокопроизводительной системы моддинга для игр. Мы рассмотрим настройку нового проекта Cargo и интеграцию Lua, а затем разработаем четкую и организованную структуру мода. Используя сильные стороны обеих технологий, мы демонстрируем, как Lua и Rust могут работать вместе, чтобы обеспечить бесконечное творчество и вдохнуть новую жизнь в игры с помощью пользовательского контента.
🎊🎊 Поздравляем, если вы зашли так далеко! Вы успешно вошли в мир Lua и Rust, создав прочную основу для гибкой и высокопроизводительной системы моддинга. Продолжая исследовать и экспериментировать, вы, несомненно, откроете для себя еще больше захватывающих возможностей и идей для улучшения ваших игровых проектов. Путешествие, в которое вы отправились, — это только начало — потенциал совместной работы Lua и Rust огромен, и нам не терпится увидеть, что вы создадите дальше. Продолжайте раздвигать границы и счастливого моддинга!
У этой статьи есть репозиторий GitHub с открытым исходным кодом.