【3D游戏编程与设计】四 游戏对象与图形基础 : 构建游戏场景+牧师与魔鬼 动作分离版

  • 基本操作演练
    • 下载 Fantasy Skybox FREE, 构建自己的游戏场景
      • 下载 Fantasy Skybox FREE
      • 创建新场景
      • 创建天空盒
      • 添加并设置地形
      • 玩家降临
      • 种树
      • 风场
      • 种草
    • 写一个简单的总结,总结游戏对象的使用
  • 编程实践"牧师与魔鬼"动作分离版
    • 游戏规则
    • 列出游戏中提及的事物(Objects)
    • 用表格列出玩家动作表(规则表)
    • 工作内容
      • 设计模式优化
      • 自定义的天空盒
      • 自定义的音效
    • 项目架构
      • 软件版本
      • 文件组织
    • 游戏素材
    • 项目地址
    • 脚本分析与设计
      • Action.cs
      • ModelController.cs
      • Judge.cs
      • 其他脚本
      • 部署脚本
    • 调整摄像头
    • 游戏效果截图

基本操作演练

操作演练使用的开发软件为Unity 3D 2020.1.4f1c1。

下载 Fantasy Skybox FREE, 构建自己的游戏场景

下载 Fantasy Skybox FREE


打开Unity 3D 2020.1.4f1c1后,切换到Asset Store选项卡,然后点击"Search online"。

可以在浏览器打开的窗口中看到Unity Asset Store界面。

在Asset Store中搜索"Fantasy Skybox FREE",可以看到如下的搜索结果。

上面的第一个搜索结果"Fantasy Skybox FREE"就是需要导入的素材库。


点击后可以看到资源的详情,点击"添加至我的资源"可以购买该资源(由于该资源免费,因此相当于直接获得该资源)。

之后,可以在刷新后的本地Unity 编辑器中的Package Manager看到已经获取了该资源。之后可以在Package Manager将该资源下载并导入到项目中。

之后,可以在项目Assets文件夹下看到新添加的Fantasy Skybox FREE资源包,之后就可以直接使用该包中的资源。

创建新场景

之后,创建一个新的Scene,如下图所示,创建了一个"Scene2"场景:

可以看到新创建的场景由于还没有任何游戏对象,只有上图中单调且简单的内容和界面。

新建的场景中默认有一个主摄像头,一个平行光源。

对于该新建的场景,主摄像头和平行光源目前不需要特殊设置,可以采用默认的。

创建天空盒

在Unity中,天空盒是一种使用天空盒着色器的材质。这种材料可以被添加到场景或摄像机中,成为渲染中出现的默认背景。通过天空盒,可以构造游戏中天空的效果。


首先,在项目中创建一个新的材料,命名为"Sky 2",然后选择着色器为Skybox/6 Sided,这样就可以得到一个初始化的六面体天空盒。

接着,考虑给该天空盒的各个面添加纹理。可以使用之前导入的Fantasy Skybox FREE素材包中的纹理图案。添加之后,得到如下的结果:

之后,将该天空盒添加为场景使用的天空盒。方法是,从菜单栏中选择 Window > Rendering > Lighting,然后在Lighting Setting的Environment选项卡中,设置使用之前创建的sky2天空盒:

可以看到如下的效果:

可以看到,天空盒创建成功。现在场景有了默认天空的效果。

添加并设置地形

之后给场景添加地形,点击菜单栏的GameObject > 3D Object > Terrain,可以创建一个新的地形,如下图所示。


点击上图中创建相邻地形的按钮,可以用于创建相邻的地形:

之后,可以切换到Paint Terrain的模式,在该模式下,可以在地形上画出起伏,可以制造出丘陵,山脉的效果。

首先,可以尝试使用Raise or Lower Terrain模式,在该模式下,点击鼠标左键可以升高地形中对应位置的高度,按住Shift点击左键可以降低地形中对应位置的高度。


可以通过Brush Size设置地形画笔的大小,也就是每次点击改变地形范围的大小。

而Opacity参数可以用于控制地形画笔效果的强度,该值越大,则增高和降低地形的效果就越强。


简单地使用地形画笔绘制,可以得到类似上图的效果。

之后可以切换到Paint Holes模式:

在该模式下,按左键可以画出地形上的洞,按Shift的同时点击左键可以消除地形上的洞。使用该功能可以画出场景中洞穴的效果。效果如下:

之后可以切换到Paint Texture的模式:

