1. 程式人生 > 實用技巧 >unity 3d 遊戲設計(三)牧師與魔鬼動作分離版

unity 3d 遊戲設計(三)牧師與魔鬼動作分離版

Homework3程式設計實踐

牧師與魔鬼 動作分離版

在上一週的程式設計實踐中,我們已經完成了一個具有MVC架構的牧師與魔鬼的版本,而我們這周的任務就是實現一個動作管理器,把我們上週寫在GameObjectControllerSceneController裡的對於GameObject動作的處理寫出來,通過一個SceneActionManager的例項來管理,讓其專門負責GameObject的動作。下面我們來談談動作管理器。



建立動作管理器的意義

在上一個版本的簡單的牧師與惡魔的遊戲中,我為了避免FirstSceneController中對於物體移動操作的程式碼量過多,把物體的動作封裝在GameObjectController

裡面,通過這種做法,FirstSceneController的程式碼量的確得到了一定的減少,但是這樣的話我每寫一個GameObjectController我就要多寫一個物體運動的函式,一旦需要移動的GameObject多起來,可能冗餘的程式碼量就會大量增多了,而且一旦我們需要修改Move,我們可能就每一個類都要進行修改了,顯然這對於開發者非常不友好。

這個時候,動作管理器的意義就體現出來了,就以我們這次的作業為例子,Move的一類的動作抽象成一個動作類MoveAction,然後通過ActionManager對其進行管理。我們在需要物體進行移動時,只需要新建一個MoveAction,呼叫ActionManaer

addAction,把具體的引數傳給它,ActionManager就會執行我們的動作,我們再也不用將Move這類的動作一個一個寫在我們的GameObjController中,大大減少了我們的程式碼量,大大提高了我們程式碼的複用性,一旦動作方面的需求修改,我們也只需要修改該Action類的內容,以及相關呼叫建立該Action的地方,更加利於我們的維護以及讓程式更能適應需求變化



在瞭解了動作管理器的意義之後,我們就可以開始我們動作管理器的編寫了,我們先畫一個動作管理器的UML圖理清一下思路

然後與上一節課的UML圖結合起來變成整個程式的UML圖,畫出來UML圖之後,我們程式的架構就很清晰了。

下面我們分類進行描述

ActionCallBack

這個方法主要是提供一個讓動作完成時呼叫的介面,一旦動作完成,該介面對應實現的方法就會被呼叫,動作管理器可以對動作的完成進行響應。

public interface ActionCallback {
	void actionDone (SSAction source);
}


SSAction

SSAction這個類就是所有動作物件類抽象出來的一個不需要繫結 GameObject 物件的可程式設計基類,其動作的實現由其子類實現Update中的內容實現,這個在後面的MoveAction就有體現,所有的SSAction物件受ActionManager管理。

public class SSAction : ScriptableObject {
	public bool enable = true;
	public bool destroy = false;

	public GameObject gameObject;
	public Transform transform;
	public ActionCallback callback;

	public virtual void Start()
	{
		throw new System.NotImplementedException();
	}

	public virtual void Update()
	{
		throw new System.NotImplementedException();
	}
}


MoveAction

MoveAction就是上面SSAction的一個子類,它的作用就是根據使用者所提供的目的地和速度,使得物體完成一定速度的向某一目的地的直線運動。其通過Update的實現來完成逐幀的對於物體位置的變動,實現一個所謂的物體的"動作"。

public class MoveAction : SSAction {
	public Vector3 target;
	public float speed;

	private MoveAction(){
	}

	public static MoveAction getAction(Vector3 target, float speed) {
		MoveAction action = ScriptableObject.CreateInstance<MoveAction> ();
		action.target = target;
		action.speed = speed;
		return action;
	}

	// Use this for initialization
	public override void Start () {
		
	}
	
	// Update is called once per frame
	public override void Update () {
		transform.position = Vector3.MoveTowards(transform.position, target, speed*Time.deltaTime);
		if (transform.transform.position == target) {
			destroy = true;
			callback.actionDone (this);
		}
	}
}


CCSequenceAction

