委托与事件
前言今天给大家分享一个游戏开发面试当中基础又高频的问题——委托与事件有什么区别
在这节分享当中,我将从这些方面为大家剖析这些问题
[*]首先回顾一下委托的基本用法
[*]但仅仅了解委托的基本用法是不够的,面试过程中,面试官更多的会问委托与设计模式的关系,熟悉委托和事件基本用法的同学都知道,两者在语法和用法层面是非常相似的,那么这两者究竟有什么区别呢?
[*]我们在设计项目架构时,应当选择使用委托还是使用事件呢?
[*]C#和Unity为了方便我们使用事件,都内置了一些泛型事件类型,那么在开发项目时,我们应当使用C#内置的事件类型还是unity内置的事件类型呢?
[*]在掌握前面四点以后,在面试中可以拿及格分,那么如何达到或者超出面试官对面试者的心理预期呢?
在委托与事件的进阶环节,我将进一步为大家深入讲解委托与事件的本质和进阶用法
版权声明
本文为“优梦创客”原创文章,您可以自由转载,但必须加入完整的版权声明
更多学习资源请加QQ:1517069595或WX:alice17173获取(企业级性能优化/热更新/Shader特效/服务器/商业项目实战/每周直播/一对一指导)
点赞、关注、分享可免费获得配套学习资源
完整视频请点击观看
如何实现攻击武器切换?
首先通过一个实际问题了解委托能帮我们解决什么问题
假设要开发一款游戏,这款游戏中游戏的主角可以使用主武器或者副武器对敌人实施攻击,当使用主武器时,使用主武器的伤害计算公式计算对敌人造成的伤害,当使用副武器时,使用副武器的伤害计算公式计算对敌人造成的伤害
那么问题来了,如何设计代码优雅地只写一行,发出攻击指令的代码就能够针对当前使用的武器,不管当前使用的是主武器还是副武器,计算武器伤害呢?
要实现这个功能,只要使用委托就够了
如何定义变量/如何定义委托
要理解如何定义委托,首先需要了解如何定义变量
[*]大家可以看到在左边定义了一个attack变量,同时还定义了两个常量,分别代表了主武器和副武器的攻击力,在如图所示的Start程序代码段中,当我把attack的值赋值为主武器攻击时,attack就能代表主武器的攻击力,当我复制为副武器攻击时,它就代表副武器的攻击力
类似的,可以定义一个委托
[*]前面讲过委托是一个函数的容器,它可以装得下任何类型的函数,所以现在定义的委托可以装下返回值为void,没有任何参数,大家注意,括号里面是参数表,参数表为空,就是没有任何参数的一种委托,我给它起个名字叫做MyDelegate
[*]于是就定义一个MyDelegate类型的变量,叫做attack,在Start方法里面,当我把attack复制为主武器攻击时,就指向了主武器攻击的函数,那么它就会运用主武器攻击函数里面定义的伤害计算公式,计算对游戏当中敌人造成的伤害
[*]同样的道理,当我把attack复制为副武器攻击时,它就会指向副武器攻击函数,从而用副武器攻击的伤害计算公式,计算对敌人造成的伤害
所以,说委托是函数的容器是有道理的,你可以这样理解,在左边的代码里面,用attack这样的房间容纳了主武器的攻击力数值3,或者副武器的攻击力数值5
在右边的代码里面,用attack这样的代理,它也相当于是一个房间,这个房间里面容纳的不是一个数字,而是一个函数,它要么是主武器攻击的函数,要么是副武器攻击的函数
当然有同学可能会问,代理是一个变量,变量怎么能容纳函数的呢?这个就要从对计算机的理解说起
[*]其实在计算机当中,每个定义的函数,不管是主武器攻击还是副武器攻击,函数内部都有一段程序代码,这个程序代码是存储在内存当中的,就像变量在内存当中有存储地址一样,函数代表的一段指令也是有存储地址的
[*]假设主武器攻击的存储地址是16进制的0xAB,是一个16进制地址,而副武器对应的存储地址是0xCD,这是函数的开始地址,把attack赋值为主武器攻击,实际上就是让attack指向0xAB的地址,而如果我把attack复制为副武器攻击,那么这时实际上是让它去指向0xCD的地址
[*]到这里大家应该能明白,其实attack作为一个函数容器,它存储的就是函数的入口地址
既然定义了attack委托,可以指向主武器或者副武器攻击,那我们如何发出一条攻击指令,针对当前使用的武器进行伤害计算呢?
[*]方法很简单,我们只需要在定义完委托以后,调用这个委托就可以了,大家可以看到在右半边的代码里面,我们在unity的一个Update函数里面,实时的检测当前用户按下的键,如果它按下的是空格键,这时就调用attack委托
[*]如果你的attack委托是指向主武器攻击,它就会调用主武器攻击的伤害计算函数,如果它指向的是副武器攻击,那么它就会调用副武器攻击的伤害计算函数
委托可能为null
在使用委托的时候,新手同学往往会忽视一个问题,委托可能是空值,那么什么情况下它会是空值呢?
[*]比如在前面的程序当中,如果既没有把attack复制为主武器攻击,也没有复制为副武器攻击,假如玩家刚从新手村出生,这时它是赤手空拳的状态,那么它既不会有主武器,也不会有副武器,这时attack的默认值等于null
[*]如果你像前面一样直接调用attack函数,当你一调用这个函数,C#包括unity的控制台就会报程序异常,告诉你空委托是不能进行调用的,所以我们写程序的时候一定要注意,对于一个委托,一定要先判断它是否为空,不为空的情况下才能调用attack
但是如果每一次用委托的时候,都写一个if判断,那这个程序看上去会非常的冗长不优雅,怎么办呢?
[*]我们完全可以用另外一种C#新的语法形式,当我去调用一个委托的时候,如图所示,可以写attack(),这是第一种写法
[*]也可以写attack.Invoke(),如果没有参数,就直接写一对括号,如果在调用函数的时候,有一些参数,要把参数写在这对括号里面,这是正常的调用attack委托的两种方式
[*]前面我们讲过了,要判断attack委托是不是为空,可以在attack和点中间加一个问号,这个问号表达的意思就是if判断,也就是说如果attack是空值,这个问号就判断它是不是为空,如果不为空就会执行Invoke,如果attack为空就跳过执行
所以C#的这个语法糖还是挺有用的,它可以帮助程序更加简洁,请大家也了解一下这个用法,特别是面试一些新手同学的时候,面试官为了考察新手同学是不是经常写代码,有项目的经验,或者是经常阅读代码,就会用这些新的语法糖,语法形式考察同学掌握的知识面和知识的深度
相信大家学到现在应该已经知道,如何使用代理去切换我们的主武器和副武器攻击了,下面是一个完整的项目示例
我们设置了通过输入数字1键,切换到主武器攻击,通过数字2键,切换到副武器攻击,通过按下空格键,释放攻击技能,大致就是这样的实现方式
多播委托
现在我们把问题延伸一下,假设要同时实现主武器和副武器攻击,用委托还能实现吗?
[*]答案是:用传统的委托不可以——但C#在很早很早的标准更新,大概是2.0的时候,对委托进行了改进,就是一个委托能够指向多个函数,我们称这种用法叫做多播委托
[*]通过多播委托,可以在程序代码当中写+=的运算符,这样就可以给attack委托增加它所指向的函数,这时只要把主武器攻击,副武器攻击都添加到attack以后,当我同样发出一个attack调用的时候,就会同时执行主武器和副武器攻击的函数,我们怎么理解这件事情呢?
[*]attack相当于一间房子,这间房子只能住一位客人,现在你可以把attack理解成一栋大楼,这个大楼有很多层楼,每一层楼都可以住一位客人,也就是说attack函数,可以同时存放,同时指向多个函数的入口地址
其实当大家能够回答委托的底层原理,也就是指向函数的入口地址的时候,就已经回答到面试问题的60分了,如何能达到90分的状态呢?
[*]这时大家就需要进一步的深入理解多播委托是如何实现的,这一部分内容会在小白的游戏梦、主程进阶之路当中给大家做系统的讲解,以及在面试模拟环节,帮大家做技术的深挖,使你在面试过程中达到令面试官满意,甚至超过面试官心理预期的状态
委托与设计模式
下面一起看一看委托与设计模式的关系,运用好设计模式不仅可以使代码更优雅,也可以使代码性能更高,并且关于委托与设计模式的问题,也是在游戏开发面试中的重点
当玩家在游戏中死亡时
下面我们从一个具体的问题看起,假设现在正在开发的一款游戏,当游戏玩家死亡时,需要执行图上面所列的这些处理
[*]通知输入系统关闭游戏的输入
[*]通知ui系统更新到死亡的UI界面
[*]通知敌人停止攻击
[*]通知场景重启到死亡状态,或者玩家重生的状态
如果要实现这个需求,程序怎样设计它的效率是比较高的,它的架构是比较优雅的呢?
[*]我们需要先对需求做一个分析,玩家死亡是一个事件,当事件发生的时候,有许多对象要处理这个事件,包括输入对象,UI对象,敌人对象,场景对象,处理的方式对于不同的对象是不一样的
[*]比如对于输入对象,应该关闭输入,对于UI对象,应该更新UI界面,对于敌人对象,应当停止攻击,对于场景对象应该把场景进行重启,每个对象如何能得知这个玩家死亡了呢?这是在处理玩家死亡的时候的要点问题,在这里处理方式有三种
解决方法1:每个模块在Update中检测
第一种方式是在每一个需要接收玩家死亡事件的模块里面,每一帧的更新函数,也就是我们所熟知的unity中Update函数当中,检测当前玩家是不是发生死亡
比如输入模块可以在Update里面检测玩家的血值是否小于零,UI模块,敌人模块,场景模块也可以这么做,但这样做会带来一个问题,就是性能比较低,为什么这样做性能会比较低呢?
[*]因为现在输入模块是四个,这四个模块需要检测血值,如果将来有更多的模块需要检测玩家的血值是否小于零,是否死亡了,那是不是要继续增加玩家死亡的检测语句,要知道每条语句的执行都是有开销的
[*]而且本身Update在游戏当中,如果游戏比较流畅的话,以每秒60次的频率进行执行,它本身每一个模块的开销就比较大,整个检测玩家死亡的性能开销是模块数乘以每个模块里面每秒钟的检测次数
[*]假设一款游戏比较流畅,每秒钟有60次的检测,那这个性能开随着模块增加,性能开销会越来越大,所以第一种方法并不是性能比较好的方式,同时第一种方式会有很多的冗余代码,因为每个模块都需要写检测代码,这些代码是重复复制粘贴的
解决方法2:玩家模块中调用每个模块
那么有没有更好点的办法呢?于是就想到了第二种方法,不是每个模块主动检测玩家的血值,而是玩家模块在玩家死亡以后,调用每个模块的死亡处理函数,这种方式看上去是不是优雅一些
在玩家模块当中Update里面,每一帧都去检测玩家自身的血值是不是小于零,如果小于零,就向输入系统发出玩家死亡的消息,向ui系统,敌人系统,场景系统也发出消息,这样的做法看上去代码会更加优雅一些,并且效率也会更高一些,为什么呢?
[*]原来是每个模块都要有玩家的血值检测,它的模块数量越多,检测次数越多,现在只要集中在一个模块里面进行检测,它的性能也提高了,另外代码看上去也更加简洁优雅
[*]但问题是这样的模块,在设计模式里面是不符合叫做对修改封闭,对增加开放的最基本的原则,也就是所谓的半开半闭原则的
[*]比如现在要增加一个xxx模块,当增加该模块的时候,就需要修改玩家.Update的代码,这样,玩家模块的代码就比较难以维护,需要因为每当增加一个模块,就要增加死亡处理的代码(形如:xxx.OnPlayerDeath())
解决方法3:用观察者模式解决问题
更加优雅的方式是第三种解决方案,在设计模式里面叫做观察者模式,首先介绍一下观察者模式
[*]观察者模式是一种软件的设计模式,它不像解决方案一一样,实时的检测某一个事件有没有发生,而是在事件发生的时候通知你,但它又不像方法二一样,当事件发生的时候,因为模块的增加去修改代码,这就是观察者模式要解决的两个问题,也是解决方案三希望达到的目标
[*]要实现观察者模式,过去我们需要编写很长的程序代码实现这样的模式,但是由于C#引入了委托的机制,相当于帮我们实现了观察者模式很大一部分工作,因此在C#里面实现观察者模式会更加容易,在这里只需要定义一个所谓的事件
[*]这里的事件只是一个概念,并不是C#语法层面的事件,是用delegate委托的形式定义一个OnGameOver的事件,就是玩家死亡的事件,然后定义一个事件委托变量onGameOver,当玩家的血值小于零的时候,通过事件委托引发这个事件
[*]因为前面讲过这个委托有一种形式叫多播委托,所以并不需要像前面的解决方法二一样,为每一个要处理玩家死亡事件的模块调用方法,而是利用多播委托
[*]相当于多播委托是一个容器,这个容器里面已经放了输入模块,UI模块,敌人模块,场景模块的事件处理函数,当invoke的时候,一次性就把这些要调用的这处理函数调用完
[*]当然如果有更多的模块,也不需要在这里增加程序代码,只需要在新增加的模块里面订阅OnGameOver委托就可以了,OnGameOver仍然能够继续调用到新增加模块的事件处理的函数
这就是委托如何帮我们实现观察者模式,优雅的解决前面讲的事件处理的问题
在前面的代码里,我们需要注意一件事情,由于OnGameOver事件是被其他模块所访问的,因此如果像刚才写的代码那样,定义一个delegate,把它定义成OnGameOver类型的对象,这时外部的程序模块是无法访问OnGameOver的
因为委托是一个变量,既然是PlayerHealth类的一个字段,就需要遵守类的规则,变量定义或者字段定义的默认情况下,类的字段是private私有的,外部访问不到,委托也是一样,因此需要把委托声明成public,像右边的代码写的这样,这样外部就能订阅到OnGameOver的事件委托了
外部订阅委托
假设有一个游戏的控制器,它是外部模块,可以在游戏控制器开启的时候,就是所谓OnEnable事件发生的时候,可以去订阅PlayerHealth模块的OnGameOver委托,用+=进行订阅,当游戏结束或者游戏退出,禁用了游戏控制器模块时,可以通过-=符号解除对OnGameOver委托的订阅
这样就实现了完整的观察者模式,在观察者模式当中有几个要素
[*]第一个,需要定义一个事件型的委托,我称之为事件委托
[*]第二个,在程序里面需要引发事件型的委托
[*]第三个,我们还需要有订阅者,订阅者就是GameController,它通过+=订阅OnGameOver事件
所以对于观察者模式事件的定义,事件的发布和事件的订阅,三者缺一不可,如果在面试的时候,有面试官问什么是观察者模式,就能达到观察者模式基本的要点
但是有一个严重的问题!
虽然可以订阅委托,但是有一个严重的问题,在进行团队开发的时候,这个问题尤其严重,委托对象可以用+=进行订阅,想象一下,现在有三个功能模块,ABC三个模块分别是由三个程序员开发
A程序员开发的A模块用+=订阅了onGameOver事件,B程序员也用+=订阅了,但是C程序员,他觉得功能模块里面ongameover的时候处理游戏退出,退出的时候认为游戏已经结束了,于是就写了onGameOver=null,把委托置为空
对于C程序员来说,他觉得这样是没有问题的,因为游戏已经结束了,这个委托不再需要了,但是对于A和B程序员,也许会造成灾难,为什么呢?
[*]也许A和B程序员在游戏结束的时候,他俩开发的两个模块需要在游戏结束的时候做额外的处理
[*]假设C程序员写的C模块优先执行,由于C程序员已经把onGameOver置为空了,再执行A和B写的模块,这时A和B程序员就会发现,他们虽然订阅了onGameOver,但是他们俩写的onGameOver的事件处理程序根本就执行不到,在团队开发的时候会造成一些程序的意外情况发生
解决方法:使用事件!
那怎么去避免这个意外情况发生呢?我们就需要对委托的使用进行约束,怎么约束呢?
[*]因为C#的开发者也发现了这个问题,所以他引入了一个所谓的事件机制,我们就可以用事件机制来解决这个问题,如果没有事件机制,那架构师就需要去手动解决这个问题
所以不管是C#语言层面有没有事件的概念,作为开发者,要知道事件产生的原因是什么,先了解事件为什么能解决这个问题
[*]事件是一种特殊类型的特殊的多播委托,一般来说,它的工作方式与常规的委托是相同的,但是委托可以被其他的脚本调用,而事件(这边说的事件委托就是事件)只能从自己所在的类当中触发
[*]大家看图中的程序代码,比如这个程序代码是前面GameController处理游戏场景的模块,因为只有玩家模块能够能够引发玩家死亡事件,所以GameController应该是不能够引发玩家死亡的,它只能处理玩家死亡
[*]所以在游戏控制GameControl模块里,当我调用玩家的onGameOver事件时,必须要把它从委托改成事件,这时你会得到一个报错的提醒,告诉你the event事件如果把onGameOver定义为事件,只能通过+=或者-=进行订阅,但你不能在PlayerHealth玩家模块之外对它进行调用,这样就保证了事件调用的安全性,这也是使用事件的第一点好处
那我们如何使用事件呢?
事件的定义
比如前面定义的代理类型,写一个onGameOver前面加Delegate关键字,这是代理也是委托的返回值,因为有些术语翻译成委托,有些术语翻译成代理,在这儿就不加区分了
那么如何把代理改成事件呢?
[*]方法很简单,只需要把原来写代理的第二行代码加上event关键字就可以了
事件的本质是什么呢?
[*]事件的本质其实是对代理的包装,它限制了引发代理的范围只能在定义委托的程序模块里引发委托,并且只能在外部模块,比如GameControl模块里面进行订阅,而不能对它进行赋值,如果在这儿写onGameOverr=null是会报错的,所以这是使用事件的第二个好处
但是如果要使用一个事件,就必须要提前定义这个事件的委托类型,什么叫委托呢?
[*]说白了,定义委托实际上就是定义委托的订阅者的规范,比如onGameOver可以有很多订阅者,每个订阅者写的订阅函数都必须是返回值为空,没有参数的函数,如果不是这样,就不符合委托规则,这样在程序的编译阶段,C#编译器会在订阅委托的时候报错
[*]事件也是一样的,如果要针对每个返回值是void,没有参数的委托,起个名字定义委托类型太麻烦了,所以C#当中就给了两种预定义事件的泛型类型
C#中两种预定义的事件泛型
[*]如果是没有返回值的委托,就直接写Action,如果有参数就把参数写在泛型的Action的尖括号(尖括号代表泛型)里,写T1表示第一个参数的类型,T2表示第二个参数类型
[*]同理如果定义的事件泛型,定义的个事件委托类型是有返回值的,那就写Func(表示函数的意思),第一个参数是这个函数的返回值,第二个参数是这个函数的第一个参数,第三个类型是这个函数的第二个参数,以此类推
[*]比如现在同样写OnGameOver函数,如果这个函数在角色死亡的时候要说一句话,可以把要说的这句话在引发事件的时候通过函数的参数传入,同时需要把事件的类型改成能够传参数的类型,那就直接写Action(Sting)就可以了
Unity中预定义的事件类型
同样unity当中也预定义了一些事件类型,比如处理玩家的死亡事件,如果对unity的事件类型比较熟悉,就知道可以定义UnityEvent,在unity的属性面板上面,在PlayerHealth脚本上面可以看到定义的属性叫On Player Death的事件,如果你写的是C#的事件,在属性面板上是看不到的,这就是unity预定义的事件跟C#的区别
并且它还有一个最本质的区别,在面试的时候一定要注意,如果你只说unity定义的事件在属性面板能看到,说明你还没有理解它的本质,为什么在属性面板能看到
[*]因为Unity所有在面板上能看到的属性是可以序列化的,所以On Player Death在UnityEvent最本质的区别是能够序列化,什么叫能够序列化?
[*]就是当你在这儿设置了On Player Death的时间处理函数,比如定义为DisableInput以后,当我把unity关掉,重新启动unity以后,仍然可以看到设置的On Player Death的事件,处理函数还在,就是设置的DisableInput在
[*]而如果在这儿像上面一样定义C#的action,那这时在属性面板上面看不到onGameOver
[*]当unity重启以后就算能看到,赋值的结果也会丢失,那么这个事件是没有指向任何的处理函数的
定义自己的UnityEvent
当然也可以定义自己的UnityEvent,比如定义带有浮点数参数的事件,可以从UnityEvent继承,UnityEvent有好几个版本,其中有带一个参数的泛型版本,可以从UnityEvent,float去继承
这个float是要填充的泛型参数,表示它带有一个参数,这个参数是浮点数类型的,于是就可以根据FloatEvent的类型定义一个变量,这个变量叫onPlayerHurt,它可以在属性面板上看到,可以指定一个参数
onPlayerHurt在图上面是指向UpdateHealthBar函数的,大家注意,因为事件带一个参数,所以当你定义了一个事件处理程序叫做UpdateHealthBar的时候,这个UpdateHealthBar一定要带有一个浮点类型的参数
如果这是一个没有参数的UpdateHealthBar,它是不能作为onPlayerHurt事件的处理函数的
小结
大家能学到这儿,已经达成了面试“委托和事件有什么区别?”这个问题的60%了,也就是可以拿60分了,现在我们总结一下,60分的目标里面究竟包括了委托和事件哪些区别?
[*]首先委托是使用Delegate关键字来进行声明,而事件是使用Event关键字来证明
[*]第二,如果学过C++,那么你就知道前面讲过委托相当于是一栋建筑,这栋建筑里面可以存很多的函数,所以为什么它能存很多函数呢?
[*]因为委托的本质实际上就是函数的指针数组,它在运行时保存了一个或者多个方法的引用
[*]当然具体它是怎么样保存的,这就是你在面试时达成90%,九十分的问题回答,让面试官对你刮目相看,这就需要进一步学习事件和委托的进阶内容
[*]而事件实际上是一种基于委托的通知机制,通俗来说,事件是对委托更加安全的包装,所以委托是独立的,它不依赖于事件,而事件依赖于委托,没有委托就无法创建事件
[*]事件最主要的用途是为了防止用户重置委托,赋值为空,并且防止在委托所在的程序模块的外部进行调用,这就是事件比委托更安全的地方,也是为什么事件是对委托的包装的原因
[*]第三个区别,如果去了解一下委托的底层原理,那你会知道委托是通过Combine()和Remove()方法实现多播的,而事件是通过AddEventHandler()和RemoveEventHandler()添加事件的处理程序
[*]第四,委托可以作为方法参数传递,这是非常重要的一点区别,比如写一个麻将游戏,要比较麻将游戏里面的两张牌的大小,这种比较规则是比较特殊的,于是可以自己把一个方法作为比较规则传入,那这个方法怎么传入呢?
[*]可以定义一个委托,把这个委托指向具体的比较的方法作为排序函数的参数进行传入
[*]而事件是不可以的,事件只能作为类的字段成员,但不能作为方法的参数,所以委托的用途其实比事件更加广泛,事件比委托更加安全
这就是对前面内容的总结
事件与委托进阶
在面试的过程中,当然不希望在面试的时候只能拿零分,所以我们通过前面课程的学习,可以拿到面试中及格的表现
那么我们如何拿到一个进阶的表现,让面试官能够这个刮目相看,甚至能达到一些面试官都不太了解的内容呢?在学习的过程中,如果能注意我讲的这些点,因为一面的面试官可能相对工作经验没有那么丰富,你甚至可以回答一些一面的面试官都不怎么了解的内容
这样对于同学的面试是非常有利的,这其实还牵涉到一些具体的面试技巧,我会在面试模拟环节,帮大家做具体的指导,如果你想拿到关于事件与委托面试90分的回答,那么你需要了解
[*]多播委托的原理,它的底层的技术实现,因为我们在二面的时候往往是由一些项目的组长负责人帮我们面试的,所以在二面的时候,面试官会对技术进行深挖,所以掌握多播委托的原理是非常重要的
[*]我们知道多波委托是一个委托可以指向多个方法,那么我们如何取得所有方法的返回值呢?也许你没有深入的使用过多播委托,甚至都不理解讲的这个问题是什么意思,那在面试的时候对于这个问题就挂掉了,所以这个问题也是我们需要进阶了解的问题
[*]委托除了能够用在一些同步调用的环境下,也可以用在一些异步调用的环境下,那么在异步调用的环境下,我们如何使用委托呢?这是我们的第三个问题
[*]大家知道事件是比委托更加安全的,它是对委托的包装,那么在C#底层在C#内部,它是如何做到只让事件在内部被引发的呢?
关于这四个问题,希望大家能够做深入的思考,也欢迎大家在讨论区留言,我也会给大家一一回复,更进一步的技术讨论大家可以去加文末助教老师alice的联系方式,同时我也会在直播间给大家做进一步的技术分享
另外同学们可能会问如果想达到面试剩下的10%,怎么办呢?
首先需要有项目实践,而不仅仅是背一些面试题,这肯定是不够的,也需要经历完整的面试准备周期,老师会在系统课程,会在面试模拟环节里面帮助大家做一些准备
我这还准备了一些20K+的面试题精讲,帮助想要高薪入行的同学做好技术面试的准备工作,同时也会有一些面向在职开发同学的30K+的面试题精讲,也欢迎大家加文末助教老师的联系方式,了解相关的学习内容
写在最后
更多学习资源请加QQ:1517069595或WX:alice17173获取(企业级性能优化/热更新/Shader特效/服务器/商业项目实战/每周直播/一对一指导)
点赞、关注、分享可免费获得配套学习资源
完整视频请点击观看
页:
[1]