基于Unity开发的联机解谜游戏的设计与实现

目录

1 关技术概述

1.1 Mirror

1.2 Unity3D游戏引擎

1.3 MySQL数据库

1.4 C#开发语言

1.5 本章小结

2 需求分析

2.1 玩家需求分析

2.2 游戏功能需求分析

2.3 游戏结构设计

2.3.1 游戏剧情设计

2.3.2 谜题模块设计

2.3.3 游戏场景设计

2.4 可行性分析

2.5 本章小结

3 游戏功能实现

3.1 玩家人物的设计与实现

3.2 场景物品的设计与实现

3.2.1 载入场景设计

3.2.2 游戏场景设计

3.2.2.1 剧情与玩法设计

3.2.2.2 道具设计

3.2.2.3 不同NPC设计

3.2.2.4 解谜机关设计

3.2.2.5 载人载具设计

3.2.2.6 积分奖励机制设计

3.2.2.7 数据库与榜单设计

3.2.2.8 联机核心方法论述

4 游戏测试

4.1 测试环境

4.2 功能测试

4.3 局域网线上测试

5 相关资源链接:


1 关技术概述

第一章主要介绍实现游戏开发的相关技术和基础理论,最主要的就是局域网联机功能,同时也会介绍使用到的数据库和相关的Unity技术。

1.1 Mirror

Mirror是一个为Unity游戏构建多人游戏功能的系统。它建立在较低级别的传输实时通信层之上,并处理多人游戏所需的许多常见任务。传输层支持任何类型的网络拓扑,而 Mirror 是权威服务器系统,它允许参与者之一同时是客户端和服务器,所以不需要专门的服务器进程。与互联网服务一起工作,这使得多人游戏可以在互联网上玩,而开发人员几乎不需要做任何工作。如图1.1.1所示是Mirror的构成。

图1.1.1 镜像的一系列层

如图1.1.1所示,Mirror由一系列增加功能的层所构成。不难发现Mirror专注于易用性和迭代开发,并为多人游戏提供有用的功能,例如图中的消息处理程序、通用高性能序列化、分布式对象管理、状态同步、服务器与客户端的连接等。

服务器是所有其他玩家想要一起玩时连接到的游戏实例,通常管理游戏的各个方面,例如记分,并将该数据传输回客户端。客户端是游戏的实例,通常从不同的计算机连接到服务器,可以通过本地网络或在线连接。如图1.1.2展示了Mirror的连接方式。

图1.1.2 玩家连接示意图

如图1.1.2展示了使用Mirror开发的多人游戏中三名玩家的连接方式。在这个游戏中,一个客户端也充当主机,这意味着客户端本身就是“本地客户端”。本地客户端连接到主机服务器,两者运行在同一台计算机上。另外两个玩家是远程客户端,也就是说,他们在不同的计算机上,连接到主机服务器。

1.2 Unity3D游戏引擎

Unity3D是由Unity Technologies公司开发的专业虚拟交互式引擎。相比于其他游戏引擎,Unity最大的特点便是其多平台开发。在最新版本引擎中,现已支持包括Windows、Mac OS x、iOS、Android、PlayStation 3、PlaySta-tion 4、PlayStation Vita、Xbox 360、Xbox ONE、Wii U、Windows Store、WindowsPhone、Oculus Rift、Gear VR、Web GL和Web Palyer等诸多平台,用户只需进行一次开发,便可以发布至大部分主流平台中。

Unity引擎不仅支持C#、JavaScript两种脚本语言,同时还支持几乎所有美术资源文件格式。特别需要指出的是,Unity引擎还提供了一个网上资源商店(AssetStore),用户可以在这个平台上购买和销售包括3D模型、材质贴图、脚本代码、音效和UI界面扩展插件等Unity相关资源。通过Unity引擎,开发者可以在短时间内制作出一款高质量的商业项目,也可以通过Asset Store销售自己制作的产品,获得利润。本游戏设计时也参考一些安卓游戏和网络游戏的开发设计。

同时Unity也具备一个交互感良好的操作界面,该软件自带五个主要工程视图: project 视图,主要用于存放游戏中的资源文件, hierarchy 视图用于布置我们游戏每一个场景中的游戏对象,inspector视图是我们观察当前游戏资源的主要信息,包括其世界坐标位置、物品属性、加载的脚本等,scene视图主要存放我们游戏所需的各类资源文件,game视图用于我们测试和观察游戏实际运行后的整体情况。

1.3 MySQL数据库

数据库可以将我们需要保存的数据保存下来,每个项目的开发都离不开数据的支撑,数据库的操作也是非常重要的。我们通常熟知的数据库有很多,比如MySQL、Oracle、SQLite等等,数据库的选择也有很多的因素,包括了数据量、实时性、可靠性、一致性的要求,本次项目于选择的数据库是MySQL。

选择MySQL作为本次的数据库主要原因有以下几点:第一点,MySQL是我自入大学以来第一个接触到的数据库,经过长时间的学习,对MySQL有了很深的认识,相比于其他的数据库,在开发时可以大幅度的节约时间,从而高效快速的完成项目。第二点,就是MySQL本身的优势,MySQL是一种开放源代码的关系型数据库管理系统(RDBMS),使用最常用的数据库管理语言即结构化查询语言(SQL)进行数据库管理。并且MySQL是开放源代码的,因此任何人都可以在General Public License的许可下下载并根据个性化的需要对其进行修改。MySQL因为其速度、可靠性和适应性而备受关注,大多数人都认为在不需要事务化处理的情况下,MySQL是管理内容最好的选择。虽然MySQL还是有一些局限性的,它的功能相比与Oracle来说是弱一些的,但是MySQL胜在当操作简单时性能还不错,而且它的体积相对较小一些,也更加的方便。

另外,使用命令行来操作MySQL是较为复杂的,所以在创建表和自定义数据等操作时,我使用了Navicat。Navicat是一个可视化的可以对数据库进行管理的工具,在连接到数据库之后,可以创建项目对应的数据库,然后创建所需要的表,自定义存储的格式与数据类型,非常方便。

1.4 C#开发语言

C#是微软公司于2000年发布的一种在NET Framework和.NET Core运行的,面向对象的高级程序设计语言。C#与Java类似,是一种面对对象语言,一种编程语言工具,并且与JAVA语言有着几乎一模一样的语法使用习惯,但与JAVA不同的是,C#借鉴了Delphi,与com是直接集成的。但由于Unity是跨平台的,而C#并不是一种跨平台的语言。在unity开发中只是由于Mono的重新实现,使得Unity能够使用C#来开发。由于C#语法明了和类库使用方便的优点,所以这也成为了广大开发者们在Unity开发中首先考虑的语言。在本次设计中也将使用C#参考一些程序实现案例完成游戏开发与数据库的连接。

1.5 本章小结

本章节主要介绍了本次游戏开发所使用的开发工具与相关的技术支持,从技术层面来说明开发本系统的可行性是成立的。根据Unity跨平台优势,Mirror实现联机功能的特性以及进行MySQL和C#的特点介绍总结优点。Unity3D引擎是一款非常优秀的游戏开发引擎,近年来在游戏开发领域及建筑可视化,实现三维动画等相关类型互动内容领域受到越来越多的开发者们追捧使用,其未来布局前景也将十分广阔。MySQL数据库的使用,从创建数据库到建表,逻辑的关系要非常的严谨。

2 需求分析

2.1 玩家需求分析

作为一款解密游戏,要具备解谜游戏的几大重要要素:剧情、谜题、道具,同时这也是吸引玩家的关键要素。因此我们整体的游戏功能要围绕着这几个重要要素去设计。玩家对于剧情最直观的感受来自于谜题线索,道具等内容,因此我们要做到剧情合理不突兀,谜题逻辑清晰,道具线索动向清晰明了。一个完整且合理的剧情不仅能引导玩家一步步推进游戏进度,随着玩家不断的探索解密拼凑一个完整的故事剧情,在这其中的探索解密过程能激发玩家解密的动力跟热情,且得到一种满足感。在具体游戏体验中要做到操作简单方便,交反馈突出等,这样才能保证游戏的可玩性。

2.2 游戏功能需求分析

本游戏由于是开放世界类解密游戏,其核心玩法在于在一个极大的场景中解开设置的谜题,然后根据线索逃出生天。既然是开放世界,必然有着复杂的地形环境与大型的地图供玩家探索,并在地图上设置谜题和一些线索来引导玩家,同时根据玩家的不同行为触发不同的剧情,从而让玩家有更好的体验。在本游戏中还加入了一定的生存元素,玩家在死亡后会回到出生点,以丧失一定的剧情进度作为惩罚,在一定程度上为玩家带来操作感,也使得游戏更加紧张刺激。

2.3 游戏结构设计

2.3.1 游戏剧情设计

游戏剧情采用浮现式叙事的方法,逐步为玩家揭开真相,玩家可以通过自己的选择产生不同的剧情分支,主要设计为:飞机失事坠落荒岛,这是一座被诅咒的岛屿,玩家需要扮演幸存者寻找离开岛屿的办法,会有僵尸群追赶玩家,玩家可以在岛上玩家可以寻找强力武器、越过坦克守卫、穿越幽灵迷宫、拾取弹药和恢复血量的食物以及加强人物属性的祝福,有独特的NPC小火龙可以庇护玩家不被僵尸发现,地图上有石碑解密方阵,需要鼠标点击旋转正确位置才能解开,逃生点的必经之路上有巨龙守护,击杀巨龙或者在祭坛向巨龙供奉才能逃离。

