跳至正文

源码参考


源码参考

Mod制作中还有一个必不可少的资料就是原版代码了,Mod制作的大部分内容都是要基于原版的机制的,那么源码的重要性可想而知。但是可惜的是,想要获得源码只有通过反编译这一途径,而且反编译出来的代码跟真正的源码有很大区别。

具体来说,反编译出来代码是机器生成的,而真正的源码时人写的,所以虽然反编译出来的代码逻辑是正确的,但是却很凌乱,因此要在反编译以后的代码上开发Mod是十分困难的。

不过好在逻辑正确,所以我们可以通过阅读反编译出来的源码对TR的整个系统有一个初步的了解,这也能帮助你们解释在Mod制作过程中遇到的问题。(比如,这个属性是干嘛用的)

反编译TML源码下载地址:

或者可以进群在群文件下载,如果在这里下载比较慢的话。

别看这是TML源码,TML和原版代码是融合在一起的,而且TML源码的重要性在于,你可以通过这个源码了解一下TML自带的钩子是如何在原版代码里面运行的。

首先,我们把源码解压到一个文件夹,然后打开Terraria.csproj

因为TR的源码十分庞大,所以你不可能把这几百万行代码从头到尾的读一遍,我们需要先看着这堆文件夹,猜测一下哪些文件时干什么的。首先是图中的Terraria文件夹,这很明显就是泰拉瑞亚主要代码的存放地点了。

点进去可以看到里面有很多类,并且每个文件点进去代码量都十分恐怖,不过不要怕,我们先从TML的重写函数入手,一步步分析一下TR代码的结构。

查询重写函数

我们一开始使用到的重写函数就是ModItemSetDefaults了吧?我们就来看看它是怎么实现的,首先我们先查找ModItem这个类:

可以看到,我们要找的类就在箭头标记的地方,点进去看看吧。

ModItem这个类虽然有1000多行代码,但是大部分都是注释和可重写的虚函数,我们使用Ctrl+F搜索SetDefaults,或者这里可以直接查找:

/// <summary>
/// This is where you set all your item's properties, such as width, damage, shootSpeed, defense, etc. 
/// For those that are familiar with tAPI, this has the same function as .json files.
/// </summary>
public virtual void SetDefaults()
{
}

这些注释也挺重要的,是TML的开发者提供的,会告诉你这个重写函数的主要作用,也是一个非常有帮助的资料。

这样我们就成功定位到了ModItemSetDefaults函数的位置了,这里面什么都没有,因为函数本体是由我们去写的。

接下来我们就看看,设置物品属性的这个SetDefaults函数,究竟是在原版代码中的哪一部分被执行的,我们需要使用VS的一个功能:对着函数右键查找所有引用

你会看到好多引用……

哎呀怎么这么多,哪个才是我们要找的啊?

仔细观察一下你会发现,很多都是开发者物品的SetDefaults,也就是TML的开发人员内置的物品。只有一个地方跟这些物品无关,大概就是滑到最下面的一行

点进去,你会来到一个新的SetDefaults函数,这个函数的作用就是对所有Mod物品进行SetDefaults,而之前的那个SetDefaults是对某个特定物品的。我们离真相越来越近了

我们在进行一次右键查找所有引用。很幸运,这次我们只有一个地方可以去,点进去吧

然后我们就来到了Item.cs文件,你会看到我们到了第三个SetDefaults函数这里,这也就是真正的原版物品属性设置的函数

你会在这个函数附近发现更多的SetDefaults函数,原版把物品的3000多个ID以1000为单位装进了4个函数里面,这种做法……一言难尽

不过我们可以通过这个函数了解一些物品属性的设计,比如我想知道天龙之怒(ID:3858)的物品属性,我们就可以在这个文件ctrl+F:

模仿原版AI

我们也可去查看弹幕的属性,比如说AI:

我们又可以通过之前的方法定位到Projectile.cs里面的AI方法

然后我们可以通过搜索弹幕ID或者aiStyle来找到某个弹幕的AI代码,比如说泰拉剑气弹幕(ID:132):

