Unity游戏设计 牧师与魔鬼

MVC游戏架构

  • MVC的全名是Model View Controller,是模型(Model)-视图(view)-控制器(controller)的缩写,是一种设计模式。它是用一种业务逻辑、数据与界面显示分离的方法来组织代码,将众多的业务逻辑聚集到一个部件里面,在需要改进和个性化定制界面及用户交互的同时,不需要重新编写业务逻辑,达到减少编码的时间,提高代码复用性。
  • 使用的MVC的目的:它将这些对象、显示、控制分离以提高软件的的灵活性和复用性,MVC结构可以使程序具有对象化的特征,也更容易维护。
    MVC 模式中三个组件的详细介绍如下:
  • 模型(Model):用于封装与应用程序业务逻辑相关的数据以及对数据的处理方法。Model 有对数据直接访问的权力,例如对数据库的访问。Model 不依赖 View 和 Controller,也就是说, Model 不关心它会被如何显示或是如何被操作。但是 Model 中数据的变化一般会通过一种刷新机制被公布。为了实现这种机制,那些用于监视此 Model 的 View 必须事先在此 Model 上注册,由此,View 可以了解在数据 Model 上发生的改变。(如,软件设计模式中的“观察者模式”);
  • 视图(View):能够实现数据有目的的显示(理论上,这不是必需的)。在 View 中一般没有程序上的逻辑。为了实现 View 上的刷新功能,View 需要访问它监视的数据模型(即 Model),因此应该事先在被它监视的数据那里注册;
  • 控制器(Controller):起到不同层面间的组织作用,用于控制应用程序的流程。它处理事件并作出响应。“事件”包括用户的行为和数据 Model 上的改变。

完整代码见:github 演示视频 bilibili

一、游戏资源预制

游戏中的角色在脚本中使用场景控制器 Load’Resources 方法加载,因此先将各个游戏角色的模型设计好并放在Resources文件夹中。

Unity3D流水效果_i++


使用不同大小比例的长方体代表游戏中的不同角色;

紫色正方体代表牧师,绿色长方体代表河岸,蓝色的长方体代表水流,黄色扁形长方体代表船。

红色小球代表恶魔。

二、导演类监控游戏状态

继承Object类实现Director,其他类通过获取Direcor类中的静态对象来实现控制。

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;
		}
	}

三、游戏对象运动的处理 —— Moveable类

游戏中存在游戏角色从河岸上船和从船上到河岸以及船在两个河岸来回运动的三个运动过程,因此需要引入一个变量记录运动方式 -> moving_status (int),当该变量的值为0时,不做运动;值为1时,以向量 middle 为运动终点;值为2时,以向量 dest 为运动终点。

int moving_status;	
	Vector3 dest;
	Vector3 middle;

由于该类继承自 MonoBehaviour ,物体的运动就可以在Update()中使用 transform.position 来移动物体。

void Update() {
		if (moving_status == 1) {
			transform.position = Vector3.MoveTowards (transform.position, middle, move_speed * Time.deltaTime);
			if (transform.position == middle) {
				moving_status = 2;
			}
		} else if (moving_status == 2) {
			transform.position = Vector3.MoveTowards (transform.position, dest, move_speed * Time.deltaTime);
			if (transform.position == dest) {
				moving_status = 0;
			}
		}
	}

不同的角色都是用Moveable 类来管理运动,类中的两个向量值需要不断做更改,为了保证类成员变量的安全性,通过函数setDestination()来实现更改向量变量值。

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;
	}

reset()重置运动状态

public void reset() {
		moving_status = 0;
	}

四、控制器 —— Control

  1. 场景控制器 —— loadResources()
    声明接口 loadResources(),在不同的角色类中实现该接口从而在不同类中加载不同资源。
public interface SceneController {
		void loadResources ();
	}
  1. 角色控制 —— MyCharacterController
    游戏中的角色包括 牧师魔鬼 ,要实现对与这两种游戏角色的控制,首先需要获取对应的GameObject ,然后通过鼠标点击响应,改变 GameObject 的运动状态,即改变上面提到的 Moveable 类的属性,从而让Moveable 类实现角色的运动。
    游戏角色的运动可以划分为在船上和不在船上两种状态,这里使用布尔变量 _isOnBoat 记录角色是否在船上。
readonly GameObject character;
	readonly Moveable moveableScript;
	readonly ClickGUI clickGUI;
	readonly int characterType;	// 0->priest, 1->devil
	bool _isOnBoat;
	CoastController coastController;

构造函数,通过传入的字符串(牧师还是魔鬼)加载对应的游戏资源