2.3.2 谜题模块设计

根据前文讨论这是我们核心功能,这里实现三个谜题模块,分别为:石碑谜题模块,线索谜题模块,迷宫谜题模块。

石碑谜题模块,场景中有悬浮等距排列的石碑方块,方块前后左右的表面刻有文字,玩家需要将石碑旋转至正确角度才能获取剧情线索。这里我们会使用unity3D引擎自带的点击触发器Event Trigger来检测鼠标是否点击,并编写相应代码控制自身与左右相邻方块的旋转。

线索机关谜题模块,在场景中设置多个告示牌提供一定的线索引导玩家前往特定地点,包括前往森林捕捉狼从而获得肉食才能在祭坛献上贡品,特殊道具的位置提示信息,不同剧情的触发条件提示,前往逃生点的路线等等。告示牌信息多采用隐晦的提示,需要玩家结合其他告示板和其他谜底方可推测正确的路径或规避不必要的风险。场景中还有许多机关,比如能大幅跨越地图或者让玩家离开无法正常离开的危险境地的传送门,能开启隐藏道路的魔法石等等。

迷宫谜题模块,场景中的必经之路上有一座迷宫,门前有炮台守护,玩家需要快速穿越进入迷宫,玩家还可以获取手电筒减少迷宫探索的难度,在迷宫中有游荡的幽灵,幽灵发现玩家后会缓慢追逐玩家,幽灵接触玩家会使玩家持续损失血量,玩家需要尽可能躲避幽灵穿越迷宫,这里将会使用unity3D引擎自带的Navigation自动寻路控件来实现。另外,玩家可以拾取迷宫中的特殊道具与金币,从而获得更强大的属性与更多的积分。

2.3.3 游戏场景设计

开放世界游戏场景为一个极大的场景,场景开放,根据背景故事本游戏作品中的场景选择为欧洲中世纪风格场景,许多材质选用色彩较暗的石砖类材质,同时因为是荒岛,许多地方都有丛林,河流与湖泊还有山脉也是不可或缺的地貌。场景素材在游戏开发过程开发者通常使用3D建模软件C4D,3DMAX等创作模型素材,本文作品的场景模型素材大都采用网上免费模型进行开发,unity3D支持常规FBX、MAX等常见格式的模型导入。

2.4 可行性分析

通过Unity3D引擎搭建开放世界场景并设计各种解谜环节,灵活运用C#语言编写各种道具的交互与地图上敌方AI的巡逻、侦测与攻击逻辑,以及地图上小道具的随机生成,人物的传送与场景切换,并且结合动画控制器完成各种不同的动画效果,如走路、奔跑、攻击、发射炮弹、死亡等。玩家可以操作角色移动、跳跃、用不同的武器攻击以及拾取道具。同时利用各种协同程序、预设体、碰撞检测、导航网格等技术完成不同NPC与玩家的交互。敌方NPC有僵尸,幽灵,坦克和巨龙,每个不同的敌方单位有不同的特点。游戏支持多人联机,并利用Mirror组件的同步技术实现共同解谜,前往逃生点,完成闯关,趣味十足。并利用MySQL数据库记录不同玩家的得分情况,增加游戏的竞赛性。

2.5 本章小结

本章主要对本项目的需求分析进行了不同角度的介绍。再详细的阐述了游戏中每个模块功能的设计。最后进行可行性分析从技术层面来说明开发本游戏的可行性是成立的。

3 游戏功能实现

3.1 玩家人物的设计与实现

玩家人物的设计直接关乎玩家的游戏体验,是重中之重,不过本游戏人物制作的模型较为简单如图3.1.1 Player设计图所示。

图3.1.1 Player设计图

图3.1.1 Player设计图展示了玩家预制体的详细结构,首先用胶囊体作为玩家的身体,用黑色的长方体调整位置作为玩家的墨镜,标记玩家朝向,如此一来简单的人物的简单模型就做好了。

其次我们需要设计人物的名字和血条,添加一个画布改为世界坐标,调整位置放置在玩家头顶,同时在画布中添加一个Slider更名为PlayerHealth并添加HealthLookUs(Script)控制玩家血条一直朝向摄像头,删除多余子对象并在Background下添加Image更名Fill,Background采用红色,Fill采用绿色,用代码PlayerHealth(Script)控制。武器系统需要添加导入的枪支模型作为武器,这里我们添加不同的三把武器分别更名为Weapon1、Weapon2、Weapon3,其中最强力的武器Weapon3作为需要玩家自行寻找拾取的强力武器,为武器们添加Weapon(Script)代码进行基础属性的控制,不同的武器会发射不同的子弹,但不同子弹的逻辑是一样的即命中时获取命中对象信息进行判断并造成伤害后销毁。模型还需要添加一个光源作为手电筒和一个空物体GroundCheck来检测人物是否处于地面上。

最后还需要添加相应的一些组件,Character Controller控制角色移动,Capsule Collider作为碰撞器,Sphere Collider作为触发器,Rigidbody刚体并作为运动体使人物不会乱飞,Network Identity作为网络物体标识,Network Transform用来同步人物位置,下文中其他对象身上的Network Transform同样是同步人物位置的关键组件,Audio Source控制炮弹爆炸的声音。

通过玩家预制体中的GroundCheck判断玩家是否触地,否则根据重力公式为玩家角色增加下坠加速度,相关核心代码设计如下:

if (characterController.isGrounded){VerticalVelocity = 0f;return;}
else{VerticalVelocity += gravity * Time.deltaTime;}//规定重力

利用SyncVar挂钩weaponArray变量控制玩家角色武器切换,这里注意需要新旧两个参数,相关核心代码设计如下:

private void OnWeaponChanged(int oldIndex, int newIndex)
{if (0 < oldIndex &&oldIndex<weaponArray.Length&&weaponArray[oldIndex] !=null)
{weaponArray[oldIndex].SetActive(false);}
if(0<newIndex&&newIndex<weaponArray.Length&&weaponArray[newIndex] !=null)
{weaponArray[newIndex].SetActive(true);
activeWeapon=weaponArray[newIndex].GetComponent<Weapon>();
if(isLocalPlayer)
{sceneScript.canvasBulletText.text = activeWeapon.bulletCount.ToString();
if (activeWeapon &&activeWeapon.Have == false)
{sceneScript.canvasBulletText.text = "你未持有该武器";}}}
else
{activeWeapon = null;
if (isLocalPlayer) 
sceneScript.canvasBulletText.text = "No Weapon!";}}

控制玩家开火与子弹生成需要由玩家发出命令,服务器调用玩家本地代码生成子弹,相关核心代码设计如下:

[Command] private void CmdActiveWeapon(int index)
{currentWeaponSynced = index;}//向服务端发出武器激活请求命令

[Command] void CmdShoot()
{RpcWeaponFire();}//向服务端发出武器开火请求命令

[ClientRpc] public void RpcWeaponFire()
{var bullet = Instantiate(activeWeapon.bullet,activeWeapon.firePos.position,activeWeapon.firePos.rotation);
bullet.GetComponent<Rigidbody>().velocity = bullet.transform.forward * activeWeapon.bulletSpeed;
 Destroy(bullet, activeWeapon.bulletLife);}//服务端调用所有端代码同步子弹生成

利用SyncVar挂钩playerName变量控制玩家昵称和颜色的同步,这里应由玩家向服务端发送变更名字与颜色或打招呼以及请求数据库信息存储的命令,相关核心代码设计如下:

[Command] public void CmdSetupPlayer(string _name, Color _col)
{playerName = _name;playerColor = _col;sceneScript.statusText = $"{playerName} joined.";} //向服务端发出改变颜色和昵称请求命令      

[Command] public void CmdSendPlayerMessage()
{if (sceneScript){sceneScript.statusText = $"{playerName} says hello";}}//向服务端发出打招呼请求命令

[Command] public void CmdConMysql()
{mySQL.GameOverAndAdd(playerName,MyScore);mySQL.Show();}//向服务端发出玩家信息录入请求命令

玩家初始化的相关核心代码设计如下:      

Camera.main.transform.SetParent(transform);
Camera.main.transform.localPosition = new Vector3(0, 0, 0);
floatingInfo.transform.localPosition = new Vector3(0, -0.3f, 0.6f);
floatingInfo.transform.localScale = new Vector3(0.1f, 0.1f, 0.1f);//初始化玩家镜头等

string name = "Player" + Random.Range(100, 999);Color color = new Color(Random.Range(0f, 1f), Random.Range(0f, 1f), Random.Range(0f, 1f));CmdSetupPlayer(name, color);//随机给定昵称与颜色

sceneScript.playerScript = this;//与本地场景信息绑定

控制玩家操作时一定要判断是否为本地玩家,通过ws控制前后移动和ad控制人物转向实现玩家移动,相关核心代码设计如下:

if (!isLocalPlayer) 
{floatingInfo.transform.LookAt(Camera.main.transform);return;} //判断是否为本地玩家

isGround = Physics.CheckSphere(GroundCheck.position, CheckRadius, layerMask);
if (isGround && Velocity.y < 0){Velocity.y = 0;}//判断玩家纵向高度位置

if (isGround && Input.GetButtonDown("Jump"))
{Velocity.y += Mathf.Sqrt(JumpHight - 2 * gravity);}//判断跳跃条件与控制玩家跳跃