在该模式下,可以画出地形表面的纹理。

点击上图中的按钮,点击Add Layer…,会弹出如下的窗口:

在上图的窗口中,可以将一些预制的Terrain Layer添加到地形中。上图看到,已经有预制的TerrainDirt,TerrainGrass,TerrainRock地形层。给每个地形添加的第一个Layer时,会将整个地形都填充为对应的Texture(之前地形的纹理为类似棋盘的图案)。

现在使用TerrainRock填充所有地形,然后再添加TerrainDirt和TerrainGrass的Layer,之后使用画笔在地形上绘制TerrainDirt和TerrainGrass的Texture,可以得到类似下图的效果:

之后可以切换到Set Height的模式:

在这个模式下,可以使用地形画笔绘制地形中对应的位置为固定的高度。使用该功能可以绘制一定高度的平面,道路等,简单使用后的效果如下图所示:

之后可以切换到Smooth Height的模式:

在这个模式下,可以使用画笔将地形中的高度平滑化,使得地形高度的过渡更加平滑,符合玩家的审美和设计的游戏需求。简单使用后的效果如下图所示:

可以看到,地形比处理之前过渡平滑了很多,也更加类似于自然界的天然地形的地貌。

之后可以切换到Stamp Terrain的模式:

在该模式下,可以选择画笔的形状,并像粘贴邮票一样设置画笔区域内的高度取max或者加上固定值。

简单使用后的效果如下图所示:

可以看到Unity的地形绘制功能非常强大,使用Unity的地形编辑器,可以方便快捷地创建出各种想要的地形。

玩家降临

现在考虑如何使用第一人称视角来体验下前面创建的游戏场景。可以使用标准素材库中预制的第一人称玩家。

采用和之前类似的方法,在Unity的Assets Store中下载Standard Assets素材包。然后可以看到素材包内有名为FPSController的第一人称角色预制,可以将其导入前面创建的游戏场景中。

然后运行游戏:

然后就可以在游戏中行动和体验创建的场景了。

种树

之后可以切换到地形下的Paint Trees模式:

在该模式下,可以种树和绘制森林。种树也需要导入标准素材,这里可以使用Free_SpeedTrees素材包中预制的树素材。

点击Edit Tree > Add Tree,之后在弹出的选项卡中选择预制的树的素材。

然后看到树被成功添加到下面的Trees栏中。

之后可以使用画笔在地形上的选定区域绘制选中的树。简单绘制后的效果如下:

游戏效果:

风场

现在创建的树在场景中一动不动,缺少真实感。现在还可以创建风场,使得树会随着风的吹拂摆动,增加树的真实感。

点击GameObject > 3D Object > WindZone,就可以创建一个定向风场。还可以通过模式的设置来创建球形风场。

可以给风场设置参数。对于定向风场,其位置并不影响风的效果。

游戏效果:


可以看到现在树叶和树枝随着风摆动,看起来效果更加真实。

种草

最后,还可以在Paint Details模式下给地形种草。

在这里,点击New Brush,可以选择需要绘制的纹理。

选择上面的Brush_Grass_02的Texture,就可以通过地形画笔绘制游戏场景中的草了。

上面的界面可以配置草的相关参数,如宽度,高度,分布,健康颜色,干燥颜色等。

简单绘制后的效果如下图所示:

游戏效果:

玩家可以看到草被微风吹拂的效果。

以上,构造了一个简单的游戏场景。

写一个简单的总结,总结游戏对象的使用

Unity中的游戏对象是构建游戏的最基础的部件。尽管实际游戏设计中需要依赖更多的模型、预制,但它们都是由一个个最基本的游戏对象构成。

基本的游戏对象包括:

  • Empty (无形却是最常用对象之一)

    • 可以作为子对象的容器
    • 创建新的对象空间
  • 3D 物体
    • 基础 3D 物体(Primitive Object):立方体(Cube)、球体(Sphere)、胶囊体(Capsule)、圆柱体(Sylinder)、平面(Plane)、四边形(Quad)等等。其中,胶囊体可以用于人体的碰撞检测。
    • 构造 3D 物体:由三角形网格构建的物体:如地形等。可以采用计算机图形学的技术和方法,使用三角形网格将复杂的现实生活中的物体建模。
  • Camera 摄像机,玩家观察游戏世界的窗口
  • Light 光源,游戏世界的光源
  • Audio 声音
  • Vedio 视频
  • UI 基于事件的 new UI 系统
  • Particle System 粒子系统与特效

