Attack仅定义包含一个int名为Damage和一个bool名为IsCritical的类。
AttackDefinition定义攻击所需数值比如范围暴击几率等,并返回Attack类型方法计算确定最终damage和iscritical,类似ItemDefinition_SO,player和enemy通用。
IAttackable为接口,内含一个方法,表示所有使用该接口的类在被攻击时做些什么,比如生命值减少,播放被攻击动画和音效等等。此教程里是发送Log显示攻击对象名称和攻击值。player和enemy通用。
Scripts文件夹下新建文件夹Monobehaviours和ScriptableObjects,新建脚本Attack,作用是定义Attack类的type,这个type包含一个int Damage,一个bool IsCritical。这两个值只能在类内定义,类外只读。
然后打开在Beginner Programming: Unity Game Dev Course(5)- Creating a Character Stat System教程里提到的CharacterStat脚本修改,获得character当前的基础damage和抗性。注意Character在教程里player和enemy共用。
接下来在ScriptableObjects文件夹下新建脚本AttackDefinition,它继承自ScriptableObject。该脚本定义Attack行为需要的基本数值,包含了一个通过基本数值计算最终damage,并传出上面定义的Attack类型的damage和isCritical。
然后在项目里新建AttackDefinition,并命名为DemoAttack作为Player的Attack类型,在此处先不挂载,我们继续写脚本。
新建脚本IAttackable脚本,它是一个接口,用于之后测试并显示Attack的对象名称及具体数值。
Monobehaviours文件夹下新建脚本AttackedDebug,此例用来被攻击后显示Log。比如生命值减少,播放被攻击叫声,动画之类都可以写在此处。同样player和enemy都可共用。
场景里新建一个Cube作为被攻击测试对象,挂上Rigidbody,Layer选择Clickable,挂上刚写的脚本AttackedDebug
然后打开之前教程Beginner Programming: Unity Game Dev Course(4)- Swords and Shovels Character Controller and AI写的MouseManager脚本修改,为了改变攻击时的光标样式,以及把点击对象传递到攻击者。
先新建Unity事件
然后定义事件
修改点击方法
然后修改同教程里的HeroController脚本,用来计算攻击值并触发被攻击者身上的OnAttack方法,这个方法是一系列被攻击后的逻辑,此例只显示log。
回到Unity,在MouseManager上触发HeroController上新写的方法
然后把之前建的AttackDefinition拖入Hero
build后点击Cube即可显示。
ScriptableObjects文件夹下新建脚本Weapon,它继承自AttackDefinition。作用为包含weapon的prefab和对AttackDefinition中的攻击行为扩写,比如添加判定距离方向等。
注意红框中的IsFacingTarget方法,这没有在该类中具体实现,它是一个扩展方法,接下来我们来写它的实现。新建脚本名为ExtensionMethods
教程中扩展方法的解释
对该扩展方法内Vector3.Dot的说明,即二者向量夹角小于45°判定为面向。
写好Weapon后我们在项目里新建Weapon数据命名为SwordAttack
之前我们任何Item都是ItemPickUp类型,现在为武器单独写了类,我们把之前与武器相关的地方类型都改变一下。
ItemPickUp_SO脚本
CharacterStat_SO 脚本,加上.weaponPreb
CharacterStats脚本
然后修改之前ItemPickUp_SO类的Sword
点击运行游戏,可以看到开箱捡起剑后摁1可以顺利装备了。
但我们刚才只写了weapon形式并把其他脚本里相关武器的类型都改了过来,使其顺利执行一起写好的换上装备。写的执行攻击并没有触发,下面我们来让weapon进行攻击。
打开player身上的AnimatorController,拖入攻击动画,并设置转换,转换条件为trigger,名为Attack。
注意攻击动画上已设置了一个Event名为Hit
打开HeroController脚本来实现攻击,逻辑是这样 点击对象接近对象 → 直到攻击距离内播放攻击动画 →触发动画Event执行攻击逻辑
Monobehaviours文件夹下新建脚本AttackedForce,使用IAttackable接口,这意味着重写OnAttack方法,并在攻击时触发。
把脚本挂在Cube上,运行游戏就看到攻击cube会击退它
接下来添加一个新的weapon,新建一个空对象命名为Pole,添加Rigidbody组件,勾选isKinematic。下面新建一个圆柱体Cylinder子对象。
然后把该新建的weapon拖入prefab文件夹。
新建weapon数据类型命名为PoleAttack并拖入Prefab
再把生成该武器的ItemPickUp_SO数据类型,为了方便复制一下Sword,改名为Pole,并拖入PoleAttack
然后把场景里箱子将要掉落的物体换成该ItemPickUp_SO Pole
运行游戏,打开箱子后就可以捡到一根水管子了。
Hierechy下新建空对象命名为ScrollingText,添加TextMesh组件,该组件的作用是在worldspace里显示text。其CharacterSize用来设置该对象大小。
然后在Monobehaviours文件夹下新建脚本ScrollingText,并挂在ScrollingText对象上,然后把该对象存为Prefab并在场景中删除。(记得设置Duration和Speed值)
ScrollingText脚本,里面包含了本对象在一定时间内向上飘,设定本TextMesh的内容和颜色 3个方法。还是和以前一样,该脚本类似于_SO写具体方法实现,然后再有对应脚本引用这些方法并触发。
接着在Monobehaviours文件夹新建脚本AttackedScrollingText脚本,使用IAttackable接口,用来调用之前写的方法。
挂到被攻击对象Cube上,并选择颜色。
运行游戏后就可看到攻击一次字网上飘一次了。
新建CharacterStats_SO数据型对象,命名为Block,代表这个被攻击方块的各项CharacterStats
然后修改CharacterStat脚本,即生成一个新的CharacterStats_SO模板的实例。
新建脚本IDestructable,该脚本表示对象死亡后的一系列执行的接口
Monobehaviours文件夹下新建脚本AttackedDamage,用来实现Cube受到攻击后减少声明值,并在生命值为0后触发所有IDestructable接口
再实现接口的具体语句,即摧毁对象,Monobehaviours文件夹下新建脚本DestructedDestroyObject
把这两个脚本都挂在Cube上,并把之前的属于Cube的CharacterStats_SO对象Block拖入
运行游戏可以看到Cube被砍几刀后销毁。
现在我们来给哥布林添加攻击性,把哥布林的Layer选为Enemy,添加Collider,然后把四个攻击相关的脚本挂上,TextMesh Prefab也填入。然后记得点击Apply使所有哥布林有同样的设置,Layer改变时会提示改变包括子对象,还是仅改变此对象。选择改变此对象就好。
MouseManager里添加Enemy Layer使鼠标点击哥布林时能转换光标样式。
运行游戏,可以看到哥布林也能攻击player且跳字了。
布娃娃Ragdoll效果就像被砍死了后啊的一声掉下楼的效果。之前我们已经写了生命值低于0后销毁使用IDestructable接口,之后我们再写销毁了同时生成个布娃娃给布娃娃一个力并在N秒后消失,通过这种方式呈现布娃娃效果。
首先我们找到哥布林的模型(注意不是Prefab),复制哥布林的模型命名为BatGoblin_for_Ragdoll记得把新模型的Optimaze Game Object关掉。这个选项的作用是优化游戏对象的骨骼层级提高性能,但我们之后需要拖拽骨骼层级设置Ragdoll,所以新的模型不需要这个优化。
把新模型拖到场景命名为BatGoblinRagdoll,在Hierachy里添加Ragdoll,拖入对应骨骼后点击Creat生成。
Monobehaviours文件夹下新建脚本Ragdoll,用来确定脊柱骨骼,给脊柱添加一个力,并在n秒后摧毁。它的触发由普通哥布林prefab触发,即哥布林死了生成ragdoll哥布林,然后触发其添加力的方法。
把脚本挂到Rogdoll哥布林上,拖入脊骨骨骼,填好时间,点击Apply后存成prefab,然后把此对象在场景中删除。
Monobehaviours文件夹下新建脚本DestructableRagdoll,用来触发生成ragdoll并调用添加力方法。
拖入场景里普通哥布林身上,把ragdoll哥布林的prefab填入。
运行游戏后可看到效果。
Monobehaviours文件夹下新建Projectile脚本用来代表飞弹这一子弹对象本身。它包含了飞弹自己的移动和在适当情况下销毁,以及最关键的,传递释放者和飞弹打到的对象这两个参数,所以它包含一个委托Action。
接下来写远程魔法攻击这一行为本身,ScriptableObjects文件夹下新建Spell,它继承自AttackDefinition。记得之前的近战攻击Weapon也是继承自AttackDefinition。
我们先制作一个魔法飞弹的Prefab,场景里新建3D对象Sphere,下路拖入项目里FBX的火焰粒子效果,拖入飞弹脚本Projectile后保存到Prefab并从场景中删除。
项目内新建魔法攻击数据类型Spell命名为Fireball,填入对应数值和prefab。
新添加两个Layer如下,EnemySpell作用于player,playerSpell作用于enemy。防止自己发出的火球打到自己。
点击Physics设置管理
对Layer勾选,确保两者间的对应作用关系。
Spell脚本里有飞弹发射点,一般为右手,但我们的动画都是Optimize Game Object过的,所以先把右手的transform标记出来。
点击哥布林的模型(注意是模型,不是prefab),在添加位置中找到右手。
然后点击apply
找到场景里随便一个哥布林,把哥布林的模型拖入变成其子对象。然后把子对象中的右手位置对象拖入到场景的哥布林。
然后把子对象哥布林的模型删除就行了。记得点击apply到prefab里。
接着我们来改写NPCController让哥布林攻击,包括近战攻击和远程攻击。红框标注的都是此次新加内容。
最后填入飞弹发射点右手和fireball数据类型就好了。
此例的精髓在于把攻击这一行为设置成了SO类型,所有的攻击,远程近战都继承自这一类型。这样做的好处是抽象了代码,减少了很多重复逻辑判断,在项目很大的情况下是必须的,不然就是无尽的判定bug。
所有有关受到攻击接收伤害的脚本AttacktedTakeDamage,它使用IAttackable接口。所有有关对象死亡并摧毁的脚本DestructedDestroyObject脚本继承自接口IDestructable。我们把这两个脚本挂到Player上。
运行游戏后发现player可以正常受到伤害,但player死亡销毁后提示bug找不到引用。原因在于enemy的NPCController一直在调用player.tranform.position,所以我们要修改NPCController让player死亡后所有哥布林回归正常。
Monobehaviours文件夹下新建脚本DestructedEvent,它使用IDestructable接口,包含一个Action用来通知NPCController。
然后修改NPCController
然后把这三个脚本都挂在player上,点击运行可看到player死亡后,哥布林回去原来的位置。
ScriptableObjects文件夹下新建脚本Aoe,它继承自AttackDefinition,和Weapon,Spell一样是一种攻击形式。这里只写了检测半径内敌人,播放aoe攻击效果,造成伤害并跳字。
这里只是一个示例,如果想要造成击飞一圈敌人的效果,就给所有enemy挂上之前写的AttackedForce。
战斗框架的学习完成了,最精髓的是通过定义IAttackable接口进行所有攻击类执行,IDestructable接口进行所有死亡类执行。通过AttackDefinition抽象了所有攻击种类,通过NPCController里判定哥布林到底是远程攻击还是近战攻击来确定攻击。
完毕。