Coder Social home page Coder Social logo

priests-and-devils's Introduction

游戏规则:

  • 你要运用智慧帮助3个牧师(方块)和3个魔鬼(圆球)渡河。
  • 船最多可以载2名游戏角色。
  • 船上有游戏角色时,你才可以点击这个船,让船移动到对岸。
  • 当有一侧岸的魔鬼数多余牧师数时(包括船上的魔鬼和牧师),魔鬼就会失去控制,吃掉牧师(如果这一侧没有牧师则不会失败),游戏失败。
  • 当所有游戏角色都上到对岸时,游戏胜利。

项目资源

https://github.com/csr632/Priests-and-devils

游戏截图:

开始游戏

游戏失败

游戏胜利


在Unity中体验

从Github中下载我的项目。 将我的Asserts文件夹覆盖你的Unity项目中的Asserts文件夹。在你的Assets窗口中双击“ass”,然后就可以点击运行按钮了!


游戏架构

使用了MVC架构。

  • 场景中的所有GameObject就是Model,它们受到Controller的控制,比如说牧师和魔鬼受到MyCharacterController类的控制,船受到BoatController类的控制,河岸受到CoastController类的控制。
  • View就是UserGUI和ClickGUI,它们展示游戏结果,并提供用户交互的渠道(点击物体和按钮)。
  • Controller:除了刚才说的MyCharacterController、BoatController、CoastController以外,还有更高一层的Controller:FirstController(场景控制器),FirstController控制着这个场景中的所有对象,包括其加载、通信、用户输入。 最高层的Controller是Director类,一个游戏中只能有一个实例,它控制着场景的创建、切换、销毁、游戏暂停、游戏退出等等最高层次的功能。

Director

Director的定义:

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

Director是最高层的控制器,运行游戏时始终只有一个实例,它掌控着场景的加载、切换等,也可以控制游戏暂停、结束等等。

虽然Director控制着场景,但是它并不控制场景中的具体对象,控制场景对象的任务交给了SceneController(场景控制器),我们等一下会谈到。

Director类使用了单例模式。第一次调用Director.getInstance()时,会创建一个新的Director对象,保存在_instance,此后每次调用getInstance,都回返回_instance。也就是说Director最多只有一个实例。这样,我们在任何Script中的任何地方通过Director.getInstance()都能得到同一个Director对象,也就可以获得同一个currentSceneController,这样我们就可以轻易实现类与类之间的通信,比如说我在其他控制器中就可以使用Director.getInstance().somethingHappen()来告诉导演某一件事情发生了,导演就可以在somethingHappen()方法中做出对应的反应。


SceneController接口

SceneController接口定义:

public interface SceneController {
	void loadResources ();
}

interface(接口)不能直接用来创建对象!必须先有一个类实现(继承)它,在我的这个游戏中就是FirstController类。 SceneController 是用来干什么的呢?它是导演控制场景控制器的渠道。在上面的Director 类中,currentSceneController (FirstController类)就是SceneController的实现,所以Director可以调用SceneController接口中的方法,来实现对场景的生杀予夺。

在这个游戏中SceneController的定义非常简单,因为这个游戏做得并不完整。我们刚才说过导演可以加载、切换、销毁场景、暂停游戏,所以SceneController 还可以规定void switchScene()void destroyScene()void pause()这些方法,供给导演来调用。


Moveable

Moveable是一个可以挂载在GameObject上的类:

public class Moveable: MonoBehaviour {
	
	readonly float move_speed = 20;

	// change frequently
	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, 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;
			}
		}
	}
	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;
	}
}

GameObject挂载上Moveable以后,Controller就可以通过setDestination()方法轻松地让GameObject移动起来。

在这里我没有让物体直接移动到目的地dest,因为那样可能会直接穿过河岸物体。我用middle来保存一个中间位置,让物体先移动到middle,再移动到dest,这就实现了一个折线的移动,不会穿越河岸。moving_status记录着目前该物体处于哪种移动状态。


MyCharacterController

MyCharacterController封装了一个GameObject,表示游戏角色(牧师或恶魔)。

public class MyCharacterController {
	readonly GameObject character;
	readonly Moveable moveableScript;
	readonly ClickGUI clickGUI;
	readonly int characterType;	// 0->priest, 1->devil

	// change frequently
	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);
	}
}

在构造函数中实例化了一个perfab,创建GameObject,因此我们每new MyCharacterController()一次,场景中就会多一个游戏角色。 构造函数还将clickGUI挂载到了这个角色上,以监测“鼠标点击角色”的事件。

MyCharacterController还定义了一些方法提供给场景控制器来调用,方法名已经能够表明这个方法是做什么的了。


BoatController和CoastController