在实际的游戏设计中,可以将上面基本的游戏对象经过设置,组合成为预制。将预制作为一个游戏对象使用。

游戏对象一般有这些共同拥有的属性:Active、Static、Name、Tag、Layer、TransForm等。

游戏对象的外观和内在逻辑设计可以通过添加组件来实现,这采用了设计模式中的组合模式。在游戏开发中,组合常优先于继承。这是因为多重继承过于复杂,且现实世界中的一些对象关系较为复杂,也难以通过继承来表征。

通过给游戏对象添加脚本的方式,可以用于控制和操控游戏对象的行为。

在游戏中创建游戏对象的实例,既可以通过在场景中预先设置,也可以通过代码动态产生。

编程实践"牧师与魔鬼"动作分离版

游戏规则

Priests and Devils
Priests and Devils is a puzzle game in which you will help the Priests and Devils to cross the river within the time limit. There are 3 priests and 3 devils at one side of the river. They all want to get to the other side of this river, but there is only one boat and this boat can only carry two persons each time. And there must be one person steering the boat from one side to the other side. In the flash game, you can click on them to move them and click the go button to move the boat to the other direction. If the priests are out numbered by the devils on either side of the river, they get killed and the game is over. You can try it in many > ways. Keep all priests alive! Good luck!

要帮助三个牧师和三个魔鬼在规定的时间内过河。在游戏中过河只能靠一条船,且这条船上最多只能同时载两个人(牧师/魔鬼)。船上必须要有至少一个人(牧师/魔鬼),才能开到对岸。当任何一边岸上的魔鬼数目比牧师的数目多时,这些魔鬼就会杀掉同处一岸的牧师,游戏就会失败。需要运用智慧让三个牧师和三个魔鬼都顺利过河。

列出游戏中提及的事物(Objects)

游戏中出现的事物包括三个牧师,三个魔鬼,一条船,两边的河岸,阻挡过河的一条河流。在后面需要将这些对象制作成预制,并在游戏初始化时将这些对象生成出来。

用表格列出玩家动作表(规则表)

游戏操作之后会触发规则判断,可能会触发游戏成功或失败。

玩家动作 操作条件 操作结果
点击船 船上至少有一个人(牧师/魔鬼) 船移动到另一侧河岸
点击岸上的人(牧师/魔鬼) 船已经靠岸,且与被点击的人处于同一河岸,且船上的人数少于2 被点击的人移动到船上
点击船上的人(牧师/魔鬼) 船已经靠岸 被点击的人移动到船同侧的岸上

工作内容

设计模式优化

在上一版本的牧师与魔鬼中,控制器负责处理的工作太多,不仅要处理用户交互事件,还要游戏对象加载、游戏规则实现、运动实现等等,这就显得非常臃肿。一个最直观的想法就是让更多的角色来管理不同方面的工作。显然,这就是面向对象基于职责的思考。通过设置更多的角色来分摊不同的工作,并通过消息协同来完成更为复杂的工作。

为了用一组简单的动作组合成复杂的动作,我们采用 cocos2d 的方案,建立与 CCAtion 类似的类。设计思路如下:

  1. 设计一个动作的抽象类作为游戏动作的基类。
  2. 通过门面模式(控制器模式)输出组合好的几个动作,给原来程序调用。也就是 使用CCActionManager组合多个动作。
  3. 设计一个动作管理器类管理一组游戏动作的实现类。
  4. 通过接口回调实现管理者与被管理者解耦,并且实现动作完成时的通知。
  5. 通过组合模式实现动作组合,按组合模式设计方法。

对于这个游戏具体来说,需要把每个可以移动的游戏对象的移动方法共同提取出来,建立一个动作管理器类来管理不同的移动方法。例如在之前的版本中的Move类,当游戏对象需要移动时候,游戏对象自己调用Move类中的方法让自己移动。而优化后的动作分离版牧师与魔鬼,游戏对象则失去了调用自己动作的能力。在新版本中,需要建立一个动作管理器,通过场景控制器把需要移动的游戏对象传递给动作管理器,让动作管理器去移动游戏对象。

优化后的设计模式如下:

这样的设计模式具有如下的优点

  1. 程序更能适应需求变化。
  2. 对象更容易被复用。
  3. 程序更易于维护。

自定义的天空盒

用这一章学到的Skybox对象。创建一个合适的Skybox天空背景,代替默认的天空背景,添加到照相机中。

