前言&游戏介绍
游戏为本人毕业设计,功能实现较为简陋,这里只是简要描述下开发思路,不包含深入的Gameplay框架分析,项目可无缝升级至UE4.26,素材全部来源于虚幻商城与互联网。完整项目下载,提取码:demo | 视频演示
游戏主体逻辑采用蓝图系统实现,支持多平台运行,包含存档功能,只可在特定位置购买和升级防御塔。游戏共有5波敌人,包括最终的BOSS关卡。在击败BOSS通关后即可进入无尽模式,此时游戏难度会不断提高,直到游戏结束。
文中大部分内容摘自毕业论文,若出现错误,欢迎评论区指出
总体框架概述
框架图
UI
开始菜单
游戏开始时会首先检测本地有无存档,如果未检测到存档,则继续游戏按钮会被禁用,仅可选择开始新游戏或者退出。
信息显示菜单
屏幕左上角为玩家的HP与当前金币,右下角为波次显示,中间为暂停按钮。当敌人到达终点时,扣除一定的HP,当击败敌人时,增加一定的金币,UI会实时更新信息。点击暂停按钮或者按下键盘的ESC键游戏会暂停且弹出暂停菜单。
暂停菜单
点击“返回游戏”即可回到游戏进程,若想退出游戏点击“返回主菜单”后退出即可。
暂停菜单拥有一层背景模糊,会模糊游戏场景用于突出选项按钮。
通关菜单
当达成通关条件后,弹出通关菜单,此时游戏进程暂停,可选择继续无尽模式,或者重新开始新游戏或者返回主菜单。
失败菜单
当HP归0时,游戏判定失败,弹出失败菜单。此时只能选择重新开始或者返回主菜单。不过值得注意的是,因为游戏拥有存档机制,此时返回主菜单选择继续游戏,可从失败前的波次继续进度。如果选择重新开始,则游戏会删除旧存档。
防御塔管理菜单
此菜单仅在点击防御塔时弹出,升级消耗的金币数与出售获得的金币数与防御塔等级有关,防御塔满级后升级按钮会消失并提示已满级,若未购买防御塔则出售按钮禁用无法点击。
防御塔
游戏中的防御塔最高5级,每一级对应不同的属性,炮塔和基座模型分开设置,升级会改变炮塔模型但基座模型不变,不同等级对应的炮口位置也不相同,保证每一级防御塔的子弹都是正确的从炮口射出。
塔基座底部还有不同颜色光环,当鼠标移到炮塔上时显示炮塔的攻击范围,鼠标移开后消失。单击炮塔会弹出管理菜单,通过菜单可以完成炮塔升级或出售。
敌人
当敌人从出生点刷新后会立即寻找下一个路径点并移动过去,当到达目标路径点后再次搜索下一个路径点,直到终点为止。
通过派生敌人基类,可以得到多个敌人子类,代表不同敌人,BOSS也是通过派生敌人基类而来,相比普通敌人更强力。
存档系统
每当波次更新时游戏会自动创建存档,此时退出游戏后重新进入可从存档处进度开始。当选择继续游戏后,会首先读取本地存档数据,然后按照存档数据初始化进度。当选择重新开始游戏时,会删除本地已有存档。
游戏模式
不同波次的敌人刷新有时间间隔,且只有上一波敌人全被消灭后,下一波才会刷新。
游戏第5波最后会刷新一只体积巨大的BOSS,击败BOSS后方可通关,达成通关条件后才可进入到无尽模式。
在无尽模式下,右下角波次提示会多出无尽
两字,此时敌人会循环刷新,每次刷新的敌人的HP都会比上一轮高10%。不管是普通模式还是无尽模式,每刷新一轮敌人时,本地存档都会更新,无需手动存档。
地图设计
本游戏地图与传统塔防游戏设计一致。每张地图敌人前进路径固定,地图中包含敌人的出生点
、路径点
、终点
。其中终点为一个触发盒子,敌人到达终点后对玩家造成伤害且销毁自身。
下图为地图设计。其中红圈为出生点
,黄菱形为路径点
,蓝色箭头为敌人移动路径,紫色圆角矩形为终点
,白色圆圈为防御塔放置位置,玩家仅可在固定的位置建造防御塔。
功能实现思路
场景搭建
放置光源
新建空项目后场景中没有光照,是纯黑的。打开引擎的放置Actor
菜单,点击左侧菜单中的光源选项,向场景中拖入一个天光
,此光照不产生阴影仅作为背景光。之后添加定向光源
,此光照会以一个特定角度照射,可对场景的对象投射阴影。
为方便项目后期编辑,天光与定向光源的移动性
均设为可移动
,否则每次场景中对象变动时都要重新编译一次光照,下图光源设置
地形绘制
打开UE4.26引擎,点击上方工具栏模式
按钮,选择地形模式。在地形管理菜单中选择新建
即可进入地形创建界面,选择基础材质并将分段设置为[2x2]
,组件数量选择16x16
,最后点击创建按钮即可新建地形。若后期地形大小不合适,还可以通过地形选择模式新增或缩减地形区块。
为了增添细节,可进入地图绘制
模式,可以添加多个Layers
来绘制草坪与其他地形,增强场景画面表现力。
放置静态网格体
为了增加场景细节,可在场景中放置一系列的模型,如本游戏中的石板路实际上由许多静态网格体
组成,将多个石子actor合并为一整个静态网格体,就可以很方便的拖拽到场景中组成石板路,下图为合并后的石板路
绘制植被
如果场景中植被全部手动放置,会非常繁琐且不利于密度管理,UE提供了植被绘制系统,可以简便高效的管理场景中的植被。
点击引擎上方工具栏模式
按钮,选择植物
模式,左侧窗口即可弹出植被管理菜单。此时点击添加植物类型
按钮,在新建资产中选择新建静态网格体植物
,选择资产保存路径后即可创建成功,选择项目中植物的静态网格体即可添加一种植被类型。
添加完成后,在植被管理菜单即可看到已添加的植被类型,左键选中设置密度
等属性,之后右键激活,即可在地形上任意绘制植被,按下shift可移除植被,下图为植被绘制菜单
放置敌人出生点、路径点、终点
在本项目中,出生点、路径点、终点均为Actor蓝图类,首先在资产目录下新建三个Actor类,分别命名为敌人出生点
、敌人路径点
、终点
。将出生点
和终点
放置在地图首尾,在石板路每个拐角处都放置一个敌人路径点。需要注意的是,Actor放置不能过高或者过低,z坐标(高度轴)应当比敌人模型略矮,且比地形更高,否则敌人生成可能出现异常。
此时通过蓝图通信
可确定每一个路径点的后驱点,类似链表
结构,敌人从出生点刷新后即沿着设定好的路径前行到终点。
放置防御塔Actor
本项目中的防御塔也是Actor蓝图类,默认为0级,0级时塔基座和炮塔模型均不可见,且鼠标悬浮时不会显示攻击范围,点击此Actor即可弹出防御塔管理菜单。
将防御塔Actor拖拽到场景中,沿敌人路径放置,防御塔密度与位置需要考虑玩家实际游戏体验酌情设置。
防御塔Actor实现
锁定敌人
防御塔会循环检测四周一定范围内的所有敌人,本游戏中的敌人全部为Pawn类。
搜索敌人需要用到的蓝图节点为MultiSphereTarceForObjects
,将防御塔自身的位置设为起始点
与终点
。此节点会以防御塔为圆心,以一定半径搜索场景中所有的对象,通过将搜索到的对象类型转换为“敌人基类”并判断此敌人是否已经死亡,将未死亡的所有敌人都添加到一个数组中,表示搜索到的全部敌人。
因为在游戏中,防御塔需要锁定搜索到的第一个敌人,也即是离终点最近的敌人会被优先攻击。所以敌人基类拥有一个累计移动路程
的属性,通过对数组中的敌人进行对比即可得到路程最大的敌人,优先锁定。为方便调用,将该功能封装成一个函数,命名为找到第一个敌人
,将搜索到的敌人基类数组传入,即可返回一个BOOL值和敌人基类,若有符合条件的敌人则BOOL值为真,否则为假,下图为函数的蓝图实现
由图可得,该函数获取路程最大的敌人,只需要遍历一遍即可,因此
时间复杂度为O(n),不会产生太多额外的性能开销。因为防御塔需要时刻搜索敌人,所以需要用到
EventBeginPlay
节点,当防御塔对象被实例化时就会执行扫描并锁定敌人。
防御塔发射子弹前还需要旋转到敌人方向,这里先需要获取防御塔到敌人的旋转体,可通过
FindLookAtRotation
节点实现,起始位置为防御塔位置,目标位置为锁定的敌人位置,即可获得防御塔到敌人的旋转体,接着通过
SetWorldRotation
节点即可将炮塔转向敌人位置。下图为炮塔旋转蓝图实现
Yaw
、
pitch
和
roll
是3D空间中描述旋转的术语,分别代表绕Y轴(垂直于水平面)的旋转、绕X轴(垂直于左侧)的旋转和绕Z轴(垂直于前后方向)的旋转。通常,这些术语用于描述物体相对于三个轴的旋转角度。在游戏开发中,这些术语通常用于描述3D物体的旋转,Yaw代表左右旋转,Pitch代表上下旋转,Roll代表翻滚旋转。
发射子弹
当锁定到敌人后,首先需要判断当前炮塔是否装填完毕,为防御塔基类添加一个是否可发射
的变量,初始为可发射状态,每发射一次设置为不可发射且延迟一定时间后重新设置为可发射以此模拟弹药装填。在可发射状态下,首先检测当前锁定的敌人是否有效或者是否死亡,以为炮塔发射的时候敌人可能已经被销毁或者死亡,只有敌人有效且未死亡才可以发射子弹。下图为子弹发射流程图
发射子弹可以使用
SpawnAtcorFromClass
节点,该节点会在场景中生成一个特定的Actor对象,这里将生成的Actor设置为
子弹基类
。生成子弹后还要将防御塔等级传入,以便于子弹属性初始化。
升级与出售
防御塔的升级与出售均通过管理菜单实现,要想实现单击防御塔弹出菜单的效果,需要借助ActorOnClicked
事件,当鼠标点击Actor时触发此事件。该事件触发后首先判断当前游戏是否为暂停状态,如果不是暂停状态则弹出管理菜单。因为同一时间只能打开一个防御塔的管理菜单,因此弹出菜单前首先把其他的管理菜单销毁。
管理菜单构造时首先判断当前防御塔的等级,如果为0则无法出售,如果为5则无法升级。当点击出售按钮时,将防御塔等级设置为0,基座与炮塔模型设置为不可见,同时增加特定金币。当点击升级按钮时,首先判断当前金币是否足够,若足够则将当前防御塔的等级+1,扣除对应金币。
底部光环与攻击范围显示
首先在PS中绘制一张圆形图片与光环图片,导入ue4后右键新建纹理,之后再次右键可新建材质,此时可设置材质的颜色和透明度等各种参数,可以方便的创建多种材质对应不同等级的防御塔,下图为材质参数调整界面
在
防御塔Actor
中新增两个静态网格体,材质选择新建的攻击范围与塔底光环材质,并缩放到合适大小,且根据防御塔等级的不同光环和攻击范围的材质也不同,攻击范围初始为不可见状态。
攻击范围的显示由
ActorBeginCursorOver
事件控制,当鼠标悬浮于Actor之上时触发该事件,首先判断游戏是否处于暂停状态与防御塔等级,当未暂停且等级>0时将攻击范围设置为可见。当鼠标不再悬浮时,触发
ActorEndCursorOver
事件,此时将攻击范围重新设置为不可见,至此可实现只有当鼠标悬浮时才会显示防御塔范围。
敌人Pawn实现
寻路实现
敌人作为一个AI,要实现移动首先需要构造寻路范围,UE4提供了Nav Bounds Volume Actor(导航网格体边界体积)
,意为AI寻路的体积,只有在这个体积内才能构建AI移动路径,将此Actor拖入场景,且设置好缩放,确保该体积将敌人全部的移动路径覆盖。
因为敌人需要移动,所以将敌人基类设置为Pawn
类型,且在类内增加一个变量,名称为下一路径点
,类型为路径点Actor
。当敌人在场景中实例化后,通过获取出生点Actor
内的路径点变量即可获得后驱点,之后借助AI MoveTo
节点实现寻路。
当敌人移动至路径点后,接着获取该路径点内存储的下一路径点。其结构类似链表,每一个节点都存储着指向下一节点的指针。
受伤扣血实现
当子弹击中敌人后,会触发敌人基类内的AnyDamage
事件,并传入子弹造成的伤害,该事件触发后首先扣除当前敌人一定HP,接着判断HP是否归0,若归0则将敌人设置为死亡状态且增加玩家金币。死亡后的敌人开启物理模拟且不再前行,体现在画面中就是敌人死亡倒地的效果,延迟1s后将敌人Actor
销毁。
统计走过的路程
因为防御塔需要锁定第一个敌人,因此需要记录敌人走过的总路程,敌人基类新增一个累计路程变量
,变量类型为浮点型
。在敌人实例化时,首先获取初始向量坐标且记录为变量。
游戏每一帧都会调用EventTick
事件,获取敌人当前向量并用此向量减去开始记录的初始向量得到长度,该长度即为与敌人一帧走过的距离,将该距离与累计路程相加,累加完后再将当前位置设置为上一帧位置。
多样化的敌人
游戏需要有不同的敌人,这些敌人的HP,价值金币、移动速度、体积、攻击力都不尽相同,此时可以新建继承于敌人基类的子类,用这些子类代表不同的敌人,子类的属性都可以自定义来实现多样化的需求。
子弹Actor实现
子弹追踪敌人
子弹需要一定初速度从炮口发射出去,这需要添加一个发射物移动组件
,该组件可支持受影响后反弹或向目标前进等行为。
子弹从炮口射出后会不停追踪敌人,每一帧都需要获取敌人位置,之后通过Find Look at Rotation
节点计算出当前子弹与敌人之间的旋转体,通过Get Forward Vector
节点获取该旋转体朝前的向量,将该向量乘上子弹飞行速度后传入Set Velocity in Local Space
节点,该节点的目标是发射物移动组件
,可以为子弹提供一个速度与方向,使子弹始终朝敌人飞。下图为子弹追踪蓝图实现
子弹击中敌人
子弹击中判定采用碰撞检测实现,首先为子弹Actor
添加一个SphereCollision
,接着在蓝图中调用该碰撞盒的On Component Begin Overlap
事件,将事件输出节点的Other Actor
转换为敌人基类
,这样只有当子弹与敌人发生碰撞后才会触发后续逻辑。
子弹击中敌人后首先判断敌人是否死亡,若已死亡则销毁子弹,若未死亡则调用ApplyDamage
节点并将当前锁定的敌人Actor
传入节点的DamageActor
引脚,将子弹伤害传入BaseDamage
引脚,这样便可以调用敌人Actor
中的AnyDamage
事件,实现敌人受伤扣血。
游戏模式实现
新建一个GameMode蓝图类,命名为塔防GameMode
,该蓝图类主要负责实现游戏中的刷怪、关卡判定、金币变更等功能。
刷怪实现
首先新增一个自定义Event,命名为单次刷怪循环,该事件负责完成单次刷怪逻辑判定。怪物的生成借助Spawn AIFrom Class
节点实现,将不同的敌人子类传入Pawn Class
引脚即可实现刷新不同怪物的功能,下图为刷怪流程图
通关判定
当刷新BOSS后,即进入通关检测,设置一个定时器,每隔0.2s检测一次场景中敌人数量,当敌人数量为0,清除定时器且判定通关,延迟2s后弹出通关菜单,玩家可在通关菜单中点击无尽模式继续体验游戏。进入无尽模式后不设波次上限,怪物会一直刷新,每增加一个波次敌人都会变得更加强力。
存档系统实现
存档
首先在资产管理器中新建一个SaveGame
蓝图类,并重命名为SaveGame_BP
,该蓝图类为存档专用类,可以在存档插槽中新建与读取.sav文件,实现游戏的存档系统。UE4存档系统中的插槽是指用于存储和加载不同类型数据的一种机制。在一个存档文件中可以包含多个插槽,每个插槽可以存储不同类型的数据,插槽还可以进行重命名、复制、删除等操作。这样,玩家可以在游戏中选择不同的插槽进行存档和加载,以达到不同的游戏进度和状态。
每当刷新新的一波敌人时,游戏都会更新存档,为方便调用,将存档逻辑封装一个函数,重命名为保存游戏
,该函数的返回值为SaveGame
。因为本游戏中的防御塔属性全部与等级挂钩,因此无需存储整个防御塔基类,仅需要新建一个整数数组存储所有塔的等级,读取存档时根据等级初始化每个塔的属性即可。
除了防御塔外,还需要存档玩家当前的金币数、HP,刷怪波次。首先通过Create Save Game Object
节点创建一个SaveGame
并将其返回值提升为变量,重命名为SaveGame_Ref
,该变量即为新建的游戏存档,将当前玩家的属性与地图中所有防御塔的等级全部存入SaveGame_Ref
。最后调用Save Game to Slot
节点将插槽存入SaveGame_Ref
,Slot Name
引脚设置为TowerDefense
,此时在.sav存档文件的TowerDefense
插槽内存储的就是当前玩家进度数据,下图为创建存档插槽的蓝图实现
读档
在塔防GameMode
蓝图类中新建一个自定义事件,命名为读取游戏
负责读取本地存储的数据。事件会传入一个SaveGame_BP
类,读取该类中存储的玩家HP、金币、敌人波次、防御塔等级,并为游戏初始化。其中防御塔等级是一个整数数组,需要调用For Each Loop
节点为场景中每一个防御塔设置等级。读取到敌人波次后,判断当前波次是否>5,如果是则将游戏设置为无尽模式。
当游戏关卡构造时,首先调用Does Save Game Exist
节点,Slot Name
引脚设置为TowerDefense
,该节点的作用是读取本地存档中对应插槽的数据,返回一个BOOL值表示数据是否存在,若不存在说明本地没有存档。
若读取到存档,则继续调用Load Game from Slot
节点,SlotName
引脚设置为TowerDefense
,并且将返回值转换为SaveGame_BP
类,接着调用读取游戏事件并将转换后的SaveGame_BP
传入事件,此时完成全部的读档操作,玩家进度被设置为存储进度,下图为读取存档插槽的蓝图实现
参考教程:https://www.bilibili.com/video/BV15z411B7sb/