目 录

摘 要 I
Abstract II
引 言 1
1.相关技术 3
1.1 Unity基础界面 3
1.2 C#脚本编写 3
1.3 Unity脚本 3
1.4 Unity物理引擎 3
1.5 UGUI 3
1.6 Unity动画系统 4
1.7 本章小结 4
2. 系统分析 5
2.1游戏内容需求分析 5
2.2游戏流程需求分析 5
2.3游戏场景需求分析 5
2.4怪物系统需求分析 5
2.5UI界面需求分析 6
2.6人物动画需求分析 6
2.7本章小结 6
3. 系统设计 7
3.1游戏环境设计 7
3.1.1场景设计 7
3.1.2角色设计 7
3.1.3音乐设计 7
3.2战斗系统设计 8
3.2.1巡逻系统 8
3.2.2战斗检测 10
3.2.3怪物的攻击与搜索 10
3.2.4伤害判定 11
3.3角色控制设计 11
3.4 UI设计 12
3.4.1UI的整体设计 12
3.4.2对话框UI和任务栏 13
3.5预制体加载 14
3.6摄像机控制 14
3.7本章小结 16
4. 系统实现 17
4.1脚本实现 17
4.2场景实现 18
4.3NPC对话实现 19
4.4人物动作实现 19
4.5UI实现 21
4.6战斗碰撞监测实现 21
4.7本章小结 22
5. 系统测试 23
5.1功能测试 23
5.2人物动作测试 23
5.3战斗系统测试 24
5.4UI测试 24
5.5性能测试 25
5.6本章小结 26
结 论 27
致 谢 28
参考文献 29
附录 源程序清单 31

摘 要

随着计算机图像处理和虚拟现实技术的发展,游戏效果和画面质量不断提高,游戏已成为一种新的娱乐趋势。与此同时,随着Android/IOS应用越来越遍及,游戏也逐渐向移动端的方向发展。
Unity 3D游戏引擎在3D游戏设计、游戏体验升级和优质画面强化等方面具有很好的优势,良好的平台可移植性可以满足在计算机和移动终端中进行游戏切换和移植的需求。新一波科技创新浪潮的到来,使人们的生活更加舒适、便捷、快捷。虚拟现实技术作为前沿科技领域的研究热点之一,得到了国家政策的大力支持。5G通信网络技术的诞生,视觉信息数据的传输渠道进一步扩大,和数据的准确性和可视化程度一直在创新和发展中,也提供了一个广阔的发展空间和建筑虚拟现实技术的一个新时代。
近年来,虚拟现实技术在教育、游戏、房地长等行业广泛应用中。掌握软件开发工具Unity3D是实现虚拟现实技术必不可少的核心技能。
本项目以Unity3D为游戏搜索引擎,设计了一款角色扮演游戏,将文化教育融入RPG游戏中,达到教育与娱乐的平衡。在游戏中玩家操控角色来完成和游戏情节有关的一系列既定活动,从而起到寓教于乐的作用。

关键词:Unity3D; RPG; 3D游戏

Abstract

With the development of computer image processing and virtual reality technology, game effect and picture quality have been constantly improved, and game has become a new entertainment trend. At the same time, with the popularity of Android/IOS applications, games are increasingly expanding to mobile terminals.
The Unity 3D engine has good advantages in 3D game design, quality screen enhancement and game experience upgrade, and good platform portability can meet the needs of game switching and porting in computers and mobile terminals. the arrival of a new wave of scientific and technological innovation has made people’s life more comfortable, convenient and fast. as one of the research hotspots in the field of frontier science and technology, virtual reality technology has been strongly supported by national policies. with the birth of 5G communication network technology, the transmission channels of visual information data are further expanded, and the accuracy and visualization degree of data have been in the process of innovation and development, which also provides a broad space for development and a new era of building virtual reality technology.
In recent years, virtual reality has been widely used in education, news media, games, e-commerce, real estate and other industries. mastering software development tool Unity3D is an essential core skill to realize virtual reality technology.
In this project, Unity3D is used as the game search engine to design a role-playing game, which integrates culture and education into the RPG game to achieve the balance between education and entertainment. the player controls the characters in the game and completes the established activities related to the plot of the game, thus playing the role of teaching and entertaining.

Keywords: Unity3D; RPG; 3DGame

引 言

Unity3D游戏引擎是一个兼容多平台的综合游戏开发工具,通过使用它用户可以较为轻松地创建交互式内容。比如说3D视频游戏、实时动画以及建筑可视化[1]。作为一个专业游戏引擎,它是完全集成的。它具有跨平台的优势,并且具备一个效果非常棒的统一的粒子照明编辑器。由于Unity在框架和架构上具有的种种优势,通过Unity游戏引擎制作的游戏可以在很大程度上避免崩溃和闪回的情况,同时它也备有很强大的性能分析工具,通过这种性能分析工具可以很容易地找到内存和CPU瓶颈。Unity支持Android和iOS的真实机器运行分析[2]。
本文从介绍该项目游戏的作用游戏技术和游戏的大致流程展开,从系统分析部分、系统实现部分等各个角度展开,对整个项目进行系列的相关说明和探讨。游戏采用Unity3D作为游戏搜索引擎作为角色扮演游戏,在其中虚拟了一个游戏的世界,游戏的玩家通过使用键盘以及鼠标来操纵游戏中的主人物来完成游戏中一系列相关活动。本RPG的主要内容是冒险世界的一个小村庄一个勇者开始他第一步冒险的故事。故事整体内容相对简洁,看似简单的过程包含了UI界面、战斗系统等各种功能模块,是许多代码共同调试互相配合调用的结果。文章中大片文字描写这个项目使用到的一些技术和制作流程,配合相关的图片和代码,使描述更为详尽。其中包括游戏UI设计,游戏人物和UI的设计应当具有美感并且与整体环境相互协调,游戏人物的动作要自然且连贯的互相切换显示出来;游戏其他角色的UI设计,怪物自己的巡逻系统、侦测系统、战斗系统;以及其他的关于摄像机控制、脚本二次编辑、场景的调节和各个组件参数的设置等等制作方式的说明。
本文首先描述了项目游戏中使用到的相关技术,对它们进行了简单的说明。然后描述了进行游戏制作时的系统分析,这里主要是需求分析,包括游戏的环境需求、游戏流程需求、游戏场景需求和怪物系统等分析,为接下来的制作做铺垫。在需求分析部分写完后就是对系统功能各个部分设计的描述,合适的系统设计在游戏制作中有着非常重要的作用。其次是系统实现,对每个功能具体是如何实现的进行相关描述,在系统设计和系统实现中分别加入了相关代码使描述更为详细具体。在以上都写好后,本文第五章是系统测试,这部分是游戏全部制作完毕后对游戏中各个性能的测试,包括游戏功能的测试、战斗系统方面的测试、UI的测试等等。第五部分是游戏的结论,对游戏的整体制作的总结,包括实现的功能,不足和需要改善的地方以及使用到的技术等等。最后是参考文献、代码清单以及致谢[3]。

1.相关技术

1.1 Unity基础界面
Unity中含有很多面板,首先是Project面板放置游戏的所有资源,如音频文件、视频文件等,这些文件也可以从Project中打包出去。项目中的Hierarchy面板显示了当前场景中的各个游戏对象之间的父子级关系,这些游戏对象包括游戏场景中的GameObject。Scence是场景面板,其中有很多方便游戏制作的常用快捷键,场景中物体可以在里面直接移动。
1.2 C#脚本编写
C#是一种面向对象的编程语言,它包含的变量、数据基本运算、语句、方法、数组、数据类型、类和对象还有结构这些知识点和曾经在大学课堂上学习过的其他语言如C语言、C++有相似的地方,它由这两种语言衍生而来。Unity的游戏通过C#脚本挂载在组件上进行控制,在Unity编程中脚本也需要严谨的算法和细致的语句编写[5]。
1.3 Unity脚本
要实现Unity游戏中的大部分功能,编写脚本是其中必不可少的一部分。脚本的语言链接着Unity基本面板中的各项组件以及它们的属性,通过将脚本挂载在游戏中的游戏对象上,用代码来控制这些游戏对象之间的各种反应比如光线的调节、碰撞体的检测以及碰撞后的反应等等。
1.4 Unity物理引擎
物理引擎包含刚体、碰撞器和触发器。游戏对象添加组建刚体后就会受物理引擎控制,当受到外力时会产生类似真实世界包含物理特性的运动。刚体有很多属性,包含物体的质量、阻力、重力等等,通过控制刚体组件的这些属性控制物体的运动。物体添加碰撞器后就具有了碰撞的效果,产生类似真实世界的碰撞反应。如果两个物体想要产生碰撞效果,它们都要添加碰撞器组件,并且至少其中一个具备刚体组件。除了使用碰撞器和刚体控制游戏对象的碰撞还可以使用触发器,它在碰撞体组建的Is Trigger上,对它勾选就会触发物理检测函数。
1.5 UGUI
UGUI是Unity的图形用户界面,在Unity4.6以上版本中新加入的界面显示系统,与之前的版本NGUI相比自适应系统更加的完善也可以进行更加方便的深度处理,也能够做出更加绚丽的UI特效[7]。绘制UI元素需要用到Canvas画布。Canvas拥有很多属性,比如Render Mode控制渲染的方式;Pixel Perfect设置完美像素,如果选中它就会对屏幕显示效果进行锐化处理;Sort Order是处理渲染顺序的属性,当存在很多Canvas时,该属性值越大越会渲染到更上层;Render Camera是对摄像机进行渲染。通过对Canvas不同属性的设置达到UI中想要实现的各种画面效果。除此之外还有Text作为文本标签和图像的插入等
1.6 Unity动画系统
Unity的动画系统Mecanim是一个效果十分丰富但是完成的时候相对复杂的难度较高的动画系统,这个动画系统在用户使用的时候提供了五大功能。这些功能使角色动画可视化的展现出来,使操作更为便捷[8]。
1.7 本章小结
本章讲述了游戏中用到的各种相关技术,这些技术贯穿于整个项目中,各个知识点相互联系相互配合构成了游戏的整体。其中C#的学习和从前课上学过的知识想通,所以使用时也相对容易一些。