自定义的音效

为了使游戏更加有趣味和可玩性,在对课程内容学习的基础上,添加了音效。音效选择使用课程链接指向的flash版本的牧师与魔鬼,将录制后的音频文件作为背景音乐。用这一章学到的Audio对象添加到游戏场景中。

项目架构

软件版本

项目使用的开发软件为Unity 3D 2020.1.4f1c1。

文件组织


项目的资源文件夹包括Assets和Packages两个子文件夹。其中,Packages子文件夹存储了系统自带的一些包,在这个项目中并没有特别使用。而Assets文件夹则存储了这次游戏项目使用的资源,场景,脚本等。其中,Assets使用了标准素材库中Standard Assets的water材料(用于构建河流对象),以及Assets Store中的Fantasy Skybox FREE素材包。而Scenes文件夹存储了游戏的场景(这个游戏中只有一个场景)。Scripts文件夹存储了游戏中使用的脚本。Resources文件夹存储了游戏中用到的材料和预制对象。

其中,项目的脚本包括如下文件:

游戏素材

游戏素材和上一个项目的基本相同。除了导入了Fantasy Skybox FREE素材包,使用其中的一个天空盒作为游戏背景。

项目地址

由于整个游戏文件夹过大,这里按照实验要求仅将Assets文件夹传到了公开的仓库上。仓库的链接为https://github.com/alphabstc/PriestsAndDevils2。新建一个Unity 3D项目,按照下面的指引将仓库内容导入,将脚本拖到对应的对象上,应该可以创建出一个可以正常运行的游戏。

脚本分析与设计

Action.cs

该文件实现了游戏中的SSAction类和继承SSAction的各个动作类,实现了游戏中动作管理的功能。

根据上面的UML图,SSAction类是所有动作的基类,SSAction继承了ScriptableObject,而ScriptableObject 是Unity中不需要绑定 GameObject 对象的可编程基类。这些对象受 Unity 引擎场景管理。其子类有Sequence Action和MoveToAction,根据门面模式将其功能抽象为SSActionManager。

除此之外,在SSAction类中通过protected 防止用户自己 new 实例对象;使用 virtual 申明虚方法,通过重写实现多态。这样继承者就明确使用 Start 和 Update 编程游戏对象行为;利用接口(ISSACtionCallback)实现消息通知,避免与动作管理者直接依赖。

对于实现具体的动作,将一个物体移动到目标位置,并通知任务完成。让 Unity 创建动作类,这样可以确保内存正确回收。当动作完成时,则期望管理程序自动回收运行对象,并发出事件通知管理者进行下一步处理。

对于动作组合序列的实现,顺序播放动作让动作组合继承抽象的动作,使其可以被组合;实现回调接受,能接收被组合动作的事件创建一个动作顺序执行序列,-1 表示无限循环动作,而start则表示动作的开始。Update方法执行执行当前动作;SSActionEvent 收到当前动作执行完成,进行下一个动作,如果完成一次循环,减少循环次数。当动作都完成时,则通知该动作的管理者;Start 执行动作前,为每个动作注入当前动作游戏对象,并将自己作为动作事件的接收者;OnDestory 函数则实现了,当自己被注销时,应该释放自己管理的动作。

ISSActionCallback是动作事件接口。该接口作为接收通知对象的抽象类型。

对于动作事件类型定义,使用了枚举变量类型。其定义了事件处理接口,所有事件管理者都必须实现这个接口,来实现事件调度。因此,动作组合事件序列需要实现它,而事件管理器也必须实现它。

SSActionManager是动作管理基类。其是动作对象管理器的基类,实现了所有动作的基本管理。使用上述的各个实现的移动方法,实现游戏对象与动作的绑定,确定回调函数消息的接收对象。管理动作之间的切换。