public MyCharacterController(string which_character) {
			
	if (which_character == "priest") {
		character = Object.Instantiate (Resources.Load ("Perfabs/Priest", typeof(GameObject)), Vector3.zero, Quaternion.identity, null) as GameObject;
		characterType = 0;
	} else {
		character = Object.Instantiate (Resources.Load ("Perfabs/Devil", typeof(GameObject)), Vector3.zero, Quaternion.identity, null) as GameObject;
		characterType = 1;
	}
	moveableScript = character.AddComponent (typeof(Moveable)) as Moveable;
	clickGUI = character.AddComponent (typeof(ClickGUI)) as ClickGUI;
	clickGUI.setController (this);
}

一些属性的设置和读取

public void setName(string name) {
	character.name = name;
}

public void setPosition(Vector3 pos) {
	character.transform.position = pos;
}

public void moveToPosition(Vector3 destination) {
	moveableScript.setDestination(destination);
}

public int getType() {	// 0->priest, 1->devil
		return characterType;
}

public string getName() {
	return character.name;
}

public void getOnBoat(BoatController boatCtrl) {
	coastController = null;
	character.transform.parent = boatCtrl.getGameobj().transform;
	_isOnBoat = true;
}

public void getOnCoast(CoastController coastCtrl) {
	coastController = coastCtrl;
	character.transform.parent = null;
	_isOnBoat = false;
}

public bool isOnBoat() {
	return _isOnBoat;
}

public CoastController getCoastController() {
	return coastController;
}

public void reset() {
	moveableScript.reset ();
	coastController = (Director.getInstance ().currentSceneController as FirstController).fromCoast;
	getOnCoast (coastController);
	setPosition (coastController.getEmptyPosition ());
	coastController.getOnCoast (this);
}
  1. 河岸控制 —— CoastController
    河岸控制类需要控制两个河岸,设置两个向量作为两个河岸的初始化位置,另外添加一个变量记录某一个河岸对象是两个河岸中的哪一个(to or from ?)
readonly GameObject coast;
readonly Vector3 from_pos = new Vector3(9,1,0);
readonly Vector3 to_pos = new Vector3(-9,1,0);
readonly Vector3[] positions;
readonly int to_or_from;	// to->-1, from->1
MyCharacterController[] passengerPlaner;

构造函数通过传入string字符串判断是两个河岸中的哪一个,同时加载对应资源。

public CoastController(string _to_or_from) {
	positions = new Vector3[] {new Vector3(6.5F,2.25F,0), new Vector3(7.5F,2.25F,0), new Vector3(8.5F,2.25F,0), 
	new Vector3(9.5F,2.25F,0), new Vector3(10.5F,2.25F,0), new Vector3(11.5F,2.25F,0)};

	passengerPlaner = new MyCharacterController[6];

	if (_to_or_from == "from") {
		coast = Object.Instantiate (Resources.Load ("Perfabs/Stone", typeof(GameObject)), from_pos, Quaternion.identity, null) as GameObject;
		coast.name = "from";
		to_or_from = 1;
	} else {
		coast = Object.Instantiate (Resources.Load ("Perfabs/Stone", typeof(GameObject)), to_pos, Quaternion.identity, null) as GameObject;
		coast.name = "to";
		to_or_from = -1;
	}
}

为了实现游戏角色上下船的流畅性,每次从船上岸时需要获取目标河岸的空位,角色进入空位中。

public Vector3 getEmptyPosition() {
	Vector3 pos = positions [getEmptyIndex ()];
	pos.x *= to_or_from;
	return pos;
}

public void getOnCoast(MyCharacterController characterCtrl) {
	int index = getEmptyIndex ();
	passengerPlaner [index] = characterCtrl;
}

角色从岸上上船时,返回上船的对象,在河岸对象中删除对应的对象。

public MyCharacterController getOffCoast(string passenger_name) {	
	for (int i = 0; i < passengerPlaner.Length; i++) {
		if (passengerPlaner [i] != null && passengerPlaner [i].getName () == passenger_name) {
			MyCharacterController charactorCtrl = passengerPlaner [i];
			passengerPlaner [i] = null;
			return charactorCtrl;
		}
	}
	return null;
}
  1. 船控制 —— BoatController
public class BoatController {
		readonly GameObject boat;
		readonly Moveable moveableScript;
		readonly Vector3 fromPosition = new Vector3 (5, 1, 0);
		readonly Vector3 toPosition = new Vector3 (-5, 1, 0);
		readonly Vector3[] from_positions;
		readonly Vector3[] to_positions;


		int to_or_from; 
		MyCharacterController[] passenger = new MyCharacterController[2];

