又见面啦
如你所见 这个教程并没有弃坑 剩下的内容会在哈维创建的模组教程站继续更新!
在之前的部分教程中 你应该已经实现了修改部分slugbase提供的预设属性并且初步实现了一个有一些自定义特性的游戏角色,接下来的教程会更进一步 讲解如何修改代码来实现更多独特的自定义能力。即使在并非以自定义角色为核心内容的模组中 这里得到的经验也仍然有用。
不过 也无需担心 虽然我建议你在尝试这一部分之前先去阅读在序言中提到的C#教程 但是事实上即使完全没了解过也不会过多影响这一部分,我也会在教程中穿插解释相应的基础。
如果你能看到这里 那么你就离创作自己的mod更进一步了。祝你好运!
查看源码文件
双击SlugTemplate.sln或者SlugTemplate.csproj以打开工程
提示:这两个文件分别是VS解决方案文件和C#工程文件,只有打开这两个文件才能正常加载工程并且启用代码补全
(sln文件在这里。csproj在src文件夹里面)
这时候右边的解决方案资源管理器里面应该是这样的。
选择Plugin.cs(和之前修改类名时一样),打开它的代码页面。
基础语法和格式
让我们再看看这里面的代码。
注释
如你所见,代码中有很多显示为绿色的字,标识出每一段代码的功能 这些就是注释
顾名思义,注释本身不参与代码的执行 在编译时也会直接跳过 它们的作用就是为了方便理解和阅读代码 不至于看到自己几天前写的代码就忘记它是做什么的了
引用
在代码最上方出现的这一串“using”就是引用。
引用使你可以从其他代码中借用公开方法和字段,你也可以把自己的一部分代码写成一个单独的.cs文件并且给它确定类名,之后通过添加引用就可以在其他代码中使用被独立出去的那一部分 熟练运用这个有助于保持代码的整洁和易于调试。不论如何 暂时不需要关注这个
不过 你会看到引用里出现了BepInEx,随后的代码中就出现了来自这个程序集的类名BaseUnityPlugin,这应该可以让你对它的作用有大致的了解
命名空间
往下看 你应该会找到类似这样的代码(忽略大括号内部的部分)
namespace是命名空间,它的作用是方便管理和查找代码。当出现重名的类和字段时,命名空间不同也可以避免混淆。如果你把鼠标移到下面的青色的Plugin上 你会看到它的全名显示为SlugTemplate.Plugin。这就避免了你的Plugin类和其他人的mod里的Plugin类重名造成混乱。
命名空间的格式大致是这样的
(C#的格式较为自由,代码中的空格和换行很多情况下都可以按需使用)类名
再往下就会看到这个,也就是所谓的类名
在C#中,任何可被操作的内容都被视为是“类的实例”,因此能够概括这些内容性质和可进行的操作的模板就称为“类”。在这个例子中,Plugin就是你所创建的模组的类。
”:“后面的部分是被继承的类,也就是说这个Plugin类会得到BaseUnityPlugin类的属性。把鼠标移动到BaseUnityPlugin上就会看到提示
因此我们了解到这个类的命名空间是BepInEx(还记得引用里面添加了这个命名空间吗 就是为了这里可以直接查找到),是一个可以被BepInEx这个插件加载器加载的插件类的模板,以他为基础创建的Plugin类自然就是一个可被加载的插件。
对于每一个模组,一般情况下继承了BaseUnityPlugin的类只能有一个。以后如果要创建自定义生物和物品的话 也有可以被继承的基础生物模板。除此之外 你还可以自己建立新的类用于整理和缩短代码,这是之后会涉及到的内容。
变量与方法
全局变量 也称为字段 是在类里构建的变量类型,本身也是类的实例。
private const string MOD_ID = "author.slugtemplate";
//这个字段是一个私有的 常值的 字符串类的实例 名为MOD_ID,赋值为"author.slugtemplate"
//如果你可以给它一个很明确的赋值 这个类型“string”也可以改成“var”,代表由系统根据等号后面的内容自动判断MOD_ID是什么类型。
//但是最好避免滥用var以免代码难以阅读(
方法可以理解为对于一个类的实例可以执行的特殊操作。你有可能听说过函数与方法两种概念 但实际上对C#来说两者没有区别 并且一般只称方法.....
private void Player_Die(On.Player.orig_Die orig, Player self)
{
bool wasDead = self.dead;
}
//这个方法是一个私有的 不会直接输出数值的(输出类型填了void) 名为Player_Die的方法
//带有Player.orig_Die类型的实例orig和Player类型的实例self两个参数,参数可以理解为预先获取的局部变量。
//里面有一个布尔值类型的实例 名为wasDead的局部变量 被赋值为self.dead
//前面说过self是Player类的实例 实际上这个实例就是玩家本身,Player类里面定义了dead这个布尔值,self自然也就带有这个数值
//self.dead就是获取了当前玩家的dead字段是否为真
//也就是说死透了没(
在方法里构建的变量称为局部变量 只能在这个方法内部访问 因此即使多个方法有重名的局部变量也不会产生混淆 这里的wasDead就是一个局部变量。
格式问题
你可能会注意到互相嵌套的代码被按照层级错开四个空格长度(或者说,VS里默认的1tab的长度)
//这边参考了DXTsT的泰拉瑞亚模组教程的表述方式
namespace SlugTemplate {
// 属于命名空间这块的代码
class Plugin : BaseUnityPlugin {
// 属于class(类)的代码
void function() {
// 属于function(方法)的代码
}
}
}
(为了你的编辑方便着想.....)
修改属性
到了这里 你大概已经能结合注释看懂大部分的代码了。那么就开始大胆地修改吧!
不过在此之前 还有一个关键的概念需要解释.....
关于挂钩
(挂钩有非常特殊的行为 其原理也较为复杂 尤其是在对同一方法多次挂钩时。我无法确切描述相关的内容,实际的效果自己可以去尝试 这里还是建议暂时尽可能避免这样写)
你或许已经注意到这个角色的跳跃高度格外高,对应部分的代码是这样的:
public void OnEnable()//此处是被称作挂钩的操作
{
On.Player.Jump += Player_Jump;
//在玩家触发跳跃时执行Player_Jump
}
private void Player_Jump(On.Player.orig_Jump orig, Player self)
{
orig(self);//执行原版的跳跃方法
if (SuperJump.TryGet(self, out var power))
{
self.jumpBoost *= 1f + power;
}
}
在这个例子中,玩家跳跃时实际执行的方法是Player_Jump。原版的方法Player.Jump被作为一个参数orig传入委托中,因此使用orig(self);就能执行原版的跳跃方法。
当然 你希望你对跳跃做出的修改不会影响到其他角色 所以你需要一个条件判断以确定只有当前角色是有特定属性的mod角色时才会使用额外的操作。
这个SuperJump.TryGet(self, out var power)可能会有点令人疑惑,但是如果看到这个
(在角色配置的json文档里)
还有这个
(在plugin.cs的第十五行)
事实上 这就是一个自定义的角色属性(或者至少是用来判断这个角色有没有这个属性的标签),它会读取角色的json文档里是否具有这个属性并且视情况返回数值。到这里 整个plugin.cs的内容都已经很明确了。
修改跳跃属性
现在再回头看看Player_Jump方法,也就是你的角色跳跃时实际执行的代码。去掉关于执行原版跳跃和条件判断的部分 剩下的内容只有一行
它的大致含义就是 在执行这一句时,玩家的jumpboost属性被乘以(1+角色json文件里面设置的superjump的数值大小)。 把这个1f改成10f 然后点编译 按之前说过的方式把mod文件夹复制到游戏里面,然后打开游戏逝一逝吧
(如你所见 你的每一次跳跃都是致命的)
....意料之中 希望你没有真的把自己摔死 不过如果你这样做了 你会看到这只奇怪的活体火箭在落地时发生了爆炸,也就是你在代码里看到的 挂钩在Player.Die方法上的名为Player_Die的方法。
修改爆炸属性
虽然这个方法里有一串比较长的条件判断 但是看一眼就大概能猜出来它的作用,这是用于判断玩家在什么条件下爆炸的逻辑。那么爆炸的效果是怎么做的?
var room = self.room;
var pos = self.mainBodyChunk.pos;
var color = self.ShortCutColor();
room.AddObject(new Explosion(room, self, pos, 7, 250f, 6.2f, 2f, 280f, 0.25f, self, 0.7f, 160f, 1f));
room.AddObject(new Explosion.ExplosionLight(pos, 280f, 1f, 7, color));
room.AddObject(new Explosion.ExplosionLight(pos, 230f, 1f, 3, new Color(1f, 1f, 1f)));
room.AddObject(new ExplosionSpikes(room, pos, 14, 30f, 9f, 7f, 170f, color));
room.AddObject(new ShockWave(pos, 330f, 0.045f, 5, false));
room.ScreenMovement(pos, default, 1.3f);
room.PlaySound(SoundID.Bomb_Explode, pos);
room.InGameNoise(new Noise.InGameNoise(pos, 9000f, self, 1f));
接下来的内容是一串的room.开头的语句,它们都是在调用Room类的方法并且执行给这个名为room的实例。
以第一句为例,它向room(玩家当前所在的房间)使用AddObject方法(添加一个物体实例),而被添加的实例是使用new语句新建的一个Explosion类的实例,后面的部分就是这个实例的可以控制的参数,把鼠标移到Explosion上就可以看到。下面的语句也就不难理解了。
自己在添加冲击波特效的AddObject方法下面留下了一点挑战,试试看做出来 然后观察效果?也许你只需要再次编译并且装载mod 然后出门跳一下并摔死就好。你也可以试着魔改任何其他的数值 看看效果是否和自己想得一样。
......酷炫!
进阶项目
你大概已经知道如何修改属性并且创造有意思的效果了。现在咱留下几个小挑战 先尝试自己完成 再去看咱给出的示范 看看与你的设想是否一致。
1.让玩家在跳跃时产生冲击波,并且带有蜗牛(或称波动龟)的“Pop”音效。(提示:使用代码自动补全搜索到跟pop相关的音效)
2.让玩家在偶数次跳跃时闪光(提示:建立一个int字段来计算跳跃次数,计算除法余数使用的符号是%)
3.让玩家死后尸体迅速飞天(提示:玩家的身体是self.mainbodychunk,你可以给它的vel属性赋值,竖直向上的预设值是Vector2.up)
答案
1.把这些代码插入Player_Jump里的逻辑判断内,和其他修改跳跃的代码在一起
var room = self.room;
var pos = self.mainBodyChunk.pos;
room.AddObject(new ShockWave(pos, 500f, 0.080f, 10, false));
room.PlaySound(SoundID.Snail_Pop, pos);
//详细参数可以自己修改 也可以尝试不同的SoundID
2.把这一行插入Plugin类中,但不在方法中的任何位置。建议放在class Plugin下面,和其他字段在一起
然后把这些代码插入Player_Jump里的逻辑判断内jump_count += 1;
if(jump_count % 2 == 0)
{
room.AddObject(new Explosion.ExplosionLight(pos, 230f, 1f, 3, new Color(1f, 1f, 1f)));
}
//同样的 参数可以自己修改
3.不用说也知道该放在哪里了吧
下一节中我们将挑战实操,从挂钩开始创建一个全新的角色能力,让玩家在空中按住跳跃键触发“羽落”技能。之后你就可以删去代码中无用的部分 重新整理代码 然后发挥自己的想象力进行创作啦....
另外 也建议你看到这里后去尝试阅读序言中推荐的C#基础,可以获得更明确和清晰的代码理解。