2. 系统分析

2.1游戏内容需求分析
游戏的剧本以一个冒险世界展开,讲述了在一个静谧的小村庄中,勇士进行他冒险的人生第一步的故事。主角在需要寻求帮助的NPC那里领取任务,对扰乱居民的小怪兽进行打击,帮助别人解决了困扰。游戏的剧情较为简单,主旨是表达每场漫长的旅行开始,中途会遇上数不清的挫折和困难,就如同游戏世界中的一个个小小的怪兽和关卡。中途总是不易的,但一次次的克服一次次的胜利就会收获更加坚强的自己。
2.2游戏流程需求分析
游戏开始会弹出对话框提示有任务生成,主角在NPC处领取任务。主角本身拥有多项动画,包括加速、奔跑、拔刀、左闪右闪等等,在玩家鼠标和键盘的控制下可以平滑的切换各种状态。领取任务后角色在地图上寻找怪物并将其打败,可以使用不同的技能,不同技能造成伤害不同。来自怪物的伤害会通过UI控制的血量条的变化提示[9]。每打败一个小怪物任务面板会产生变化,当打败所有小怪物任务面板会提示提交任务或者继续在场景中漫游,提交任务给NPC会有相应剧情。
2.3游戏场景需求分析
场景达到视觉上的美观,场景风格卡通,靠近游戏剧情的氛围,包含多种物件如各种树木、岩石等等,选取合适的Shader对素材进行合理的搭配,配合光照,在游戏中添加碰撞体确保人物在移动过程中不会有穿模的情况发生。场景中含有声音特效如风声使场景更逼真[10]。
2.4怪物系统需求分析
初始怪物会分布在地图上不同地点,每个怪物有拥有不同的等级、血量等属性,这些属性在UI上显示,怪物头顶的UI在主角靠近时会变大,在远离主角的时候UI会变小,防止遮挡人物视线。怪物在遇到主角之前会在指定的路线巡逻,不断徘徊[11]。当怪物遇到主角后怪物的UI会变成红色,并且一直面向主角然后朝向主角奔跑,接触到主角后会停止奔跑进行攻击,攻击的伤害值由等级影响。在主角打算脱离战斗时怪物会进行追击。当主角离开怪物侦测范围,怪物会步行回到巡逻路线中继续巡逻。当怪物被主角打败会死亡,游戏模型在地图上消失。
2.5UI界面需求分析
UI界面包括和NPC的对话框、任务面板、技能小图标、主角的血量条、怪物头上等级和血量显示以及菜单栏。UI界面在美观的同时保证简洁不太遮挡游戏场景从而影响游戏体验[12]。与NPC的对话框设置调整好NPC立绘与对话框的出现时间间隔,注意文字的缩进且不超过对话框。每有一个怪物被打倒任务面板就随之改变直到任务完成。
2.6人物动画需求分析
人物和怪物都有自带的一系列动作,根据不同的状态采用不同的动作。在人物模型中,初始是行走状态,通过数遍控制视角和行走时的转向,行走状态中按下F键变为拔刀奔跑状态,W、A、S、D分别为奔跑时的方向,Q键是技能键人物使用技能的动作,鼠标左键单击是攻击动画,拔刀时Q、E键分别是左闪和右闪。所有动作在Animator中与脚本共同控制[13]。为了使动作之间的切换不突兀,设置x轴和y轴使动作间变换更为平滑。
2.7本章小结
本章主要是项目的需求分析。合适的分析使之后的设计工作更为明确,有了方向,同时在一定程度上对避免一些早期错误有帮助,从而提高代码编写的效率,增加游戏整体的质量。

3. 系统设计

3.1游戏环境设计
3.1.1场景设计
游戏中的场景搭建主要是3D场景,在Unity官网找到的场景素材包拖入到Unity工程中的Project中,游戏中有树木素材、场景素材等都放在了这个素材包中[14]。游戏场景分为较为卡通,整体观感比较清新自然,会给人更轻松惬意的游戏感受。游戏中的人物模型——游戏角色、NPC、小怪兽的3D模型同样是网上寻找的素材包资源。寻找到的人物模型尽量贴合游戏场景,使得整体画面上更为协调美观,如图3.1。
在这里插入图片描述

图3.1 环境预览
3.1.2角色设计
角色分为NPC、主人物和怪物,每个游戏角色都有属于它的丰富的动作,如主角包含行走、冲刺、奔跑、拔刀行走、攻击等等动作,不同的动作通过动画系统衔接到一起,通过,Animation和相关代码进行控制,绘制动作树,在反复的调试中使人物的动作尽可能的流畅,不出现太过突兀,直接跳转的情况[15]。
3.1.3音乐设计
音乐在游戏中占有重要的地位,影响到玩家的游戏体验。有特色的游戏环境音乐和相关特效能够生动的体现出整体的环境氛围和氛围,使玩家能够身临其境。
3.2战斗系统设计
这里的战斗系统是一个很多子系统的集合,其中包括小怪兽攻击、巡逻、搜索、战斗检测(怪物脱离战斗和寻找角色进行战斗)以及怪物和主角的伤害判定和血量减少。首先在脚本MonsterControl中设置好怪物的默认值,里面包括预设的血量、伤害、移动速度、攻击间隔、怪物头顶上方显示它的血量和等级,怪物的行为、预设的攻击间隔、路径点以及一些用来判断的bool值[16]。在脚本中,当玩家按下控制相应动作的按键时,脚本里的条件语句会进行判断,每一个按键都对应相应的条件语句控制bool值的参数,从而调控组件中的动作。
怪物的动作树如图3.2所示。使用了Unity中动画系统中的动画状态机(Animation),每一只小怪物都挂在了Animator组件,这个组件的作用是将怪物的动作动画更好的进行图形化管理。Animation可以理解为每一个动画。将准备好的带有动作的怪物素材拖拽到游戏中,添加Animator Controller,在其中添加执行条件,动作树中的方框和箭头在代码中分别对应与怪物动作有关的一些变量和数值。可以通过动作树的调节和代码的编写共同对小怪物的活动进行细致的控制。
在这里插入图片描述

图3.2 怪物的动作树
3.2.1巡逻系统
游戏中怪物的战斗检测使用到了Unity中的相交球,相交球和射线在Unity中都可以对周围物体进行检测,相交球在这个游戏中用作怪物检测主人物的活动。它的参数分别是相交球的中心点还有检测范围和与特定层发生碰撞的反应。可以通过设置来决定相交球的初始位置、以球心设置半径范围和层级Layer[17]。
怪物默认有巡逻时的颜色显示,在场景中用空物体设置好了怪物行走的路径点,游戏中一共有5只小怪物,每只怪物在Hierarchy里面有存储它们指定路径的空物体。这些连起来的空物体形成了怪物自己的行动路线。行走路线的设置如图3.3。
在这里插入图片描述

