牧师与魔鬼小游戏
零、写在前面
-
自定义的数据类不要继承Unity的MonoBehavior类,一开始在判断游戏角色对象是否为空的时候,即判断是否
== null
,发现判断的结果一直为true,后来发现是因为Character类继承了MonoBehavior类,此类及其子类均不允许被new出来,Unity重载了UnityEngine
命名空间下的Object的==
运算符,故导致了==
的使用出现了问题。 -
此次作业运用了MVC架构,体会到了模块之间低耦合度的好处,在修改代码时很方便,只需要找到对应的模块进行修改,修改结果不会对其它模块造成太大的影响。各个模块的明确分工,职责驱动使得整个项目能够方向明确地推进。
-
利用组件的概念,当对象添加了Moveable组件,则其具有了移动的功能,当对象添加了GUIClick组件,则其具有对鼠标点击做出反应的功能,由此实现了代码的重用。
-
通过运用facade设计模式也进一步体验到接口是如何实现解耦合的——控制器去完成UserAction接口,即UserAction实现了用户动作和控制器的解耦合。
一、 项目配置
-
首先创建一个新项目,选择3D模板
-
新项目的文件结构如下:
-
Assets/Resources下存放的是项目动态加载所需的图片以及预制,预制包括按要求制作成预制的牧师、魔鬼、船、河流和河岸,图片则是用于GUI装饰
-
Assets/Materials
-
Assets/Scripts中则存放的是项目代码,各个类之间遵守MVC架构
-
-
由于图片是动态加载,故需要将在Inspector菜单中,将图片设置为可读可写状态,才能保证图片能被正常加载。
-
最后将FirstController代码拖到Main Camera中,Ctrl+B即可运行。
二、游戏提及的事物
-
牧师
-
魔鬼
-
船
-
河流
-
岸
三、玩家动作表(规则表)
玩家动作 | 前提条件 | 结果 |
---|---|---|
点击岸上的牧师或魔鬼 | 船上有空位 | 被点击的牧师或魔鬼移动到船上 |
点击船上的牧师或魔鬼 | 无 | 被点击的牧师或魔鬼移动到岸上 |
点击GO按钮 | 船上有乘客 | 船移动到对岸 |
点击Rstart按钮 | 无 | 游戏重置 |
四、 实现过程和方法
1. 总体设计思路
-
整个项目使用MVC架构,各个代码文件划分如下
-
Model部分:Character、Boat、Land
-
View部分:GUI
-
Controller部分:Director、SceneController、FirstController
-
UserAction是一个接口,控制器去实现UserAction接口,实现用户动作和游戏逻辑实现的解耦合,也即门面设计模式的应用
-
GUIClick和Moveable则是作为两个组件,通过组合的方式作为游戏对象的组件,实现代码的复用
-
-
Model部分就是将游戏中出现的事物封装为各个类,管理各自的数据,然后受Controller的调用、控制,最后View部分实现将Model数据可视化通过用户界面的呈现。
2. 模块分析
Model部分
-
Character
-
牧师和魔鬼就是通过实现Character类,其管理的数据的主要为一个GameObject对象,通过Object.Instantiate函数动态加载预制中的牧师或魔鬼模型。
-
Character类的功能主要有实现离岸、上岸、上船、下船的数据更新,以及通过moveable组件控制GameObject对象的移动。
using System.Collections; using System.Collections.Generic; using UnityEngine; public class Character { readonly GameObject character; readonly int type; //0-priest 1-devil readonly Moveable moveableScript;// 移动脚本组件 readonly GUIClick _GUIClick;// 响应鼠标点击组件 private bool isOnBoat;// 是否在船上 private bool isFinished;//是否已经过河 // 构造函数,根据传入的字符串选择创建牧师对象或者魔鬼对象 public Character(string sel){ // 动态加载预制 if(sel == "priest"){ character = Object.Instantiate(Resources.Load("Prefabs/priest", typeof(GameObject)),Vector3.zero, Quaternion.identity, null) as GameObject; type = 0; } else{ character = Object.Instantiate(Resources.Load("Prefabs/devil", typeof(GameObject)),Vector3.zero, Quaternion.identity, null) as GameObject; type = 1; } // 为对象添加移动和点击响应组件 moveableScript = character.AddComponent (typeof(Moveable)) as Moveable; _GUIClick = character.AddComponent(typeof(GUIClick)) as GUIClick; _GUIClick.bindCharacter(this); // 初始化变量 isOnBoat = false; isFinished = false; } public int getType(){ return type; } public void setName(string name) { character.name = name; } public string getName() { return character.name; } public void getOnBoat(Boat boat){ // 设置transform为船的子组件,这样当船移动的时候就可以随着船移动 character.transform.parent = boat.getGameobj().transform; isOnBoat = true; } public void getOffBoat(){ character.transform.parent = null; isOnBoat = false; } public void getOnLand(Land land){ if(land.getType() == 0){ isFinished = false; } else{ isFinished = true; } isOnBoat = false; } public bool getIsOnBoat(){ return isOnBoat; } public void setPosition(Vector3 pos){ character.transform.position = pos; } public void moveToPosition(Vector3 pos){ moveableScript.setDestination(pos); } public bool getIsFinished(){ return isFinished; } // 重置对象 public void reset(){ moveableScript.reset(); // 如果对象已经过河 if(isFinished){ // 通过导演对象获取当前场景的控制器,取得其控制的Land对象 Land endLand = (Director.getInstance ().currentSceneController as FirstController).endLand; endLand.leaveLand(this); Land startLand = (Director.getInstance ().currentSceneController as FirstController).startLand; getOnLand(startLand); setPosition(startLand.getOnLand(this)); } // 如果对象在船上 if(isOnBoat){ getOffBoat(); Land startLand = (Director.getInstance ().currentSceneController as FirstController).startLand; Boat boat = (Director.getInstance ().currentSceneController as FirstController).boat; boat.removePassenger(this); getOnLand(startLand); setPosition(startLand.getOnLand(this)); } character.transform.parent = null; } }
-
-
Boat
-
主要就是实现船对象,其主要的数据是一个GameObject对象,通过Object.Instantiate函数动态加载预制中的船模型。
-
通过维护一个Character类数组,实现船上乘客的更新。
-
主要功能包括:添加乘客、移除乘客、获得当前船上牧师和魔鬼的数量,以及通过moveable组件控制GameObject对象的移动。
using System.Collections; using System.Collections.Generic; using UnityEngine; public class Boat{ readonly GameObject boat; readonly Moveable moveableScript; readonly Vector3 startPos = new Vector3(-1.95F,0.68F,0);// 在起始岸边的位置 readonly Vector3 endPos = new Vector3(1.95F,0.68F,0);// 在终点岸边的位置 readonly Vector3 startFirstPos = new Vector3(-2.3F,1.15F,0);// 在起始岸边船的第一个座位位置 readonly Vector3 startSecondPos = new Vector3(-1.54F,1.15F,0);// 在起始岸边船的第二个座位位置 readonly Vector3 endFirstPos = new Vector3(1.54F,1.15F,0);// 在终点岸边船的第一个座位位置 readonly Vector3 endSecondPos = new Vector3(2.37F,1.15F,0);// 在终点岸边船的第二个座位位置 private Character[] passenger;// 乘客对象数组,存放船上的对象 private int curPosition;//当前船在哪个岸边 0-start 1-end private int count;// 船上人数 public Boat(){ // 初始化对象数组,一开始船上无人,故置为空 passenger = new Character[2]; passenger[0] = null; passenger[1] = null; // 动态加载船的预制 boat = Object.Instantiate (Resources.Load ("Prefabs/boat", typeof(GameObject)), startPos, Quaternion.identity, null) as GameObject; boat.name = "boat"; // 添加moveable组件使得船可以移动 moveableScript = boat.AddComponent (typeof(Moveable)) as Moveable; // 初始变量 curPosition = 0; count = 0; } public void move(){ if(curPosition == 0){ moveableScript.setDestination(endPos); curPosition = 1; } else{ moveableScript.setDestination(startPos); curPosition = 0; } } public bool isEmpty(){ return count == 0; } public bool isFull(){ return count == 2; } public bool addPassenger(Character ch){ if(count == 2){ return false; } // 找到空位置 for(int i = 0;i < 2;++i){ if(passenger[i] == null){ passenger[i] = ch; break; } } count++; return true; } public bool removePassenger(Character ch){ if(count == 0){ return false; } // 找到要删除的乘客 for(int i = 0;i < 2;++i){ if(passenger[i] != null && passenger[i].getName() == ch.getName()){ passenger[i] = null; } } count--; return true; } public int getCurPosition(){ return curPosition; } public Vector3 getEmptyPosition(){ // 若船已满,则返回坐标(0,0,0) if(count == 2){ return new Vector3(0,0,0); } if(curPosition == 0){ if(passenger[0] == null){ return startFirstPos; } else{ return startSecondPos; } } else{ if(passenger[0] == null){ return endFirstPos; } else{ return endSecondPos; } } } public GameObject getGameobj(){ return boat; } public int getNumOfPriest(){ int res = 0; for(int i = 0;i < 2;++i){ if(passenger[i] != null && passenger[i].getType() == 0){ res++; } } return res; } public int getNumOfDevil(){ int res = 0; for(int i = 0;i < 2;++i){ if(passenger[i] != null && passenger[i].getType() == 1){ res++; } } return res; } public void reset(){ count = 0; passenger[0] = null; passenger[1] = null; // 若船在终点,则回到起始岸 if(curPosition == 1){ moveableScript.setDestination(startPos); curPosition = 0; } } }
-
-
Land
-
实现的是河岸对象,包含了一个GameObject对象,t对象,通过Object.Instantiate函数动态加载预制中的河岸模型。
-
Land类主要通过维护三个数组:当前河岸上的对象数组、位置数组、位置状态数组,这三者的下标一一对应。河岸上有六个位置,用于放置游戏角色,当某个位置上有角色的时候,即该位置被占有,此时位置状态数组相同下标对应的值会由变为1.
-
因此,当有新的角色要上岸时,会先通过查找位置状态数组找到空位置,然后返回相同下标的位置数组的值,并将该对象存入相同下标的对象数组中,实际上就是一个哈希的过程。
using System.Collections; using System.Collections.Generic; using UnityEngine; public class Land{ private GameObject land; readonly Vector3 startPos = new Vector3(-6.6F,0,0);// 起始岸的位置 readonly Vector3 endPos = new Vector3(6.6F,0,0);// 终点岸的位置 private int type;//河岸类型 0-start 1-end private int count;// 岸上人数 private Character[] curCharacter;// 当前岸上的角色 readonly Vector3[] place;// 岸上的六个位置坐标 角色数组下标和位置数组下标保持一致 private int[] emptyPlace;// 岸上六个位置的情况 0-empty 1-not empty public Land(string sel){ place = new Vector3[6]; if(sel == "start"){ land = Object.Instantiate(Resources.Load("Prefabs/land", typeof(GameObject)),startPos, Quaternion.identity, null) as GameObject; land.name = "start"; type = 0; count = 0; for(int i = 0;i < 6;++i){ place[i] = new Vector3(-3.5F-0.8F*i,1.5F,0); } } else{ land = Object.Instantiate(Resources.Load("Prefabs/land", typeof(GameObject)),endPos, Quaternion.identity, null) as GameObject; land.name = "end"; type = 1; count = 0; for(int i = 0;i < 6;++i){ place[i] = new Vector3(3.5F+0.8F*i,1.5F,0); } } curCharacter = new Character[6]; emptyPlace = new int[6]; for(int i = 0;i < 6;++i){ curCharacter[i] = null; } for(int i = 0;i < 6;++i){ emptyPlace[i] = 0; } } public int getType(){ return type; } public int getCount(){ return count; } public Vector3 getOnLand(Character ch){ int index = 0; // 找到空位置,将角色放入对应下标的角色数组中,方便后面查找删除 while(true){ if(emptyPlace[index] == 0){ emptyPlace[index] = 1; curCharacter[index] = ch; count++; break; } else{ index++; } } // 返回角色所在位置 return place[index]; } public void leaveLand(Character ch){ for(int i = 0;i < 6;++i){ if( curCharacter[i] != null && curCharacter[i].getName() == ch.getName()){ curCharacter[i] = null; emptyPlace[i] = 0; count--; return; } } } public Vector3 getEmptyPosition(){ for(int i = 0;i < 6;++i){ if(emptyPlace[i] == 0){ return place[i]; } } return new Vector3(0,0,0); } public int getNumOfPriest(){ int res = 0; for(int i = 0;i < 6;++i){ if(curCharacter[i] == null){ continue; } if(curCharacter[i].getType() == 0){ res++; } } return res; } public int getNumOfDevil(){ int res = 0; for(int i = 0;i < 6;++i){ if(curCharacter[i] == null){ continue; } if(curCharacter[i].getType() == 1){ res++; } } return res; } public void reset(){ if(type == 0){ for(int i = 0;i < 6;++i){ emptyPlace[i] = 1; } count = 6; } else{ for(int i = 0;i < 6;++i){ emptyPlace[i] = 0; } count = 0; } } }
-
View部分
-
_GUI
-
此类通过将用户的行为传递给控制器,从而修改model中的数据,并不断呈现最新的数据
using System.Collections; using System.Collections.Generic; using UnityEngine; public class _GUI : MonoBehaviour{ private SceneController sc; private UserAction ac; private GUIStyle style; private int status; private Texture2D priest; private Texture2D devil; private Texture2D rule; void Start(){ style = new GUIStyle(); style.fontSize = 40; // 获得控制器,通过控制器更新model部分的数据 sc = Director.getInstance ().currentSceneController; ac = Director.getInstance().currentSceneController as UserAction; status = (Director.getInstance ().currentSceneController as FirstController).status; // 动态加载图片 priest = Instantiate (Resources.Load ("Pictures/牧师", typeof(Texture2D))) as Texture2D; devil = Instantiate (Resources.Load ("Pictures/恶魔", typeof(Texture2D))) as Texture2D; rule = Instantiate (Resources.Load ("Pictures/规则", typeof(Texture2D))) as Texture2D; // Debug.Log("Screen.width"+Screen.width); // Debug.Log(" Screen.height"+ Screen.height); } void OnGUI(){ // 同步游戏状态 status = (Director.getInstance ().currentSceneController as FirstController).status; if(GUI.Button(new Rect(795,490, 120, 120),"GO")){ if(status == 0){ ac.goButtonIsClicked(); } } if(GUI.Button(new Rect(1095,490, 120, 120), "Restart")){ ac.restart(); } if(status == 1){ GUI.Label(new Rect(800,100,300,250),devil); GUI.Label(new Rect(247, 50, 100, 50), "You lose!",style); } if(status == 2){ GUI.Label(new Rect(800,100,300,250),priest); GUI.Label(new Rect(247, 50, 100, 50), "You win!",style); } GUI.Label(new Rect(0,800,1500,1500),rule); } }
-
Controller部分
这一部分基本就是按照课程网站上的指示做
-
Director
-
Director继承自C#根对象,因此不受unity引擎的管理,无需加载,且通过应用单例模式保证全局只有一个实例对象
-
其主要实现:获取当前游戏的场景、管理游戏全局状态
using System.Collections; using System.Collections.Generic; using UnityEngine; public class Director : System.Object{ private static Director _instance; public SceneController currentSceneController { get; set; } public static Director getInstance() { if (_instance == null) { _instance = new Director (); } return _instance; } }
-
-
SceneController
-
SceneController是一个接口,由具体的场记来实现,对于这个游戏,此接口只有两个函数分别用于实现加载游戏资源和检查游戏状态
using System.Collections; using System.Collections.Generic; using UnityEngine; public interface SceneController { void loadResources (); void checkGameStatus(); }
-
-
FirstController
-
FirstController是此游戏唯一的一个场记,负责实现GUI和模型数据之间的同步。
-
其主要就是存放各个对象,并实现SceneController和UserAction接口,实现对模型对象的操作
using System.Collections; using System.Collections.Generic; using UnityEngine; public class FirstController : MonoBehaviour, SceneController, UserAction{ readonly Vector3 water_pos = new Vector3(0,-0.4F,0);// 河流预制的位置 public Land startLand;// 起始河岸 public Land endLand;// 终点河岸 public Boat boat;// 船 public int status;//游戏状态 0-gaming 1-lose 2-win private Character[] characters;// 六个游戏角色 private _GUI userGUI;// GUI组件 void Awake() { // 将自身设置为当前场记 Director director = Director.getInstance (); director.currentSceneController = this; // 将GUI作为一个组件 userGUI = gameObject.AddComponent <_GUI>() as _GUI; characters = new Character[6]; // 加载游戏资源 loadResources(); } // 实现SceneController接口,加载游戏资源 public void loadResources() { GameObject water = Instantiate (Resources.Load ("Prefabs/river", typeof(GameObject)), water_pos, Quaternion.identity, null) as GameObject; water.name = "water"; startLand = new Land ("start"); endLand = new Land ("end"); boat = new Boat(); loadCharacter (); } // 实现SceneController接口,检查游戏状态 0->not finish, 1->lose, 2->win public void checkGameStatus() { // 船在出发点 int startNumOfPriest = startLand.getNumOfPriest(); int startNumOfDevil = startLand.getNumOfDevil(); int endNumOfPriest = endLand.getNumOfPriest(); int endNumOfDevil = endLand.getNumOfDevil(); int boatNumOfPriest = boat.getNumOfPriest(); int boatNumOfDevil = boat.getNumOfDevil(); if(endNumOfPriest + endNumOfDevil + boatNumOfPriest + boatNumOfDevil == 6){ status = 2; return ; } if(boat.getCurPosition() == 1){ if((startNumOfPriest < startNumOfDevil && startNumOfPriest > 0)||(endNumOfPriest + boatNumOfPriest < endNumOfDevil + boatNumOfDevil && endNumOfPriest + boatNumOfPriest > 0)){ status = 1; return ; } } else{ if((startNumOfPriest + boatNumOfPriest < startNumOfDevil + boatNumOfDevil && startNumOfPriest + boatNumOfPriest > 0) || (endNumOfPriest < endNumOfDevil && endNumOfPriest > 0)){ status = 1; return ; } } status = 0; } // 加载游戏对象 private void loadCharacter() { for (int i = 0; i < 3; i++) { Character ch = new Character("priest"); ch.setName("priest" + i); ch.getOnLand(startLand); ch.setPosition (startLand.getOnLand(ch)); characters[i] = ch; } for (int i = 0; i < 3; i++) { Character ch = new Character("devil"); ch.setName("devil" + i); ch.setPosition (startLand.getOnLand(ch)); ch.getOnLand(startLand); characters [i+3] = ch; } } // 实现UserAction接口的goButtonIsClicked函数 public void goButtonIsClicked(){ if(!boat.isEmpty()){ boat.move(); checkGameStatus(); } } // 实现UserAction接口的characterIsClicked函数 public void characterIsClicked(Character ch){ // 角色在船上 if(ch.getIsOnBoat()){ if(boat.getCurPosition() == 0){ ch.moveToPosition(startLand.getOnLand(ch)); ch.getOnLand(startLand); } else{ ch.moveToPosition(endLand.getOnLand(ch)); ch.getOnLand(endLand); } ch.getOffBoat(); boat.removePassenger(ch); } // 角色在岸上 else{ if(!boat.isFull()){ if(ch.getIsFinished() && boat.getCurPosition() == 1){ ch.getOnBoat(boat); endLand.leaveLand(ch); ch.moveToPosition(boat.getEmptyPosition()); boat.addPassenger(ch); } if(!ch.getIsFinished() && boat.getCurPosition() == 0){ ch.getOnBoat(boat); startLand.leaveLand(ch); ch.moveToPosition(boat.getEmptyPosition()); boat.addPassenger(ch); } } } } // 重置游戏 public void restart(){ status = 0; for(int i = 0;i < 6;++i){ characters[i].reset(); } boat.reset(); startLand.reset(); endLand.reset(); } }
-
组件部分
-
GUIClick
using System.Collections; using System.Collections.Generic; using UnityEngine; public class GUIClick : MonoBehaviour{ UserAction action; Character bindingCharacter;// 组件当前绑定的角色对象 SceneController sc; int status;// 游戏状态 public void bindCharacter(Character ch){ bindingCharacter = ch; status = (Director.getInstance ().currentSceneController as FirstController).status; } void Start(){ action = Director.getInstance().currentSceneController as UserAction; sc = Director.getInstance().currentSceneController; } void OnMouseDown(){ // 同步游戏状态 status = (Director.getInstance ().currentSceneController as FirstController).status; // 只有在游戏中点击才有效 if(status == 0){ action.characterIsClicked(bindingCharacter); } } }
-
Moveable
此代码参考了师兄博客的实现,由于河岸和船不在同一水平线,故让游戏角色在上下船的时候以折线移动
using System.Collections; using System.Collections.Generic; using UnityEngine; public class Moveable : MonoBehaviour{ readonly float speed = 10; int moving_status; // 0->not moving, 1->moving to middle, 2->moving to dest Vector3 dest; Vector3 middle; void Update() { if (moving_status == 1) { transform.position = Vector3.MoveTowards (transform.position, middle, speed * Time.deltaTime); if (transform.position == middle) { moving_status = 2; } } else if (moving_status == 2) { transform.position = Vector3.MoveTowards (transform.position, dest, speed * Time.deltaTime); if (transform.position == dest) { moving_status = 0; } } } public void setDestination(Vector3 _dest) { dest = _dest; middle = _dest; if (_dest.y == transform.position.y) { // boat moving moving_status = 2; } else if (_dest.y < transform.position.y) { // character from coast to boat middle.y = transform.position.y; } else { // character from boat to coast middle.x = transform.position.x; } moving_status = 1; } public void reset() { moving_status = 0; } }
五、效果展示
[]: Unity3D作业-牧师与魔鬼过河演示视频_哔哩哔哩_bilibili