var horizontal = Input.GetAxis("Horizontal");

var vertical = Input.GetAxis("Vertical");

var move=transform.forward*Speed*vertical*Time.deltaTime;

characterController.Move(move);
Velocity.y+=gravity*Time.deltaTime;characterController.Move(Velocity * Time.deltaTime);
transform.Rotate(Vector3.up, horizontal * RotateSpeed);//控制角色移动

控制玩家重生的相关核心代码设计如下:

PlayerHealthSlider.value = Health / HealthMax;//血条控制

if (this.transform.position.y <= -30)
{TakeDamage(100);}//考虑玩家是否存在脱离地图的情况导致游戏进程卡死,所以设置高度阈值-30为脱离地图判定

if(isLocalPlayer)
{transform.position=Spawn.position;DeathMassage.GetComponent<CanvasGroup>().alpha = 1;}//死亡弹窗控制

控制玩家血量实时显示,相关核心代码设计如下:

PlayerHealthSlider.value = Health / HealthMax;

控制玩家受到不同伤害后血量扣除也应不同,代码只需要根据受伤类型判断即可,故不作过多展示。

武器Weapon3有一个代码控制的换弹动画,与玩家发射子弹逻辑类似,相关核心代码设计如下:

RPGBullet.gameObject.GetComponent<MeshRenderer>().enabled = true;//显示        

IEnumerator ExchangeBullet(float _t){float _timer = 0;
while (_timer < _t)
{yield return 0;RPGBody.transform.Rotate(new Vector3(0, 0, 1), 180f * Time.deltaTime);
_timer += Time.deltaTime;}}//利用协程使其围绕自身的z轴旋转_t秒

RPGBullet.gameObject.GetComponent<MeshRenderer>().enabled = false; StartCoroutine(ExchangeBullet(playerScript.activeWeapon.coolDown));//开火后隐藏炮弹并开启协程进行旋转换弹

所有的血条必须实时面向摄像机,相关核心代码设计如下:

transform.LookAt(Camera.main.transform);

玩家的子弹在命中时获取命中单位的脚本信息从而造成伤害,相关核心代码设计如下:

var hit = collision.gameObject;//根据命中单位获取对象脚本如EnemyHealth、WolfAI、BossHealth从而进行下一步判断

var health = hit.GetComponent<EnemyHealth>();

if(health !=null)
{health.TakeDamage(damageEnemy);}//判断获取到的对象脚本再调用脚本中的相应代码实现对命中不同单位的不同效果

3.2 场景物品的设计与实现

3.2.1 载入场景设计

首先需要四个场景,两个菜单场景与两个游戏场景,其中一个游戏场景作为等待大厅,在等待的过程中可以熟悉游戏操作,如图3.2.1 场景一设计。

图3.2.1 场景一设计

如图3.2.1 场景一设计所示:游戏启动后的列表界面GameList场景,添加一个按钮使其可以跳转至下一个界面,当然我们也可以多添加一些其他的东西,比如目前游戏共有的设置、说明、开发者名单等等,因为不是本文重点在此我们不做赘述。

图3.2.2 场景二设计

如图3.2.2 场景二设计所示:菜单界面Menu场景,在这个界面添加一个按钮play,点击后会弹窗提示,还需要在场景中添加一个空物体作为网络管理器,并添加Network Manager、Network Manager HUD、Kcp Transport组件。

在NetworkManager的设计中,我们需要充分考虑离线场景和在线场景与一系列网络对象,如图3.2.3 NetworkManager设计。

图3.2.3 NetworkManager设计

如图3.2.3 NetworkManager设计所示,我们将Menu场景作为游戏的离线场景,将SampleScene场景作为在线场景,同时将我们准备好的Player预制体和接下来要所用到的一些带网络属性的预制体按图添加至相应地方。这样我们在启动游戏进入Menu界面时会激活NetworkManager,左上角会出现网络管理界面如图3.2.4 Menu场景的游戏界面。

图3.2.4 Menu场景的游戏界面

如图3.2.4 Menu场景的游戏界面所示,此时点击play按钮会弹窗提示,提示界面设计如图3.2.5 Menu场景弹窗。

图3.2.5 Menu场景弹窗

如图3.2.5 Menu场景弹窗详细说明了联机方法,此时我们可以选择成为主机或客户端或仅作为服务端。成为主机会同时成为服务端和客户端,在没有服务端时,客户端无法连接,所以第一个进入游戏的玩家必须成为主机或只作为服务端进行服务。在同一局域网中,客户端需要输入服务端的ip进行连接,服务端需要关闭防火墙,否则客户端可能无法连接,如果主机和客户端都在同一台电脑上运行,ip默认localhost本地连接则不用关闭防火墙。

场景三是玩家等待大厅,玩家也可以在这里熟悉游戏,如图3.2.6 场景三设计。

图3.2.6 场景三设计

如图3.2.6 场景三设计所示为游戏界面等待大厅SampleScene场景,在这个场景中我们稍微设计了一些建筑,添加了一个告示牌提示玩家如何操作,以及加入了基础的弹药箱补充玩家弹药和一把强力武器RPG供玩家拾取。在平面后半部分边上的四个点处添加空物体作为出生点同时添加Network Start Position,玩家加入后会在这四个出生点循环载入。我们还需要一个场景管理器来显示信息、切换场景、发送玩家信息,在场景中添加一个空物体命名为SceneScript并挂载Network Identity和SceneScript(Script)脚本进行控制。

同时我们还需要设计玩家游戏时的画布界面来显示场景信息和玩家发出的信息,如图3.2.7 场景三中的画布设计。

图3.2.7 场景三中的画布设计

如图3.2.7 场景三中的画布设计所示新建画布在右上角分别添加两个Text和两个Bottom作为场景信息显示和武器弹药的显示以及打招呼按钮和切换场景的按钮,另外还需要添加一个image在屏幕中间,调整大小并设为红色,添加文字“你已死亡,即将在起点复活”和确认按钮“我命由我不由天”,同时还需要添加Canvas Group组件和Massage(Script)脚本进行控制,在按钮事件上挂载点击触发Massage(Script)的CloseAndContinue()函数来关闭弹窗,这样一来就完成了弹窗信息的触发与关闭。

控制场景切换与信息显示同样需要SyncVar挂钩一定的变量进行场景信息的同步,相关核心代码设计如下:

void OnStatusTextChanged(string _Old, string _New)
{canvasStatusText.text = statusText;}//挂钩函数需要新旧俩个参数才能使用,调用函数时会同时调用所有端口的挂钩函数,此处仅展示一个场景文本同步函数,其他SyncVar挂钩方法函数逻辑一致故不作展示

控制场景切换只能由主机进行,相关核心代码设计如下:

if (isServer)//判断是否为服务端
{NetworkManager.singleton.ServerChangeScene(scene.name == "SampleScene" ? "MyScene" : "SampleScene");}//判断并切换场景

控制弹窗信息的Massage核心代码如下:

Massagee.GetComponent<CanvasGroup>().alpha = 0;//控制画布透明度

3.2.2 游戏场景设计

3.2.2.1 剧情与玩法设计

由于开放世界属于开放式探索的关卡设计,玩家几乎可以在任何场景中自由移动,并且开放世界的每一个角落都具备独有的特点,所以开放世界的游戏往往会把场景分割成多个区域,每个区域中设置特殊地标,然后再从细节出发为各区域添加特色。

另外考虑到本游戏有一定的求生类游戏属性,多种机关、道具、地貌、NPC、敌人都在同一场景中,所以必须规划好玩家的前进路径,还需要考虑玩家当前游戏进行阶段所需要的道具,比如在本游戏中祭坛需要供奉祭品,但作为祭品的狼却在地图的另一端等待着玩家,若玩家没有在开局拿到祭品,原路返回需要跨越大半个地图才能拿到道具,这显然是不合理的,为此我设计了传送门来提高玩家的游戏感受,解决一些因为地形地图限制而产生的不合理。本游戏详细游戏场景与剧情设计如图3.2.8 游戏场景。

图3.2.8 游戏场景

以图3.2.8 游戏场景视角所见为例,左上角为飞机失事地点作为玩家出生点,玩家可以选择向z轴方向前进或向x轴方向探索,由于是联机游戏,在多人时可以考虑分头行动。x方向路径上是地图左下角的丛林,其中有狼群活动,丛林的尽头存在能直接将玩家传送至右上角祭坛处的传送门,供玩家们跨越地图汇合。z方向上有着许多解谜方碑与僵尸敌人和一些恢复血量与弹药的道具,玩家需要在对抗敌人的同时获取更多的逃生信息。继续向前是门前有俩炮台守卫的黑色迷宫,迷宫内昏暗且有幽灵敌人,玩家需要躲避幽灵并在迷宫中寻找出口,迷宫内有着强力武器与增加积分的金币。迷宫出口有着能载一人的小船,需要一名玩家驶入复杂的流域在其中获取信息并前往地图右上角的祭坛,注意玩家在水中会持续受到伤害。同时流域也连通着前往巨龙栖息地的长桥,玩家顺着路一直走即可进入由石砖铸就的龙巢,在与巨龙会面之时,若玩家在祭坛进行了供奉,巨龙会高兴地离开享用祭品,反之,巨龙则会愤怒攻击玩家,玩家只能与巨龙对抗。巨龙身后是巨龙收集而来的财富,以及一块能开启机关使一座桥浮现的魔法石,玩家需要激活魔法石使桥浮现出来才能前往逃生点。地图正下方的逃生点处有一艘巨轮,可以载着所有人离开,但如果有人拾取了巨龙的财富则会受到诅咒,在逃离的必经之路上会有等量的僵尸阻挠。

