跳轉至

又见面啦

如你所见 这个教程并没有弃坑 剩下的内容会在哈维创建的模组教程站继续更新!

在之前的部分教程中 你应该已经实现了修改部分slugbase提供的预设属性并且初步实现了一个有一些自定义特性的游戏角色,接下来的教程会更进一步 讲解如何修改代码来实现更多独特的自定义能力。即使在并非以自定义角色为核心内容的模组中 这里得到的经验也仍然有用。

不过 也无需担心 虽然我建议你在尝试这一部分之前先去阅读在序言中提到的C#教程 但是事实上即使完全没了解过也不会过多影响这一部分,我也会在教程中穿插解释相应的基础。

如果你能看到这里 那么你就离创作自己的mod更进一步了。祝你好运!


查看源码文件

双击SlugTemplate.sln或者SlugTemplate.csproj以打开工程

提示:这两个文件分别是VS解决方案文件和C#工程文件,只有打开这两个文件才能正常加载工程并且启用代码补全

图片

(sln文件在这里。csproj在src文件夹里面)

这时候右边的解决方案资源管理器里面应该是这样的。

图片

选择Plugin.cs(和之前修改类名时一样),打开它的代码页面。


基础语法和格式

让我们再看看这里面的代码。

注释

如你所见,代码中有很多显示为绿色的字,标识出每一段代码的功能 这些就是注释

图片

顾名思义,注释本身不参与代码的执行 在编译时也会直接跳过 它们的作用就是为了方便理解和阅读代码 不至于看到自己几天前写的代码就忘记它是做什么的了

//在C#中 以双斜杠开头的一行文本会被视为注释

/*你也可以这样写
被夹在两个星号之间的文字 不论有几行
都会被视为注释*/

引用

在代码最上方出现的这一串“using”就是引用。

图片

引用使你可以从其他代码中借用公开方法和字段,你也可以把自己的一部分代码写成一个单独的.cs文件并且给它确定类名,之后通过添加引用就可以在其他代码中使用被独立出去的那一部分 熟练运用这个有助于保持代码的整洁和易于调试。不论如何 暂时不需要关注这个

不过 你会看到引用里出现了BepInEx,随后的代码中就出现了来自这个程序集的类名BaseUnityPlugin,这应该可以让你对它的作用有大致的了解

命名空间

往下看 你应该会找到类似这样的代码(忽略大括号内部的部分)

图片

namespace是命名空间,它的作用是方便管理和查找代码。当出现重名的类和字段时,命名空间不同也可以避免混淆。如果你把鼠标移到下面的青色的Plugin上 你会看到它的全名显示为SlugTemplate.Plugin。这就避免了你的Plugin类和其他人的mod里的Plugin类重名造成混乱。

命名空间的格式大致是这样的

namespace SlugTemplate {/*命名空间内部*/}
(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(方法)的代码
        }
    }
}
它的作用非常简单,就是为了让你可以更轻松地分辨代码之间的嵌套关系。事实上,VS会为你自动生成这种缩进... 虽然没有这些缩进代码也可以运行 但是那样读起来会特别费劲....

图片 (为了你的编辑方便着想.....)


修改属性

到了这里 你大概已经能结合注释看懂大部分的代码了。那么就开始大胆地修改吧!

不过在此之前 还有一个关键的概念需要解释.....

关于挂钩

(挂钩有非常特殊的行为 其原理也较为复杂 尤其是在对同一方法多次挂钩时。我无法确切描述相关的内容,实际的效果自己可以去尝试  这里还是建议暂时尽可能避免这样写)

你或许已经注意到这个角色的跳跃高度格外高,对应部分的代码是这样的:

 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;
            }
        }
这段代码由上方OnEnable方法里面的On.语句和下面的Player_Jump方法构成。 挂钩的作用是 在本该执行原版的方法时 实际执行了+=后面的那个,这涉及到委托的相关知识而且有一定难度 因此这里不深入讨论 暂时只要知道怎么用就好。

在这个例子中,玩家跳跃时实际执行的方法是Player_Jump。原版的方法Player.Jump被作为一个参数orig传入委托中,因此使用orig(self);就能执行原版的跳跃方法。

当然 你希望你对跳跃做出的修改不会影响到其他角色 所以你需要一个条件判断以确定只有当前角色是有特定属性的mod角色时才会使用额外的操作。

这个SuperJump.TryGet(self, out var power)可能会有点令人疑惑,但是如果看到这个图片

(在角色配置的json文档里)

还有这个

图片

(在plugin.cs的第十五行)

事实上 这就是一个自定义的角色属性(或者至少是用来判断这个角色有没有这个属性的标签),它会读取角色的json文档里是否具有这个属性并且视情况返回数值。到这里 整个plugin.cs的内容都已经很明确了。

修改跳跃属性

现在再回头看看Player_Jump方法,也就是你的角色跳跃时实际执行的代码。去掉关于执行原版跳跃和条件判断的部分 剩下的内容只有一行

self.jumpBoost *= 1f + power;
它的大致含义就是 在执行这一句时,玩家的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));
...好长!但是它的内容实际上不难理解:Player类里面包含了很多的内容(你可以双击它看看里面究竟都有什么),其中就包括了room pos和color,三者分别是表示玩家所在在房间的Room类的实例、表示玩家在房间中位置的Vector2(二维向量)实例和表示玩家的标志颜色的Color类实例。 所以最前面这三行其实就是新建了三个变量 直接用玩家self获取了数值并且分别赋值给它们。

接下来的内容是一串的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下面,和其他字段在一起

private int jump_count = 0;//也可以是public,如果你需要在其他的类读取它..
然后把这些代码插入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.不用说也知道该放在哪里了吧

self.mainBodyChunk.vel = Vector2.up * 3000f;
//好吧还是说一下 要塞在Player_Die的逻辑判断内
//飞升(物理)

下一节中我们将挑战实操,从挂钩开始创建一个全新的角色能力,让玩家在空中按住跳跃键触发“羽落”技能。之后你就可以删去代码中无用的部分 重新整理代码 然后发挥自己的想象力进行创作啦....

另外 也建议你看到这里后去尝试阅读序言中推荐的C#基础,可以获得更明确和清晰的代码理解。

評論