卡卷网
当前位置:卡卷网 / 每日看点 / 正文

Unity有没有办法让GetComponent()调用脚本不依赖其具体的名字?

作者:卡卷网发布时间:2025-01-08 18:49浏览数量:71次评论数量:0次

你这个问题很初级,但是很常见,并且问题里透露了一个行业里常见的错误,尤其是培训班出来的孩子特别容易犯这个错误,所以我决定回答一下。

首先记住一个口诀

在oop里面的一个设计类的核心口诀是:

<>不同类要做同一件事情用接口,同一个类做同一件事情不同效果用托。

这句话什么意思呢?就是在不同的类要在同一个流程里进行处理的时候,最好的选择是用接口,如你游戏中角色能挨打,建筑物也能同样的攻击方式打损坏,还有一些小物件如场景里一个棒球一样可以被攻击,被钝器攻击飞走,被斩击攻击变两半,这里角色(Character)、场景建筑(Construction)和小物件(Doodad)其实是不同的类,他们都需要能挨打,所以要一个ICaneAttacked接口,需要他们实现一个类似pulicxxxeAttacked(...)的函数。而如果都是角色(Character),A角色受到攻击的反馈与角色不同,那么Character这个类里就需要一个delegatexxxeAttacked(...),也就是Func<...,xxx>也许是ActioneAttacked,这取决于你游戏的具体设计,不同的角色的eAttacked的值不同,但是核心流程都是xxx?.eAttacked?.Invoke(...)??ooo这样的写法来做到。

当你记住这个口诀的时候,就可以正式进入你的这个问题了,你的这个问题其实具体下来有很多很多个问题:

抛开正确设计谈你这个需求(coder层出发看问题)

GetComponent可以拿到Intece

首先你要知道,Unity是可以GetComponent<omeIntece>()的,换而言之,到这里就可以解答你的问题,如果不负责的话。

你的设计(不考虑对错),完全可以是