可以看到这个弹幕的aiStyle是27,也就是说它属于第27类AI,我们一定要先从aiStyle入手而不是从物品ID,因为如果你搜物品ID所得到的AI代码可能只是这个弹幕AI的一小部分,真正主导这个弹幕AI的其实是aiStyle

对于这些AI代码,我们要忽略掉判断弹幕ID,也就是type的代码,因为那些代码是针对某个特定ID的弹幕有效的,我们要找的是对于所有aiStyle等于27,且弹幕ID为132(泰拉剑气)有效的AI代码,我们继续往下看。

呐,我们看到了泰拉剑气的粒子效果,注意这个localAI[1]可能是个计时器哦,因为就在它的下面有这样一段代码

不过这些名字,怎么这么……,什么num454,dust43什么鬼?有人这么写代码的吗?反正如果有人这么写代码我一定会去锤爆他的头。

不过话又说回来,这些代码是机器生成的,对于局部变量,电脑也无法知道他们在编写的时候叫什么名字,所以也无可奈何啊ε=(´ο`*)))唉 (但是字段,方法,属性,类名等等确是可以知道的,这是.Net程序集的特性)

这就是为什么TR源码只能用来参考,你要是真的读这样的代码会痛苦死的。

接下来的代码都是不针对与某个特定的弹幕ID了,这正是我们想要的

往下翻我们会看到另一个localAI[0],啊,原来这个localAI[0]的作用是调整弹幕的透明度和大小变化啊,难怪泰拉剑气会有忽明忽暗,忽大忽小的效果。不过这里面的逻辑不是很明显,需要认真的阅读。

再往下看有一个很有意思的东西

这段话非常重要,这就是为什么你的弹幕不会朝着弹幕速度的方向,而原版的弹幕却会。因为你需要让弹幕在飞行的时候自己把旋转角度设置为速度的旋转角度,仔细观察一下这个弹幕的贴图。

你会发现它是斜着45度的,如果你想让它朝向等于速度朝向,你需要把这斜上方45度的角度加上。这也就是0.785f的由来。

因为0.785弧度就是45度角,那么如果你的弹幕是90度朝上的,那么就要加上1.57f,或者 \(\frac{\pi}{2}\)。

接下来我们要做的就是利用这些原版代码,复原出一个泰拉之刃的弹幕的AI。其中这些代码是需要复制到你的AI函数中的(注意不要设置任何aiStyle和aiType属性):

public override void AI() {
    if (localAI[1] > 7f) {
        int num454 = Dust.NewDust(new Vector2(base.position.X - base.velocity.X * 4f + 2f, base.position.Y + 2f - base.velocity.Y * 4f), 8, 8, 107, oldVelocity.X, oldVelocity.Y, 1
        Dust dust43 = Main.dust[num454];
        dust43.velocity *= -0.25f;
        num454 = Dust.NewDust(new Vector2(base.position.X - base.velocity.X * 4f + 2f, base.position.Y + 2f - base.velocity.Y * 4f), 8, 8, 107, oldVelocity.X, oldVelocity.Y, 100, 
        dust43 = Main.dust[num454];
        dust43.velocity *= -0.25f;
        dust43 = Main.dust[num454];
        dust43.position -= base.velocity * 0.5f;
    }
    if (localAI[1] < 15f) {
        localAI[1] += 1f;
    } else {
        if (localAI[0] == 0f) {
            scale -= 0.02f;
            alpha += 30;
            if (alpha >= 250) {
                alpha = 255;
                localAI[0] = 1f;
            }
        } else if (localAI[0] == 1f) {
            scale += 0.02f;
            alpha -= 30;
            if (alpha <= 0) {
                alpha = 0;
                localAI[0] = 0f;
            }
        }
    }
    if (this.ai[1] == 0f) {
        this.ai[1] = 1f;
        Main.PlaySound(SoundID.Item60, base.position);
    }
    rotation = (float)Math.Atan2(base.velocity.Y, base.velocity.X) + 0.785f;
    if (base.velocity.Y > 16f) {
        base.velocity.Y = 16f;
    }
}

我进行了一点删改(删掉了对于具体某个id起效的代码,保留了对于泰拉之刃起效的代码),对比一下你复制出来的代码,看看是否和我认为需要用到的代码一致呢?但是你很快会发现:

这是怎么回事?哦,因为我们的原版代码是写在Projectile类里面的,而Projectile类的属性里面已经包含了弹幕的属性了,但是我们现在正在写的是ModProjectile,并不包含那些属性,不过还好ModProjectile提供了指向当前弹幕的实例projectile

所以我们只要在这些属性前面加上projectile.XXXX就好了。如果遇到了this.base.这些关键字,我们也要把它变成projectile字段哦。但是一个个替换有点麻烦,还好我们有替换功能:

以下就是进行了亿点修复以后的代码

public override void AI() {
    if (projectile.localAI[1] > 7f) {
        int dustID = Dust.NewDust(new Vector2(projectile.position.X - projectile.velocity.X * 4f + 2f,
            projectile.position.Y + 2f - projectile.velocity.Y * 4f), 8, 8, 107, projectile.oldVelocity.X,
            projectile.oldVelocity.Y, 100, default(Color), 1.25f);
        Dust dust = Main.dust[dustID];
        dust.velocity *= -0.25f;
        dustID = Dust.NewDust(new Vector2(projectile.position.X - projectile.velocity.X * 4f + 2f,
            projectile.position.Y + 2f - projectile.velocity.Y * 4f), 8, 8, 107, projectile.oldVelocity.X,
            projectile.oldVelocity.Y, 100, default(Color), 1.25f);
        dust = Main.dust[dustID];
        dust.velocity *= -0.25f;
        dust = Main.dust[dustID];
        dust.position -= projectile.velocity * 0.5f;
    }
    if (projectile.localAI[1] < 15f) {
        projectile.localAI[1] += 1f;
    } else {
        if (projectile.localAI[0] == 0f) {
            projectile.scale -= 0.02f;
            projectile.alpha += 30;
            if (projectile.alpha >= 250) {
                projectile.alpha = 255;
                projectile.localAI[0] = 1f;
            }
        } else if (projectile.localAI[0] == 1f) {
            projectile.scale += 0.02f;
            projectile.alpha -= 30;
            if (projectile.alpha <= 0) {
                projectile.alpha = 0;
                projectile.localAI[0] = 0f;
            }
        }
    }
    if (projectile.ai[1] == 0f) {
        projectile.ai[1] = 1f;
        Main.PlaySound(SoundID.Item60, projectile.position);
    }
    projectile.rotation = (float)Math.Atan2(projectile.velocity.Y, projectile.velocity.X) + 0.785f;
    if (projectile.velocity.Y > 16f) {
        projectile.velocity.Y = 16f;
    }
}

然后我们就可以换个贴图去试试效果了:

额……好像成功了,但是效果怎么和原版的天差地别啊-_-||,是不是我们漏掉了什么?

当然漏掉了,因为我们只看了AI部分,弹幕又不是只有AI。那怎么办呢?我们要去哪里找其他部分?当然是Ctrl+F啦,不过这次我们要把范围扩大亿点

于是我们就在Main.cs的DrawProj函数里面看到了这样的代码:

啊啊,这个Draw是什么,我们是不是还没有教?咳,这是第三部分的内容,不过我们至少知道了aiStyle为27的弹幕还会重新绘制弹幕。这可就难办了,重新绘制弹幕我没教,但是我们又想复原效果,怎么办呢?哎呀当然是复制啦

我们把这一整行复制到弹幕的PreDraw重写函数里(这个函数是干嘛的?)

修复后的代码是这样的(想一想,我为什么这么修复?你修复的时候又是怎么想的?):

public override bool PreDraw(SpriteBatch spriteBatch, Color lightColor) {
    spriteBatch.Draw(Main.projectileTexture[projectile.type],
        new Vector2(projectile.position.X - Main.screenPosition.X + (float)(projectile.width / 2),
        projectile.position.Y - Main.screenPosition.Y + (float)(projectile.height / 2)),
        new Microsoft.Xna.Framework.Rectangle(0, 0, Main.projectileTexture[projectile.type].Width, Main.projectileTexture[projectile.type].Height),
        Color.White, projectile.rotation,
        new Vector2(Main.projectileTexture[projectile.type].Width, 0f),
        projectile.scale, SpriteEffects.None, 0f);
    // 返回false阻止原版的绘制
    return false;
}

再试一次,我们发现位置好像对了一点,但是还是效果很差啊!不要急,我们刚刚在只搜索了aiStyle,还没有搜type == 132呢。我们重新回到Projectile.cs搜索type == 132,我们会在GetAlpha这个地方发现一段有意思的代码:

我们还是把它搬到ModProjectile里面:

public override Color? GetAlpha(Color lightColor) {
    if (projectile.localAI[1] >= 15f) {
        return new Color(255, 255, 255, projectile.alpha);
    }
    if (projectile.localAI[1] < 5f) {
        return Color.Transparent;
    }
    // 这是个插值,只不过是颜色插值
    int num54 = (int)((projectile.localAI[1] - 5f) / 10f * 255f);
    return new Color(num54, num54, num54, num54);
}

这是让弹幕变得明亮的代码,这里面也包含的有计时器,总体来说就是弹幕出生后5帧内都是透明的,之后到第15帧都是淡入(即变得越来越不透明),最后变得完全高亮(不受到环境光影响变暗)。

这下弹幕就正常多了,颜色也变得好看了。不过位置的问题还是没有太解决,不过为了不让这篇文章变得太长,我们可以通过调整Dust的生成位置解决等方法这个问题。

最终效果

至此,虽然效果不够理想,我们算是成功还原了原版泰拉之刃的效果,不过我们没有自己写一行,代码,全靠抄抄抄。在这个过程中,我们了解了制作一个成功的弹幕所需要的细节,同时也对TR的原版弹幕AI有了初步的了解。

但是,如你所见,反编译出来的TR源码写的又长又臭,而且很多细节貌似没法完全复制,所以说我们看源码只是为了学习,如果你觉得仅凭借抄源码就能抄出个Mod那你就错了。很多时候我们需要自己去想怎么去实现某个效果,看看原版是怎么实现的,然后把原版的思路拿来借鉴,看看原版的思路有哪些地方我没有想到的,这才是真正Mod制作需要学习的。

刚刚我们复制了那么多代码,就拿那一大串Dust生成代码来讲,能不能写的更好看一点呢?当然:

Dust dust1 = Dust.NewDustDirect(projectile.position - projectile.velocity * 4f, 8, 8,
    MyDustId.RedTrans, projectile.oldVelocity.X,
    projectile.oldVelocity.Y, 100, default(Color), 1f);
dust1.velocity *= -0.25f;

Dust dust2 = Dust.NewDustDirect(projectile.position - projectile.velocity * 4f, 8, 8,
    MyDustId.RedTrans, projectile.oldVelocity.X,
    projectile.oldVelocity.Y, 100, default(Color), 2f);
dust2.velocity *= -0.25f;
dust2.noGravity = true;
dust2.position -= projectile.velocity * 0.5f;

再比如说那段Draw的代码:

var tex = Main.projectileTexture[projectile.type];
spriteBatch.Draw(tex,
    projectile.Center - Main.screenPosition, tex.Frame(),
    Color.White, projectile.rotation,
    tex.Size() * 0.5f,
    projectile.scale, SpriteEffects.None, 0f);

还有那么一大坨localAI,可不可以让它变得更好看一点?

所以说重要的不是它写了什么,而是他想干什么。我是如何知道这些代码可以被这样优化的呢?其实就是,多看源码,多了解一些函数的功能,多去想。


练习

  1. 模仿一下箭的AI吧。
  2. 阅读源码的Player.cs,找到Shoot函数所在的位置。

《源码参考》有6个想法

  1. Pingback: Tiger 的 IL 教程 - 裙中世界

发表回复