BoatController和CoastController也类似MyCharacterController,封装了船GameObject和河岸GameObject。实现这两个类的难度主要在于它们是一种“容器”,游戏角色要进入它们的空位中。因此它们要提供getEmptyPosition()方法,给出自己的空位,让游戏角色能够移动到合适的位置。

/*-----------------------------------CoastController------------------------------------------*/
public class CoastController {
	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

	// change frequently
	MyCharacterController[] passengerPlaner;

	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 int getEmptyIndex() {
		for (int i = 0; i < passengerPlaner.Length; i++) {
			if (passengerPlaner [i] == null) {
				return i;
			}
		}
		return -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) {	// 0->priest, 1->devil
		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;
			}
		}
		Debug.Log ("cant find passenger on coast: " + passenger_name);
		return null;
	}

	public int get_to_or_from() {
		return to_or_from;
	}

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

	public void reset() {
		passengerPlaner = new MyCharacterController[6];
	}
}

/*-----------------------------------BoatController------------------------------------------*/
public class BoatController {
	readonly GameObject boat;
	readonly Moveable moveableScript;
	readonly ClickGUI clickGUI;
	readonly Vector3 fromPosition = new Vector3 (5, 1, 0);
	readonly Vector3 toPosition = new Vector3 (-5, 1, 0);
	readonly Vector3[] from_positions;
	readonly Vector3[] to_positions;

	// change frequently
	int to_or_from; // to->-1; from->1
	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;
		clickGUI = boat.AddComponent (typeof(ClickGUI)) as 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;
			}
		}
		Debug.Log ("Cant find passenger in boat: " + passenger_name);
		return null;
	}

	public GameObject getGameobj() {
		return boat;
	}

	public int get_to_or_from() { // to->-1; from->1
		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];
	}
}

另外一个需要注意的是MyCharacterController、BoatController、CoastController有一些方法名是重复的,比如说getOnBoat在MyCharacterController和BoatController中都有(BoatController中的GetOnBoat是我当时手抖了,第一个字母应该小写)。看起来似乎功能有点重复,为什么不只用一个函数操控游戏角色的上船呢?原因是不要在一个类中操作另一个类,那会加强两个类之间的耦合性。MyCharacterController中的getOnBoat()只应该操作MyCharacterController中的成员,BoatController中的GetOnBoat()只应该操作BoatController中的成员。 我们在FirstController中想让游戏角色上船的时候,两个类的getOnBoat都要调用:

whichCoast.getOffCoast(characterCtrl.getName());
characterCtrl.moveToPosition (boat.getEmptyPosition());
characterCtrl.getOnBoat (boat);
boat.GetOnBoat (characterCtrl);

UserAction

这个接口实际上使用了门面模式。 FirstController必须要实现这个接口才能对用户的输入做出反应。

public interface UserAction {
	void moveBoat();
	void characterIsClicked(MyCharacterController characterCtrl);
	void restart();
}

在这个游戏中,对用户输入做出反应,有这三个方法就够了。 UserAction是如何得到用户的输入的呢?原来,在ClickGUI和UserGUI这两个类中,都保存了一个UserAction的引用。当ClickGUI监测到用户点击GameObject的时候,就会调用这个引用的characterIsClicked方法,这样FirstController就知道哪一个游戏角色被点击了。UserGUI同理,只不过它监测的是“用户点击Restart按钮”的事件。

门面模式的好处:通过一套接口(UserAction)来定义Controller与GUI交互的渠道,这样实现Controller类的程序员只需要实现UserAction接口,他的代码就可以被任何支持这个接口的GUI类所使用;实现GUI类的程序员也不需要知道Controller的实现方式,它只需要调用接口中的方法,后面的事情就交给Controller吧!


ClickGUI

ClickGUI类是用来监测用户点击,并调用SceneController进行响应的。

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

我们可以看到UserAction action实际上是FirstController的对象,它实现了UserAction接口。ClickGUI与FirstController打交道,就是通过UserAction接口的API。ClickGUI不知道这些API是怎么被实现的,但它知道FirstController类一定有这些方法。


可以做的扩展:

  • 游戏失败以后不能再响应用户点击的事件,用户只能点击Restart。
  • 增加计时的功能(这应该由SceneController来控制)。
  • 增加暂停/恢复游戏的功能(这应该由Director来控制)。
  • 在开始游戏之前做一个欢迎界面,与用户进行交互(这就是另一个场景了)。
  • 让用户可以在游戏中切换到欢迎界面,再切换回游戏界面的时候,游戏状态要和之前一样(场景的切换)。用户可以在游戏中放弃游戏,回到欢迎页面(场景的销毁)。
  • 让用户能够在欢迎界面指定有几个牧师几个恶魔,然后开始游戏。(运行时决定场景的创建)
  • 增加一种更难的模式,开始3秒以后牧师和恶魔外观相同,玩家需要凭借记忆来操作。
  • 美化游戏对象!

priests-and-devils's People

Contributors

csr632 avatar

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.