音乐音效是场景中必不可少的元素,本游戏中提供了三种音乐设计进行参考,一种是游戏开始后全地图播放的背景音乐,如图3.2.9 背景音乐设计。

图3.2.9 背景音乐设计

背景音乐设计如图3.2.9 背景音乐设计在本游戏中可以通过代码进行关闭和开启。第二种是进入特定地点后由远到近音量越来越高越来越清晰循环播放的地点特效音乐,如图3.2.10 鸟叫音效设计。

图3.2.10 鸟叫音效设计

如图3.2.10 鸟叫音效设计是在路过丛林时设计的随距离减小而音量增大的鸟叫音效,该音效方案也可用于其他游戏场景如迷宫和龙巢,增加游戏趣味。

第三种则是满足某一条件比如炮台开炮或特殊弹药爆炸时触发的转瞬即逝的特效音乐,这种音效只需要添加在相应触发代码中即可。

3.2.2.2 道具设计

道具主要设计为四种道具:恢复血量的果实道具如图3.2.11 食物道具樱桃、增强人物属性的强化道具如图3.2.16 属性道具月之祝福、使玩家得到某种能力的功能道具如图3.2.12 功能道具手电筒、补充弹药或更强火力的武器道具如图3.2.14 武器道具弹药箱。

3.2.11 食物道具樱桃

果实道具设计如图3.2.11 食物道具樱桃:采用来自Unity商城中樱桃的免费模型,添加相应的Network Identity组件、Food(Script)代码和Box Collider组件,并勾选Is Trigger作为触发器检测玩家是否吃到,在代码中添加了额外的鼠标显示信息功能,玩家在吃到樱桃后可以恢复一定血量。

鼠标悬停显示信息的相关核心代码如下:

void OnMouseEnter(){isShowTip = true;}//鼠标移入

void OnMouseExit(){isShowTip = false;}//鼠标移出

void OnGUI(){if (isShowTip){GUIStyle style = new GUIStyle() ;style.fontSize = 30;style.normal.textColor = Color.blue;GUI.Label(new Rect(Input.mousePosition.x,Screen.height - Input.mousePosition.y,100,100),"恢复食物",style);}}//文字显示

吃掉食物恢复血量的相关核心代码如下:

if(other.tag=="Player"){playerHealth=other.GetComponent<PlayerHealth>();
playerHealth.Health += 50;}//判断玩家过后增加血量

图3.2.12 功能道具手电筒

功能道具设计如图3.2.12 功能道具手电筒:采用来自Unity商城中手电筒的免费模型,添加相应的Network Identity组件、FlashLight(Script)代码和Box Collider组件,并勾选Is Trigger作为触发器检测玩家是否吃到。在代码中添加了额外的鼠标显示信息功能,玩家在获得手电筒之后可以开启照明效果。

照明效果相关核心代码如下:

playerScript = other.GetComponent<PlayerScript>();//在FlashLight(Script)代码获取对象玩家playerScript脚本

playerScript.CmdGetLight();//调用playerScript脚本中的CmdGetLight()

[Command(requiresAuthority = false)]public void CmdGetLight(){NetworkIdentity opponentIdentity = this.GetComponent<NetworkIdentity>(); TargetGetLight(opponentIdentity.connectionToClient);}//对象玩家向服务器发送自身网络ID并命令服务器调用TargetGetLight在对象玩家本地执行

[TargetRpc]public void TargetGetLight(NetworkConnection target){HaveLight = true;sceneScript.canvasStatusText.text = "你得到了手电筒,按 Z开启 X关闭";}//本地调用激活手电筒并在场景文本中提示

图3.2.13 武器道具火箭筒

武器道具设计如图3.2.13 武器道具火箭筒:武器RPG采用来自Unity商城中武器的免费模型,弹药箱由正方体添加任意材质所制作。武器RPG添加相应的Rigidbody组件、Network Identity组件、GetRPG(Script)代码和Box Collider组件,并勾选Is Trigger作为触发器检测玩家是否吃到,同时添加了一部分特效进行美化,玩家在获得RPG之后武器3即可正常使用。

获取武器与手电筒激活逻辑类似,相关核心代码如下:

playerScript = other.GetComponent<PlayerScript>();//获取对象玩家playerScript脚本

if(playerScript.activeWeapon && playerScript.activeWeapon.Have == false)
{playerScript.CmdGetRPG();}//判断获取条件,调用CmdGetRPG()

activeWeapon.Have = true;//PlayerScript//CmdGetRPG()中激活人物关键变量

图3.2.14 武器道具弹药箱

弹药箱设计如图3.2.14 武器道具弹药箱:添加相应的Rigidbody组件、Network Identity组件、AmmunitionBox(Script)代码和Box Collider组件,并勾选Is Trigger作为触发器检测玩家是否吃到。玩家若没有持有武器,则弹药箱不会进行补给,也不会消失。

弹药箱恢复弹药相关核心代码如下:

playerScript = other.GetComponent<PlayerScript>();//获取接触弹药箱玩家的脚本

if (playerScript.activeWeapon)
{playerScript.CmdGetChangeBulletNumber();}//判断武器是否激活,并为调用函数为该武器增加弹药

地图上野外的弹药箱由弹药箱随机生成器生成,生成器设计如图3.2.15 弹药箱随机生成器设计。

图3.2.15 弹药箱随机生成器设计

如图3.2.15 弹药箱随机生成器设计所示,生成器的设计十分简单,只需要代码控制并添加网络ID即可。

弹药箱随机生成相关核心代码如下:

for (int i = 0; i < Boxnumber; i++)
{BoxX = Random.Range(300.0f, 350.0f);BoxZ = Random.Range(370.0f, 410.0f);
BoxPositon = new Vector3(BoxX, BoxY, BoxZ);
GameObject NewBox=Instantiate(TBox,BoxPositon,Quaternion.identity);
NetworkServer.Spawn(NewBox);}//随机生成坐标,并由服务器创建

图3.2.16 属性道具月之祝福

属性道具设计如图3.2.16 属性道具月之祝福:采用来自Unity商城中的免费模型,添加相应的Network Identity组件、Moon(Script)代码和Sphere Collider组件,并勾选Is Trigger作为触发器检测玩家是否吃到。在代码中同样添加了额外的鼠标显示信息功能,玩家获得月之祝福后移动速度与转动速度都会获得大幅提升。

属性加成相关核心代码如下:

playerScript = other.GetComponent<PlayerScript>();//获取对象玩家脚本

playerScript.Speed += 12;//增加移动速度

playerScript.RotateSpeed += 4;//增加移动速度
3.2.2.3 不同NPC设计

非玩家角色主要设计为四种:可被攻击的敌人角色,不可被攻击的敌人角色,会帮助玩家的善良角色,可被攻击的野生动物角色。可被攻击的敌人角色包括僵尸和巨龙,他们拥有血量,被玩家攻击后血量如果归零会死亡。而不可攻击的敌人角色包括幽灵和炮台,他们都没有血量,玩家没有攻击他们的手段,但这类敌人会有一些独特的限制,炮台固定无法移动,幽灵暴露在阳光之下便会立刻消散死亡。会帮助玩家的善良角色小火龙在与玩家交互过后会跟随玩家,提供一定的保护。可被攻击的野生动物角色狼遇到玩家后会立刻逃窜,但被玩家命中后会当场死亡不动,玩家可以捕获他们作为祭品供奉。

首先设计的是地图上作为小怪的僵尸如图3.2.17 僵尸设计。

图3.2.17 僵尸设计

僵尸设计如图3.2.17 僵尸设计所示,僵尸是来自Unity商城中的免费模型,导入模型之后我们需要添加Animator、RigidBody、Capsule Collider、Sphere Collider、Nav Mesh Agent、Network ldentity、Network Transform、Network Animator、Network Rigidbody这些组件,另外还需要控制血量的Enemy Health (Script)脚本和控制僵尸行为逻辑的Zombie Al(Script)脚本。为控制僵尸的动画,还需要创建一个僵尸的动画控制器zombie control (Animator Controller)如图3.2.18 僵尸动画设计。

图3.2.18 僵尸动画设计

如图3.2.18 僵尸动画设计所示,添加俩个变量对僵尸行为进行控制,在变量改变时,动画控制器能控制动画自然过渡到下一个动作的动画上,接下来将设计好的动画控制器放入Animator组件中即可。僵尸的自动寻路离不开Nav Mesh Agent组件,首先需要将地图的导航网格烘焙好,再对寻路参数如台阶高度等进行一定的调整,接下来只需要在行为逻辑代码中为不同情况更新不同的速度与前进方向就可以了。僵尸作为网络对象一定拥有Network ldentity,它的位置和动画同步则由Network Transform和Network Animator完成。僵尸的行为包括原地待命、走路、奔跑、攻击和死亡,在行为逻辑代码中设定一个随机数,当这个随机数大于某个值时,认为这个僵尸闻到了玩家的气息,会缓慢走路走向玩家所在地,反之随机数小于这个值则原地待命,在Sphere Collider组件中勾选Is Trigger作为触发器进行是否发现了玩家的检测,即侦查范围,并合理调整侦查范围的大小与相对位置,而Capsule Collider不勾选Is Trigger,作为碰撞器检测是否近身玩家进行攻击,僵尸的血条制作与玩家血条类似,当血量归零时僵尸死亡。

