一个开发者的点滴积累
使用工具:UE4 (4.20.3)
使用资源:Dungeon_Areas(付费)、ParagonShinbi(免费)、ParagonSunWukong(免费)
开发耗时:3756分钟
运行流程
打开游戏,先进入游戏开始界面,界面上有两个按钮:游戏开始和退出游戏。
点击游戏开始,加载场景,场景中有两个英雄,我的界面上有3张牌,以及回合结束按钮。
选中牌,然后移动到战场中,召唤一个随从。拖拽自己的英雄到对方的英雄身上,自己的英雄会走过去,攻击,然后退回来。
点击回合结束按钮,然后敌方英雄进行攻击。攻击的时候显示血量减少。然后回合结束。
再由我进行攻击,然后游戏结束,显示结束界面。然后退回到开始界面。
目前开发完成的功能是:
拖拽自己的英雄到对方英雄身上,自己的英雄走过去,攻击,然后退回来。
开发过程与反思
游戏模式
原本没有在意游戏模式,因为几乎所有的教程都只会告诉你游戏模式设置成什么样子,至于游戏模式是用来做什么的都不会提。我也是在机缘巧合之下才会仔细地阅读了一遍游戏模式的文档,虽然没有解决我遇到的问题,但是也让我对游戏模式和游戏状态有了一个很好的了解,这一节就是来介绍游戏模式的。
先来看看UE4文档中时如何介绍游戏模式的:
即便是最开放的游戏也有一些基础规则,这些规则在UE4中被称为游戏模式(Game Mode)。对大多数关卡来说,基础规则包括:
- 当前玩家和旁观者数量,以及允许的最大玩家数和旁观者数。
- 玩家如何进入游戏,这包括选择出生点的规则,以及其他出生/重生行为的规则。
- 游戏能否被暂停,如果可以被暂停,那么如何处理暂停。
- 关卡之间如何切换,游戏是否要从电影模式启动。
你可以看到,这些规则真的很基础,基础到基本不用理会的地步,事实上,我们真的不需要花太多时间在上面。
大多数情况下,我们的游戏需要一个自定义的Game Mode。而这个Game Mode通常是一个继承自Game Mode的蓝图类。
这是我们可以重新定义类的一部分,这个项目里,只需要重新定义两个类就行了:Player Controller Class和Default Pawn Class。没错,就是后面有黄色返回箭头的那两个。UE4真是太人性化了!
Player Controller是用来定义如何操作的类。UE4默认不会显示鼠标,要显示的话就需要定义一个Player Controller类,然后将鼠标控制的相关操作打开。不过说实话,对Game Mode,Player Controller和Default Pawn三个东西还是自己重新定义来的心安。
最后是character。这里的character并没有实际的意义,游戏本身是一个策略游戏,一个上帝视角的游戏,不是只操作一个character的游戏。所以,这里的character是一个空类,占个位罢了。
观察视角
官方文档中指出:如果你的游戏中没有一个Player Start(玩家起始)对象,那么玩家会从坐标(0,0,0)的位置开始。所以,不管需不需要,最好还是在地图中放一个Player Start对象。
不过我在这个项目里就没有放,因为我觉得不需要的东西就不用存在。
游戏需要一个45度俯视的观察角度,所以在场景中加了一个CameraActor。有两种方法设置这个视角,第一种方法是调用Set View Target with Blend函数,将Camera Actor启用。这种方法的特点是非常灵活,如果你需要切换多个摄像机的话可以使用。第二种方法是直接在CameraActor的细节面板上把Auto Activate for Player设置成Player 0,就跟下面的图片一样:
这种方法就是简单,如果不需要切换摄像机,那么这种方法是最合适的。显然,我采用的就是这种方法。
逻辑控制
如何让角色响应鼠标的控制呢?
首先需要明确,有多少的鼠标消息要响应。满打满算只有四个消息:悬停、不悬停、按下、释放。鼠标移动到角色上时,需要明确地提示玩家这个角色是可以操作的,所以,鼠标的样式需要改变。当玩家按下左键,鼠标样式也要改变,示意玩家已经抓住了这个角色。当玩家抓住这个角色的时候,将鼠标移动到其他地方,角色的朝向就会改变。此时,松开鼠标,角色就会移动过去进行攻击。
两个关键点:1、如何判定已经抓住角色?2、如何计算角色的朝向?
1、如何判定已经抓住角色?
鼠标能抓住角色只有角色在原点的时候才有效。所以,角色有一个初始状态,Idle(一开始的时候用一个bool变量IsAtOrigin来判断角色是否处于原点,随着玩家的状态越来越多,发现这种方式非常不适合,于是改成用玩家的状态来区分,简洁大方)。鼠标必须在角色上(通过IsOnCharacter来判断),然后按下左键才能算是抓住(将这种状态保存到变量IsGrabbed中)。具体的逻辑如下图所示:
2、如何计算角色的朝向?
要计算朝向,需要一个前置条件,那就是能否触发。触发的条件有两个,其一:角色必须被抓住;其二:鼠标移动的距离必须大于一定数值(这里是200)。两个条件都满足后,触发这种状态就会被保存到IsTroggleOn变量中。
如果能触发,还需要计算角色的旋转角度。方法是这样,先在鼠标位置与角色位置之间拉一个向量(鼠标位置-角色位置),将这个向量标准化后,计算与角色朝向向量之间的点积,这个点积就是旋转角度的cos值。然后通过acosd函数将其转换成角度,特别要注意的是,输出的角度需要有正负,用来区分角色是从当前朝向往左转(加负值)还是往右转(加正值):
算完角度后,更新角色的朝向就简单了,只需在原来的旋转角度值上加上计算出的角度,像这样:
整个刷新流程就是这样:
角色动画
对于角色动画的实现,真的是走了很多弯路。我是一个做棋牌游戏开发的程序员,脑子里的思维模式是触发式思维。
触发式思维就是如果要产生什么动作,必须有触发这个动作的一次调用
但是3D游戏不一样,3D游戏的场景永远处于刷新之中,所以角色的动画应该是轮询式(每次刷新时都获取当前的状态,来决定需要绘制成什么样)。所以一开始我做的时候对动画的控制都是放在关卡蓝图里,然后直接设置Animation Blueprint。这种方式如果场景中只有一个角色还能应付,多两个就应付不来了。要意识到这两种模式之间的区别真不是一件容易的事情,我的运气真是太好了。
播放角色动画有两个关键点,一是如何切换角色的动画,二是如何更新角色的位置。
如何切换角色动画?
官方推荐做法:使用Animation Blueprint来控制动画。方式是从新建目录中定位到动画->动画蓝图,然后在弹出的窗口中选好目标骨架,这样就创建好了一个动画蓝图(Animation Blueprint)。
在动画蓝图中,最适当的方式应该就是创建一个状态机,让动画蓝图根据当前的状态来播放适当的动画。角色有六种状态,分别是:待机、前进、停止前进、攻击、后退、停止后退。状态切换的顺序也是按照这个顺序,也就是说,角色只能顺序切换这些状态,不能跳状态切换(例如从待机到攻击)。在状态机中的表现就是这样:
切换的条件分别是:
- Idle -> Moving:角色状态变成MovingFwd(前进)
- Moving -> Stop:角色状态变成FwdStop(停止前进)
- Stop -> Attacking:前一个动画剩余时间<=2.5秒
- Attacking -> MovingBack:前一个动画剩余时间<=0.5秒
- MovingBack -> BackStop:角色状态变成BwdStop(停止后退)
- BackStop -> Idle:前一个动画剩余时间<=2.5秒
因为从停下到攻击到返回时一套完整的流程(停止后退到待机状态也是这样),所以不需要根据状态改变来切换,动画放到一定程度就可以到下一阶段了,而且这时候需要将动画的状态同步给角色,因此还需要添加一些动画事件。
动画事件有3个:进入攻击状态、进入返回状态、进入待机状态。这些事件可以在状态上添加,也可以在状态之间的切换操作上添加。下图就是在Attacking上添加的事件:
动画状态的切换需要让角色知道,动画蓝图也需要从角色蓝图中获取状态以便选取适当的动画。前者可以通过定义一个事件调度器(ShinbiAnimationsStatus)然后调用来实现,后者可以在每一帧都会调用的Update事件里去获取角色状态。
如何更新角色位置?
角色位置更新首先需要在前进或者后退状态才会有效。通过计算原点和目标点之间的向量,标准化之后乘上一个速度,再加到当前位置上就非常容易实现:
前进与后退的区别也就是向量的指向不同了而已。
设置角色的位置,调用SetActorLocation即可,另外,当玩家到目标位置或者玩家回到原点之后,需要更新一下玩家的状态,这个操作由Calculate Character Status函数来完成。
项目管理
U盘。资源实在太大了,没法上传github,甚至连码云都没法上传(码云单个文件限制大小为100M)。
当前进展
目前要做的就是角色之间的碰撞(collision)响应。只是我对碰撞的了解甚少,所以需要仔细的阅读碰撞相关文档,要完成相关的碰撞功能,时间估计不会低于500分钟。
总结
想要尽快地实现功能,直接参考已有的项目并不是最快的方法(如果没人解释为什么要这么做的话),最快的方式还是老老实实地阅读文档,对整个功能有一个完整的理解。
参考资料:
官方文档(英文)