图3.3 小怪物巡逻
代表路径点的空物体越多,怪物在指定的路线上行走的效果就越平滑越自然[18]。怪物就在这些指定路线上自动不断移动。在怪物巡逻的过程中,当挂载在怪物上的相交球检测到了主角的靠近,就会自动脱离怪物的巡逻状态。
//巡逻时,怪物名字颜色
ui_Clon.transform.GetChild(1).transform.GetChild(0).GetComponent ().color = safetyNameColor;
//缩小触发战斗碰撞
rangeValue = dangerRange;
//动画状态机
monsterAima.SetBool(“IsAttck_Run”, false);
//移动到指定路径点
monsterObject.transform.position = Vector3.MoveTowards(monsterObject.transform.position,
new Vector3(aiPatrolPath[path].transform.position.x,
monsterObject.transform.position.y, aiPatrolPath[path].transform.position.z), Time.deltaTime * monsterMoveSpeed * .4f);
//转向下一个路径点
monsterObject.transform.LookAt(new Vector3(aiPatrolPath[path].transform.position.x, monsterObject.transform.position.y, aiPatrolPath[path].transform.position.z));
//归零路径点,循环巡逻
if (path == aiPatrolPath.Count)
{
path = 0;
}
3.2.2战斗检测
战斗检测的脚本在MonsterControl…cs中,在怪物的巡逻过程中相交球检测到了主角的存在,进入战斗状态[19]。怪物头上的UI样式会变成指定的颜色,怪物会在脚本控制下始终看向玩家。这个时候计算玩家与怪物之间的距离,如果距离大于1,怪物在动画状态机的控制下进行奔跑,朝着主人物方向跑去,当怪物接近任务,距离小于1,怪物会停止奔跑动画,开启站立并攻击的状态。在这里设置了计时器,用计时器来控制怪物的攻击时间间隔,当计时器大于指定时间间隔便进行一次攻击。当玩家和怪物的距离大于2,怪物开始追击。这里使用了协程来决定攻击的时间间隔[20]。
3.2.3怪物的攻击与搜索
游戏中很多功能的实现都是需要碰撞器来完成,包括人物站立在地表上、避免人物穿模、角色之间的攻击、人物在场景中行走不会撞到物体等等。碰撞器使游戏中的刚体具有碰撞效果,产生物理因素。使用时要在人物上添加刚体组件。碰撞器分为静态碰撞器和刚体碰撞器,怪物的攻击适用于刚体碰撞器,受物理引擎影响[21]。
该游戏中怪物攻击通过添加的相交球搜索周围的碰撞体,一旦搜索到了玩家,怪物就会面向玩家并朝着玩家移动,当相交球检测到了玩家时,代码中对应的布尔值变为true,并执行这个布尔值所控制的相应动作,在怪物的手上加一个碰撞盒,怪物在播放动画的同时碰撞盒随着怪物的手移动动,通过碰撞盒的检测判定是否打到玩家。
//通过相交球检测玩家是否处在怪物警戒范围
plyer = Physics.OverlapSphere(monsterObject.transform.position, rangeValue, layer = 1 << layervalue );
//如果检测到玩家,中断巡逻
if (plyer.Length != 0)
{
IsCombat = false;
}
//如果玩家离开战斗范围,重新回到巡逻
else
{
IsCombat = true;
}
3.2.4 伤害判定
游戏属性是否合理对游戏的平衡性很重要,同时属性的设定也会影响到游戏的难度。在这个游戏中的游戏属性有血量、移速、伤害等等[22]。
玩家与怪物一开始拥有初始的int类型的血量,在脚本里面有一个参数,当玩家碰触到怪物或者怪物碰触到玩家会减血。通过玩家控制进行相应技能的释放,通过对碰撞体的检测来判断怪物受到的伤害,当怪物的收到的总伤害大于怪物本身的血值,血脂数值为0怪物死亡然后消失。
怪物的等级不同伤害不同,根据脚本里的加减乘除的运算计算不同的等级对人物造成的伤害是多少,具体的运算是怪物等级×5。基础伤害在脚本里面初始设置好[23]。
3.3角色控制设计
找到主人公的相关素材,拖入游戏中形成预制体。素材配有人物的一些列动作。通过Animation和相关代码共同操控人物的各种行为。游戏中主人物的动作树如图3.4所示。
在这里插入图片描述

图3.4 主角的动作树
在脚本中通过条件判断语句根据玩家在游戏中输入的按键,产生不同的状态为了使人物不同的动作之间转换的更加平滑,通过差值运算处理,使X与Y平滑的归零。通过bool值调控人物动作的转变[24]。
部分代码如下
if (Input.GetKey(KeyCode.W) || Input.GetKey(KeyCode.S))
{
//此时状态为行走状态
if (playerAnimatorContor.GetBool(“IsDraw”) == true)
{
stateValue = 3;
}
else if (playerAnimatorContor.GetBool(“IsDraw”) != true)
{
stateValue = 1;
}
//移动时,让移动逻辑为真(即让Walk动画播放)
playerAnimatorContor.SetBool(“IsWalk”, true);
//移动时讲Vertical和Horizontal分别绑定给xValue和yValue(即绑定给行为树的X和Y)
yValue = Input.GetAxis(“Vertical”);
xValue = Input.GetAxis(“Horizontal”);
}
//退出移动控制
else
{
//此时状态为待机状态
stateValue = 0;
//停止移动,让移动逻辑为否 (即还原为待机动画)
playerAnimatorContor.SetBool(“IsWalk”, false);
//通过差值运算平滑的让X和Y归零
yValue = Mathf.Lerp(Input.GetAxis(“Vertical”), 0, 0f);
xValue = Mathf.Lerp(Input.GetAxis(“Horizontal”), 0, 0f);
}
3.4 UI设计
3.4.1UI的整体设计
合适美观的UI界面给予玩家第一印象,甚至在某种程度上决定了玩家的留存。操作不方便并且设计不够美观的UI界面会很影响游戏体验,那样尽管游戏逻辑和可玩性很好,仍会有很多玩家丧失对游戏的兴趣[25]。
项目中的游戏使用的是UGUI,它是Unity的图形用户界面,在Unity4.6以上版本中新加入的界面显示系统,与之前的版本NGUI相比自适应系统更加的完善也可以进行更加方便的深度处理,也能做出更绚丽的UI特效。
游戏中的UI包含整体界面的UI如图3.4,里面包含主角血量条的显示,血量条会根据脚本中控制的血量数值在攻击的影响下递减,减少的血量在UI中有显示,以及任务面板、菜单栏和技能面板,点击菜单栏中的不同选项在代码控制下实现重新开始游戏退出游戏等效果,技能面板包含了角色的一些动作和相关技能。如图3.5。除此之外UI包括初始与NPC对话的对话框和怪物头顶的UI。任务面板会随着怪物的减小数值发生变化,怪物头顶的UI大小会随着主角的靠近进行变化。
在这里插入图片描述

图3.5 UI具体界面
项目中的UI设计在简洁和美观之间寻找平衡点,整体UI左上角是玩家的剩余血量,下方是人物的技能,右上角是设置和任务栏。UI风格和色彩选择和游戏场景中的画面保持了一致,使得视觉上更为协调。简洁的界面使玩家更方便的体验游戏的操作。UI界面在制作过程中的显示如图3.6。
在这里插入图片描述

图3.6 Scence面板中的UI
3.4.2对话框UI和任务栏
人物初始会有NPC的对话提示,根据点击按键不同的次数提示不同的对话语句。在完成任务后也会有相关的提示显示任务结束然后提示提交,部分代码如下。
if (_buttonValue == 5)
{
sq.Append(transform.GetComponent ().DOText(“”, .1f));
sq.Append(transform.GetComponent ().DOText(“好像是谁在叫我”, 3));
}
if (_buttonValue == 6)
{
sq.Append(transform.GetComponent ().DOText(“”, .1f));
sq.Append(inset.DOFade(1, 1));
sq.Append(transform.GetComponent ().DOText(" “看你的打扮似乎是一位冒险家啊,我掉的银斧头被邻居家的猫抢走了,你可以帮我找回来吗” ", 3));
}
3.5预制体加载
在代码编辑中常常会将一些重复用到的功能制作成函数反复调用,游戏中的预制体和函数的这个功能很像。游戏中将一些能够多次使用的对象制作成.prefab的预制体,而后在使用时加载在场景中完成实例化[26]。
游戏中的角色在场景中出现使用了预制体加载。首先将人物资源拖拽到项目中形成它的预制体,预制体显示为蓝色。如图3.7所示。
在这里插入图片描述

图3.7 预制体示例
点击预制体会进行对预制体的编辑,这样通过脚本调用到的预制体模型进行克隆,克隆后的物体都会根据默认设置好的属性出现,不用重复编辑脚本。
3.6摄像机控制
摄像机是每次开启新工程默认自带的组件,它用来向玩家获取和显示世界到Game面板中,相当于玩家看向游戏世界的“眼睛”,不光可以用作视角,还可以制作小地图等功能。在场景中可以放置的摄像机数目不等,在这个游戏中我放置了一个摄像机。
摄像机含有不同的组件,有和其他所有组件同样的Transform变换,还有它独有的Camera组件、音频监视器Audio Listener。Camera是用来捕获和显示世界,Audio Listener用来接收场景中的声音,并在游戏中通过计算
机的扬声器进行播放。同时摄像机的Culling Mask剔除遮挡功能可以通过勾选游戏场景中设定好的层,控制游戏进行时玩家哪些游戏对象是看得到的哪些看不到,从而提高游戏界面的整洁度。
为了在游戏中产生第三人称视角,在游戏中将摄像机放在Hierarchy中角色下取名为PlayerCamera,摄像机跟随角色移动,就可以实时捕捉到主人物和周围物体的画面。在Scene中进行调节摄像机的位置,使摄像机固定在主角的后方位置。而后在参数面板对摄像机的各项参数进行调节,同时之后的脚本中可以控制相应参数。
调节相应的初试参数如图3.8所示。
在这里插入图片描述

