Modding(zh) - Gamepiaynmo/BallanceModLoader GitHub Wiki

模组开发

English Version

最高效的学习方式之一是学习其他人的作品。例如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());
	}
}