		public BoatController() {
			to_or_from = 1;

			from_positions = new Vector3[] { new Vector3 (4.5F, 1.5F, 0), new Vector3 (5.5F, 1.5F, 0) };
			to_positions = new Vector3[] { new Vector3 (-5.5F, 1.5F, 0), new Vector3 (-4.5F, 1.5F, 0) };

			boat = Object.Instantiate (Resources.Load ("Perfabs/Boat", typeof(GameObject)), fromPosition, Quaternion.identity, null) as GameObject;
			boat.name = "boat";

			moveableScript = boat.AddComponent (typeof(Moveable)) as Moveable;
			boat.AddComponent (typeof(ClickGUI));
		}


		public void Move() {
			if (to_or_from == -1) {
				moveableScript.setDestination(fromPosition);
				to_or_from = 1;
			} else {
				moveableScript.setDestination(toPosition);
				to_or_from = -1;
			}
		}

		public int getEmptyIndex() {
			for (int i = 0; i < passenger.Length; i++) {
				if (passenger [i] == null) {
					return i;
				}
			}
			return -1;
		}

		public bool isEmpty() {
			for (int i = 0; i < passenger.Length; i++) {
				if (passenger [i] != null) {
					return false;
				}
			}
			return true;
		}

		public Vector3 getEmptyPosition() {
			Vector3 pos;
			int emptyIndex = getEmptyIndex ();
			if (to_or_from == -1) {
				pos = to_positions[emptyIndex];
			} else {
				pos = from_positions[emptyIndex];
			}
			return pos;
		}

		public void GetOnBoat(MyCharacterController characterCtrl) {
			int index = getEmptyIndex ();
			passenger [index] = characterCtrl;
		}

		public MyCharacterController GetOffBoat(string passenger_name) {
			for (int i = 0; i < passenger.Length; i++) {
				if (passenger [i] != null && passenger [i].getName () == passenger_name) {
					MyCharacterController charactorCtrl = passenger [i];
					passenger [i] = null;
					return charactorCtrl;
				}
			}
			return null;
		}

		public GameObject getGameobj() {
			return boat;
		}

		public int get_to_or_from() { 
			return to_or_from;
		}

		public int[] getCharacterNum() {
			int[] count = {0, 0};
			for (int i = 0; i < passenger.Length; i++) {
				if (passenger [i] == null)
					continue;
				if (passenger [i].getType () == 0) {	// 0->priest, 1->devil
					count[0]++;
				} else {
					count[1]++;
				}
			}
			return count;
		}

		public void reset() {
			moveableScript.reset ();
			if (to_or_from == -1) {
				Move ();
			}
			passenger = new MyCharacterController[2];
		}
	}

五、GUI

  1. UserGUI
    通过游戏状态来进行相应的GUI渲染,以标签来显示结果。
public class UserGUI : MonoBehaviour {
	private UserAction action;
	public int status = 0;
	GUIStyle style;
	GUIStyle buttonStyle;

	void Start() {
		action = Director.getInstance ().currentSceneController as UserAction;

		style = new GUIStyle();
		style.fontSize = 40;
		style.alignment = TextAnchor.MiddleCenter;

		buttonStyle = new GUIStyle("button");
		buttonStyle.fontSize = 30;
	}
	
	void OnGUI() {
		if (status == 1) {
			GUI.Label(new Rect(Screen.width/2-50, Screen.height/2-85, 100, 50), "Gameover!", style);
			if (GUI.Button(new Rect(Screen.width/2-70, Screen.height/2, 140, 70), "Restart", buttonStyle)) {
				status = 0;
				action.restart ();
			}
		} else if(status == 2) {
			GUI.Label(new Rect(Screen.width/2-50, Screen.height/2-85, 100, 50), "You win!", style);
			if (GUI.Button(new Rect(Screen.width/2-70, Screen.height/2, 140, 70), "Restart", buttonStyle)) {
				status = 0;
				action.restart ();
			}
		}
	}
}
  1. ClickGUI
    该类主要对于鼠标点击的不同对象做出不同响应
public class ClickGUI : MonoBehaviour {
	UserAction action;
	MyCharacterController characterController;

	public void setController(MyCharacterController characterCtrl) {
		characterController = characterCtrl;
	}

	void Start() {
		action = Director.getInstance ().currentSceneController as UserAction;
	}

	void OnMouseDown() {
		if (gameObject.name == "boat") {
			action.moveBoat ();
		} else {
			action.characterIsClicked (characterController);
		}
	}
}

六、场景加载效果

Unity3D流水效果_i++_02


点击不同角色,做出响应动作。

Unity3D流水效果_1024程序员节_03


点击船时如果船上有角色船会运动到对岸

Unity3D流水效果_i++_04


运动到对岸后再点击角色,角色会上岸

Unity3D流水效果_数据_05

七、游戏结果测试

Unity3D流水效果_MVC_06


Unity3D流水效果_MVC_07