這是一個動作順序執行序列的是SSAction的子類,其通過一個Action的List,和actionDone對於List的維護以及Update對於呼叫哪個Action的Update方法的控制,實現了一個動作順序執行的效果。十分方便了我們對於一個連續動作的程式設計。

public class CCSequenceAction : SSAction, ActionCallback {
	public List<SSAction> sequence;
	public int repeat = 1; // 1->only do it for once, -1->repeat forever
	public int currentActionIndex = 0;

	public static CCSequenceAction getAction(int repeat, int currentActionIndex, List<SSAction> sequence) {
		CCSequenceAction action = ScriptableObject.CreateInstance<CCSequenceAction>();
		action.sequence = sequence;
		action.repeat = repeat;
		action.currentActionIndex = currentActionIndex;
		return action;
	}

	public override void Update() {
		if (sequence.Count == 0)return;
		if (currentActionIndex < sequence.Count) {
			sequence[currentActionIndex].Update();
		}
	}

	public void actionDone(SSAction source) {
		source.destroy = false;
		this.currentActionIndex++;
		if (this.currentActionIndex >= sequence.Count) {
			this.currentActionIndex = 0;
			if (repeat > 0) repeat--;
			if (repeat == 0) {
				this.destroy = true;
				this.callback.actionDone(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() {
		foreach(SSAction action in sequence) {
			DestroyObject(action);
		}
	}
}


SSActionManager

這個就是我們這一期的主角ActionManager了,其負責了action的增加、刪除、執行。它通過在Update中呼叫SSAction的Update方法,實現對於動作的一個排程,管理動作的自動執行。

public class SSActionManager : MonoBehaviour, ActionCallback {
	private Dictionary<int, SSAction> actions = new Dictionary<int, SSAction>();
	private List<SSAction> waitingToAdd = new List<SSAction>();
	private List<int> watingToDelete = new List<int>();

	protected void Update() {
		foreach(SSAction ac in waitingToAdd) {
			actions[ac.GetInstanceID()] = ac;
		}
		waitingToAdd.Clear();

		foreach(KeyValuePair<int, SSAction> kv in actions) {
			SSAction ac = kv.Value;
			if (ac.destroy) {
				watingToDelete.Add(ac.GetInstanceID());
			} else if (ac.enable) {
				ac.Update();
			}
		}

		foreach(int key in watingToDelete) {
			SSAction ac = actions[key];
			actions.Remove(key);
			DestroyObject(ac);
		}
		watingToDelete.Clear();
	}

	public void addAction(GameObject gameObject, SSAction action, ActionCallback callback) {
		action.gameObject = gameObject;
		action.transform = gameObject.transform;
		action.callback = callback;
		waitingToAdd.Add(action);
		action.Start();
	}

	public void actionDone(SSAction source) {

	}
}



FirstSceneActionManager

FirstSceneActionManager就是我們上面SSActionManager的一個子類,對於我們場景中具體的動作進行了封裝,我們只需要在FirstSceneController中呼叫該方法,就可以實現我們之前的Move了,十分方便了我們的程式設計。

public class FirstSceneActionManager : SSActionManager {
	public void toggleBoat(BoatController boat) {
		MoveAction action = MoveAction.getAction (boat.getTarget (), boat.speed);
		this.addAction (boat.getBoat (), action, this);
		boat.toggle ();
	}

	public void moveCharacter(ICharacterController character, Vector3 target) {
		Vector3 nowPos = character.getPos ();
		Vector3 tmpPos = nowPos;
		if (target.y > nowPos.y) {
			tmpPos.y = target.y;
		} else {
			tmpPos.x = target.x;
		}
		SSAction action1 = MoveAction.getAction(tmpPos, character.speed);
		SSAction action2 = MoveAction.getAction(target, character.speed);
		SSAction sequenceAction = CCSequenceAction.getAction(1, 0, new List<SSAction>{action1, action2});
		this.addAction(character.getInstance(), sequenceAction, this);
	}
}

FirstController

在完成了FirstSceneActionManager後,我們把GameObjectController中以及FirstController中一些關於動作的部分刪除,然後在原來動作的部分呼叫FirstSceneActionManager提供新的方法就可以了。(不過因為一開始設計架構的時候沒有考慮加入動作管理器,加之最近時間較緊,沒有太多時間對遊戲進行重構,所以FirstController中仍然有著部分關於判斷物體運動的邏輯)

public class FirstController : MonoBehaviour, ISceneController, IUserAction {
    UserGUI userGUI;

    public LandController rightLand;
    public LandController leftLand;
    public BoatController boat;
    public ICharacterController[] characters;
	private FirstSceneActionManager actionManager;

    void Awake()
    {
        GameDirector director = GameDirector.getInstance();
        director.currentSceneController = this;
        userGUI = gameObject.AddComponent<UserGUI>() as UserGUI;
        genGameObjects();
    }

	void Start() {
		actionManager = GetComponent<FirstSceneActionManager> ();
	}

    public void genGameObjects()
    {
        characters = new ICharacterController[6];
        boat = new BoatController();
        leftLand = new LandController(-1);
        rightLand = new LandController(1);

        for (int i = 0; i < 3; i++)
        {
            ICharacterController priest = new ICharacterController(0, "priest" + i);
            priest.setPosition(rightLand.getEmptyPosition());
            priest.getOnLand(rightLand);
            rightLand.getOnLand(priest);
            characters[i] = priest;
        }

        for (int i = 0; i < 3; i++)
        {
            ICharacterController demon = new ICharacterController(1, "demon" + i);
            demon.setPosition(rightLand.getEmptyPosition());
            demon.getOnLand(rightLand);
            rightLand.getOnLand(demon);
            characters[i+3] = demon;
        }
    }


    public void ClickCharacter(ICharacterController character)
    {
        if (userGUI.status != 0 || !boat.available())
        {
            return;
        }
        if (character.isOnBoat()) {
            LandController land;
            if (boat.getBoatPos() == 0)
            {
                land = leftLand;
            }
            else
            {
                land = rightLand;
            }
            boat.getOffBoat(character.getName());
			actionManager.moveCharacter (character, land.getEmptyPosition ());
            character.getOnLand(land);
            land.getOnLand(character);
        }
        else
        {
            LandController land = character.getLandController();
            if (boat.getEmptyIndex() == -1)
                return;
            int landPos = land.getType(), boatPos = (boat.getBoatPos() == 0) ? -1 : 1;
            if (landPos != boatPos)
                return;
            land.getOffLand(character.getName());
			actionManager.moveCharacter (character, boat.getEmptyPosition ());
            character.getOnBoat(boat, boat.getEmptyIndex());
            boat.getOnBoat(character);
        }
        userGUI.status = checkResult();
    }

    public void ToggleBoat()
    {
		if (userGUI.status != 0 || boat.isEmpty() || !boat.available())
            return;
		actionManager.toggleBoat (boat);
        userGUI.status = checkResult();
    }

    int checkResult()
    {
        int leftPriests = 0;
        int rightPriests = 0;
        int leftDemons = 0;
        int rightDemons = 0;

        int[] leftStatus = leftLand.getStatus();
        leftPriests += leftStatus[0];
        leftDemons += leftStatus[1];

        if (leftPriests + leftDemons == 6)
            return 2;

        int[] rightStatus = rightLand.getStatus();
        rightPriests += rightStatus[0];
        rightDemons += rightStatus[1];

        int[] boatStatus = boat.getBoatStatus();
        if (boat.getBoatPos() == 0)
        {
            leftPriests += boatStatus[0];
            leftDemons += boatStatus[1];
        }
        else
        {
            rightPriests += boatStatus[0];
            rightDemons += boatStatus[1];
        }

        if (leftPriests > 0 && leftPriests < leftDemons)
            return 1;
        if (rightPriests > 0 && rightPriests < rightDemons)
            return 1;

        return 0;
    }

    public void restart()
    {
        boat.reset();
        leftLand.reset();
        rightLand.reset();
        for (int i = 0; i < characters.Length; i++)
            characters[i].reset();
    }
}


改進

  • 根據上課學到的內容,給Camera加上了SkyBox,美化了程式的UI
  • 給遊戲添加了切換視角的功能,玩家可以根據自己喜好在全域性視角以及船隻跟隨視角之間任意切換,遊戲的體驗更佳


結果演示