僵尸的行为逻辑核心代码如下:

if(AI>6)
{this.transform.LookAt(PlayerPos.transform);
ZombieAniCtrl.SetInteger("ZombieAni", -1);
ZombieNMA.destination = PlayerPos.transform.position;ZombieNMA.speed = 0.5f;}//初始判断僵尸是待命还是朝玩家缓慢移动

if (health.ZomDeath)
{ZombieNMA.speed = 0;ZombieNMA.isStopped = true;
ZombieAniCtrl.SetInteger("ZombieAni", 3);
ZombieAniCtrl.SetBool("Death", true);}//update中时刻监测是否死亡

if(!other.GetComponent<PlayerScript>().isSafe)
{ZombieNMA.destination = other.transform.position;
ZombieNMA.speed = 2;
ZombieAniCtrl.SetInteger("ZombieAni", 1);}//OnTriggerEnter侦查发现玩家后进一步判定isSafe后改变行为与动画,向玩家奔跑追击

if (!other.GetComponent<PlayerScript>().isSafe)
{ZombieNMA.destination = other.transform.position;
ZombieNMA.speed = 2;
ZombieAniCtrl.SetInteger("ZombieAni", 1);}}}//在OnTriggerStay中判断玩家停留在侦查时进一步判断isSafe后锁定目标持续向玩家奔跑追击

if(other.tag=="Player")
{ZombieNMA.destination=-ZombiePos.position;
ZombieNMA.speed = 0;
ZombieAniCtrl.SetInteger("ZombieAni", 0);}}//通过OnTriggerExit判断玩家甩开僵尸后僵尸回到待机状态

var playerHealth = player.GetComponent<PlayerHealth>(); //通过OnCollisionEnter判断僵尸近身玩家后获取玩家生命脚本信息

ZombieNMA.speed = 0;
ZombieAniCtrl.SetInteger("ZombieAni", 2);//切换僵尸攻击动画

playerHealth.TakeDamage(AttackDamage);//调用玩家受伤函数

ZombieNMA.speed = 2;
ZombieAniCtrl.SetInteger("ZombieAni", 1);//通过OnCollisionExit判断脱离僵尸攻击距离,切换为奔跑追击

僵尸的血量与血条控制核心代码如下:

if (!isServer)return;
currentHealth -= amount;//血量减少前需进行服务端判定

healthBar.sizeDelta = new Vector2(currentHealth, healthBar.sizeDelta.y);//血条实时更新

其次是作为游戏Boss的巨龙如图3.2.19 巨龙设计。

图3.2.19 巨龙设计

巨龙设计如图3.2.19 巨龙设计所示,也是来自Unity商城中的免费模型,导入模型之后同样需要添加Animator、RigidBody、Capsule Collider、Sphere Collider、Nav Mesh Agent、Network ldentity、Network Transform、Network Animator、Network Rigidbody这些组件,以及需要控制血量的Boss Health (Script)脚本和控制巨龙行为逻辑的Purple Boss(Script)脚本。同样Capsule Collider作为碰撞器检测碰撞,Sphere Collider作为触发器用于侦查,但与僵尸敌人不同的是作为Boss的巨龙多了许多攻击手段,伤害判定方式也从直接的近身碰撞判定改为了运动触发器接触判定,并且巨龙在玩家第一次遇到巨龙时会根据玩家是否供奉而产生不同的剧情行为,如图3.2.20 剧情分支巨龙离开和图3.2.21 剧情分支巨龙战斗弹窗设计所示。

图3.2.20 剧情分支巨龙离开

图3.2.21 剧情分支巨龙战斗

若玩家在祭坛进行了供奉,那么巨龙会高兴地离开前往祭坛享用祭品并弹窗提示玩家如图3.2.20 剧情分支巨龙离开,反之巨龙会对入侵者宣泄它的怒火并弹窗提示玩家进行战斗准备如图3.2.21 剧情分支巨龙战斗。巨龙的动画控制与自动寻路采用和僵尸敌人一样的方法,如图3.2.22 巨龙动画设计。

图3.2.22 巨龙动画设计

同样用Animator控制动画播放如图3.2.22 巨龙动画设计和Nav Mesh Agent控制巨龙寻路,巨龙的动画控制因为多种攻击手段而略微复杂,在逻辑行为代码中,当发现玩家时,对祭坛是否存在贡品进行判定,若存在贡品则起飞并飞向祭坛,若不存在贡品则向玩家发起攻击,根据玩家的距离进行不同的攻击,较远时会吐息喷火,借助协同函数周期性向玩家发射火球,距离适中时会扑咬玩家,龙爪上的触发器接触玩家后立刻造成伤害,距离较近时会用嘴撕咬玩家,同样利用龙嘴上的触发器对玩家造成伤害,贴紧巨龙时会召唤狂风对玩家造成伤害,这里召唤狂风与吐息喷火共用同一个类似于咆哮的动画Flame Attack,在巨龙血量低于0时巨龙死亡并播放死亡动画。

相比僵尸,巨龙的行为逻辑更加复杂,相关核心代码如下:

if (health.BossDeath)
{BossNMA.speed = 0;BossAniCtrl.SetBool("Death", true);
BossNMA.isStopped = true;}//实时判断巨龙是否死亡

