Unity2D游戏制作入门 | 11(之人物属性及伤害计算)

作者 : admin 本文共9233个字,预计阅读时间需要24分钟 发布时间: 2024-06-7 共4人阅读

上期链接:Unity2D游戏制作入门 | 10 (之人物跳跃动画制作)-CSDN博客

上期我们学习了如何让人物在空中可以播放跳跃动画,先是用混合树(Blend Tree)的方式将跳跃的四个阶段合并成一体,然后根据y轴传回速度参数来切换人物动画,不仅如此我们还使用了Any State在任何状态下我们都能进行人物动画的打断,比如我在攻击敌人但是这个时候我要跳跃来躲避敌人的攻击等。我们还修复了一点bug,如果我们下落到最低位置了一般都要完整的播放完着陆的动画才会继续播放idle(闲置)的动画,但是如果一落地我们就立马跑步,就会出现蹲着跑的情况,所以我们规定了如果X轴的速度大于了0.1我们又立马打断着陆的动画进而播放跑步的动画(就我们打断着陆动画它会立马进入idle状态,然后又因为我们速度X轴大于0.1所以我们立马进入跑步,这个瞬间还是很短的)。这期我们来制作人物的属性及伤害的计算这部份可能没你想象的那么好理解,希望能够反复观看。

public class Character : MonoBehaviour
{
    [Header("基本属性")]
    public float maxHp;//最大血量
    public float currentHp;//当前血量

    [Header("受伤无敌")]
    public float invincibleTime;//无敌时间
    private float invincibleCounter;//一个计数器,内部计算即可,不需要在窗口可以看得到
    public bool invincible;//为了能看见无敌,我们创建一个布尔值

    private void Start()//开始游戏时,要满血
    {
        currentHp = maxHp;
    }

    private void Update()
    {
        if(invincible)
        {
            invincibleCounter-=Time.deltaTime;
            if(invincibleCounter  0)//血量健康才能减血
        {
        currentHp -= attcker.damage;//如果学过python,你会知道这行代码等于currentHp = currentHp - attcker.damage;
        TriggerInvincible();
        }
        else
        {
            currentHp=0;
            //触发死亡
        }

    }

    //受伤触发无敌
    private void TriggerInvincible()
    {
        if(!invincible)
        {
            invincible= true;
            invincibleCounter = invincibleTime;

        }
    }
}
public class Attack : MonoBehaviour
{
    public int damage;//攻击力
    public float attackRange;//攻击范围,仅限Bee蜜蜂
    public float attackRate;//攻击频率仅限Bee蜜蜂