SceneActionManager则是场景下动作管理器类。其实现了当前场景下的动作管理的具体内容,在场景控制类中调用它的各个方法,实现对当前场景的动作管理。其中需要管理两个动作,一个是场景中船的移动,这是一个单独的移动。对于牧师或魔鬼的移动,由于这是一系列的动作,需要先将人平移到船上方的位置,再将人移动到河岸/船上。这需要两个动作,也就是需要调用动作序列的函数来实现这个动作,我们定义好这两个动作,然后在调用动作管理基类的函数就可以实现人的移动动作了。对于船的移动,可以通过船的当前位置和需要移动到的位置可以通过调用上面动作管理基类中的函数来实现船的移动动作。

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using controller;
namespace action{public class SSAction : ScriptableObject     //所有动作的基类    {public bool enable = true;                     public bool destroy = false;                    public GameObject gameobject;                   public Transform transform;                    public ISSActionCallback callback;   //回调          protected SSAction() { }                        public virtual void Start()     //虚函数      {throw new System.NotImplementedException();}public virtual void Update()//虚函数  {throw new System.NotImplementedException();}}public class SSMoveToAction : SSAction   //具体的动作                   {public Vector3 target;       public float speed;           private SSMoveToAction() { }//防止被外部调用来构造public static SSMoveToAction GetSSAction(Vector3 target, float speed){SSMoveToAction action = ScriptableObject.CreateInstance<SSMoveToAction>();//构造自己action.target = target;action.speed = speed;return action;}public override void Update()//移动{this.transform.position = Vector3.MoveTowards(this.transform.position, target, speed * Time.deltaTime);//进行移动if (this.transform.position == target){this.destroy = true;//销毁this.callback.SSActionEvent(this);//通过回调接口发出通知     }}public override void Start(){}}public class SequenceAction : SSAction, ISSActionCallback //动作序列组合{public List<SSAction> sequence;    public int repeat = -1;           public int start = 0;           public static SequenceAction GetSSAcition(int repeat, int start, List<SSAction> sequence)//构造自己{SequenceAction action = ScriptableObject.CreateInstance<SequenceAction>();action.repeat = repeat;action.sequence = sequence;action.start = start;return action;}public override void Update(){if (sequence.Count == 0) return;//执行完毕 返回if (start < sequence.Count)//还没有执行完序列{sequence[start].Update();     //执行}}public void SSActionEvent(SSAction source, SSActionEventType events = SSActionEventType.Competeted,int intParam = 0, string strParam = null, Object objectParam = null){source.destroy = false;         this.start++;if (this.start >= sequence.Count){this.start = 0;if (repeat > 0) repeat--;if (repeat == 0){this.destroy = true;               this.callback.SSActionEvent(this);}}}public override void Start(){foreach (SSAction action in sequence){action.gameobject = this.gameobject;action.transform = this.transform;action.callback = this;               action.Start();}}void OnDestroy(){}}public enum SSActionEventType : int { Started, Competeted }//动作事件public interface ISSActionCallback{void SSActionEvent(SSAction source, SSActionEventType events = SSActionEventType.Competeted,int intParam = 0, string strParam = null, Object objectParam = null);}public class SSActionManager : MonoBehaviour, ISSActionCallback  //动作管理器                    {private Dictionary<int, SSAction> actions = new Dictionary<int, SSAction>();    private List<SSAction> waitingAdd = new List<SSAction>();                      private List<int> waitingDelete = new List<int>();                                       protected void Update(){foreach (SSAction ac in waitingAdd){actions[ac.GetInstanceID()] = ac;                    }waitingAdd.Clear();foreach (KeyValuePair<int, SSAction> kv in actions){SSAction ac = kv.Value;if (ac.destroy){waitingDelete.Add(ac.GetInstanceID());}else if (ac.enable){ac.Update();}}foreach (int key in waitingDelete){SSAction ac = actions[key];actions.Remove(key);DestroyObject(ac);}waitingDelete.Clear();}public void RunAction(GameObject gameobject, SSAction action, ISSActionCallback manager)//执行动作{action.gameobject = gameobject;action.transform = gameobject.transform;action.callback = manager;waitingAdd.Add(action);action.Start();}public void SSActionEvent(SSAction source, SSActionEventType events = SSActionEventType.Competeted,int intParam = 0, string strParam = null, Object objectParam = null){}}public class MySceneActionManager : SSActionManager  //场景动作管理器{private SSMoveToAction moveBoatToEndOrStart;     private SequenceAction moveRoleToLandorBoat;    public Controller sceneController;protected void Start(){sceneController = (Controller)GameDirector.GetInstance().CurrentScenceController;sceneController.actionManager = this;}public void moveBoat(GameObject boat, Vector3 target, float speed)//移动船{moveBoatToEndOrStart = SSMoveToAction.GetSSAction(target, speed);this.RunAction(boat, moveBoatToEndOrStart, this);}public void moveRole(GameObject role, Vector3 middle_pos, Vector3 end_pos, float speed)//移动角色{SSAction action1 = SSMoveToAction.GetSSAction(middle_pos, speed);SSAction action2 = SSMoveToAction.GetSSAction(end_pos, speed);moveRoleToLandorBoat = SequenceAction.GetSSAcition(1, 0, new List<SSAction> { action1, action2 });this.RunAction(role, moveRoleToLandorBoat, this);}}
}