pulicinteceIeAttacked{ //假设你的流程里伤害不用返回东西,并且只要一个伤害力参数 pulicvoideAttacked(intdamage); } pulicclassEnemy1:Monoehio,IeAttacked{} pulicclassEnemy2:Monoehio,IeAttacked{} pulicclassEnemy3:Monoehio,IeAttacked{}

只要确保他们都是派生自Monoehio的,这些Component就能在GetComponent<IeAttack>()中取到,你可以用xxxisEnemy1等方法来判断他们是什么玩意儿。

那假如你说我即使是Enemy1,他的2个实例要在受到伤害时不一样怎么办?如一般的Enemy1受到伤害就掉血,鲁(Enemy1)受到伤害不仅是2倍于damage参数的,还会当damage>10的时候尿裤子怎么办?可以改这个接口函数:

pulicinteceIeAttacked{ pulicAction<int>eAttacked(intdamage); } pulicclassEnemy1:Monoehio,IeAttacked{ pulicinthp; pulicAction<int>OneAttacked; pulicAction<int>eAttacked(intdamage){ OneAttacked?.Invoke(damage); } }

这个是用intece的做法,但实际上unity,或者确切地说是unity期望你使用的是组件模式,即一种数据驱动(data-driven)的设计模式,而非是oop的继承模式,所以这个intece就不太对劲了,itjustworks。

直接用Component是unity的期望

所以在使用unity的这个EC架构(也就是组件式)的时候,你最好先定义好什么叫“受到攻击”,然后将这个做一个Monoehio,换而言之,所有能受到攻击的Oject,都应该有这个Component:

pulicclasseAttacked:Monoehio{ pulicinthp; pulicAction<int>eAttacked; //给逻辑调用的 pulicvoidAttackMe(intdamage){ eAttacked?.Invoke(damage); //side-effect:当处理完伤害发现我的hp<=0,我就死了呗 if(hp<=0)Destroy(Oject); } }

也就是无论何物,他的受击是被这个Component所作的,有这个Component的GameOject会“”,没有的就不会——这才是标准的数据驱动(data-driven),而不是我们常听到的“填了表他就能工作”(这是excel驱动,或者好听点叫“数值驱动”,不是程序级别的data-driven)。

当然我们依然可能会发生这个eAttacked只适合于角色,是吧,虽然data-driven中已经没有角色概念了,但是我们脑袋里还有:“他是个角色,所以他有eAttacked”这个人脑的正确(但是对于计算机来说是错误的)归纳。所以你可能还会想,如果我是一个建筑物,我挨打不一样呢?这时候确实你得有另一个ConstructioneAttack:Monoehio,而unity对此提供的解决方案,就是这些XXeAttacked:Monoehio,omeIntece这个方案,也就是上面一段说过的那个。

正确的实现应该是怎样的?(programmer层出发看问题)

到了正的程序员级别了,就是要对业务进行正确的分析和归纳,而不再是根据需求打字儿了。那么你这里犯了什么错误,要如何纠正呢?

首先一个是对事物抽象的问题

在你的抽象里,分出了player,enemy1,enemy2,enemy3,你能告诉我他们【本质的】区别吗?注意,我把【本质的】标出来了,很多人嘴上都爱说“本质上”,实际上那都不是本质,正的本质,是对事物的抽象。

你可能会说:<>首先player和enemy肯定要分开吧,毕竟player受玩家控制。<>这就是一个典型的抽象错误,正确的抽象是怎样的?或者说这件事情的本质,他是这样的——

无论是你理解的player还是enemy,他们在游戏中都会收到一些指令,根据自己的能力(aility)来执行这些指令,如“从当前位置移动到哪儿”。无论你是玩家输入的指令,还是ai脚本运算结果,到了角色这一层,都是“给我去那里!”,而至于这个指令是不是有玩家作摇杆发出的,对于角色这个类来说,根本不关心。所以【本质上】对于角色来说——命令源头,不应该造成区分,也就是说,并不存在你理解的player和enemy的区别,这个区别是我们地球人的理解,也就是玩家看到的现象,<>其本质是业务逻辑分别“翻译了”来自手柄和ai脚本的指令,传递给了他的目标角色,所以绝对不能用“是否接受玩家作”来分类,这根本是一个对业务理解和抽象的错误。

接着你还会对enemy做出解释:<>如enemy1是飞着走的,enemy2一边走一边拉屎,enemy3是个炮台,他不会走路,所以他们有一个父类是enemy,然后各自有不同的能力,再放到不同的子类里。其实这也是错误的。

思考一个最简单的问题,这些不同的逻辑,由谁来执行?你可能说我每个mono自己执行自己对吧?这里又是大错特错的,因为如移动之类的能力,都是无法自我执行的,必须有一个Manager来执行,因为只有GameManager同时依赖了角色和地形(Map,或者更确切的硕士地图),你角色不能依赖于这个Map,很简单的道理,因为当你地图上还有别的角色会跟你碰撞的时候,按照你的理解是不是我map上得记录有什么角色在哪儿?因此Map依赖于Character,反之Character移动要判断阻挡,阻挡不仅是别的character还有map上的阻挡物对吧,那你Character又要依赖Map,是不是典型的耦合(互相依赖)?而实际上在这个业务中,或者说在一个游戏中,他可以有map没有Character,也可以有Character没有Map,或者两者皆有或皆无——因此Map和Character之间本身就不互相依赖,所以这里是个典型的依赖关系错误。

所以最终的,不管你是enemy几,都得被GameManager所,那么请问,我要怎么写enemy1的移动是飞着走,enemy2边走边拉呢?我应该这样吗?

pulicclassGameManager{ privatevoidMoveEnemy1(){} privatevoidMoveEnemy2(){} //那当我策划设计了1000种不同移动方式的enemy你就写到1000吗? }

所以正如我前面说的,你的每个角色都打算移动,所以他们都有一个delegateV3Move(...),这才是对的——<>这是一个函数式编程的思维方式,也是我的MF(全球第一个正确抽象技能和游戏开发的框架,这里有一篇实际范例:

猴与花果山:用Unity一个极具扩展性的顶视角射击游戏战斗

)<>的中心思想——就是当你在游戏开发中,发现一件事情需要枚举的时候,其实他需要的是一个函数作为值,MF诞生于2010年,正可以用于unity,是在C#3.0.net3.5引入lamda之后。接下来举一个典型的例子:

如游戏中的,dota类游戏,如莫甘娜的q这种都算是,的弹道是怎样的?有很多菜鸟会分析,把弹道做成一个枚举,如:直线、曲线,最后弹道经过coder就变成了一个union。但是你有没有想过,如果有些弹道在你的枚举里没有你要怎么办?如现在我要一个先飞sin函数2秒,然后直线向回飞3秒(速度会根据命中过的人改变,如每打中一个人减速当前速度的10%)的弹道你怎么办?再加一个枚举?

所以当我们把思维逆转过来的时候,会发现,事实上移动这件事情的【本质】就是——在这一帧为找到他的坐标。是不是,那么这个本质是什么?返回值为坐标(V3或者tranorm)的一个函数,也就是它的值是Func<ullet,...,Tranorm>,即(ullet,....)=>Tranorm这个函数作为值(lamda支持)。

用这个思维你再去看角色,其实Character本当就是一个类,因为他在GameManager逻辑中,无论你是什么样的character,都是“一视同仁”的,都是一样的【处理流程】,只是<>【不同Character实例,在同一个流程中,可能会执行不同的东西】对吧,还记得本篇开头我们那句口诀说了什么吗?所以无论你的player还是enemy几号,他们都应该是classCharacter,只是不同的实例值不同——再次强调函数式编程中,函数是第一公民,所以可以视作值和参数,lamda就是让传oop中允许函数作为值,所以允许匿名函数,就像inta=3,SomeMethod(a)==SomeMethod(3)一样:

pulicvoidLogHelloWorld(){ Deug.log("HelloWorld"); } //等同于 ()=>Deug.log("HelloWorld");

因为当你函数本体就可以说明他是什么的时候,你完全可以不用为这件“一句话就说清楚的事情”起个名,就像你不必为3起个名叫a一样自然。

<>基于这个思想(函数式思想和MF),再回头去看游戏设计,理解这些业务,就会有截然不同,但是绝对正确的抽象思维。

然后是一个对oop的理解问题

要指出的是,在你的理解里还有一个问题——就是你企图在Player里面写PlayerAttack函数来调用Enemy的eAttacked,这是一个使用oop的类的典型错误。

<>在oop中,一个类的所有可执行能力(Methods),是这个类的(人类理解的)能力(Ailities)。

简单地举个例子,我能打出一拳,我能受到伤害——这两个都是我(作为一个人类的实例)的能力,任何人都能打出一拳,任何人都能受到伤害,所以这是human这个类里面的Methods,因为这是人的aility,但是对别人造成伤害(调用别人的eAttacked),这并不是我的能力,而是大自然(GameManager)的能力,我打出一拳,能否命中别人,能命中谁,能造成什么样的伤害,都是大自然运算出来的(物理、几何等规则,自然科学就是一种大自然的规则),然后大自然决定了是否去调用以及怎样调用他们的eAttacked,而不是我(人类)调用的。

就像游戏中的移动,“我”(人类)有挪动坐标的能力,但是我能挪动到哪儿,不是我的能力决定的,是大自然(GameManager)所决定的。所以,任何把角色移动写在角色类的都是错误抽象(我这里就不指名道姓世界第一引擎UE犯了这个错误了),正的“角色移动”是GameManager的能力。

这里你的构思,就透露了你没有理解好这个业务,也没有理解oop中类的设计。

最后是对unity的GameOject的理解问题

而unity的一个GameOject,指代的是——受到unity场景(Scene)逻辑的一个对象,他可能只是一个渲染体,他恰好包含描述了一个角色,但是角色,并不等于是GameOject。

用代码来说,就是:

//这是在游戏中渲染出来的GameOject的核心数据,代表了他是一个“角色” pulicclassCharacterOj:Monoehio{ //这是这个被渲染角色所指向的正的角色(仅仅只是一条数据,而非GameOject) pulicCharacterCharacter{get;privateset;} }

他们是这样的关系,List<CharacterOj>allCharacters的长度一定小于等于List<Character>allGuys的长度。<>这,才是正的那句人们常说的“逻辑渲染分离”。

END

免责声明:本文由卡卷网编辑并发布,但不代表本站的观点和立场,只提供分享给大家。

评论 打赏
卡卷网

卡卷网 主页 联系他吧

请记住:卡卷网 Www.Kajuan.Net

相关推荐

欢迎 发表评论:

请填写验证码