if (find)
{BossNMA.destination = AttackPlayer.transform.position;//更新攻击目标位置

Distance = Vector3.Distance(this.transform.position, AttackPlayer.transform.position);//计算目标距离并根据距离采取不同行动

if (fly)
{BossNMA.baseOffset += Time.deltaTime;}//判断是否起飞

if (BossNMA.baseOffset>15)
{fly = false;BossAniCtrl.SetInteger("BossAni", 1);BossNMA.destination = altar.transform.position;BossNMA.speed = 30;}达到飞行高度后向祭坛移动

if (this.transform.position.x==altar.transform.position.x&& this.transform.position.z == altar.transform.position.z)//判断抵达目的地祭坛
{if (BossNMA.baseOffset >= 0.5)
{BossAniCtrl.SetBool("Fly", false);
BossAniCtrl.SetInteger("BossAni", 2);
BossNMA.speed = 0;BossNMA.baseOffset -= 4*Time.deltaTime;}//降落并根据降落阶段变更动画

IEnumerator FireAttack()
{while (true){if (coolDown){BossNMA.destination = AttackPlayer.transform.position;CreatFire();
coolDown = false;}yield return new WaitForSeconds(2f);}}//协程周期性控制喷火

if (altar.isHappy)
{BossAniCtrl.SetBool("Fly", true);
MessageShow(true);fly = true;once = false;}//判断玩家供奉则离开并弹窗提示

else
{BossAniCtrl.SetInteger("BossAni", 1);
BossNMA.destination = other.transform.position;MessageShow(false);
AttackPlayer = other.gameObject;coolDown = true;find = true;once = false;}//未供奉则进入战斗状态

巨龙的喷火会发射火球,近身会召唤狂风,创造火球与狂风的相关核心代码如下:

GameObject FireMissile = Instantiate(Missile, MissilePos.position, Quaternion.identity);//指定坐标创建火球            

GameObject Flower = Instantiate(BoomFlower, TuxiMagicalPos.position, Quaternion.identity);
Flower.transform.LookAt(AttackPlayer.transform);//同时创建喷火特效并调整角度

FireMissile.transform.LookAt(AttackPlayer.transform);//规定火球向攻击目标发射

GameObject Wind = Instantiate(wind, AttackPlayer.transform.position, Quaternion.identity);//指定坐标生成狂风

Wind.transform.LookAt(AttackPlayer.transform);//调整狂风呼啸角度

巨龙的血量控制核心代码与僵尸基本一致,仅仅倍数缩小了受到的伤害,故不展示代码。

然后是迷宫中的幽灵如图3.2.23 幽灵设计。

图3.2.23 幽灵设计

幽灵设计如图3.2.23 幽灵设计所示,也是来自Unity商城中的免费模型,模型自带一些组件Animator等不做处理,导入模型之后需要添加Sphere Collider、Nav Mesh Agent、Network ldentity、Network Transform这些组件,因为幽灵没有实体,所以并不需要刚体组件和碰撞器组件,Sphere Collider组件中勾选Is Trigger作为触发器并合理调整侦查范围,同样使用Nav Mesh Agent组件自动寻路,在幽灵的行为逻辑代码中,未发现玩家时开启协程进行自动巡逻,发现玩家时追逐玩家并关闭协程,当追上玩家即玩家和幽灵距离小于1.5时玩家会不断受到伤害直到甩开幽灵,另外当幽灵离开迷宫范围便会直接销毁,这样便实现了暴露在阳光下死亡的设定。

幽灵的行为逻辑核心代码:

if(this.transform.position.x>=412 && this.transform.position.z<=670 && this.transform.position.z >= 760&& this.transform.position.x <= 327)//判断是否离开迷宫
while (true)
{RandomNum = (int)Random.Range(0.0f, 5.0f);GhostPos = GameObject.Find("GhostP"+RandomNum.ToString()).GetComponent<Transform>();
this.transform.LookAt(GhostPos.transform);GhostNMA.destination=GhostPos.position;GhostNMA.speed = 1f;
yield return new WaitForSeconds(Random.Range(10.0f, 30.0f));}}//通过携程函数完成周期性巡逻

if (other.tag == "Player"){StopCoroutine(GhostPatrol());}//发现玩家后停止巡逻而追击玩家

if (Vector3.Distance(this.transform.position, other.transform.position) <= 1.5){playerHealth.TakeDamage(Damage);}//判断是否与玩家近距离接触并造成伤害

最后一个敌方NPC是不可被攻击的炮台,玩家只能躲避它的炮火,如图3.2.24 炮台设计。

图3.2.24 炮台设计

炮台设计如图3.2.24 炮台设计所示,炮台由Unity商城中的免费坦克模型和一个正方体组成,这里需要在坦克的子对象坦克的炮塔上进行Sphere Collider组件和Audio Source组件的添加,用于设计侦查范围和控制开火音效,当然我们还需要网络对象必备的Network Identity,同时我们还需要创建一个空物体作为炮弹生成的坐标并添加到坦克逻辑代码中,坦克的炮弹也需要添加相应的代码并作为预设体添加到坦克逻辑代码中,当有玩家进入炮台攻击范围,炮台便会开启协同函数周期性向玩家发射炮弹。炮弹的设计非常简单,只需要代码控制向前移动即可。

炮台逻辑相关核心代码:

if (other.tag == "Player")
{CanFire = true;StartCoroutine(CreateMissile(other));}//发现玩家后开启炮弹生成协程函数

if (other.tag == "Player")
{this.transform.LookAt(other.transform);}//发现玩家后炮台始终看向玩家

private void OnTriggerExit(Collider other)

StopCoroutine(CreateMissile(other));//离开侦查范围后停止协程生成炮弹

生成炮弹的协程函数与火球协程函数类似,故不作代码展示。

除了敌方NPC的设计外,还有一些能为玩家提供帮助的友善NPC,如图3.2.25 小火龙设计。

图3.2.25 小火龙设计

小火龙设计如图3.2.25 小火龙设计所示,小火龙模型来自Unity商城中的免费模型,对于小火龙我们需要添加Box Collider、Nav Mesh Agent、Network ldentity、Network Transform和行为逻辑代码DragonAI(Script),在Box Collider中勾选Is Trigger作为触发器用来检测身边周围是否存在玩家,另外由于小火龙是飞在空中的,所以我们需要将Nav Mesh Agent中的Base Offset调整为0.5。小火龙在与玩家接触前会根据协同函数规定的路径巡逻游荡,接触玩家后关闭协同函数,并一直追随玩家提供庇护,庇护手段为更改玩家isSafe变量,这样在玩家进入僵尸侦查范围时僵尸会对玩家视而不见。

小火龙的行为逻辑核心代码如下:

if (Follow)
{DragonNMA.destination = player.transform.position;}//保持跟随玩家

if (other.tag == "Player")
{player = other.gameObject;playerScript = player.GetComponent<PlayerScript>();//获取对象玩家的玩家脚本代码

StopCoroutine(DragonPatrol1());//停止协程巡逻

this.transform.LookAt(player.transform.position);
Follow = true;//跟随玩家

playerScript.isSafe = true;//为对象玩家添加“安全”条件

小火龙的自动巡逻相关核心代码如下:

IEnumerator DragonPatrol1()
{……
RandomNum = (int)Random.Range(0.0f, 5.0f);
DragonPos=GameObject.Find("P"+RandomNum.ToString()).GetComponent<Transform>();
DragonNMA.destination = DragonPos.position;
……}//在点位之间随机巡逻

除了友善的NPC外还有一类充当道具的中立NPC,如图 3.2.26 狼的设计。

图 3.2.26 狼的设计

狼设计如图 3.2.26 狼的设计所示,狼的模型来自Unity商城中的免费模型,狼与僵尸类似,不同的是僵尸遇到玩家会跑向玩家进行攻击,而狼则是会逃窜。我们需要添加Animator、RigidBody、Capsule Collider、Sphere Collider、Nav Mesh Agent、Network ldentity、Network Transform、Network Animator、Network Rigidbody和行为逻辑代码WolfAI(Script),在Sphere Collider中勾选Is Trigger作为触发器用来检测周围是否存在玩家,未发现时随机前往协同函数规定的坐标,发现玩家时停止协程并向特定点逃离,在逃离玩家后再次开启协程随机前往坐标。动画控制器的设计非常简单如图3.2.27 狼的动画设计。

图3.2.27 狼的动画设计

如图3.2.27 狼的动画设计所示,只需要用两个变量控制狼的走路和逃离时的奔跑以及中弹后的死亡动画即可。

狼的行为逻辑代码核心如下:

if(other.tag=="Player")
{StopCoroutine(WolfPatrol1());
this.transform.LookAt(WolfPos.transform);
WolfNMA.destination = WolfPos.position;...}//发现玩家马上跑路

狼的巡逻与小火龙巡逻代码基本一致,不作展示。

玩家获得狼肉的核心代码如下:     

if (collision.collider.tag == "Player")//被玩家抓住
{var player = collision.collider.gameObject;
playerScript = player.GetComponent<PlayerScript>();
WolfNMA.isStopped = true;playerScript.HaveWolf = true;
...}//获取玩家对象脚本代码并修改关键值
3.2.2.4 解谜机关设计

解谜机关主要有四种机关:石碑机关、魔法石机关、台阶机关、传送门机关。

石碑机关由一系列悬浮的正方体组成,每个正方体在四周表面用画布写上文字,其中有一面写上正确的文字信息,设计如图3.2.28 石碑机关设计。

图3.2.28 石碑机关设计

如图3.2.28 石碑机关设计所示,同时添加Box Collider、Event Trigger、Network Identity这些组件和Stele(Script)脚本代码进行旋转控制,每次旋转90度,在Event Trigger中添加鼠标点击事件并选择代码中的相应函数,注意石碑机关点击其中任意一个那么相邻的正方体都会旋转,所以点击事件中应加入相邻的正方体。

石碑机关的核心代码如下:

[Command(requiresAuthority =false)]public void CmdRotateStele()
{RpcRotateStele();}//玩家点击向服务端发出请求命令

[ClientRpc]public void RpcRotateStele()
{this.transform.Rotate(0, 90, 0);}//服务端调用所有客户端执行方块的旋转

玩家接触从而启动的魔法石机关如图3.2.29 魔法石机关设计和图3.2.30 桥的设计。

图3.2.29 魔法石机关设计

图3.2.30 桥的设计

模型来自Unity商城中的免费模型,如图3.2.29 魔法石机关设计所示选择其中中间最大的石头添加Network Identity、Network Transform组件和Magic Stone(Script)脚本代码完成与玩家互动,在玩家接触魔法石之后会使特定的桥如图3.2.30 桥的设计在必经之路显现出来,桥上需要挂载Bridge(Script)脚本代码根据魔法石是否激活完成桥的出现。

石碑机关的核心代码如下:

if (other.tag == "Player")
{RpcOnBridgeShowChanged();...}//判断玩家接触后调用Rpc函数

[ClientRpc]public void RpcOnBridgeShowChanged()
{Stone.EnableKeyword("_EMISSION");isShow = true;...}//显示机关激活特效以及改变关键变量

机关激活后桥出现的核心代码如下:

if (stone.isShow)
{this.gameObject.SetActive(true);}//将对象“桥”活化

台阶机关由一些列的扁平的长方体构成的,如图3.2.31 台阶设计。     

图3.2.31 台阶设计

如图3.2.31 台阶设计,先把一个长方体调整大小作为底座,再将这些长方体作为底座的子对象,并添加Box Collider组件作为触发器和Stairs UP(Script)脚本代码控制台阶长方体升起,玩家靠近时它们会从底座中按递增的速率同时升起。

台阶升起的核心代码如下:

StairsP1.position = new Vector3(StairsP1.position.x, StairsP1.position.y + 1 * Time.deltaTime / 8, StairsP1.position.z);//控制第一节台阶升起

StairsP2.position = new Vector3(StairsP2.position.x, StairsP2.position.y + 2 * Time.deltaTime / 8, StairsP2.position.z);//控制第二节台阶升起,后面台阶依次类推

传送门机关由Unity基础模型包中的模型、特效与3D字体构成,顾名思义它可以实现玩家在两传送门之间的相互传送如图3.2.32 传送门设计。

图3.2.32 传送门设计

设计如图3.2.32 传送门设计所示,需要添加Box Collider组件作为触发器和TPcontrol(Script)脚本代码控制以及Network Identity网络身份组件。

传送门的核心代码如下:

[Command(requiresAuthority = false)]public void CmdTp(GameObject tp)
{NetworkIdentity opponentIdentity=this.GetComponent<NetworkIdentity>();
TargetTp(opponentIdentity.connectionToClient,tp);}//获取需要传送的玩家网络ID

需要放在玩家脚本中的辅助相关核心代码如下:

[TargetRpc]public void TargetTp(NetworkConnection target, GameObject tp)
{this.transform.position = tp.transform.position;}//修改玩家位置完成传送
3.2.2.5 载人载具设计

载人载具选择一艘小船进行具体设计如图3.2.33 小船设计

图3.2.33 小船设计

如图3.2.33 小船设计所示,小船的模型来自Unity商城的免费模型,需要添加Box Collider、Capsule Collider、Network Identity这些组件和Motor Boat(Script)脚本代码进行上下船与移动的控制。由于地形和玩家潜水扣血的设定,在复杂流域需要设计一艘小船,小船可以搭载玩家在水上长时间航行,同时应当只有一人能驾驶小船。所以需要利用Unity的约束系统 Parent Constraints将小船绑定到玩家身上,同时禁用玩家脚本代码和玩家的血量控制代码,仅使用小船的代码进行移动,同时还需要新建一个摄像机作为上船之后的新视角。

小船载人的相关核心代码如下:

if (getplayer && Input.GetKeyDown(KeyCode.B) && isOperated == false && playernear)//判断上船条件
{isOperated = true;//修改关键变量
player.GetComponent<PlayerScript>().enabled=false;
player.GetComponent<PlayerHealth>().enabled=false;
player.AddComponent<ParentConstraint>();//禁用并添加相应玩家组件
ConstraintSource constraintSource = new ConstraintSource(){sourceTransform = transform,weight = 1};
player.GetComponent<ParentConstraint>().SetSources(new List<ConstraintSource>() { constraintSource });//添加目标对象

player.GetComponent<ParentConstraint>().SetTranslationOffset(0, new Vector3(0, 0.3f, 0));
player.GetComponent<ParentConstraint>().SetRotationOffset(0, new Vector3(0, 0, 0));//设置相对偏移量

player.GetComponent<ParentConstraint>().constraintActive=true;BoatCamera.SetActive(true);//激活组件

MainCamera.transform.position = BoatCamera.transform.position;//切换视角

}else if (Input.GetKeyDown(KeyCode.B) && isOperated == true)//判断下船条件

{isOperated = false;//修改关键变量
player.GetComponent<PlayerScript>().enabled = true;player.GetComponent<ParentConstraint>().SetSources(new List<ConstraintSource>());
player.GetComponent<ParentConstraint>().constraintActive = false;
Destroy(player.GetComponent<ParentConstraint>());//删除并激活相应玩家组件

BoatCamera.SetActive(false);
MainCamera.transform.position=player.transform.position;//切换视角

playernear = false;}//修改关键变量,防止千里上船

小船移动核心代码如下:            

if (!isOperated) return;//判断本地操作

float moveX = Input.GetAxis("Horizontal") * Time.deltaTime * 110.0f;//移动速度

float moveZ = Input.GetAxis("Vertical") * Time.deltaTime * 4f;//转向速度

transform.Rotate(0, moveX, 0);//前后移动

transform.Translate(0, 0, moveZ);//左右转向
3.2.2.6 积分奖励机制设计

为了进一步增加游戏的趣味性与玩家的竞争性,需要一定的积分奖励机制。在本游戏中可以通过收集金币的方法获得积分如图3.2.34 金币设计,金币散落在迷宫与巨龙宝库之中,迷宫中零散的金币需要玩家在迷宫中自行寻找收集,而宝库中有着取之不尽的黄金如图3.2.36 黄金设计,但这些黄金都受到了诅咒,玩家每在宝库中拾取一次黄金,就会生成一只僵尸以作惩罚。

图3.2.34 金币设计

如图3.2.34 金币设计所示,金币和黄金的模型来自Unity商城的免费模型,金币需要添加Box Collider作为触发器、Rigidbody取消勾选重力、Network Identity这些组件和Gold Coin(Script)脚本代码实现玩家吃掉金币并获取积分。

黄金的设计分为俩部分,控制器和各式各样的黄金,如图3.2.35 控制器设计和图3.2.36 黄金设计。

图3.2.35 控制器设计

图3.2.36 黄金设计

控制器如图3.2.35 控制器设计所示在空物体上添加Box Collider和Network Identity还有Get Gold(Script)脚本代码,Box Collider调整合适大小覆盖所有金子并勾选Is Trigger作为触发器,再将需要生成的僵尸敌人放入代码的敌人预设体槽中,黄金如图3.2.36 黄金设计所示则需要添加Rigidbody勾选重力和Box Collider不勾选Is Trigger防止穿模,这样游戏启动后金子会在物理引擎作用下合理散布落到地上。

金币与黄金的代码都比较简单,玩家接触后相应增加积分变量即可,在此不作代码展示。

3.2.2.7 数据库与榜单设计

联机游戏中玩家每次游戏的数据都可以被记录下来制成一个排行榜,进一步激励玩家获得更多积分来提高排名,所以我们就需要一个数据库支撑大量数据的存储,并在每次玩家结束游戏时,通过服务端读取数据库内容制成最新榜单发送给所有客户端。

排行榜应在游戏结束时展示,游戏结束需要玩家抵达逃生点,逃生点设计如图3.2.37 逃生点设计。

图3.2.37 逃生点设计

如图3.2.37 逃生点设计所示,在轮船上添加Rigidbody、Network Identity和Network Transform还有End Boat(Script)脚本代码控制游戏结束,还需要增加一个挂载了Box Collider作为触发器的空物体作为子对象。这样在玩家上船后前往船内的控制台启动轮船即可离开岛屿结束游戏,此时所有玩家便会弹窗游戏结束界面如图3.2.38 结束界面设计。

图3.2.38 结束界面设计

如图3.2.38 结束界面设计所示,玩家点击按钮“查看榜单”后该弹窗关闭跳转至榜单界面,这里的弹窗设计与玩家死亡和遇到巨龙时的弹窗设计比较相似,不同的是当某个玩家点击按钮后,所有玩家都会跳转至榜单界面,如图3.2.39 榜单界面设计和图3.2.40 榜单实际效果。

图3.2.39 榜单界面设计

榜单界面设计如图3.2.39 榜单界面设计所示,整个界面分为五个部分分别是标题、数据名称、玩家排行信息、我的信息、跳转按钮。实际效果如图3.2.40 榜单实际效果所示。

图3.2.40 榜单实际效果

如图3.2.40 榜单实际效果所示,玩家排行由分数从高到低进行排列,其中队伍次号同时也是游戏次号,是唯一的,图3.2.40 榜单实际效果展示中的游戏次号1数据为手动添加的测试数据,同时可以看到在数据库中也记录了本次游戏的数据信息包括队伍次序、玩家昵称和得分情况如图3.2.41 数据库设计。

图3.2.41 数据库设计

如图3.2.41 数据库设计所示的22号Player616即为本次测试的玩家昵称如图3.2.40 榜单实际效果,标志着玩家信息在服务端已成功录入数据库。在游戏中,数据库的设计主要由一个空对象搭载,完成对游戏内各事件的交互如图3.2.42 数据库连接设计。

图3.2.42 数据库连接设计

如图3.2.42 数据库连接设计所示,服务端的数据库控制器会读取数据库内容并加载到玩家排行信息中,通过SyncVar标记的字符串变量同步给所有端口进行展示。

游戏结束的轮船核心代码:

if (letgo&&this.transform.position.x<1200)
{this.transform.Translate(Vector3.left * Time.deltaTime * Speed);
this.transform.Rotate(0, RSpeed * Time.deltaTime, 0);}//判断玩家上船游戏结束,轮船开船离开

跳转榜单的核心代码如下:

endMassage.GetComponent<CanvasGroup>().alpha=0;showScore.SetActive(true);
showScore.GetComponent<CanvasGroup>().alpha = 1;//调整弹窗的透明度完成跳转显示

榜单展示的核心代码如下:

previousScoreText.text = mysql.AllPreviousScore;
myScoreText.text = mysql.OurScore;//将服务端数据库信息进行拷贝

数据库输出数据的核心代码如下:

[SyncVar(hook = nameof(OnShowPSChanged))]

public string AllPreviousScore;//同步历史数据

[SyncVar(hook = nameof(OnShowOSChanged))]

public string OurScore;//同步本次游戏数据

...

cmdShow.CommandText = "select * from graduationproject order by guid DESC limit 1"; MySqlDataReader reader = cmdShow.ExecuteReader();//获取上一次游戏编号以确定本次游戏的录入编号

...

mycon.Open(); //创建数据库命令

MySqlCommand cmdShow = mycon.CreateCommand();

try{

cmdShow.CommandText = "select * from graduationproject order by score DESC limit 6";//创建查询语句

MySqlDataReader reader = cmdShow.ExecuteReader();//从数据库中读取数据流存入reader中

while (reader.Read())//从reader中读取下一行数据,如果没有数据,reader.Read()返回flase

{gguid=reader.GetInt32(reader.GetOrdinal("guid"));
nname=reader.GetString(reader.GetOrdinal("name"));
sscore=reader.GetInt32(reader.GetOrdinal("score"));//规定方式读取数据

Debug.Log(gguid + "--------" + nname + "--------" + sscore);

previousScore.Append(gguid+"--------"+nname+"--------"+sscore+"\n");}//输出数据

AllPreviousScore = previousScore.ToString();}//将所有输出的数据一次性拷贝出来

...

mycon.Close();//关闭数据库连接

数据库录入数据的核心代码如下:

string add = String.Format("insert into graduationproject (guid,name,score)values('{0}','{1}','{2}');", guidText, nameText, scoreText);//创建添加语句

MySqlCommand cmd = new MySqlCommand(add,mycon);//创建数据库命令
3.2.2.8 联机核心方法论述

网络游戏中最为核心的便是同步问题,如何将服务端数据同步到客户端,客户端数据同步至其他客户端,哪些数据需要同步,什么时候同步,这些都是游戏开发者必须解决的问题,所有的网络对象都必须拥有网络身份即添加Network Identity组件。

玩家的同步:在联机游戏中,玩家是必须同步的变量,首先是玩家的模型、名称、积分与血量,由于本游戏中玩家外形统一,故以颜色进行区分,对于这四个变量采用SyncVar的方法进行同步,只要标记了SyncVar的变量发生了变化,在每个游戏端口都会调用SyncVar中hook挂钩的相关函数,以此完成同步,另外玩家的位置同步朝向同步等等由Network Transform组件完成。其次是玩家发射的子弹,玩家发射的子弹不仅需要在本地生成,同时其他玩家也必须拥有该玩家发射子弹的数据,对于这个事件的同步,需要由该玩家发送“我开枪了”的命令给服务端,再由服务端发送“这个玩家发射了子弹”的命令给所有客户端,使子弹在所有端口本地生成,所以对于这个事件我们采用Command-ClientRpc方法即用command标记开枪的代码函数和ClientRpc标记子弹生成的函数,由开了枪的客户端调用开枪函数命令服务端执行开枪函数,服务端再调用子弹生成函数并要求所有客户端执行玩家代码中子弹生成函数,以此完成子弹在所有端口的生成工作,完成子弹的同步,使用同样方法的事件还有玩家改变自己昵称与颜色。最后还有玩家与道具或机关交互后属性改变的同步问题,这个问题在下文进行详细阐述。

道具的同步:道具的同步比较简单,由于道具都是网络对象,都在服务器上生成,所以也只在服务器上监控触发器是否触发,故触发器函数都需要标记ServerCallback,仅在服务端执行,触发器触发即道具获得后便会在服务端销毁直接同步给所有客户端。值得注意的是,如果道具影响的属性是被SyncVar标记的变量,那么会由SyncVar直接完成同步工作比如血量,但如果影响的属性是某些道具的获得如获得RPG开启武器3和获得手电筒开启光源还有接触弹药箱获得弹药,那么服务端的网络物体是没有更改客户端玩家这些属性的权限的,所以我们需要使用Command-TargetRpc的方法使特定客户端玩家产生目的效果,即由玩家触发的触发器调用该玩家的玩家代码中标记了Command的相应函数向服务器发出命令执行该函数,由于网络物体没有权限,可以采用Command(requiresAuthority = false)的方法绕过权限检查,服务器再根据该玩家的网络ID通过TargetRpc方法调用该标记的相应函数使其在指定客户端调用,从而使指定玩家达到目的效果。

武器的同步:武器的切换同步可以使用SyncVar标记一个变量记录当前武器序号完成同步,同时在持有某序号的武器时,应用SetActive(false)方法使其他武器处于失活状态,这样便不会显示其他武器。对于特殊武器即需要拾取RPG才能使用的武器3,它有独特的换弹动画,即绕自身旋转一周和子弹模型开火后消失装填完毕时再出现的动画,该动画由使用了协同函数的代码控制,对于它的同步采用Command-ClientRpc的方法,并将协同函数放在ClientRpc标记的函数中,由服务端调用所有客户端同时执行该协程,以此完成换弹的同步。

NPC的同步:敌人需要同步位置、动画、刚体、血量等信息,其中位置、动画、刚体分别由Network Transform、Network Animator、Network Rigidbody这些组件来完成,血量和玩家一样采用SyncVar标记血量变量完成同步,同样的对于敌人的触发器碰撞器也是由服务器完成检测,应由ServerCallback标记触发器碰撞器相关代码。

机关的同步:机关与道具类似,但不会消失而是会触发一些其他效果。石碑机关的同步采用Command-ClientRpc的方法,点击事件调用Command标记函数向服务端发出命令,同样因为没有权限所以需要使用Command(requiresAuthority = false)的方法绕过权限检查,然后通过服务端调用所有客户端上的石碑代码中ClientRpc标记的旋转函数来完成同步。魔法石机关的同步采用ServerCallback-ClientRpc的方法,因为网络物体本就处于服务器上,所以不需要发出命令,由触发器直接调用ClientRpc标记的函数让所有客户端的魔法石发光并使桥显现出来完成同步。传送门机关的同步涉及更改玩家位置,所以需要采用ServerCallback-Command-TargetRpc的方法,同样因为没有权限所以需要使用Command(requiresAuthority = false)的方法绕过权限检查,由玩家触发传送门的用ServerCallback标记的触发器来调用该玩家的玩家代码中标记了Command的相应函数向服务器发出命令执行该函数,服务器再根据该玩家的网络ID调用TargetRpc标记的传送函数使其在指定客户端调用,从而使该玩家的位置信息在客户端本地与服务端同步完成修改,再借由Network Transform组件完成服务端到其他客户端的位置同步,至此传送门的同步问题得以解决。诅咒黄金机关的同步涉及玩家的得分与敌人的实时生成,由于玩家按键只会在本地生效且服务端检测玩家按键会消耗大量流量,所以采用Command-ClientRpc和ServerCallback的方法同步得分与生成敌人,设计为在玩家按下F键时发出命令使服务端执行Command标记函数,函数内判定玩家是否在黄金旁边,由服务端增加玩家得分,再由SyncVar将得分同步至所有客户端完成得分同步,按下F的同时使玩家代码中的一个布尔变量KeyF变为true,服务器上的触发器检测到该玩家的KeyF为true则调用敌人生成的代码,由服务端完成敌人在服务器上的生成,这些代码仅由服务端执行所以都需要ServerCallback进行标记,这样就完成了诅咒黄金机关的同步。

4 游戏测试

4.1 测试环境

软件测试主要使用来测试项目的准确性、整体性和安全性的。软件测试的技术形成是跟随着软件的发展而产生的,随着发展,软件的开发逐渐趋近复杂,软件测试的也随之提高,本章将采用软件测试的方法进行游戏测试。

本章节中用于测试的计算机硬件环境与软件环境如表 4.1.1 测试用计算机软硬件环境所示。

表 4.1.1 测试用计算机软硬件环境

硬件与软件

环境版本

计算机型号

CPU

GPU

内存

操作系统

Unity版本

联想Legion Y7000

Intel(R) Core(TM) i5-9300H CPU @ 2.40GHz八核

NVIDIA GeForce GTX 1050

8+16G

Windows 11 专业版

 2021.3.16f1c1

计算机型号

CPU

GPU

内存

操作系统

神舟 战神 Z7-KP7GC

Intel(R) Core(TM) i7-8750H CPU @ 2.20GHz八核

NVIDIA GeForce GTX 1060

8+8G

Windows 10 家庭中文版

将本文所研发的游戏在unity3D引擎菜单栏中File->Build Settings 中选择PC,MAC&Linux Standalone,点击 Build,Build出为PC端的EXE应用软件。打包完成后将目标文件夹中所有文件压缩拷贝给其他设备,这样便可在其他设备上解压后进行测试,需要注意到是项目Build过后目录下的所有文件都需要一同拷贝。

4.2 功能测试

用黑盒测试和白盒测试的测试方法对游戏的各个模块进行有效测试,以确保游戏的质量,测试过程如表 4.2.1 功能测试所示。

表 4.2.1 功能测试

测试编

测试目标

测试范围

操作步骤

测试结果

1

UI界面

开始界面、菜单界面、游戏界面、各弹窗界面

点击各界面按钮观察是否正常且正确交互、文字显示是否正确

按钮交互无误,文字显示全部正确

2

玩家操作

玩家的移动、跳跃、武器切换、子弹发射等行为

操作玩家人物活动

按键反馈正常

3

机关交互

玩家与所有机关的交互

操作玩家人物激活各种机关、观察机关反馈

机关反馈正常

4

NPC交互

玩家与所有NPC的交互、NPC的各种行为与动画、玩家和敌人的死亡与载入

操作玩家人物与各种NPC进行交互、观察各情况下NPC行为动画是否匹配是否正确、观察玩家和敌人能否正常死亡和重生或生成

与NPC交互无异常,NPC行为正确动画匹配,游戏角色都可以正常死亡

5

数据库操作

数据库的读写功能

游戏结束观察榜单是否与数据库内容匹配

数据匹配无异常

4.3 局域网线上测试

在同一局域网下,用多台计算机进行测试,首先需要主机玩家关闭一定程度上的防火墙,点击Host按钮,客户端需要在菜单界面输入主机的IPv4地址进行连接如图4.3.1 连接主机。

图4.3.1 连接主机

如图4.3.1 连接主机所示,点击左上角Client连接成功之后,玩家会进入等待大厅等待所有玩家进入后由主机切换场景开始游戏,如图4.3.2 主机加载客户端玩家。

图4.3.2 主机加载客户端玩家

如图4.3.2 主机加载客户端玩家所示,在所有玩家确认连接成功后,由主机玩家切换场景开始游戏,客户端自由探索进行测试,同时主机玩家使用Unity进行后台监控,最后游戏结束时进行数据库的测试,如图4.3.3 数据库与榜单实测和图4.3.4 数据库记录实测。

图4.3.3 数据库与榜单实测

图4.3.4 数据库记录实测

如图4.3.3 数据库与榜单实测和图4.3.4 数据库记录实测所示,玩家数据可以正常读取显示到客户端和正常存储到服务端的数据库之中。

5 相关资源链接:

基于unity开发的解谜游戏_unity解密游戏资源-CSDN文库

  • 29
    点赞
  • 27
    收藏
    觉得还不错? 一键收藏
  • 3
    评论
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值