图3.8 摄像机的设置参数
摄像机设置的相关代码如下。
void Start()
{
Cursor.visible = false;
}
void LateUpdate()
{
MouseControl();
}
void MouseControl()
{
float mouser_Y = Input.GetAxis(“Mouse Y”);
transform.RotateAround(Player.transform.position, transform.right, Mathf.Lerp(mouser_Y, mouser_Y * 5f, 0));
if (Input.GetKey(KeyCode.LeftAlt))
{
Cursor.visible = true;
Cursor.lockState = CursorLockMode.Confined;
}
else if (Input.GetKeyUp(KeyCode.LeftAlt)) Cursor.visible = false;
}
3.7本章小结
本章是整篇论文的重要部分,主要内容是游戏制作中的各个主要模块,以及其中用到的Unity相关技术的使用和这些技术的简单说明,这些系统设计为之后的一系列操作做了铺垫。同时附着上一些相关的主要代码,使文章表达更加直观。

4. 系统实现

4.1脚本实现
为了在游戏制作过程中更加便捷的观察各种参数的变化并加以改动,游戏中使用到了脚本拓展,可以将原本的编程语言在Hierarchy面板中用中文表示出来,很方便理解。
首先在脚本文件夹中创建专门用来负责脚本二次编辑的文件夹Editor。这里是用到了下载好的用作代码重写的插件,通过调用插件中的Api进行代码重写的实现,具体调用并重写的代码一部分如下:
AlignTextCue(“设置怪物”);
//monsterPatrol_Edior.targetCamera = (Camera)EditorGUILayout.ObjectField(“玩家摄像机”, monsterPatrol_Edior.targetCamera, typeof(Camera), true);
monsterPatrol_Edior.monsterObject = (GameObject)EditorGUILayout.ObjectField(“怪物指定”, monsterPatrol_Edior.monsterObject, typeof(GameObject), true);
monsterPatrol_Edior.monsterName = (string)EditorGUILayout.TextField(“怪物预设名字”, monsterPatrol_Edior.monsterName);
monsterPatrol_Edior.monsterLevel = (int)EditorGUILayout.IntField(“怪物预设等级”, monsterPatrol_Edior.monsterLevel);
monsterPatrol_Edior.monsterHP = (float)EditorGUILayout.FloatField(“怪物预设血量”, monsterPatrol_Edior.monsterHP);
//dataControl._camera = (Camera)EditorGUILayout.ObjectField(“摄像机指定”, dataControl._camera, typeof(Camera), true);
dataControl._DeffaultPlyerHp = (float)EditorGUILayout.FloatField(“玩家预设血量”, dataControl._DeffaultPlyerHp);
dataControl._DeffaultPlyerAttck = (int)EditorGUILayout.IntField(“玩家预设攻击”, dataControl._DeffaultPlyerAttck); foldout = EditorGUILayout.Foldout(foldout, “战斗检测参数”,false);
if (foldout)
{
monsterPatrol_Edior.layervalue = EditorGUILayout.LayerField(“怪物可感知层”, monsterPatrol_Edior.layervalue);
monsterPatrol_Edior.dangerNameColor = EditorGUILayout.ColorField(“怪物战斗颜色”, monsterPatrol_Edior.dangerNameColor);
stance = EditorGUILayout.Slider(“怪物追击距离”, monsterPatrol_Edior.followDistance, 0, 10);
}
通过该脚本将原本复杂的代码在Hierarchy中小怪兽挂载的脚本控制变成如图4.1。这样视觉上效果更加清晰,如果开发者在过后想要新添加小怪兽或者其他用户想通过这个项目来对游戏进行一些修改就会十分方便如图可以通过拖拽、挂载的方式加载预制体新建小怪物,设置空物体为它的路线,然后直接通过脚本二次编辑后的界面进行战斗颜色、追击等等相关属性的调节。这里以小怪兽脚本的挂载为例,主角的脚本二次编辑效果同理。
在这里插入图片描述

图4.1 脚本拓展的效果
4.2场景实现
游戏的场景主要是Unity官网的素材,配有设置好的Shader,项目中对场景的控制主要是对场景中的细节进行一些编辑,调节各个游戏对象包括天空、树木的颜色等,调节模块如图4.2。
在这里插入图片描述

图4.2环境调节面板
4.3NPC对话实现
进入游戏开始出现对话框,在脚本Frame中设置变量buttonValue记录用户按下鼠标左键的次数,通过buttonValue的值依次显示对话框中的每一句内容。效果如图4.3。
在这里插入图片描述

