-
Notifications
You must be signed in to change notification settings - Fork 6
Modding(zh)
最高效的学习方式之一是学习其他人的作品。例如BML自带模组。
学习此教程你将需要C++编程、Virtools和Ballance的基础知识。
Virtools SDK的文档在开发Ballance模组时也非常有用。
所有mod都放在ModLoader/Mods文件夹中。共有三种组织mod文件的方式:
-
单一的.bmod文件,在不需要其他外部资源的时候(模型,贴图,声音等)。
-
一个文件夹,包含你的.bmod文件和其他资源文件,用于开发环境。
-
一个.zip压缩包,包含.bmod文件和其他资源文件,用于发布的模组。
模组的文件结构大致如下:
你的模组文件夹
3D Entities
PH
机关NMO文件
其他NMO文件
Textures
贴图文件
Sounds
声音文件
你的模组名.bmod
其他Dll文件
BML会将这些资源的路径加入Virtools的Path Manager中,你可以像访问Ballance原有文件一样访问它们。
我个人推荐使用Visual Studio 2019作为你的开发环境,但其他的软件也可以。
-
首先前往发布页面下载dev包;
-
使用模板动态链接库(DLL)创建一个新工程;
以下的部分步骤不是必须的。你可以自行配置,前提是你明白它们的原理。
- 删除所有自动创建的代码文件(.cpp, .h),并为你的模组创建一个.cpp和一个.h文件;
- 在工程的属性页面中:
- 设置输出路径为ModLoader/Mods,或者ModLoader/Mods/你的模组名,如果你需要外部资源的话;
- 设置输出文件的扩展名为.bmod;
- 设置字符集为ANSI(或未设置);
- 设置调试命令为Ballance/Bin/Player.exe,这样就能够在VS中调试你的mod;
- 设置调试工作目录为Ballance/Bin;
- 添加你下载的dev包中的"include"和"lib"目录到你的工程中;
- 在C/C++中,设置不使用预编译头;
- 添加BML.lib到链接器的输入之一。
- 在你的.h文件中,输入以下代码:
#pragma once
#include <BML/BMLAll.h>
extern "C" {
__declspec(dllexport) IMod* BMLEntry(IBML* bml);
}
class ExampleMod : public IMod {
public:
ExampleMod(IBML* bml) : IMod(bml) {}
virtual CKSTRING GetID() override { return "ExampleMod"; }
virtual CKSTRING GetVersion() override { return BML_VERSION; }
virtual CKSTRING GetName() override { return "BML Example Mod"; }
virtual CKSTRING GetAuthor() override { return "Gamepiaynmo"; }
virtual CKSTRING GetDescription() override { return "Example Mod of BML."; }
DECLARE_BML_VERSION;
};
- 你可以根据你自己的模组的内容修改这些信息,但是不要用中文。中文字符无法在游戏中正确显示。
- 类的名字可以任取。
- 模组ID应仅包含英文字母,而且尽可能简洁。
- 你可以使用自己的版本编号。
- 模组名可由英文字母和空格组成,也应尽可能简洁。
- 描述是一句描述你的模组功能的话。
- 在你的.cpp文件中,输入以下代码:
#include "ExampleMod.h"
IMod* BMLEntry(IBML* bml) {
return new ExampleMod(bml);
}
- 生成项目并启动游戏,你可以在模组目录中找到你自己的模组。
一个模组总体而言只做了两种事情:订阅和注册。
订阅指订阅一些游戏内的消息,每个消息都会在一些特定的条件下触发(加载关卡时,通过盘点时,死亡时等等),你的模组就能够在这些消息触发时执行一些自定义的操作。
你需要在C++类中覆写一个函数来订阅消息。
比如,你可以订阅加载关卡后这个消息:
virtual void OnPostLoadLevel() override;
并实现这个函数,输出一行字到游戏内的命令行:
void ExampleMod::OnPostLoadLevel() {
m_bml->SendIngameMessage("Hello World !");
}
生成并启动游戏,每次进入关卡时这个消息就会出现。
一些重要的消息:
- OnLoad:当Virtools初始化完成时触发,是时候初始化你自己的模组了。
- OnProcess:每个游戏循环内触发。别在这里处理太耗时的东西。
- OnRender:每个渲染帧内触发。规则同上。
- OnUnload:游戏要退出了,该做些清理工作了。
注册指一些由BML封装好的函数,可以便捷地向游戏添加自定义内容,包括新机关、新球种等。注册功能让你一行C++代码实现添加新机关。
所有注册代码都需要写在模组加载消息中:
void ExampleMod::OnLoad() {
m_bml->RegisterFloorType("Phys_Floor_E0", 0.7f, 0.0f, 1.0f, "Floor", true);
}
上面地代码注册了一种新的无弹力路面。将来在所有自制图中如果有名为Phys_Floor_E0的组,那么组中的所有物体都将被物理化为无弹力的路面。
BML提供了一种便捷管理配置的方案。模组的配置由类别和条目组成。
首先在模组类中声明一些条目实例:
IProperty* m_props[2];
然后在模组加载消息中初始化它们:
void ExampleMod::OnLoad() {
GetConfig()->SetCategoryComment("Integers", "Here are Integers");
m_props[0] = GetConfig()->GetProperty("Integers", "Integer1");
m_props[0]->SetComment("Here is Integer 1");
m_props[0]->SetDefaultInteger(1);
GetConfig()->SetCategoryComment("Strings", "Here are Strings");
m_props[1] = GetConfig()->GetProperty("Strings", "String1");
m_props[1]->SetComment("Here is String One");
m_props[1]->SetDefaultString("One");
}
生成并运行游戏,你可以在该模组的条目中找到这些设置。
BML使用.cfg文件来为模组存储配置信息。在ModLoader/Config文件夹里,你可以找到属于ExampleMod的配置文件,其内容如下:
# Configuration File for Mod: BML Example Mod - 0.3.24
# Here are Integers
Integers {
# Here is Integer 1
I Integer1 1
}
# Here are Strings
Strings {
# Here is String One
S String1 One
}
游戏未启动时你可以修改这些文件,它们将在下一次游戏启动时被读取。
当需要读取这些配置的值时,使用Get系列函数:
void ExampleMod::OnPostLoadLevel() {
if (m_props[0]->GetInteger() == 1) {
m_bml->SendIngameMessage(m_props[1]->GetString());
}
}
有些时候你也许需要知道某个选项什么时候被修改了,这里有一个修改配置的消息可用:
void ExampleMod::OnModifyConfig(CKSTRING category, CKSTRING key, IProperty* prop) {
if (prop == m_props[1]) {
// m_props[1] 被修改了。
m_bml->SendIngameMessage(m_props[1]->GetString());
}
}
BML实现了一些游戏内指令,例如/bml,/help。BML也允许每个mod创建自己的指令。
你需要实现ICommand接口来创建指令:
class CommandExample : public ICommand {
public:
virtual std::string GetName() override { return "example"; };
virtual std::string GetAlias() override { return "exp"; };
virtual std::string GetDescription() override { return "An example command."; };
virtual bool IsCheat() override { return false; };
virtual void Execute(IBML* bml, const std::vector<std::string>& args) override {
if (args.size() > 1) {
if (args[1] == "plus")
m_number++;
if (args[1] == "minus")
m_number--;
}
std::string msg = "Number is now: " + std::to_string(m_number);
bml->SendIngameMessage(msg.c_str());
}
virtual const std::vector<std::string> GetTabCompletion(IBML* bml, const std::vector<std::string>& args) override {
return args.size() == 2 ? std::vector<std::string>{ "plus", "minus" } : std::vector<std::string>{};
};
private:
int m_number = 0;
};
- Name:命令的名字即为输入命令时斜杠'/'之后的单词。其应仅包含小写字母。
- Alias:别名提供了另一种调用此命令的方式,例如/help有一个别名/?。
- Description:描述是一句描述此命令用途的话。
- IsCheat:作弊如果返回true,那么此指令在未启用作弊模式时无法使用。
- Execute:在玩家输入完成按下回车时调用。
- GetTabCompletion:返回一系列可以作为下一个命令参数的字符串,在玩家按下Tab时调用。args.size指示了需要补全第几个参数。
实现了接口之后,在模组加载消息中注册:
void ExampleMod::OnLoad() {
m_bml->RegisterCommand(new CommandExample());
}
然后你就可以在游戏中调用它了。
// ExampleMod.h
#pragma once
#include <BML/BMLAll.h>
extern "C" {
__declspec(dllexport) IMod* BMLEntry(IBML* bml);
}
class CommandExample : public ICommand {
public:
virtual std::string GetName() override { return "example"; };
virtual std::string GetAlias() override { return "exp"; };
virtual std::string GetDescription() override { return "An example command."; };
virtual bool IsCheat() override { return false; };
virtual void Execute(IBML* bml, const std::vector<std::string>& args) override {
if (args.size() > 1) {
if (args[1] == "plus")
m_number++;
if (args[1] == "minus")
m_number--;
}
std::string msg = "Number is now: " + std::to_string(m_number);
bml->SendIngameMessage(msg.c_str());
}
virtual const std::vector<std::string> GetTabCompletion(IBML* bml, const std::vector<std::string>& args) override {
return args.size() == 2 ? std::vector<std::string>{ "plus", "minus" } : std::vector<std::string>{};
};
private:
int m_number = 0;
};
class ExampleMod : public IMod {
public:
ExampleMod(IBML* bml) : IMod(bml) {}
virtual CKSTRING GetID() override { return "ExampleMod"; }
virtual CKSTRING GetVersion() override { return BML_VERSION; }
virtual CKSTRING GetName() override { return "BML Example Mod"; }
virtual CKSTRING GetAuthor() override { return "Gamepiaynmo"; }
virtual CKSTRING GetDescription() override { return "Example Mod of BML."; }
DECLARE_BML_VERSION;
private:
virtual void OnLoad() override;
virtual void OnPostLoadLevel() override;
virtual void OnModifyConfig(CKSTRING category, CKSTRING key, IProperty* prop) override;
IProperty* m_props[2];
};
// ExampleMod.cpp
#include "ExampleMod.h"
IMod* BMLEntry(IBML* bml) {
return new ExampleMod(bml);
}
void ExampleMod::OnLoad() {
GetConfig()->SetCategoryComment("Integers", "Here are Integers");
m_props[0] = GetConfig()->GetProperty("Integers", "Integer1");
m_props[0]->SetComment("Here is Integer 1");
m_props[0]->SetDefaultInteger(1);
GetConfig()->SetCategoryComment("Strings", "Here are Strings");
m_props[1] = GetConfig()->GetProperty("Strings", "String1");
m_props[1]->SetComment("Here is String One");
m_props[1]->SetDefaultString("One");
m_bml->RegisterFloorType("Phys_Floor_E0", 0.7f, 0.0f, 1.0f, "Floor", true);
m_bml->RegisterCommand(new CommandExample());
}
void ExampleMod::OnPostLoadLevel() {
if (m_props[0]->GetInteger() == 1) {
m_bml->SendIngameMessage(m_props[1]->GetString());
}
}
void ExampleMod::OnModifyConfig(CKSTRING category, CKSTRING key, IProperty* prop) {
if (prop == m_props[1]) {
m_bml->SendIngameMessage(m_props[1]->GetString());
}
}