这篇文章是记录我自学unity完全从小白阶段到做出一个作品踩过的坑。遇到哪些坑和值得记录的地方就写下来吧。
前期准备:
1.我有一个idea,我要做成一款游戏!那么如何才能做成一款游戏呢?
unity和UE4我都简单接触过,虽然UE4还挺上手的而且蓝图系统和公司的蓝图系统很类似,但UE4竟然要扣10%的销售分成!而且网上看了很多攻略都觉得Unity更适合做手游和小项目,UE4比较适合做大项目,再掐指一算,反正也要学代码,自己写脚本可比蓝图效率高多了,而且unity对其他工具的扩展支持也很广,资料查找也方便些。UE4不知道坑要多多少,其实也不怕坑多,起码别人都走过了给你输了个牌子“此处有坑”还会告诉你怎么走出坑。看不见的路才是最坑的。所以果断选择unity了!
接下来就是了解什么是unity,unity能干什么,unity能做到什么地步,unity应用最广的游戏类型是哪些,unity适合哪些平台,哪些平台目前还不适合。unity要搭配哪些东西使用,哪些是核心且必须的,哪些是高性价比的,哪些是可有可无的,哪些是能极大提升效率的。
坑1:
目前unity还不是很适合开发微信小游戏,微信小游戏用得最多的还是cocosceator。
2.选择好了引擎后,就要开始规划制作整个项目的资源了。虽然是个小项目,但我毕竟是一个完全不会开发的人员,所以学习如何实现,把整个项目从0到1跑起来是最重要也是最基础的一步。
(1)程序方面:C#语言搭建框架编写脚本是必须的。
(2)UI系统公司项目中使用的是FGUI,既然公司已经踩过其他坑了,我也不纠结了,FGUI学习使用也比较简单。
(3)美术方面:美术资源暂时用cube替代,不影响玩法验证。后续可行的解决方案是unity的商店购买相对合适的美术资源,比起外包或者自己画性价比要高一些。
不过特效系统我希望后期有时间了可以自己做,一是特效定制化要求比较高,比较难找到完全合适的,即便买了感觉还行的资源也需要自己手动调一下;二是自己做特效感觉挺有意思;三是特效描述起来相对比较花时间,整理特效需求文档的时间都可以自己做大概的效果了。
光照系统也需要研究一下,光照对于画面表现力的提升非常大,能起到遮瑕凸瑜的效果。
(5)策划方面:虽然自己就是策划,项目体积也不大,但也需要在动工之前把整个项目想清楚,最好有个项目文档来帮助理清思路方便日后查阅。不过整个小工程我已经了然于胸了,不做也罢!此处暂不赘述。
(6)音乐方面:暂时没什么想法,由于这个项目比较简单,只需要一些简单的音效即可。要么网上白嫖要么在资源商店购买。
(7)资源管理:
工作计划表:计划每周末抽4个小时出来做项目工程,平时每天学习1个小时的视频或书籍了解基础知识(事实证明在自律性极差的人面前这种计划极不靠谱)。
学习内容表:unity,c#,FGUI,特效。
计划完成时间:6个月(事实证明在自律性极差的人面前这种计划极不靠谱)。
那就,开始学习吧!
1.了解unity,学习使用unity,如果项目中能使用到unity熟悉起来极快。
课程:siki学院,跟着做2-3个小项目课程(一个打砖块,一个塔防,还有个啥忘了)
学习时长:1个月。
学习结果:大概了解了unity是如何运作的,如果能熟练使用,制作起来还是挺快的。对于独立完成一款游戏有了一些信心。
2.学习C#。
学习教材:C#入门经典(第七版),C#语言入门详解(bilibili-刘铁锰),大话设计模式,C#本质论,Unity3D脚本编程
学习时长:C#入门经典--2周
C#语言入门详解--1个月
其余几本还没开始看
学习心得(此处过于唠叨,可跳过不看):我先是看完siki的教程后就开始动手做项目了,然后在编写脚本的过程中发现对C#太陌生了,只会一些简单的if else之类的逻辑语法,对于什么是类,类有几个成员,参数传递这些东西概念和用法十分模糊。虽然大家都说C#很简单,但对我这样的程序小白来说,还是太南了!
然后我请教了一下大佬之后停下项目开始看C#入门经典,把第一部分C#语言看完了,说实话看到正则表达式的时候就感觉很吃力了(后来发现其实前面委托也没看太懂- -)。感觉差不多够用了我又继续开始做项目,做到某个部分,又感觉到自己的基础非常不牢固,已经到了无从下手的地步。于是我又停下项目,浏览了下bilibili上的C#课程,然后发现刘铁锰老师的C#语言入门详解非常适合我!先从原理、定义、使用环境讲起,而不是一上手就讲这个是怎么用的。
中间由于公司项目进度非常紧张,基本9106的作息,所以停止了半年...
拖到过年由于疫情原因,我终于把timiLiu的教程看完了!!但是由于中间拉的时间太长而且前面讲的我都懂,后面讲的我比较赶进度,都没跟着练习!实际上只是有了一个印象而已,没达到理解的地步!后来很多时候我都是用到了某个东西又看着视频一遍遍练...
看完timi的视频后信心大增,翻出大半年前的项目开始撸 。然后发现关于unity脚本编程的知识又忘得差不多了- -,不过还好,脚本都看得懂,慢慢也就想起是怎么回事了。这时候看着自己之前写的代码乱得像一坨屎一样,好像草翻重做。不过朋友阻止了我,他说你这时候优化没什么意义,过不了多久就会发现自己现在重构的代码也是一坨屎。先实现版本最重要。我想想也是,我现在C#的功底这么差,肯定还有很大的进步空间,没必要现在就做优化。
在接下来撸代码的过程中发现对于一个小白来说太多坑了!我决定都记录下来,即可作为经验,也是日后复习梳理的宝贵资料。
学习结果:对C#半懂不懂,但至少可以自己撸代码了TAT
3.FGUI、特效和其他内容,还没开始(以后填)。
这个游戏想要做成啥样?
以下是核心逻辑:
1.期望PC和手机端能同时进行
2.两边人数相等,蓝方在上,红方在下
3.玩家只可左右移动,不可上下移动
4.在移动过程中玩家会蓄力,当停止移动时会根据蓄力多少进行攻击
5.受到攻击会后退,退出边界即离场。如果一方的全部玩家离场,即失败
那就,开始动手制作吧!
1.基础布局。
先实现战斗场景中的所有元素:地板,光照,三个小圆球代表友军,三个小圆球达标敌军。这一步很简单,利用unity现有的资源拖拽即可。
2.视角。
确定游戏视角:俯视。我是打算做成2D游戏的,但仍在3D场景中建模,考虑到美术表现,光照,后期可能增加额外的视角等原因,仍创建的3D工程。其实3转2很简单啦,Y轴永远是0就可以了
3.移动操作。
实现操作代表自己的小圆球的的移动。这一步开始就要用到脚本了,创建了第一个Player的脚本!一边看siki的教程一边自己动手。然后遇到了第一个问题,输入指令怎么玩儿?我在Update里用Input方法判断玩家是否进行了输入,but又遇到了问题,我是打算在PC和手机端都能游戏,这里两端的输入指令不一样,怎么弄呢?不过暂时不纠结这个,毕竟我还是在PC端调试。所以先实现了PC端的输入指令逻辑。
然后又遇到问题:判断输入指令的方法有很多种,用哪一种呢?unity本身自带Input接口
可以用下面这种方式实现输入操作小圆球移动的逻辑
但这种方式和我的需求不符,我要的是按下方向键,小圆球立刻马上给我朝对应的方向移动,我在Input的参数设置里调了许久也没调出这种立刻马上就能变向的体验。所以我换了个方法。自己写逻辑吧!先判断是否按下了对应的按键
然后在Update()里调用IsInput(),如果有输入的话,进行移动。
这里得Move()就直接调用这个方法了!当然,这里免不了先在Update()里写得一团乱麻,然后把方法一个个抽离出来。
实现移动还是花了大半天时间,真正在运行模式下能移动的时候心里贼开心!
4.子弹系统
可以移动了,接下来就该干点男人该干的事了!发射子弹!
逻辑需求:如果玩家在移动过程中停止移动了,就发射一颗子弹。
首先开始创建了一个红色的小圆球代表子弹,做成了prefab。此处开始意识到文件分类的问题了,创建了几个子目录来分别存放prefab,材质等。(Resources是后面用到的,之前并没有这个目录分类)
子弹自然应该有个属于自己的脚本,创建了一个Bullet的脚本先挂在子弹的prefab上,暂时不处理。
那么问题来了,玩家停止移动的时候怎么创建这个子弹对象呢?这里我还扣了好久的脑袋
当然了,最开始的代码不是这样的,这个Attack()的方法一开始也是放在Update()的判断里的..这里踩了个坑,不知道怎么创建出子弹的prefab,搞了好久,最后发现原来在unity的Inspector窗口直接拖上去就可以了...
不过现在我换了个方式,我觉得把对象一个一个拖上去挺麻烦的,后面如果我有100种子弹岂不是要拖100遍?而且Inspector窗口也被污染了!我就想能不能直接找到这个资源读取啊?(我进步了!)面向百度查了一下,发现可以用Resources.Load()方法读取!说起这个Resource文件夹,在公司的时候我还纳闷为什么程序给策划调用的资源路径都配置在Resources路径下,当时我还以为只是资源结构设计如此。现在才发现原来这个文件夹还有特殊的用途!(奇怪的知识又提升了!)
当然,这还不是最舒服的配置方法,后续扩展维护也不方便,不过对于目前实现功能绰绰有余了。
能创建子弹后,子弹总得干点什么吧,不然宁还配叫子弹么?
把蓄力值传递给子弹!让子弹飞一会儿!子弹怎么飞?先直线飞吧!子弹飞多久?子弹接触到人还飞吗?子弹时间到了后怎么表现?子弹打到人怎么扣血?子弹消失时候的爆炸效果怎么表现?子弹属于谁的?子弹的伤害值怎么算?……求豆麻袋!我捋一捋= =
一个一个来,既然是直线飞行,那就很简单了,移动的逻辑一句话就写完了。
如果子弹的计时器超过了自身的endTime呢,就执行Die()的逻辑。
but这个endTime是怎么传过来的,是我遇到的第一个感觉到自己基础太差的地方...也就是如何传参的问题。实际上方法如下图就可以了
但是这短短一句话非常的知识点其实非常多!!首先是GetComponent<>()方法,功能其实很简单,获取对象身上的脚本。但学完timi的教程后又心生疑惑,不对啊?这个脚本是个什么东西?看他的结构明明就是一个类嘛!我为什么能通过一个对象获取一个类?Unity里的实例对象又是什么?到底是一个个实例还是一个个类?陷入沉思...
于是我又梳理了一下gamObject和脚本的关系,梳理完了之后发现这和GetComponent怎么取到另一个脚本的半毛钱关系都没有啊喂!(虽然结构更加清晰了)
暂时不管了,此处花了大概三天的时间才搞清楚怎么传递参数的(其实我觉得还是八太行,不过能用暂时就这样吧)
既然子弹能飞行了,那么怎么判定子弹是否撞到了敌人呢?
又经过漫长的思考后,发现原来unity的组件里已经准备好了碰撞器供检测使用了
当然了,这里查询碰撞器,trigger碰撞器,刚体之间的关系又花了许久……(而且这玩意儿没有理解深入的话隔几天就忘了)
有了碰撞器后呢,就可以用下面这段方法啦
other是谁?自然就是你碰到的那个对象,然后用tag来检测一下是否是“Enemy”。这里顺便就给红蓝双方打上了标签,Player和Enemy。
拿到碰撞的对象之后,这里的逻辑是对象受到多少伤害就击退多远。这里怎么控制碰撞对象的移动呢?由于没用物理引擎,实际上要控制对象的移动还是通过脚本Enemy来实现的。
计算呢,先瞎瘠薄写一个放着吧
对方已经受到一万点暴击了,子弹的使命也就完成了,接下来就该把他销毁掉
先播个自己做的爆炸特效,然后执行销毁特效和物体的方法。Destroy很贴心的给了延迟参数,我总不能自己手动等待0.5s再销毁吧
这样下来,子弹的功能暂时也就完成了!跑了一下,没有问题
5.战斗初始化。
目前我已经可以操作自己的小圆球移动并攻击敌人了,接下来先考虑下初始化创建的问题。我们可以自己选择人数,是1v1,2v2,还是3v3。先创建个GameManager的脚本挂在一个空物体上,这样只要加载这个场景后初始化场景中的物体及组件后,就会自动运行GameManager这个脚本了。
目前还是一个很粗糙的版本,技能点不够,还不知道这儿咋处理。先实现手动拖6个物体上去代表6个小人儿吧!
然后用一种非常丑陋的方法实现不同人数下创建出来的prefab实例
这里也踩了一个挺大的坑,switch的语法就不说了,熟能生巧。在一口气儿创建多个对象时候的一一应对的逻辑关系上想了好久,做一步错一步,慢慢debug才调试好的。新手debug绝对是一件非常崩溃的事情,信心值唰唰的往下掉。我是不是不行了,这好难啊,错在哪里了,一脸懵逼...TAT
熟练掌握debug和断点技术是非常重要的事情!
刚才那一步只是创建出来了这些对象,还没给他们身上挂的脚本进行初始化赋值呢。写到这里的时候,前后已经跨越大半年了。我稍加思索,既然要初始化赋值,那就在构造函数里赋值!这样创建对象的时候不是就可以传参了吗!我真skr天才!
在脚本里写完构造函数后,在这个类里实例化了Player后,总感觉哪里不对...调试的时候我才发现,实例化了Player又不代表创建了gameObject啊!实例个鬼啊!根本就不是一个东西啊,MonoBehaviour明确写了派生的类不允许自己写构造函数啊!摔!
没办法, 老老实实的挨个给所有对象赋值
一坑未平一坑又起,GerComponent<>()的泛型参数里我一开始写的是Player,死活报错。又是一顿查,才发现有些友军对象上没有Player脚本,所以报错了。由于之前已经把玩家、友军、敌人这三类都抽象出来写了个基类Contestant,询问了下大神,这里也是可以直接拿基类进行获取的。
这样子,初始化的工作算是暂告一段落。
6.武器系统
武器!是武器!我在打砖块中加了武器系统!
武器系统应该算是这个小项目的趣味点之一,通过改变武器来进攻的手段。
最开始我想的是所有武器都在一个数组中,需要用哪个就取哪个。于是有了这样一段代码
后来让我扣脑壳的是我在创建子弹的时候,由于我把创建子弹的方法也写在了武器类里,导致每次都要去取当前是什么武器,然后还要创建一个子弹的数组,再去取对应的子弹是什么。更坑爹的是,如果2个武器用的是同一个子弹prefab,我仍需要在子弹的数组里填2个一模一样的prefab...
再往深了想,每个武器的攻击方式都不一样啊,如何才能在武器类里用不同的攻击方法对应不同的武器呢...
19年由于业务繁忙到此就暂停开发了,中间看了下课程,重新理解了虚类,基类和继承。回看这段代码的时候我才意识到,由于每个武器都有自己独一无二的攻击方法,所以每个武器都应该是一个类,继承自武器这个基类。子弹是武器的一个属性,每个武器单独重写这个属性就可以把武器和子弹绑定起来了。
下面就是继承自武器类的Pistol武器,暂时只重写了创建子弹这一个方法。
好了,有了武器的子类,那么我怎么知道要调用的是它呢?显然我们需要在玩家的小圆球上挂上这样一个脚本。在Start()方法里默认挂上Pistol这个组件。后续如果更换了武器,再删除这个组件,添加新的组件即可。
在判定攻击的时候,去查找Weapon基类。然后调用Weapon里的Attack()方法,由于CreatBullet()已经被子类重写,这样就会调用到子类的CreatBullet()。
这样可行吗?打个断点调试看看走的是哪。
走到了Weapon的Attack()方法。
下一行跳到了子类的CreatBullet()方法。说明是没问题的。
到此武器系统暂告一段落(当然远远不止= =)。
7.怪物AI
来了!它来了!无数策划和程序的噩梦!
一说起AI整得还挺高大上的,但这里的需求暂时只是让对方的小圆球也能左右移动和攻击而已...由于没有寻路需求,也不需要用到其他插件(有也不会用=w=)。所以自己写一个吧!
功能需求:判断当前离自己最近的玩家,并向其移动。如果X轴偏差小于0.1就停止移动并攻击。
看起来好像不是很难的亚子。
Emmm,第一个问题是,怪物AI的脚本应该是单独的一个类呢,还是继承自Enemy呢,还是一个接口呢....单独一个类吧,处处又要用到Enemy的字段,继承吧,又感觉怪怪的,接口吧,好像也不对劲...(我还没想很清楚,欢迎大佬指导)
不过实现功能要紧,先暂定他是Enemy的一个子类吧,于是先写了这些字段
然后初始化了必要的一些信息。话说回来,多个怪物的AI都初始化同样的信息,感觉效率有点浪费,应该可以抽出来才对,后续可以优化。
然后呢,每帧检测对手的位置并调用移动或攻击的方法。
我这里之所以用继承Enemy,就是因为玩家,队友AI,敌人AI的行为模式是一样的,所以我可以把方法抽象出来放在基类Contestant里。值得注意的是,这里Mathf.Sign()返回值只有1和-1,如果参数为0时,返回值也是1.和C#System.Math.Sign()不太一样。
然后去调用获取和目标的偏移量,由于需要知道自己要不要动,所以是个bool类型的返回值,而偏移量用out参数来返回了。在这里遇到一个大坑,之前错误的把xDvalue的返回值定义为了目标需要移动向x的点,也就是xPoint,而不是x的偏差值。所以out参数返回的值是xD...导致目标一移动就一直超一个方向停不下来。。打断点调了老半天才发现代码逻辑没问题,是参数的意义定义错了...
判断目标是否可以移动。
好了,之前的复盘差不多就到这儿了,之后会新开文章继续更新开发中遇到的坑坑洼洼。
最后演示一下现在的效果。
好了,相信你也看到里面的各种bug了- -后续我会继续修(sheng)复(chan)bug的。