游戲規(guī)則:
- 你要運用智慧幫助3個牧師(方塊)和3個魔鬼(圓球)渡河蔗蹋。
- 船最多可以載2名游戲角色旁振。
- 船上有游戲角色時,你才可以點擊這個船钦睡,讓船移動到對岸墓律。
- 當有一側(cè)岸的魔鬼數(shù)多余牧師數(shù)時(包括船上的魔鬼和牧師)膀估,魔鬼就會失去控制,吃掉牧師(如果這一側(cè)沒有牧師則不會失敵芊怼)察纯,游戲失敗。
- 當所有游戲角色都上到對岸時针肥,游戲勝利饼记。
項目資源
https://github.com/csr632/Priests-and-devils
游戲截圖:
在Unity中體驗
從Github中下載我的項目。
將我的Asserts文件夾覆蓋你的Unity項目中的Asserts文件夾祖驱。在你的Assets窗口中雙擊“ass”握恳,然后就可以點擊運行按鈕了!
游戲架構(gòu)
使用了MVC架構(gòu)捺僻。
- 場景中的所有GameObject就是Model乡洼,它們受到Controller的控制,比如說牧師和魔鬼受到MyCharacterController類的控制匕坯,船受到BoatController類的控制束昵,河岸受到CoastController類的控制。
- View就是UserGUI和ClickGUI葛峻,它們展示游戲結(jié)果锹雏,并提供用戶交互的渠道(點擊物體和按鈕)。
- Controller:除了剛才說的MyCharacterController术奖、BoatController礁遵、CoastController以外轻绞,還有更高一層的Controller:FirstController(場景控制器),F(xiàn)irstController控制著這個場景中的所有對象佣耐,包括其加載政勃、通信、用戶輸入兼砖。
最高層的Controller是Director類奸远,一個游戲中只能有一個實例,它控制著場景的創(chuàng)建讽挟、切換懒叛、銷毀、游戲暫停耽梅、游戲退出等等最高層次的功能薛窥。
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是最高層的控制器,運行游戲時始終只有一個實例褐墅,它掌控著場景的加載拆檬、切換等,也可以控制游戲暫停妥凳、結(jié)束等等。
雖然Director控制著場景答捕,但是它并不控制場景中的具體對象逝钥,控制場景對象的任務(wù)交給了SceneController(場景控制器),我們等一下會談到拱镐。
Director類使用了單例模式艘款。第一次調(diào)用Director.getInstance()時,會創(chuàng)建一個新的Director對象沃琅,保存在_instance哗咆,此后每次調(diào)用getInstance,都回返回_instance益眉。也就是說Director最多只有一個實例晌柬。這樣,我們在任何Script中的任何地方通過Director.getInstance()
都能得到同一個Director對象郭脂,也就可以獲得同一個currentSceneController年碘,這樣我們就可以輕易實現(xiàn)類與類之間的通信,比如說我在其他控制器中就可以使用Director.getInstance().somethingHappen()
來告訴導(dǎo)演某一件事情發(fā)生了展鸡,導(dǎo)演就可以在somethingHappen()
方法中做出對應(yīng)的反應(yīng)屿衅。
SceneController接口
SceneController接口定義:
public interface SceneController {
void loadResources ();
}
interface(接口)不能直接用來創(chuàng)建對象!必須先有一個類實現(xiàn)(繼承)它莹弊,在我的這個游戲中就是FirstController類涤久。
SceneController 是用來干什么的呢涡尘?它是導(dǎo)演控制場景控制器的渠道。在上面的Director 類中响迂,currentSceneController (FirstController類)就是SceneController的實現(xiàn)考抄,所以Director可以調(diào)用SceneController接口中的方法,來實現(xiàn)對場景的生殺予奪栓拜。
在這個游戲中SceneController的定義非常簡單座泳,因為這個游戲做得并不完整。我們剛才說過導(dǎo)演可以加載幕与、切換挑势、銷毀場景、暫停游戲啦鸣,所以SceneController 還可以規(guī)定
void switchScene()
潮饱、void destroyScene()
、void pause()
這些方法诫给,供給導(dǎo)演來調(diào)用香拉。
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,這就實現(xiàn)了一個折線的移動勋又,不會穿越河岸苦掘。moving_status記錄著目前該物體處于哪種移動狀態(tài)。
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);
}
}
在構(gòu)造函數(shù)中實例化了一個perfab,創(chuàng)建GameObject蹲嚣,因此我們每new MyCharacterController()
一次递瑰,場景中就會多一個游戲角色。
構(gòu)造函數(shù)還將clickGUI掛載到了這個角色上端铛,以監(jiān)測“鼠標點擊角色”的事件泣矛。
MyCharacterController還定義了一些方法提供給場景控制器來調(diào)用,方法名已經(jīng)能夠表明這個方法是做什么的了禾蚕。
BoatController和CoastController
BoatController和CoastController也類似MyCharacterController您朽,封裝了船GameObject和河岸GameObject。實現(xiàn)這兩個類的難度主要在于它們是一種“容器”,游戲角色要進入它們的空位中哗总。因此它們要提供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有一些方法名是重復(fù)的涮母,比如說getOnBoat在MyCharacterController和BoatController中都有(BoatController中的GetOnBoat是我當時手抖了谆趾,第一個字母應(yīng)該小寫)∨驯荆看起來似乎功能有點重復(fù)沪蓬,為什么不只用一個函數(shù)操控游戲角色的上船呢?原因是不要在一個類中操作另一個類来候,那會加強兩個類之間的耦合性跷叉。MyCharacterController中的getOnBoat()
只應(yīng)該操作MyCharacterController中的成員,BoatController中的GetOnBoat()
只應(yīng)該操作BoatController中的成員营搅。
我們在FirstController中想讓游戲角色上船的時候云挟,兩個類的getOnBoat都要調(diào)用:
whichCoast.getOffCoast(characterCtrl.getName());
characterCtrl.moveToPosition (boat.getEmptyPosition());
characterCtrl.getOnBoat (boat);
boat.GetOnBoat (characterCtrl);
UserAction
這個接口實際上使用了門面模式。
FirstController必須要實現(xiàn)這個接口才能對用戶的輸入做出反應(yīng)转质。
public interface UserAction {
void moveBoat();
void characterIsClicked(MyCharacterController characterCtrl);
void restart();
}
在這個游戲中园欣,對用戶輸入做出反應(yīng),有這三個方法就夠了休蟹。
UserAction是如何得到用戶的輸入的呢俊庇?原來,在ClickGUI和UserGUI這兩個類中鸡挠,都保存了一個UserAction的引用。當ClickGUI監(jiān)測到用戶點擊GameObject的時候搬男,就會調(diào)用這個引用的characterIsClicked方法拣展,這樣FirstController就知道哪一個游戲角色被點擊了。UserGUI同理缔逛,只不過它監(jiān)測的是“用戶點擊Restart按鈕”的事件备埃。
門面模式的好處:通過一套接口(UserAction)來定義Controller與GUI交互的渠道,這樣實現(xiàn)Controller類的程序員只需要實現(xiàn)UserAction接口褐奴,他的代碼就可以被任何支持這個接口的GUI類所使用按脚;實現(xiàn)GUI類的程序員也不需要知道Controller的實現(xiàn)方式,它只需要調(diào)用接口中的方法敦冬,后面的事情就交給Controller吧辅搬!
ClickGUI
ClickGUI類是用來監(jiān)測用戶點擊,并調(diào)用SceneController進行響應(yīng)的脖旱。
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的對象堪遂,它實現(xiàn)了UserAction接口介蛉。ClickGUI與FirstController打交道,就是通過UserAction接口的API溶褪。ClickGUI不知道這些API是怎么被實現(xiàn)的币旧,但它知道FirstController類一定有這些方法。
可以做的擴展:
- 游戲失敗以后不能再響應(yīng)用戶點擊的事件猿妈,用戶只能點擊Restart吹菱。
- 增加計時的功能(這應(yīng)該由SceneController來控制)。
- 增加暫停/恢復(fù)游戲的功能(這應(yīng)該由Director來控制)彭则。
- 在開始游戲之前做一個歡迎界面鳍刷,與用戶進行交互(這就是另一個場景了)。
- 讓用戶可以在游戲中切換到歡迎界面贰剥,再切換回游戲界面的時候倾剿,游戲狀態(tài)要和之前一樣(場景的切換)。用戶可以在游戲中放棄游戲蚌成,回到歡迎頁面(場景的銷毀)前痘。
- 讓用戶能夠在歡迎界面指定有幾個牧師幾個惡魔,然后開始游戲担忧。(運行時決定場景的創(chuàng)建)
- 增加一種更難的模式芹缔,開始3秒以后牧師和惡魔外觀相同,玩家需要憑借記憶來操作瓶盛。
- 美化游戲?qū)ο螅?/li>