    private void OnTriggerStay2D(Collider2D other)
    {
        other.GetComponent()?.TakeDamage(this);//加问号处为语法唐,意思是对面身上有对应函数我才执行。
    }
}

正文:

如果我们的人物只会来回跑没有互动也不行,然后我们也学习一下碰撞检测有关的内容。

首先我们先整理我们的Boar(野猪),找到野猪idle状态(还有其他状态,我们现在用不到,如run、walk)的褐色的款式(还有别的颜色),然后右边提示调节一些参数,和之前一样,漏的那一项是无压缩(把下面的Compression改为None),然后用count的方式去切割,锚点在底部。
Unity2D游戏制作入门 | 11(之人物属性及伤害计算)插图

拖拽一张到场景中,如果野猪被场景遮挡住,去到Boar下的组件的Sprite Renderer进行sorting Layer的调整即可。
Unity2D游戏制作入门 | 11(之人物属性及伤害计算)插图(1)

现在的野猪就是一个纸片,我们需要给它添加刚体组件,将collision detection(碰撞检测)改为持续检测,然后记得冻结它的Z轴(Freeze Rotation冻结旋转之意)不要让它能翻转。
Unity2D游戏制作入门 | 11(之人物属性及伤害计算)插图(2)

接下来我们添加碰撞体collider,然后调节它范围只有脚底范围的大小,然后方框的范围大了,会导致我人物攻击范围会特别大,如果人物攻击到空气也可能造成野猪的受伤。我们只用脚底判断是为了然后让野猪站在平台上。如果你不见collider碰撞器的话野猪会掉下平台。然后有一个问题:如果我们运行游戏会发现,控制人物走向野猪会把野猪顶着走,原因是因为碰撞体之间可以相互影响(物理上的。

Unity2D游戏制作入门 | 11(之人物属性及伤害计算)插图(3)

所以我们希望人物和野猪是可以穿越的,然后它们也可以站在平台(挂载了collider一般就可以站在平台了)。设想一下:人物和敌人,敌人和敌人之间可以互相穿越,人物经过敌人那么敌人可以触发一定的伤害。不得不用到触发器trigger了(上图其实在碰撞器下面有Is Trigger的选项,这个就是触发器),触发器的使用机制一般和一些事件有关,比如人物经过敌人需要扣除xx血量(当然现在是测试阶段),而且如果我们打开了触发器那么会导致物体掉下平台,这是因为我们使用碰撞器是为了让物体可以站在平台上,打开了触发器会导致物体没有了碰撞属性从而掉下平台,至于为什么碰撞器和触发器不能共存,我也不明白其中的原理。我问了AI你可以参考下:

在Unity中,物体挂上了碰撞器(Collider)后,通常用于实现物理碰撞效果,如阻止物体相互穿透或触发碰撞事件。而触发器(Trigger)是碰撞器的一个特殊属性,当勾选“Is Trigger”选项后,该碰撞器将不再产生物理碰撞效果,而是会触发“OnTriggerEnter”、“OnTriggerExit”等事件。

我们先创建新的Layer(下图的右上角处)Player和Enemy敌人,让野猪选中Enemy人物选中Player,然后下方的Layer Overrides是碰撞图层的重载方法,我们给野猪在Exclude Layer选中它要忽略碰撞的图层,这样我们运行游戏人物和野猪,野猪和野猪将不会产生碰撞了。需要注意:这个忽略碰撞图层的重载方法尽量用2022.6版本之后的,之前我使用的2022.2版本就没有这个功能选项。

Unity2D游戏制作入门 | 11(之人物属性及伤害计算)插图(4)

然后我们不希望人物能无限地穿越野猪,这样野猪就没有任何的攻击性了。我们添加胶囊碰撞器,并调节到合适的大小和形状,先点击下方的Dierction的Horizontal进行横向的形状改变。然后在野猪的胶囊碰撞器上打开触发器,并覆写Layer Overrides把敌人这层忽略即敌人与敌人相遇不会触发触发器的事件。
ps:好家伙,现在我明白了 为什么这样用触发器了,之前看视频没明白,原来把碰撞器的触发器打开那么这个组件之间变成了新组件,所以为什么不单独弄一个触发器组件呢?…让人摸不着头脑。
Unity2D游戏制作入门 | 11(之人物属性及伤害计算)插图(5)

接下来我们要检测是否触发了碰撞关系,我们进入到我们的代码中去:(先把这个测试的代码写在之前的Move函数前面吧)

public class Player_control : MonoBehaviour{
    
    .....
    //测试
    private void OnTriggerStay2D(Collider2D other)
    {
        Debug.Log(other.name);//如今碰撞体进入触发器范围则打印碰到的物体的名称
    }//如果你测速成功了,请把该测试内容注释掉。ctrl+k -> ctrl+c(注释) ctrl+k -> ctrl+u(取消注释)
    
    Move()
    .....
}

将上面的代码功能加入我们的人物控制的代码中去,我们运行游戏会发现Console窗口的有打印信息,意思是我们的人物的胶囊碰撞器检测到了其他的触发器从而打印出信息,**即我们的胶囊碰撞器触碰到了野猪身上的胶囊形状的触发器,从而发现了野猪。经过我的测试,就是人物身上挂载的胶囊碰撞器在起作用(我特意把人物的胶囊碰撞器给移位了,这也解答了我之前刷视频时的疑惑,记得调回人物身上就可以)。**我们注意一下,我们之前给野猪的触发器忽略了敌人这个碰撞图层,那么之后我们给敌人去检测其他的触发,如果人物有触发器,那么野猪就可以发现我们的人物,就可以执行攻击的动画了,这很河里吧(不过现在还没用到就是了)。

Unity2D游戏制作入门 | 11(之人物属性及伤害计算)插图(6)

我们使用上面的方法来检测互相的伤害。**然后为了保证代码的唯一功能性,人物控制比如移动等就做这些,所以的计算伤害,包括攻击我们需要单独来写代码。**我们先整理我们的代码:在管理代码的文件创建一个新的文件为General(普遍的,一般),这样大部分物体都可以共同拥有这些代码了。然后把之间的物理环境检测的代码移动到这里来。然后给我们的人物和野猪挂载上character(人物的一些基本属性我们放在这里,比如攻击力,最大生命值,当前生命值等)的代码。
Unity2D游戏制作入门 | 11(之人物属性及伤害计算)插图(7)

进入基本属性的代码中:写完保存回到Unity窗口,你可以看到人物和野猪都有血量了,不过因为没有赋值所以都是0,你需要自己手动输入上去,这里up给人物设定100,野猪50血量,输入了数值不要忘记保存。

public class Character : MonoBehaviour
{
    [Header("基本属性")]
    public float maxHp;//最大血量
    public float currentHp;//当前血量
    }

因为人物和野猪身上都有character的代码,只需要做一个有关攻击的代码,每一次当攻击判断并触发时,我们执行对方身上的characte,进行(如血量)数值的减少。所以创建一个新的攻击的代码,这样有一个好处,如果人物的攻击分为了多段,那么可以设定每段攻击的伤害是多少,然后我们的敌人不可能只有一种,这样我们可以设定不同攻击力的敌人了。创建完后挂载到,我们人物和野猪的身上。
Unity2D游戏制作入门 | 11(之人物属性及伤害计算)插图(8)

下面的内容可能会有点绕,需要多讲讲。

我们先捋下思路,我们刚刚给人物和野猪分别挂上了新的两种代码,一个是攻击(因为测试需要,人物先不挂伤害的代码,先移除一下),一个是基本属性。

在攻击的代码中,我们使用触发器的办法,以此通过other来访问被攻击的物体(这里就特指人物了),即我们需要获取它身上的character代码,然后我们可调用例如人物当前的血量,所以我们可以把当前的这个attack相关的数值传过去,让每一次被攻击的那个人自行计算(这里我也不知道为什么不在攻击者这边的代码进行数值计算后再返回给被攻击者,可能是为了降低内存消耗?)。我下一行引用AI的回答吧(感觉第三、第七点可能是重点):

在Unity(以及其他许多游戏引擎和框架)中设计游戏逻辑时,关于“在哪里计算伤害”的决策通常基于多种因素,而不仅仅是内存消耗。以下是几个关键原因解释为什么通常在“被攻击者”身上计算伤害,而不是在“攻击者”那里计算后再返回:

  1. 封装和模块化:将伤害计算逻辑封装在被攻击者(例如,角色或敌人)的代码中,有助于保持代码的模块化。这意味着你可以更容易地管理和修改角色的属性和行为,而不需要更改其他不相关的部分(如攻击者的代码)。
  2. 数据局部性:伤害计算通常依赖于被攻击者的当前状态(如血量、防御力等)。将这些数据存储在被攻击者的类中,并在需要时直接访问它们,可以提高性能,因为这样可以减少不必要的内存访问和数据传输。
  3. 灵活性:在被攻击者身上计算伤害允许你实现更复杂的逻辑,如基于角色状态的伤害减免、伤害吸收、反击等。这些逻辑可能难以在攻击者代码中实现,因为它们需要访问被攻击者的状态信息。
  4. 扩展性:如果你将来想添加新的攻击类型或伤害源(如环境伤害、陷阱等),只需确保它们能够访问并修改被攻击者的状态即可。这比尝试在所有可能的攻击者代码中添加新的伤害计算逻辑要容易得多。
  5. 代码重用:通过在被攻击者身上实现伤害计算逻辑,你可以更容易地在不同的角色或敌人之间重用代码。每个角色或敌人都可以有自己的伤害响应机制,而不需要为每个攻击者编写特定的伤害计算代码。
  6. 内存消耗:虽然这不是主要原因,但在被攻击者身上计算伤害通常不会导致额外的内存消耗。相反,它可以减少不必要的数据传输和临时变量的使用,从而在某些情况下提高内存使用效率。
  7. 并发和同步:在多玩家游戏或需要同步的在线游戏中,确保伤害计算的正确性和一致性非常重要。在被攻击者身上计算伤害可以更容易地实现这些要求,因为你可以直接修改和同步被攻击者的状态,而不需要担心攻击者和其他玩家之间的同步问题。

总之,在被攻击者身上计算伤害是一种常见且合理的做法,它有助于保持代码的模块化、灵活性和可重用性,同时提高性能和内存使用效率。

我们先在character中创建一个方法,这个函数在传入的参数很重要我觉得,因为这个函数传入的参数是Attack类中的attcker这个实例,你可以先这么理解吧。我的理解是如果别的物体挂上Attack的代码(也可以称为组件),如果对方的组件中存在Attack,我认为可以触发并执行一些事件。

public void TakeDamage(Attack attcker)
    {
    Debug.Log(attcker.damage);//测试人物是否检测到野猪这个attcker身上的damage这个变量
	}

在Attack.cs代码中,我们加入新的函数:

在Unity中,this 关键字引用当前实例或对象。当你看到 this 在一个方法的参数中传递时,它通常是在告诉接收方法“我正在把自己作为参数传递给你”。但是,直接传递 this 作为参数到另一个对象的方法中时,你实际上是在传递当前对象的引用。

这里发生的事情是:

  1. other 是一个指向某个GameObject或其他组件的引用。

  2. GetComponent() 尝试从 other GameObject上获取一个 Character 组件的实例。

  3. 如果 Character 组件存在,.TakeDamage(this) 方法将被调用,并将当前对象的引用(this)作为参数传递给它。

    上面是AI的回答,==我觉得就是我们当前的类Attack在请求其他物体身上的Character组件,如果发现对方有了,就把自己作为引用传给对面的某某函数去使用。==我将我的这个想法返回给Ai,它是这么回答的:

    你的理解是正确的。在Unity的C#脚本中,this 关键字通常用来引用当前实例(即当前对象)。在你所描述的情况中,this 关键字被用作参数传递给另一个对象(即Character组件)的方法,以便在那个方法中引用当前的Attack组件或其相关的属性或方法。

    所以我感觉就是还是给授权

private void OnTriggerStay2D(Collider2D other)
    {
        other.GetComponent()?.TakeDamage(this);//加问号处为语法唐,意思是对面身上有对应函数我才执行。
    }

经过我的测试,我发现如果把野猪身上的胶囊触发器(野猪身上的Attack组件我没有关闭)关闭不使用了,运行游戏人物走过去发现没有扣血,说明了野猪身上的Attack组件和胶囊触发器有一定联系,然后人物的胶囊碰撞器和触发器有一定联系,而Attack组件又传递参数访问权给Character中的函数去调用Attack这个类,所以我可以断言,因为野猪身上的触发器被人物身上的胶囊碰撞器触发了(即触发器作为一个开关),所以就是一旦检测到野猪和人物发生碰撞关系,然后野猪的Attack代码找到到人物身上的相关代码去执行受伤的代码,则人物身上的Character代码开始启动并减血,触发器真妙啊!

接下来我又测试了一下,如果把野猪身上的胶囊触发器给取消变成了胶囊碰撞器,发现人物没有触发扣血,只是单纯地顶着野猪向前走。还没完,我又测试了一下,如果我把Attack组件加入到人物身上,并设置伤害为10,那么当我运行游戏时,我发现野猪右边的面板,也就是野猪的当前血量和受伤也是可以正常触发的。妙的,十分妙。所以还是体会this这个参数的用处。
Unity2D游戏制作入门 | 11(之人物属性及伤害计算)插图(9)

接下来,我们需要一开始就给物体完成初始的赋值,用到start函数即可。然后如果我们持续经过野猪是会持续扣血的,这人物也没有无敌啊,持续的物理检测不是盖的好吧,所以这需要设计一个无敌时间。

private void Start()//初始赋值。开始游戏时,有血量的物体要满血。
    {
        currentHp = maxHp;
    }

public void TakeDamage(Attack attcker)
    {
        //Debug.Log(attcker.damage);
        currentHp -= attcker.damage;
	}

计时器:

看这段内容标题的大小就可以知道它的重要程度了,很多项目都会用到计时器的功能,比如如果人物在短时间内受到大量伤害,那么它应该有无敌时间(如无敌0.5秒,这0.5秒内不会再受到伤害)。接刚刚需要设计无敌时间的内容,先放代码。**设计人物的无敌,我们先在最下面创建一个新的函数为TriggerInvincible即触发无敌之意。该函数中,如果我们不是无敌的(通过布尔值去判断),我们的判断无敌的布尔值就为true,并赋值给无敌时间的计数器,无敌时间invincibleTime需要我们在Unity的窗口手动输入它的一个初始值。**在Update函数中,我们计算人物触发无敌时间的计算时间,即人物需要在经过多久后无敌会消失(相当于过了xx秒后,我们更新人物的无敌状态为flase,因为计算机的计算时间会比较快,我们先判断如果无敌进去倒计时,然后 如果计算机把无敌时间减过头会出现负数的情况,如果你设置无敌时间减到0才修改人物Character下的无敌状态,那么它可能因为负数就不能实现。)。这是在Update中,然后我们在Takedamage函数中,我们规定如果人物是无敌状态(人物的初始无敌状态为flase,就如果你一开始不赋值,那么系统会自定分配初始值为flase,也相当于下面第十二行代码写的了,你可以不写,写也没事。)的话我们直接继续return让Update函数计算完无敌时间,如果人物不是无敌的状态了,我们开始执行受伤(TakeDamge函数是会按照代码顺序去运行的,请认真检测代码顺序哦!)。执行受伤后我们就会再次进入无敌时间,需要注意的是,如果我们人物的血量撑不过这一次人物的攻击,即可能会变成负数(我的血量比如为2,敌人攻击为5),那么我们规定我们的血量为0,这就是为什么判断我的血量直接归零。因为我的血量一直为0,所以我就不可能触发无敌状态了,在else中没有调用函数TriggerInvincible()。

if (currentHp - attcker.damage > 0)//血量健康才允许减血
        {
        currentHp -= attcker.damage;//如果学过python,你会知道这行代码等于currentHp = currentHp - attcker.damage;
        TriggerInvincible();//触发无敌
        }
        else
        {
            currentHp=0;
            //触发死亡。可添加死亡动画
        }
完整代码:
	[Header("基本属性")]
    public float maxHp;//最大血量
    public float currentHp;//当前血量
    [Header("受伤无敌")]
    public float invincibleTime;//无敌时间
    private float invincibleCounter;//一个计数器,内部计算即可,不需要在窗口可以看得到
    public bool invincible;//为了能看见无敌,我们创建一个布尔值

    private void Start()//开始游戏时,要满血
    {
        currentHp = maxHp;
        //invincible=flase:
    }

    private void Update()
    {
        if(invincible)
        {
            invincibleCounter-=Time.deltaTime;
            if(invincibleCounter  0)//血量健康才能减血
        {
        currentHp -= attcker.damage;//如果学过python,你会知道这行代码等于currentHp = currentHp - attcker.damage;
        TriggerInvincible();
        }
        else
        {
            currentHp=0;
            //触发死亡。可添加死亡动画
        }
    }
    
    //受伤触发无敌
    private void TriggerInvincible()
    {
        if(!invincible)
        {
            invincible= true;
            invincibleCounter = invincibleTime;
        }
    }

所以如果人物的血量归零了,那么它启用的代码应该为:我认为应该只是一直执行TakeDamage中的 currentHp=0;了,其他内容估计也用不上了。

	[Header("基本属性")]
    public float maxHp;//最大血量
    public float currentHp;//当前血量
    [Header("受伤无敌")]
    public float invincibleTime;//无敌时间
    private float invincibleCounter;//一个计数器,内部计算即可,不需要在窗口可以看得到
    public bool invincible;//为了能看见无敌,我们创建一个布尔值

    private void Start()//开始游戏时,要满血
    {
        currentHp = maxHp;
        //invincible=flase:
    }

    private void Update()
    {
    
    }

    //受到伤害
    public void TakeDamage(Attack attcker)
    {
            currentHp=0;   
    }

总结:

这节我们添加了两份代码文件,一个是和物体本身属性有关的,如初始血量(你也可以认为是初始设置的最大血量),当前血量;另一个是和攻击有关的,我们需要手动设置攻击力。然后通过一个**关键字this授权该类,以此来找到物体身上的Character组件去执行受伤。**不仅如此,我们还通过计时器给人物或敌人一些物体的时间,并把它放到Update函数中去计算无敌状态的倒计时。update函数中关键的计时代码为invincibleCounter-=Time.deltaTime;,要是小于等于0了我们就更新人物的无敌状态为flase,这时人物将不再无敌。死亡后人物血量直接归零,而且不再触发无敌机制,内容就是这些了。还是要多多运行游戏试玩,能犯错是好事,不然以后自己写都不知道bug出在哪。对了,记住无敌计时器的通用写法。也不一定是无敌计时,可能是别的,总之记住计时器的写法。

补充:

1、计时器:一布尔,二浮点(外部赋值,内部计算)。Update更新计时器的差,归零或小于0即取消人物的人物改布尔为flase,写一个函数判断无敌的触发,如果不触发就触发,并开始赋值倒计的时间,最后在某函数触发什么事件后开始调用触发无敌的函数,最在updater函数中开始无敌的倒计时,形成一个良性循环。

未尽事宜以后可能会补充。

———————-结束线

本站无任何商业行为
个人在线分享 » Unity2D游戏制作入门 | 11(之人物属性及伤害计算)
E-->