ModelController.cs

在原来的版本中,可以直接在模型控制中对船进行操作让它移动,所以船类中BoatMove函数原先直接移动船,而现在由于将动作分离,所以BoatMove函数只需要返回需要移动到的位置,然后交给动作控制器处理进行移动。同样,人物的移动也要进行类似的修改。而其他地方则和上一个游戏中的大同小异。修改后的ModelController.cs代码如下:

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using controller;
namespace ModelController{public class BoatModel{GameObject boat;                                          Vector3[] start_empty_pos;                                    Vector3[] end_empty_pos;                                                                                         Click click;int boat_sign = 1;                                                     RoleModel[] roles = new RoleModel[2];                                  public float move_speed = 250;                                public GameObject getGameObject() { return boat; } public BoatModel(){boat = Object.Instantiate(Resources.Load("Prefabs/Boat", typeof(GameObject)), new Vector3(25, -2.5F, 0), Quaternion.identity) as GameObject;boat.name = "boat";click = boat.AddComponent(typeof(Click)) as Click;click.SetBoat(this);start_empty_pos = new Vector3[] { new Vector3(18, 4, 0), new Vector3(32, 4 , 0) };end_empty_pos = new Vector3[] { new Vector3(-32, 4, 0), new Vector3(-18, 3 , 0) };}public bool IsEmpty(){for (int i = 0; i < roles.Length; i++){if (roles[i] != null)return false;}return true;}public Vector3 BoatMove(){if (boat_sign == -1){boat_sign = 1;return new Vector3 (25, -2.5F, 0);}else{boat_sign = -1;return new Vector3 (-25, -2.5F, 0);}}public int GetBoatSign(){ return boat_sign;}public RoleModel DeleteRoleByName(string role_name){for (int i = 0; i < roles.Length; i++){if (roles[i] != null && roles[i].GetName() == role_name){RoleModel role = roles[i];roles[i] = null;return role;}}return null;}public int GetEmptyNumber(){for (int i = 0; i < roles.Length; i++){if (roles[i] == null){return i;}}return -1;}public Vector3 GetEmptyPosition(){Vector3 pos;if (boat_sign == -1)pos = end_empty_pos[GetEmptyNumber()];elsepos = start_empty_pos[GetEmptyNumber()];return pos;}public void AddRole(RoleModel role){roles[GetEmptyNumber()] = role;}public GameObject GetBoat(){ return boat; }public int[] GetRoleNumber(){int[] count = { 0, 0 };for (int i = 0; i < roles.Length; i++){if (roles[i] == null)continue;if (roles[i].GetSign() == 0)count[0]++;elsecount[1]++;}return count;}}public class LandModel{GameObject land;                                Vector3[] positions;                            int land_sign;                                  RoleModel[] roles = new RoleModel[6];           public LandModel(string land_mark){positions = new Vector3[] {new Vector3(46F,14.73F,-4), new Vector3(55,14.73F,-4), new Vector3(64F,14.73F,-4),new Vector3(73F,14.73F,-4), new Vector3(82F,14.73F,-4), new Vector3(91F,14.73F,-4)};if (land_mark == "start"){land = Object.Instantiate(Resources.Load("Prefabs/Land", typeof(GameObject)), new Vector3(70, 1, 0), Quaternion.identity) as GameObject;land_sign = 1;}else if(land_mark == "end"){land = Object.Instantiate(Resources.Load("Prefabs/Land", typeof(GameObject)), new Vector3(-70, 1, 0), Quaternion.identity) as GameObject;land_sign = -1;}}public int GetEmptyNumber()                      {for (int i = 0; i < roles.Length; i++){if (roles[i] == null)return i;}return -1;}public int GetLandSign() { return land_sign; }public Vector3 GetEmptyPosition()               {Vector3 pos = positions[GetEmptyNumber()];pos.x = land_sign * pos.x;                  return pos;}public void AddRole(RoleModel role)             {roles[GetEmptyNumber()] = role;}public RoleModel DeleteRoleByName(string role_name)      { for (int i = 0; i < roles.Length; i++){if (roles[i] != null && roles[i].GetName() == role_name){RoleModel role = roles[i];roles[i] = null;return role;}}return null;}public int[] GetRoleNum(){int[] count = { 0, 0 };                    for (int i = 0; i < roles.Length; i++){if (roles[i] != null){if (roles[i].GetSign() == 0)count[0]++;elsecount[1]++;}}return count;}   }public class RoleModel{GameObject role;int role_sign;             Click click;bool on_boat;              public float move_speed = 250;  LandModel land_model = (GameDirector.GetInstance().CurrentScenceController as Controller).start_land;public GameObject getGameObject() { return role; }public RoleModel(string role_name){if (role_name == "priest"){role = Object.Instantiate(Resources.Load("Prefabs/Priests", typeof(GameObject)), Vector3.zero, Quaternion.Euler(0, -90, 0)) as GameObject;role_sign = 0;}else{role = Object.Instantiate(Resources.Load("Prefabs/Devils", typeof(GameObject)), Vector3.zero, Quaternion.Euler(0, -90, 0)) as GameObject;role_sign = 1;}click = role.AddComponent(typeof(Click)) as Click;click.SetRole(this);}public int GetSign() { return role_sign;}public LandModel GetLandModel(){return land_model;}public string GetName() { return role.name; }public bool IsOnBoat() { return on_boat; }public void SetName(string name) { role.name = name; }public void SetPosition(Vector3 pos) { role.transform.position = pos; }public void GoLand(LandModel land){  role.transform.parent = null;land_model = land;on_boat = false;}public void GoBoat(BoatModel boat){role.transform.parent = boat.GetBoat().transform;land_model = null;          on_boat = true;}}
}