图4.3 NPC对话实现
主要代码如下
void Start()
{
inset.DOFade(0, 0);
sq = DOTween.Sequence();
GameDate.Game_data._IsMonUi = false;
Skill.SetActive(false);
}
public void ButtoValue()
{
buttonValue++;
if (_buttonValue == 1)
{
sq.Append(transform.GetComponent ().DOText(“”, .1f));
sq.Append(transform.GetComponent ().DOText(“伊撒尔镇是一个和平宁静的小镇,在它平和的外表下诞生了许多于这个世界披荆斩棘的勇士们。在这个冒险世界中,任何坚毅果敢的勇士多需要不断地磨炼,从迎接他的第一次任务开始。”, 3));
}
4.4人物动作实现
在Project面板中右键选择,创建一个Animator Controller,将人物的多种模型包括动画片段拖拽进Animator Controller窗口中,可以在资源中分别点击人物模型的动作文件预览每一个不同的动作是什么。如图是Animator动画状态机,使用脚本控制一个角色的每一个动作是一个复杂的动作,通过动画状态机方便更加简单的控制和序列化动画。如图4.4。
在这里插入图片描述

图4.4 动画状态机
动画状态机中的每一个箭头都是一个变量,在脚本中是通过各种条件控制的bool值。在动作树中对动作进行拖拽,设置箭头指向它们之间的关系。在代码的控制下人物的动作之间进行过渡。为了动画的过渡更加平滑,游戏的动作实现添加了混合树,在动作树下双击动作的名称进入混合树面板。混合树通过脚本中的混合参数控制人物素材中的每一个动作对于最终效果的权重。混合树如图4.5.
在这里插入图片描述

图4.5 混合树示例
游戏中在Walk中设置了混合树,因为游戏中的很多动作都是在行走的状态中实现的,比如行走时右转、行走时奔跑、行走时拔刀。在混合树种通过x轴与y轴的变换来实现平滑的动画效果。怪物的动作和主角的动作同理。
4.5UI实现
在项目中的Hierarchy面板中右键创建Canvas画布,在2D下的UI面板就在这个画布中显示。找到相应的UI素材存放在Hierarchy面板中的UI文件下,每一张图片都是UI整体的一部分,通过代码控制这些图片的显示。控制UI的脚本主要由三个文件,在Project下Script文件夹中,分别是Frame、SimpleUI、ValueUI,分别控制UI文字对话框的显示、菜单栏(包括重新开始游戏、继续游戏、关闭游戏)以及任务框任务完成后的提示。这些UI与其他模块也息息相关有相通的地方,比如任务面板中的怪物剩余的显示,在怪物相关脚本中和控制怪物的变量联系起来,互相配合完成任务中怪物剩余量的显示。相关代码如下。
void Update()
{
gameObject.GetComponent ().text = GameDate.Game_data._Score.ToString();
if (GameDate.Game_data._Score==5)
{
renwu.text = “任务完成,继续漫游或者退出游戏”;
}
}
4.6战斗碰撞监测实现
首先设置主角的碰撞体设置,在人物上添加刚体组件,设置质量Mass为1; Use Gravity处勾选,表示角色会受到重力的影响;Is Kinematic处取消勾选,这个是采取动力学的选项,如果勾选则角色不再受物理系统的影响;Drag阻力为0,空气阻力是角色运动时收到空气阻力的大小,这个值越大角色运动越慢;。小怪物的刚体组件Rigidbody属性设置与主角的相同。
小怪物和主角的碰撞体选择都是Capsule Collider胶囊碰撞体组件;Material是材质,这里没有拖拽进去其他材质,因为人物模型是预先编辑好的;Radius和Height属性分别对碰撞体组建的半径和高进行设置,这里设置为0.2和1.41。小怪兽的设定和主角的碰撞体设定类似。设置好Unity面板中的这些参数后就可以用脚本来对这些参数进行控制。
private void Update()
{
Debug.Log(PlyerDefault.PlyerHp);
AddDanmeger();
}
public virtual void CombatDetection()
{
Debug.LogWarning(PlyerDefault.PlyerHp);
}
//检测到两个接触到了就会造成伤害
void AddDanmeger()
{
Debug.Log(CollisionObject);
if (CollisionObject == 2 && AnimationKey.damage ==true)
{
GameDate.Game_data._isDamager = true;
AnimationKey.damage = false;
CollisionObject = 0;
}
}
4.7本章小结
本章主要是系统的实现,系统经过一系列设计和编程之后指定的功能大多数处理完毕,然后进行了脚本的二次编辑。到这里整个项目大致完成了,接下来还需要进行游戏测试来完善它。

5. 系统测试

5.1功能测试
为了确保该项目的游戏质量,在游戏制作后需要进行整体的测试,主要就是找到游戏中存有的未发现的bug,这些bug有可能是程序本身逻辑错误造成的,也有可能是代码细节上的小错误,还可能是版本问题和美术资源的问题造成的。修改这些游戏中可能出现的纰漏以及改进游戏中的缺陷来提高游戏的整体质量,使得游戏更加合理,从而有更为良好的游戏体验。
由于在修改bug的过程中本身可能会造成新的bug出现,所以需要不断的进行全面的测试。
5.2人物动作测试
游戏运行中通过鼠标和键盘的按键可以顺畅的控制人物的各种动作,主角的行走、奔跑、奔跑时拔刀、跳跃等等动作都可以在相应按键条件下顺利执行,这些动作在调整好的参数下互相之间切换顺畅,没有出现卡顿,突然变换动作这样不自然的效果。人物攻击的时候,人物施展动作动画碰撞体检测到小怪物,怪物随之掉血。如图5.1和5.2展示了其中两个动作。
在这里插入图片描述

图5.1 角色动作示例1挥刀
在这里插入图片描述

图5.2 角色动作示例2左闪
5.3战斗系统测试
5.4UI测试
打开游戏后按下esc键开始游戏,开始游戏后游戏界面是黑色的,下方弹出对话框,鼠标每点击一下对话框中的内容就会往下变换一次。主角听到有人叫他后NPC的立绘会在对话框上方显示,全部对话结束后立绘和对话框消失,切换到游戏画面中,主角开始可以自由行动。如图5.3。
在这里插入图片描述

图5.3 UI效果示例1
UI界面可以看到主角的剩余血量,血量条是紫色,右方的怪物面板还有右上角的设置主角向怪物区域行走后可以看到小怪物头顶的UI,包含怪物本身的血量和等级。当主角离小怪物比较远的时候小怪物头顶的UI会变小,当主句离小怪物较近的时候小怪物头顶的UI会随之变大,主人物和小怪物互相攻击的时候,血量条会根据双方的攻击造成的伤害递减,每打败一个小怪物右方任务面板的当前进度就会产生变化[27]。
点击右上角的设置,进入菜单栏的UI,可以看到三个按钮。继续游戏,双击继续游戏回到刚才的状态继续运行;重新开始,双击重新开始游戏会回到最开始的样子;返回桌面,双击返回桌面游戏退出。如图5.4,5.5。
在这里插入图片描述

图5.4 UI效果示例2
在这里插入图片描述

图5.5 UI效果示例3
5.5性能测试
在游戏的运行过程中曾经多次出现过BUG,从一开始处理游戏模型时动作衔接不上导致玩家动作怪异,到过程中斜写下的代码没有成功执行效果,每添加一个新的功能都会存在新增BUG的可能。BUG的测试主要通过Debug来进行调节,还有在代码的行前面添加断点,通过在Console上显示的语句和断点所在变量的数值变化考虑代码的问题出现在哪里。
经过多次修改,通过多次运行后检查到游戏没有出现功能上的bug,然后开始对游戏的性能进行测试。
对游戏性能的检测可以通过drawcall来反应。draw在Game面板下的Statue中显示drawcall是由CPU发起由GPU接收的用于绘制渲染的次数,它的数值越大性能占用就会越多。在代码中可以通过控制drawcall的方式来优化性能。比如说使用遮挡剔除,通过设置不同的层级控制不同的景色,在角色行走的过程中将不应该看到的景象剔除掉。
同时也可以观察对性能的占用,打开左上角的Windows,观察其中的Profiler,当游戏运行的时候可以通过Profiler来观察屏幕后面发生了什么并且通过这些信息进行跟踪,查看造成性能问题的原因。
比如说以CPU的占用为例。打开游戏,在游戏运行的时候观察Profiler里面CPU分析器的层次结构图,观察当前帧数哪一项CPU花费时间是最多的。如图可以看到在PlayerLoop下Camera.Render是最耗时的。这样就可以明白性能是怎样随着时间共同变化的。在这里可以通过CPU一帧一帧的寻找单个最为耗时的函数。具体图像如图5.6。同理内存的消耗也可以通过这种方式进行观察。

在这里插入图片描述

图5.6 性能占用数值
该游戏是属于pc端的游戏,通过Profiler可以看出CPU耗时在每帧花费20到80毫秒不等,游戏过程中运行比较通顺,没有感到卡顿的地方。
5.6本章小结
本章的主要内容是进行游戏测试,包括对UI的测试、环境的测试和各个功能之间的测试。通过测试可以发现游戏中的不足。通过一次次的运行调节在项目中发现问题并进行改善,从而提高整个游戏的质量。

结 论

以上就是制作改项目游戏的步骤和一些分析。项目基本达到预期的效果,该游戏将轻松的剧情融入到氛围怡然的游戏场景中,角色在游戏场景中动作切换自然,游戏中一些系统功能的效果也达到了最初的要求。在游戏中用户可以体会战斗的乐趣,在精致的场景中漫游作为休闲。
虽然完成了该游戏的设计和制作,仍然有一些不足的地方。比如C#脚本的语言和算法的编写需要更加精进,代码结构需要更加规整有层次,剧情上有一定需要完善的地方,都有待后续不断提高和加强。
制作这个游戏,涉及到Unity很多基础知识,比如编辑器界面中不同组件和窗口的使用、UI的设计、如何使用物理功能、调试优化脚本,以及一些很方便的小功能如脚本拓展。借助Unity引擎开发的这款游戏,从最开始的游戏规则设计、场景的设计和调节、游戏人物的设计、到游戏中场景和游戏对象交互功能的实现,再到音乐的使用优化和UI的设计,整个游戏制作流程按照规范进行开发,并且它的测试达到了功能设计中的要求。用户在使用Unity游戏引擎进行游戏开发的过程中可以看到清晰的界面和相关说明,便于理解各种使用方法和函数、组件的功能,通过Unity设计出来的游戏在视觉效果上、游戏动作的变换形式和场景变换上都有非常棒的效果。并且能够在很多平台上进行移植,具有很好的交互性,在使用中非常方便。

致 谢

在绿岛的四年光阴似水,满眼繁华,转眼间就到了分别的时候,离开时是剪不断的牵念。每一个故事都会有结局,这四年,快乐也有,悲伤也罢,我总会记得在自己青春最美好的四年里,水上图书馆外宁静的绿岛湖,回宿舍的那一长串林荫道,白卿宫外的梨花。会者定离,一期一祈,脑海里的目光所及,尽是你全部的美好。我记得我曾经在夜晚的二期操场上和小伙伴奔跑,风微凉,草坪上总会看到一些熟悉的面孔;我记得在机电学院齿轮状的大楼里每天的上课下课,和室友同学走在长廊里的每一处,夏日里机电学院很闷热,后来安装了空调,就成了很多人自习喜欢去的地方;我记得校园外的水果摊;记得食堂二楼活动室偶尔传来的乐声;记得校园内的第一树桃花开放后一直到秋天满目枫叶红,每天都会有不同种类的花在绽放;最重要的是,记得大学这些年陪伴过我的每一个人,感谢他们。
相逢一面太匆匆,校里繁花几度红。终于到了这分别的时候,这篇论文也变成了我大学四年的最后一次作业。感谢这四年来遇见的每一个帮助过我的老师,每一个帮助我课业的同学。很遗憾这四年我没有为自己付出的努力更多一些,但也庆幸于这四年来我为自己做出的改变。路漫漫其修远兮,在这里经历的每一段故事,遇见的每一个人都汇聚成我人生的一部分,陪着我向未来走下去。
同时,要感谢这一年帮助我的论文指导老师高丽老师,用心指导我的论文在我有许多错误的时候打电话给我,以及我的实习指导老师聂菲老师在我实习过程中对我的帮助。还有我的系主任郭鸣宇老师和李朋老师以及班主任陈思老师和大学期间的每一个老师在我这四年以来对我的帮助。
我的论文写至这里,画上一个句点。如今别离,且行且珍惜。

参考文献

[1] 李诗瑶, 司占军, 浦英. 交互式虚拟场景在实验室安全教育中的应用与
研究[J]. 电脑知识与技术, 2020,16(04):215-217.
[2] 杨晓虎, 朱颖, 朱珣. 基于Unity3D游戏开发的批处理技术研究[J]. 科
技创新与应用, 2020(03):31-32.
[3] 林佳一. 《Unity3D应用开发》课程教学的探索和实践[J]. 现代计算机,
2020(02):75-78.
[4] 谢宏兰. 基于Unity3D射击游戏的设计与实现[J]. 现代信息科技, 2019,
3(24):89-91+94.
[5] 程弘霖, 杨键, 唐娅雯. 基于Unity3D的VR求生游戏的研究与实现[J].
信息与电脑(理论版), 2019,31(24):88-91.
[6] 陆生贵. 无线游戏体感手柄的研究与设计[J]. 福建电脑, 2019,35(12):62
-65.
[7] 区泽宇, 李晶, 魏菊霞, 严道葵, 陈灿, 许皓然. 基于Unity3D游戏的设
计与开发[J]. 无线互联科技, 2019,16(23):62-63.
[8] 郭子豪, 李灿苹. 基于Unity3D引擎的吃豆人游戏设计[J]. 现代计算机,
2019(34):91-96.
[9] 朱晴. Unity3D开发工具在3D游戏开发中的应用[J]. 电子技术与软件
工程, 2019(22):58-59.
[10] 王春艳, 甘甜, 吴倩莲, 王昱霖, 高伟. 基于Unity 3D的VR英语教育
游戏的设计与开发[J]. 计算机时代, 2019(10):74-77.
[11] 王涛. 基于Unity3D AR体感游戏的设计与实现[J]. 兵工自动化, 2019,
38(09):16-21.
[12] Elizabeth Toriz. Learning based on flipped classroom with just-in-time
3D, gamification and educational spaces[J]. International Journal on Interactive
Design and Manufacturing (IJIDeM), 2019,13(3):12-34.
[13] 李昊宇. 基于Unity3D的横版过关游戏[J]. 电子制作, 2019(16):46-48
+92.
[14] Lu. Zhang,Lian Shuan. Shi. The Platform Design and Implementation of
Campus Fire Safety Knowledge Based on Unity3D[J]. Procedia Computer
Science, 2019,154.
[15] Li Yunwang,Dai Sumei,Shi Yong,Zhao Lala,Ding Minghua. Navigation
Simulation of a Mecanum Wheel Mobile Robot Based on an Improved A*
Algorithm in Unity3D.[J]. Sensors (Basel, Switzerland), 2019,19(13):62-63.
[16] Unity Technologies. Unity 4.X从入门到精通[M]. 天津:中国铁道出版社, 2013.11.28
[17] [美]史蒂夫·迈克康奈尔. 《代码大全(第二版)》[M]. 北京:电子工
业出版社, 2006
[18] 程杰. 大话设计模式[M]. 北京:清华大学出版社, 2007
[19] 罗培羽. Unity3D网络游戏实战(第2版)[M]. 北京:机械工业出版社, 2019
[20] 冯乐乐. Unity Shader入门精要[M]. 北京:人民邮电出版社, 2016
[21] 商宇浩. Unity5.x完全自学手册[M]. 北京:电子工业出版社, 2016
[22] 蔡升达. 设计模式与游戏完美开发[M]. 北京:清华大学出版社, 2017
[23] [美]Wendy Despain. 游戏设计的100个原理[M]. 北京: 人民邮电出版社, 2015.
[24] [美]Wendy Despain. 游戏设计的100个原理[M]. 北京: 人民邮电出版社, 2015.
[25] [美]Robert Sedgewick. 算法[M]. 第4版.北京:人民邮电出版社, 2012
[26] 朱柱. 基于 Unity3D 的虚拟实验系统设计与应用研究[D]. 武汉:华中师范大学, 2012.
[27] Zhuoran Li,Jing Wang,Muhammad Shahid Anwar,Zhongpeng Zheng. An efficient method for generating assembly precedence constraints on 3D models based on a block sequence structure[J]. Computer-Aided Design,2020,118.

附录 源程序清单

//怪物检测
public class MonsterWarn : CombatControl
{
public GameObject MonsteWeapon;
private void OnCollisionEnter(Collision collision)
{
CombatDetection();
}
public override void CombatDetection()
{
CollisionObject = 1;
}
}
// 控制全局数据
public class GameDate : MonoBehaviour
{
public static GameDate Game_data;
public float _DeffaultPlyerHp = 200;
public int _DeffaultPlyerAttck = 1;
public int _Score;
public Camera _camera ;
public bool _IsUi=false;
public bool _IsMonUi = true;
public bool _isDamager = false;
private void Awake()
{
//实例化单例
Game_data = this;
//设置整数
Application.targetFrameRate = 120;
_Score = 0;
Cursor.lockState = CursorLockMode.Locked;//
Cursor.visible = false;//隐藏指针
}
}
public void AddMonsterControl()
{
gameObject.AddComponent();
}
public void AddPlyerDefault()
{
gameObject.AddComponent();
}
//战斗控制
//先对玩家和怪物进行检测 然后进行战斗控制
public class CombatControl : MonoBehaviour
{
public int CollisionObject
{
get;
set;
}
private void Update()
{
Debug.Log(PlyerDefault.PlyerHp);
AddDanmeger();
}
public virtual void CombatDetection()
{
Debug.LogWarning(PlyerDefault.PlyerHp);
}
//检测到两个接触到了就会造成伤害
void AddDanmeger()
{
Debug.Log(CollisionObject);

    if (CollisionObject == 2 && AnimationKey.damage ==true)
    {
        GameDate.Game_data._isDamager = true;
        AnimationKey.damage = false;

        CollisionObject = 0;
    }
}

}

//控制怪物自动巡逻、进入/脱离战斗、检测敌人
public class MonsterControl : MonoBehaviour
{
//public List test = new List();
[Header(“设置需要控制的怪物”)] //在面板里显示一个标题,这个标题显示在下方的第一个变量前面
public GameObject monsterObject;
[Header(“设置怪物预设血量”)]
public float monsterHP = 100;

[Header("设置怪物预设伤害")]
public static float monsterDanger = 1f;

[Header("设置怪物预设移速")]
public float monsterMoveSpeed = 1f;

[Header("设置怪物预设攻击间隔")]
public float AttackTimeGap = 2f;

[Header("设置怪物遇敌颜色")]
public Color dangerNameColor;

[Header("设置怪物脱战颜色")]
public Color safetyNameColor;

[Header("设置怪物预设攻击间隔")]
public float followDistance = 2f;

[Header("设置需要移动的路径点")]
//数组只能查、改 ,链表可以增、删、查、改
public List<Transform> aiPatrolPath = new List<Transform>();

[Header("设置怪物需要搜索的层")]
public LayerMask layer;
public int layervalue = 0;

[Header("设置怪物搜索的范围")]
public int dangerRange = 4;

[Header("设置脱离战斗距离")]
public int safetyRange = 8;

[Header("Canvas")]
public Canvas canvas;

[Header("设置怪物预设名称")]
public string monsterName;

[Header("设置怪物预设等级")]
public int monsterLevel;

[Header("设置怪物UI的偏移")]
public Vector3 uIPosOffset = new Vector3(0,1,0);

//[Header("设置怪物名称3D文本看向的摄像机")]
//public Camera targetCamera;

[Header("怪物决断参数(无需设置)")]
public bool IsCombat = true;
public int path = 0;     //记录路径点的值
public Collider[] plyer; //记录相交球碰撞信息
public int rangeValue;   //接受碰撞变化的值
public Transform NewPath;//用来添加新路径点
public Animator monsterAima; //动画组件
public bool isFollow = true;  //用来判断怪物追击
public float AttackTimeGapValue = 0f; //用来记录间隔时间
private GameObject ui_Clon; //克隆UI
public float uIShowDis = 15f;  //用来控制血条显示距离
public float uIScale = 2f;    //用来控制血条缩放系数
public int IsUIScaleSwitch = 0;
private float monsterHPMax;   //用来记录怪物的满血状态


private void Awake()
{
    ClonUI();
    dangerNameColor = Color.red;
    safetyNameColor = Color.green;
    //获取拖拽进来的怪物的动画组件
    monsterAima = monsterObject.GetComponent<Animator>();
    LogError();
    monsterHPMax = monsterHP;
}

void Update()
{
    Debug.Log(monsterObject);
    //调用自动巡逻方法
    AutoPatrol();

    //怪物警戒
    InsightFoe();

    die();

    IsDamager();
}
private void LateUpdate()
{
    //怪物名称的展示
    MonsterName();
    ClonUIImagerValue();
    IsMonsterUi();
}

void AutoPatrol()
{
    //没有检测到玩家时进入巡逻
    if (IsCombat == true)
    {
        //巡逻时,怪物名字颜色
        ui_Clon.transform.GetChild(1).transform.GetChild(0).GetComponent<Text>().color = safetyNameColor;

        //缩小触发战斗碰撞
        rangeValue = dangerRange;

        //动画状态机
        monsterAima.SetBool("IsAttck_Run", false);

        //移动到指定路径点
        monsterObject.transform.position = Vector3.MoveTowards(monsterObject.transform.position, new Vector3(aiPatrolPath[path].transform.position.x, monsterObject.transform.position.y, aiPatrolPath[path].transform.position.z), Time.deltaTime * monsterMoveSpeed * .4f);
       
        //转向下一个路径点
        monsterObject.transform.LookAt(new Vector3(aiPatrolPath[path].transform.position.x, monsterObject.transform.position.y, aiPatrolPath[path].transform.position.z));
       
        //判断是否移动路径点,然后转向下一路径点
        if (Vector3.Distance(monsterObject.transform.position, aiPatrolPath[path].transform.position) <=.5f)
        {
            path++;
        }
        //归零路径点,循环巡逻
        if (path == aiPatrolPath.Count)
        {
            path = 0;
        }
    }

    //检测到玩家时
    else if (IsCombat == false)
    {
        //战斗时怪物名字颜色
        ui_Clon.transform.GetChild(1).transform.GetChild(0).GetComponent<Text>().color = dangerNameColor;

        //扩大脱离战斗碰撞
        rangeValue = safetyRange;

        //始终看向玩家
        monsterObject.transform.LookAt(new Vector3(plyer[0].gameObject.transform.position.x, monsterObject.transform.position.y, plyer[0].gameObject.transform.position.z));

        //计算玩家与怪物之间的距离
        float distance = Vector3.Distance(monsterObject.transform.position, plyer[0].transform.position);

        //如果玩家与怪物之间距离大于1
        if (distance > 1f && isFollow == true)
        {
            //动画状态机的控制
            monsterAima.SetBool("IsAttck_Run", true);
            monsterAima.SetBool("IsAttck_Idle", false);
            //移动到指定路径点
            monsterObject.transform.position = Vector3.MoveTowards(monsterObject.transform.position, new Vector3(plyer[0].gameObject.transform.position.x, monsterObject.transform.position.y, plyer[0].gameObject.transform.position.z), Time.deltaTime * monsterMoveSpeed);

        }
        //如果玩家与怪物之间距离小于1
        if (distance <= 1f)
        {
            //关闭追击
            isFollow = false;

            //动画状态机控制
            monsterAima.SetBool("IsAttck_Idle", true);
            monsterAima.SetBool("IsAttck_Run", false);
            monsterAima.SetFloat("IsAttck", AttackTimeGapValue);

            //如果计时器大于指定间隔,执行一次攻击
            if (monsterAima.GetFloat("IsAttck")> AttackTimeGap)
            {
                PlyerDefault.PlyerHp -= MonsterControl.monsterDanger;
                AttackTimeGapValue = 0;
            }
        }
        //如果玩家距离与怪物距离大于2
        if (distance > followDistance && isFollow == false)
        {
            //开始追击
            isFollow = true;

            //计时器归零
            AttackTimeGapValue = 0;
        }
        //调用计时协程
        StartCoroutine(timeLoad());
    }

}

//通过协程(多线程)计时来决定攻击的时间间隔
IEnumerator timeLoad()
{
    yield return AttackTimeGapValue += Time.deltaTime;
}


//怪物检测玩家
void InsightFoe()
{
    //通过相交球检测玩家是否处在怪物警戒范围
    plyer = Physics.OverlapSphere(monsterObject.transform.position, rangeValue,  layer = 1 << layervalue );

    //如果检测到玩家,中断巡逻
    if (plyer.Length != 0)
    {
        IsCombat = false;
    }
    //如果玩家离开战斗范围,重新回到巡逻
    else
    {
        IsCombat = true;
    }
}

//怪物UI配置
void MonsterName()
{
    //转换怪物为屏幕坐标
    Vector3 pos = Camera.main.WorldToScreenPoint( monsterObject.transform.position + uIPosOffset) + new Vector3(-Screen.width * .5f, -Screen.height * .5f) ;

    //让ui的屏幕位置等于怪物的屏幕坐标位置
    ui_Clon.transform.localPosition = pos;

    //获取摄像机的向量
    Transform camPos = Camera.main.transform;

    //如果脚本上选择的是默认缩放模式
    if (IsUIScaleSwitch==0)
    {
        //让UI根据距离远近适当缩放
        ui_Clon.transform.localScale = (new Vector3(1f, 1f, 1) / Vector3.Distance(camPos.position, monsterObject.transform.position) * uIScale);
    }

    //将怪物的3D坐标位置减去摄像机的位置,并且归一化向量
    Vector3 dir = (monsterObject.transform.position - camPos.position).normalized;

    //将摄像机的位置与单位向量点积运算,判断怪物是否在摄像机前方
    float dot = Vector3.Dot(camPos.forward, dir);

    //如果满足怪物在摄像机前方而且还在指定范围内
    if (dot > 0 && Vector3.Distance(camPos.position, monsterObject.transform.position) < uIShowDis)
    {
        //那么显示UI
        ui_Clon.SetActive(true);
    }
    //否则不显示
    else ui_Clon.SetActive(false);
}



//克隆ui并获取生成信息
public void ClonUI()
{
    GameObject loadUI = (GameObject)Resources.Load("Monster_UI");
    ui_Clon = Instantiate(loadUI);
    ui_Clon.name = monsterObject.name;
    ui_Clon.transform.SetParent(canvas.transform);
    ui_Clon.transform.GetChild(1).transform.GetChild(0).GetComponent<Text>().text = monsterName ;
    ui_Clon.transform.GetChild(2).transform.GetChild(0).GetComponent<Text>().text = monsterLevel.ToString();
}

//血条
public void ClonUIImagerValue()
{

    ui_Clon.transform.GetChild(1).transform.GetComponent<Image>().fillAmount = monsterHP/monsterHPMax;
}


//在编辑场景中绘制相交球
void OnDrawGizmos()
{
    Gizmos.color = Color.red;
    Gizmos.DrawSphere(monsterObject.transform.position, rangeValue);
}

//用来在编辑模式下创建新路径点的方法
public void addPath()
{
    aiPatrolPath.Add(NewPath);
}

//用来在编辑模式下删除路径点的方法
public void RemovePath()
{
    for (int i = 0; i < aiPatrolPath.Count; i++)
    {
        if (i == aiPatrolPath.Count - 1)
        {
            aiPatrolPath.Remove(aiPatrolPath[i]);
        }
    }

}

//
public void die() 
{
    if (monsterHP<=0)
    {
        GameDate.Game_data._Score++;
        Destroy(monsterObject);
        Destroy(ui_Clon);
    }
}

public void IsMonsterUi()
{
    if (GameDate.Game_data._IsMonUi == true)
    {
        ui_Clon.SetActive(true);
    }
    else if (GameDate.Game_data._IsMonUi == false)
    {
        ui_Clon.SetActive(false);
    }
}

public void IsDamager()
{
    if (GameDate.Game_data._isDamager == true) 
    {
        monsterHP -= PlyerDefault.PlyerAttck;
        GameDate.Game_data._isDamager = false;
    }


}


public void LogError()
{
    if (canvas==null)
    {
        Debug.LogError("还没有在脚本中指定UI图层即Canvas");
    }

    if (monsterObject == null)
    {
        Debug.LogError("还没有指定游戏对象");
    }

    if (aiPatrolPath.Count > 1)
    {
        Debug.Log("还没有在脚本中指定路径点");
    }

    if (layervalue == 0)
    {
        Debug.LogError("还没有配置搜索层级");
    }

}

//玩家的行为树

public class PlayerContor : MonoBehaviour
{
[Header(“获取Animator组件”)][Space]
[Tooltip(“获取到的动画状态机”)][Space]
public Animator playerAnimatorContor;

[Header("行为树X和Y的参数")][Space]
[SerializeField] private float xValue;
[SerializeField] private float yValue;

[Header("角色动作状态")][Space]
private float stateValue;

[Header("角色旋转时的移动速度")][Space]
public float moveSpeed_Rotate = .3f;

[Header("角色行走时的移动速度")][Space]
public float moveSpeed_Walk = .6f;

[Header("角色奔跑时的移动速度")][Space]
public float moveSpeed_Run = 2.5f;

[Header("角色攻击状态时的移动速度")][Space]
public float moveSpeed_Attack = 1.7f;


void Awake()
{
    //获取模型Animator组件
    playerAnimatorContor = this.gameObject.GetComponent<Animator>();
    //行为树x和y的默认值
    xValue = 0;
    yValue = 0;
    //设置角色默认行为状态
    stateValue = 0;
}



void Update()
{
    //调用绑定X Y的行为树方法
    Ainima_Contor();
    //调用按键控制和逻辑判断方法
    Button_Contor();
    //执行关键帧事件(在播放指定放动画时不能移动)
    if (AnimationKey.IsOverIdle == true)
    {
        //调用移动速度方法
        Move_Contor();
    }
}



//把xValue和yValue绑定给行为树的X和Y
private void Ainima_Contor()
{
    playerAnimatorContor.SetFloat("X", xValue);
    playerAnimatorContor.SetFloat("Y", yValue);
}


//移动速度的方法

private void Move_Contor()
{
    //如果鼠标横向拖动距离等于一定值,且玩家角色状态大于0
    if (Input.GetAxis("Mouse X") > 0.8f || Input.GetAxis("Mouse X") < -.8f || stateValue > 0)
    {
        //让角色的旋转角度等于鼠标拖动的距离
        this.gameObject.transform.Rotate(new Vector3(0, Input.GetAxis("Mouse X"), 0));

        //如果角色是待机状态
        if (stateValue == 0)
        {
            //设置移动速度
            this.transform.Translate(Vector3.forward * moveSpeed_Rotate * Time.deltaTime, Space.Self);
            //让待机旋转时播放行走动画
            playerAnimatorContor.SetBool("IsWalk", true);
        }

        //如果角色是步行状态
        if (stateValue == 1)
        {
            //设置前进和左右转移动速度
            if (Input.GetKey(KeyCode.W))
            {
                this.transform.Translate(Vector3.forward * moveSpeed_Walk * Time.deltaTime, Space.Self);
                if (Input.GetKey(KeyCode.A))
                {
                    this.transform.Translate(new Vector3(-1, 0, 0) * moveSpeed_Walk * Time.deltaTime, Space.Self);
                }
                else if (Input.GetKey(KeyCode.D))
                {
                    this.transform.Translate(new Vector3(1, 0, 0) * moveSpeed_Walk * Time.deltaTime, Space.Self);
                }
            }
            //设置后退和左右转移动速度
            else if (Input.GetKey(KeyCode.S) || Input.GetKey(KeyCode.LeftShift))
            {
                this.transform.Translate(Vector3.back * moveSpeed_Walk * Time.deltaTime * 1f, Space.Self);
                if (Input.GetKey(KeyCode.A))
                {
                    this.transform.Translate(new Vector3(-1, 0, 0) * moveSpeed_Walk * Time.deltaTime, Space.Self);
                }
                else if (Input.GetKey(KeyCode.D))
                {
                    this.transform.Translate(new Vector3(1, 0, 0) * moveSpeed_Walk * Time.deltaTime, Space.Self);
                }
            }
        }
        //如果角色是奔跑状态
        if (stateValue == 2)
        {
            //设置前进和左右转移动速度
            if (Input.GetKey(KeyCode.W))
            {
                this.transform.Translate(Vector3.forward * moveSpeed_Run * Time.deltaTime, Space.Self);
                if (Input.GetKey(KeyCode.A))
                {
                    this.transform.Translate(new Vector3(-1, 0, 0) * moveSpeed_Run * Time.deltaTime, Space.Self);
                }
                else if (Input.GetKey(KeyCode.D))
                {
                    this.transform.Translate(new Vector3(1, 0, 0) * moveSpeed_Run * Time.deltaTime, Space.Self);
                }
            }
        }

        if (stateValue == 3)
        {


            //设置前进和左右转移动速度
            if (Input.GetKey(KeyCode.W))
            {
                this.transform.Translate(Vector3.forward * moveSpeed_Attack * Time.deltaTime, Space.Self);
                if (Input.GetKey(KeyCode.A))
                {
                    this.transform.Translate(new Vector3(-1, 0, 0) * moveSpeed_Attack * Time.deltaTime * .7f, Space.Self);
                }
                else if (Input.GetKey(KeyCode.D))
                {
                    this.transform.Translate(new Vector3(1, 0, 0) * moveSpeed_Attack * Time.deltaTime * .7f, Space.Self);
                }
            }
            else if (Input.GetKey(KeyCode.S))
            {
                if (xValue == 0)
                {
                    this.transform.Translate(Vector3.back * moveSpeed_Attack * Time.deltaTime * .4f, Space.Self);
                }
                else
                {
                    playerAnimatorContor.SetBool("IsWalk", false);
                }

                if (yValue < 0)
                {
                    xValue = 0;
                    this.transform.Translate(Vector3.back * moveSpeed_Attack * Time.deltaTime * .4f, Space.Self);
                }
            }
        }
    }
}



//动画按钮状态和逻辑判断
private void Button_Contor()
{
    //基础移动控制——如果按下 W 或者 S
    if (Input.GetKey(KeyCode.W) || Input.GetKey(KeyCode.S))
    {
        //此时状态为行走状态
        if (playerAnimatorContor.GetBool("IsDraw") == true)
        {
            stateValue = 3;
        }
        else if (playerAnimatorContor.GetBool("IsDraw") != true)
        {
            stateValue = 1;
        }

        //移动时,让移动逻辑为真(即让Walk动画播放)
        playerAnimatorContor.SetBool("IsWalk", true);
        //移动时讲Vertical和Horizontal分别绑定给xValue和yValue(即绑定给行为树的X和Y)
        yValue = Input.GetAxis("Vertical");
        xValue = Input.GetAxis("Horizontal");
    }
    //退出移动控制
    else
    {
        //此时状态为待机状态
        stateValue = 0;
        //停止移动,让移动逻辑为否 (即还原为待机动画)
        playerAnimatorContor.SetBool("IsWalk", false);
        //通过差值运算平滑的让X和Y归零
        yValue = Mathf.Lerp(Input.GetAxis("Vertical"), 0, 0f);
        xValue = Mathf.Lerp(Input.GetAxis("Horizontal"), 0, 0f);
    }


    //奔跑——如果按下 LeftShift
    if (Input.GetKey(KeyCode.LeftShift))
    {
        //此时状态为奔跑状态
        if (playerAnimatorContor.GetBool("IsWalk") == true && yValue > .8f && stateValue < 3)
        {
            stateValue = 2;
            //奔跑时,让奔跑逻辑为真
            playerAnimatorContor.SetBool("IsRun", true);
        }
        else if (yValue < .8f)
        {
            //奔跑时,让奔跑逻辑为真
            playerAnimatorContor.SetBool("IsRun", false);
        }
    }
    else
    {
        //停止 LeftShift 按键时,让奔跑逻辑为否
        playerAnimatorContor.SetBool("IsRun", false);
    }

    //拔刀——如果按下 F
    if (Input.GetKeyDown(KeyCode.F) && playerAnimatorContor.GetBool("IsDraw") == false)
    {
        //此时状态为拔刀攻击状态
        stateValue = 3;
        //拔刀时,让拔刀逻辑为真
        playerAnimatorContor.SetBool("IsDraw", true);
    }
    else if (Input.GetKeyDown(KeyCode.F) && playerAnimatorContor.GetBool("IsDraw") == true)
    {
        //停止 F 按键时,让拔刀逻辑为否
        playerAnimatorContor.SetBool("IsDraw", false);
    }

    //跳跃——如果按下 Space
    if (Input.GetKey(KeyCode.Space))
    {
        //此时状态为跳跃状态
        stateValue = 4;
        //跳跃时,让跳跃逻辑为真
        playerAnimatorContor.SetBool("IsJump", true);
    }
    else
    {
        //停止 Space 按键时,让跳跃逻辑为否
        playerAnimatorContor.SetBool("IsJump", false);
    }

    //左闪
    if (Input.GetKeyDown(KeyCode.Q))
    {
        playerAnimatorContor.SetBool("IsLAviod", true);
    }
    else
    {
        playerAnimatorContor.SetBool("IsLAviod", false);
    }
    //右闪
    if (Input.GetKeyDown(KeyCode.E))
    {
        playerAnimatorContor.SetBool("IsRAviod", true);
    }
    else
    {
        playerAnimatorContor.SetBool("IsRAviod", false);
    }
    //普通攻击
    if (Input.GetMouseButtonDown(0))
    {
        playerAnimatorContor.SetBool("IsSkill_One", true);
    }
    else
    {
        playerAnimatorContor.SetBool("IsSkill_One", false);
    }
    //重击
    if (Input.GetMouseButtonDown(1))
    {
        playerAnimatorContor.SetBool("IsSkill_Two", true);
    }
    else
    {
        playerAnimatorContor.SetBool("IsSkill_Two", false);
    }

    if (Input.GetKeyDown(KeyCode.Alpha1))
    {
        playerAnimatorContor.SetBool("IsSkill_1", true);
    }
    else
    {
        playerAnimatorContor.SetBool("IsSkill_1", false);
    }
}

}
void Start()
{
//_BuutonA.GetComponent();
}

public void Setting()
{
    GameDate.Game_data._IsMonUi = false;
    _task.gameObject.SetActive(false);
    _BuutonA.gameObject.SetActive(true);
    _BuutonB.gameObject.SetActive(true);
    _BuutonC.gameObject.SetActive(true);
    _Bg.gameObject.SetActive(true);
    Sequence sq = DOTween.Sequence();
    sq.Append(_Bg.DOFade(1, .1f));
    sq.Append(_BuutonA.transform.DOLocalMove(new Vector3(0, 0, 0), 0.1f));
    sq.Append(_BuutonB.transform.DOLocalMove(new Vector3(0, 0, 0), 0.1f));
    sq.Append(_BuutonC.transform.DOLocalMove(new Vector3(0, 0, 0), 0.1f));

}
Logo

开放原子开发者工作坊旨在鼓励更多人参与开源活动,与志同道合的开发者们相互交流开发经验、分享开发心得、获取前沿技术趋势。工作坊有多种形式的开发者活动,如meetup、训练营等,主打技术交流,干货满满,真诚地邀请各位开发者共同参与!

更多推荐