Judge.cs

本次实验还需要设计一个裁判类,当游戏达到结束条件(游戏胜利或游戏失败)时,其通知场景控制器游戏结束。裁判类获取游戏状态并通知场景控制器游戏是否结束,而场景控制器又通知UI游戏是否结束,在UI中查看游戏状态判断是否结束游戏即可。

在场景控制器类中通过调用Judge.Check()即可实现游戏结束。

将之前判断胜负的逻辑抽离出来,实现出下面裁判类的代码:

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using ModelController;
public class Judge {LandModel start_land;LandModel end_land;BoatModel boat;public Judge(LandModel start_,LandModel end_,BoatModel boat_){start_land = start_;end_land = end_;boat = boat_;}public int Check(){int start_priest = (start_land.GetRoleNum())[0];int start_devil = (start_land.GetRoleNum())[1];int end_priest = (end_land.GetRoleNum())[0];int end_devil = (end_land.GetRoleNum())[1];if (end_priest + end_devil == 6)     return 2;int[] boat_role_num = boat.GetRoleNumber();if (boat.GetBoatSign() == 1)         {start_priest += boat_role_num[0];start_devil += boat_role_num[1];}else                                  {end_priest += boat_role_num[0];end_devil += boat_role_num[1];}if (start_priest > 0 && start_priest < start_devil) {      return 1;}if (end_priest > 0 && end_priest < end_devil)        {return 1;}return 0;                                             }
}

其他脚本

对于ClickController.cs Controller.cs GameDirector.cs InterfaceController.cs UserGUI.cs等其他脚本,则和上一版本的牧师与魔鬼差不多相同。

部署脚本

可以直接从Assets文件夹中双击Scene载入场景。或者创建一个新场景,在场景中创建一个除了摄像头和光源之外的空对象,然后将第一个场景主控制器的脚本Controller.cs(在这个游戏中也是唯一一个)拖动到这个空对象上,就可以完成脚本的部署。

调整摄像头

调整摄像头的位置和姿态,使得其可以以比较好的视角看见游戏主场景。

这样就完成了项目的配置。

游戏效果截图

牧师为白色的球,魔鬼为黑色的正方体。

游戏过程:


游戏失败:

游戏胜利:

【3D游戏编程与设计】四 游戏对象与图形基础 : 构建游戏场景+牧师与魔鬼 动作分离版相关推荐

  1. 基于Unity开发的牧师与魔鬼动作分离版游戏设计

    1 作业要求 牧师与魔鬼 动作分离版 设计一个裁判类,当游戏达到结束条件时,通知场景控制器游戏结束 2 实现细节 在原来代码的基础上,修改如下: 将UserGUI的sign成员变量和Controlle ...

  2. unity编程实践-牧师与魔鬼动作分离版

    作业要求 牧师与魔鬼 动作分离版 [2019开始的新要求]:设计一个裁判类,当游戏达到结束条件时,通知场景控制器游戏结束 目标:建立动作管理器,使动作抽象出来,可以应用到任何游戏对象上,以此提高代码复 ...

  3. Unity3D游戏编程-牧师与恶魔 动作分离版

    Unity3D游戏编程-牧师与恶魔 动作分离版 文章目录 Unity3D游戏编程-牧师与恶魔 动作分离版 作业要求 项目配置 项目演示 视频演示 项目下载 文字说明 项目截图 实现过程和方法(算法) ...

  4. 3D游戏编程与设计作业4-Skybox_牧师与魔鬼进阶版

    基本操作演练 下载Fantasy Skybbox FREE,构建自己的游戏场景 直接在上一次作业的Priests and Devils游戏场景中添加天空盒和地形构建场景.首先在Unity Assets ...

  5. Unity游戏对象与图形基础

    游戏对象与图形基础 3D游戏设计第四次作业 前言 基本操作演练[建议做] 编程实践 动作分离设计思路 动作管理器的设计图 相对于上一版的更新 代码分析 核心代码(老师提供) 动作基类--SSActio ...

  6. 游戏对象与图形基础——作业与练习

    下载 Fantasy Skybox FREE, 构建自己的游戏场景 从Asset Store上下载的Skybox: 牧师与魔鬼 动作分离版 UML类图 动作管理Actions类 动作基类 SSActi ...

  7. 3D游戏编程与设计-井字棋

    3D游戏编程与设计-井字棋 目录 3D游戏编程与设计-井字棋 A. 简答题 1. 解释游戏对象(GameObjects)和资源(Assets)的区别与联系 ① 游戏对象 ② 资源 2. 下载几个游戏案 ...

  8. 3D游戏编程与设计 PD(牧师与恶魔)过河游戏智能帮助实现

    3D游戏编程与设计 P&D(牧师与恶魔) 过河游戏智能帮助实现 文章目录 3D游戏编程与设计 P&D(牧师与恶魔) 过河游戏智能帮助实现 一.作业与练习 二.设计简述 1. 状态图基础 ...

  9. 3D游戏编程与设计作业09

    3D游戏编程与设计作业09 UGUI基础 画布 基础概念 测试渲染模式 UI布局基础 基本概念 锚点练习 UI组件与元素 基本概念 Mask练习 动画练习 富文本测试 简单血条 血条(Health B ...

最新文章

  1. 计算机专业学嵌入式好吗?嵌入式到底该怎样学呢?
  2. 解耦的故事(一)-tmfc的开关(转)
  3. 点击新建 下拉框选择
  4. golang检查tcp是否可用_宕机处理:Kubernetes集群高可用实战总结
  5. webstorm2018破解方法
  6. sap.dfa.help.utils.adapters.hm.myadapter
  7. 深入理解 JVM Class文件格式(九)
  8. POJ3764-The xor-longest Path【Trie(字典树)】
  9. leetcode hot100(第二部分) + python(c++)
  10. 关于STM32 IAP
  11. 代理模式vs适配器模式vs外观模式
  12. 为什么在python中整数的值没有限制_为什么在Python中整数是不可变的?
  13. 苏大计算机技术招生人数,苏大省内招生计划比去年增加86名
  14. Pikachu靶机系列之目录遍历、任意文件下载、敏感信息泄露、越权
  15. 计算机网络笔记四 无线局域网
  16. Intent简单介绍
  17. 【python第3课】顺序、循环、分支
  18. Spring Boot使用宝兰德BES进行改造和部署
  19. dlink 备份文件_D-Link路由器备份路由器配置信息教程
  20. 如何在Windows版iTunes中播放Ogg音乐文件

热门文章

  1. 一款功能完善的智能匹配1V1视频聊天App应该通过的测试CASE
  2. WPS 系列:∑求和
  3. ThinkPHP5.1接入阿里云短信服务(原大鱼最新版)指导
  4. CSS基础总结(五)定位
  5. vite - 多渠道差异化打包插件
  6. iPhone/iPad忘记密码、已停用,怎么解锁?
  7. Vue在表格中格式化时间
  8. bitdock系统错误_比特工具栏下载_Bit Dock(类似Mac系统动态工具栏) 1.8.12 官方版_极速下载站...
  9. 预备作业02 : 体会做中学-20162329 张旭升
  10. 马云证婚的主播CP:太太像赵丽颖,不到一年养成两家三皇冠店