學習Unity(7)小游戲架構(gòu)改進——實現(xiàn)動作管理器

在我的學習Unity(5)學習Unity(6)中邓嘹,我們已經(jīng)完成了一個簡單的牧師與惡魔游戲并改進了它的場景。在這一篇文章中汞舱,我想從代碼的架構(gòu)來改進它——實現(xiàn)一個動作管理器來管理場景中的動作夕凝。

美化場景谈宛、控制攝像機和光源這些技巧,可能只適用于游戲編程情妖,甚至只限于Unity3D睬关,但是掌握代碼組織架構(gòu)面向?qū)ο蟮乃枷?/strong>對任何方面的編程都有巨大的提升。


下載我的項目在本地查看毡证!

我的github下載項目資源电爹,將Assets文件夾覆蓋你的項目中的Assets文件夾,然后在U3D中雙擊“ass”料睛,就可以運行了丐箩!


為什么要引入動作管理器

在一個場景中肯定有很多“會動”的物體摇邦,它們的運動是有很多共性的,如果我們?yōu)橛螒蚪巧珜崿F(xiàn)一個運動方法雏蛮,為船實現(xiàn)一個運動方法涎嚼,為將來出現(xiàn)的所有會動的物體都實現(xiàn)一個運動方法,勢必是一種資源的浪費挑秉。我們可以將運動的共性提取出來法梯,用一個管理器統(tǒng)一管理,這樣犀概,代碼的復用性和可讀性都會提高立哑。


什么是動作管理器

  • 動作管理器就是一個對象,管理整個場景所有的動作姻灶。
  • 一個SceneController(場景管理器)只配備一個動作管理器對象铛绰。
  • 不管是游戲角色的移動還是船的移動,都歸這個對象管产喉;
  • 動作管理器可以添加動作(添加的時候要指定動作所作用的GameObject)捂掰,監(jiān)測已經(jīng)完成的動作并清除。
UML類圖

我下面對重要的類做出解釋曾沈。


ActionCallback

這個接口很簡單这嚣,就一個方法。實現(xiàn)了這個接口的類塞俱,就可以知道到“某個動作已完成”(動作一完成actionDone方法就會被調(diào)用)姐帚,并對這個事件做出反應。

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

ObjAction

ObjAction是所有動作的基類障涯。ActionManager就是通過ObjAction這個接口來管理動作的罐旗。

public class ObjAction : ScriptableObject
{

    public bool enable = true;
    public bool destroy = false;

    public GameObject gameObject;
    public Transform transform;
    public ActionCallback whoToNotify;

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

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

ObjAction保存了這個動作作用的對象和這個對象的Transform組件(這里有一點多余了)。ObjAction只是一個動作的抽象唯蝶,具體如何實現(xiàn)動作要讓它的子類來實現(xiàn)Update方法九秀。注意ObjAction并不是MonoBehaviour的子類,它的Start和Update方法不會主動調(diào)用粘我,我們會在后面一個MonoBehaviour類的Update中調(diào)用這個對象的Update方法鼓蜒。

這個類還定義了一個ActionCallback的成員,用來保存動作完成時要通知的對象涂滴。

為什么要繼承ScriptableObject呢?因為ScriptableObject有一些生命周期方法晴音,等一下它的子類就要用到OnDestroy柔纵。


MoveToAction

MoveToAction是ObjAction的一個實現(xiàn),它代表一個直線移動的動作锤躁。

public class MoveToAction : ObjAction
{
    public Vector3 target;
    public float speed;

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

   public override void Update() {
       this.transform.position = Vector3.MoveTowards(this.transform.position, target, speed*Time.deltaTime);
       if (this.transform.position == target) {
           this.destroy = true;
           this.whoToNotify.actionDone(this);
       }
   }

   public override void Start() {
       //
   }

}

MoveToAction不能直接通過new來得到對象搁料,只能通過它的靜態(tài)方法getAction()來新建實例。

當MoveToAction發(fā)現(xiàn)自己的動作完成的時候,它會將自己標識為“要被銷毀”郭计,并通過this.whoToNotify.actionDone(this);告知whoToNotify動作已完成霸琴。


SequenceAction

SequenceAction是ObjAction的另一個子類,它代表一連串MoveToAction組成的動作昭伸,也就是折線移動梧乘。

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

    public static SequenceAction getAction(int repeat, int currentActionIndex, List<ObjAction> sequence) {
        SequenceAction action = ScriptableObject.CreateInstance<SequenceAction>();
        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(ObjAction 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.whoToNotify.actionDone(this);
            }
        }
    }

    public override void Start() {
        foreach(ObjAction action in sequence) {
            action.gameObject = this.gameObject;
            action.transform = this.transform;
            action.whoToNotify = this;
            action.Start();
        }
    }

    void OnDestroy() {
        foreach(ObjAction action in sequence) {
            DestroyObject(action);
        }
    }
}

按照sequence的動作順序,一個一個地執(zhí)行下來庐杨,如果repeat大于0則從頭再執(zhí)行一次选调。

注意actionDone中有一句話source.destroy = false;因為MoveToAction到達指定地點以后會自動將自己標識為destroy,這一句話阻止它被銷毀灵份,因為如果有repeat仁堪,后面還要執(zhí)行它。

SequenceAction是怎么知道某一個子動作已執(zhí)行完呢填渠?它實現(xiàn)了ActionCallback弦聂,并將子動作的whoToNotify指向自己,當子動作完成的時候就會調(diào)用自己的actionDone氛什,它就會進入下一個動作莺葫。

這里的SequenceAction和MoveToAction看似是包含與被包含的關(guān)系,實際上它們都是ObjAction的子類屉更。這就是一種組合模式徙融,這樣做的好處是它們的管理者ActionManager不需要區(qū)分誰是組合動作誰是單一動作,統(tǒng)統(tǒng)當作“動作(也就是ObjAction)”來處理瑰谜。

回憶我們在以前的文章說過欺冀,GameObject的父子關(guān)系也是一種組合模式。不管是組合的GameObject還是單一的GameObject萨脑,我們都可以當作普通的GameObject來處理(一樣地操作Transform隐轩,一樣地操作其他Component……),是不是與這里的組合模式有一些相似之處渤早?


ActionManager

ActionManager就是管理動作的類职车,它負責讓動作“真正執(zhí)行起來”,并銷毀標記為destroy的動作鹊杖。

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

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

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

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

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

    public void actionDone(ObjAction source) {
        
    }

}

ActionManager實現(xiàn)了MonoBehaviour悴灵,因此它的Update方法在每一幀都會自動被調(diào)用,而在它的Update方法中又調(diào)用了所有已添加的動作的Update骂蓖,這就是為什么一個動作只有添加到了ActionManager才會真正執(zhí)行起來的原因积瞒!我們說過ObjAction本身的Update不會自動被調(diào)用(不是MonoBehaviour的子類),它們需要靠ActionManager來“帶動”登下。


FirstSceneActionManager

FirstSceneActionManager是ActionManager的子類茫孔,F(xiàn)irstController就是通過它來管理所有動作的叮喳。本來有ActionManager似乎已經(jīng)足夠管理動作了,為什么還要實現(xiàn)一個子類FirstSceneActionManager來管理動作呢缰贝?FirstSceneActionManager針對具體的需求做了封裝馍悟,讓FirstController調(diào)用起來更簡潔。

public class FirstSceneActionManager:ActionManager {
    public void moveBoat(BoatController boat) {
        MoveToAction action = MoveToAction.getAction(boat.getDestination(), boat.movingSpeed);
        this.addAction(boat.getGameobj(), action, this);
    }

    public void moveCharacter(MyCharacterController characterCtrl, Vector3 destination) {
        Vector3 currentPos = characterCtrl.getPos();
        Vector3 middlePos = currentPos;
        if (destination.y > currentPos.y) {     //from low(boat) to high(coast)
            middlePos.y = destination.y;
        } else {    //from high(coast) to low(boat)
            middlePos.x = destination.x;
        }
        ObjAction action1 = MoveToAction.getAction(middlePos, characterCtrl.movingSpeed);
        ObjAction action2 = MoveToAction.getAction(destination, characterCtrl.movingSpeed);
        ObjAction seqAction = SequenceAction.getAction(1, 0, new List<ObjAction>{action1, action2});
        this.addAction(characterCtrl.getGameobj(), seqAction, this);
    }
}

可以想象剩晴,如果我們不封裝一個FirstSceneActionManager锣咒,而是直接使用ActionManager來管理場景中的動作,那么FirstController中的移動代碼將會有多么臃腫李破!


修改后的FirstController

最后讓我們看看修改后的FirstController是怎么使用動作管理器的:

public class FirstController : MonoBehaviour, SceneController, UserAction {

    UserGUI userGUI;

    public CoastController fromCoast;
    public CoastController toCoast;
    public BoatController boat;
    private MyCharacterController[] characters;

    private FirstSceneActionManager actionManager;

    void Awake() {
        Director director = Director.getInstance ();
        director.currentSceneController = this;
        userGUI = gameObject.AddComponent <UserGUI>() as UserGUI;
        characters = new MyCharacterController[6];
        loadResources ();
    }

    void Start() {
        // FirstController加載時要獲取掛載在該GameObject上的FirstSceneActionManager宠哄!
        // 并將它的引用保存到自己的成員變量actionManager 以便后面的調(diào)用。
        actionManager = GetComponent<FirstSceneActionManager>();
    }

    public void loadResources() {
        //GameObject water = Instantiate (Resources.Load ("Perfabs/Water", typeof(GameObject)), water_pos, Quaternion.identity, null) as GameObject;
        //water.name = "water";

        fromCoast = new CoastController ("from");
        toCoast = new CoastController ("to");
        boat = new BoatController ();

        loadCharacter ();
    }

    private void loadCharacter() {
        for (int i = 0; i < 3; i++) {
            MyCharacterController cha = new MyCharacterController ("priest");
            cha.setName("priest" + i);
            cha.setPosition (fromCoast.getEmptyPosition ());
            cha.getOnCoast (fromCoast);
            fromCoast.getOnCoast (cha);

            characters [i] = cha;
        }

        for (int i = 0; i < 3; i++) {
            MyCharacterController cha = new MyCharacterController ("devil");
            cha.setName("devil" + i);
            cha.setPosition (fromCoast.getEmptyPosition ());
            cha.getOnCoast (fromCoast);
            fromCoast.getOnCoast (cha);

            characters [i+3] = cha;
        }
    }


    public void moveBoat() {
        if (boat.isEmpty ())
            return;
        /*  old way to move boat
        boat.Move ();
        */
        // 這里使用動作管理器取代了我們之前寫的舊方法`凸ァ毛嫉!
        actionManager.moveBoat(boat);
        boat.move();
        userGUI.status = check_game_over ();
    }

    public void characterIsClicked(MyCharacterController characterCtrl) {
        if (characterCtrl.isOnBoat ()) {
            CoastController whichCoast;
            if (boat.get_to_or_from () == -1) { // to->-1; from->1
                whichCoast = toCoast;
            } else {
                whichCoast = fromCoast;
            }

            boat.GetOffBoat (characterCtrl.getName());
            //characterCtrl.moveToPosition (whichCoast.getEmptyPosition ());
            // 這里使用動作管理器取代了我們之前寫的舊方法!妇菱!
            actionManager.moveCharacter(characterCtrl, whichCoast.getEmptyPosition ());
            characterCtrl.getOnCoast (whichCoast);
            whichCoast.getOnCoast (characterCtrl);

        } else {                                    // character on coast
            CoastController whichCoast = characterCtrl.getCoastController ();

            if (boat.getEmptyIndex () == -1) {      // boat is full
                return;
            }

            if (whichCoast.get_to_or_from () != boat.get_to_or_from ()) // boat is not on the side of character
                return;

            whichCoast.getOffCoast(characterCtrl.getName());
            //characterCtrl.moveToPosition (boat.getEmptyPosition());
            // 這里使用動作管理器取代了我們之前寫的舊方法3性痢!
            actionManager.moveCharacter(characterCtrl, boat.getEmptyPosition());
            characterCtrl.getOnBoat (boat);
            boat.GetOnBoat (characterCtrl);
        }
        userGUI.status = check_game_over ();
    }

    int check_game_over() { // 0->not finish, 1->lose, 2->win
        int from_priest = 0;
        int from_devil = 0;
        int to_priest = 0;
        int to_devil = 0;

        int[] fromCount = fromCoast.getCharacterNum ();
        from_priest += fromCount[0];
        from_devil += fromCount[1];

        int[] toCount = toCoast.getCharacterNum ();
        to_priest += toCount[0];
        to_devil += toCount[1];

        if (to_priest + to_devil == 6)      // win
            return 2;

        int[] boatCount = boat.getCharacterNum ();
        if (boat.get_to_or_from () == -1) { // boat at toCoast
            to_priest += boatCount[0];
            to_devil += boatCount[1];
        } else {    // boat at fromCoast
            from_priest += boatCount[0];
            from_devil += boatCount[1];
        }
        if (from_priest < from_devil && from_priest > 0) {      // lose
            return 1;
        }
        if (to_priest < to_devil && to_priest > 0) {
            return 1;
        }
        return 0;           // not finish
    }

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

在我之前的文章里闯团,我是通過掛載MoveableScript在每一個要移動的對象上來讓物體移動起來辛臊,這次我們只要將FirstSceneActionManager掛載到空對象上,然后通過它來控制物體的移動房交。


感謝閱讀彻舰!

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市候味,隨后出現(xiàn)的幾起案子刃唤,更是在濱河造成了極大的恐慌,老刑警劉巖白群,帶你破解...
    沈念sama閱讀 211,265評論 6 490
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件尚胞,死亡現(xiàn)場離奇詭異,居然都是意外死亡帜慢,警方通過查閱死者的電腦和手機笼裳,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,078評論 2 385
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來粱玲,“玉大人躬柬,你說我怎么就攤上這事〕榧酰” “怎么了允青?”我有些...
    開封第一講書人閱讀 156,852評論 0 347
  • 文/不壞的土叔 我叫張陵,是天一觀的道長胯甩。 經(jīng)常有香客問我昧廷,道長,這世上最難降的妖魔是什么偎箫? 我笑而不...
    開封第一講書人閱讀 56,408評論 1 283
  • 正文 為了忘掉前任木柬,我火速辦了婚禮,結(jié)果婚禮上淹办,老公的妹妹穿的比我還像新娘眉枕。我一直安慰自己,他們只是感情好怜森,可當我...
    茶點故事閱讀 65,445評論 5 384
  • 文/花漫 我一把揭開白布速挑。 她就那樣靜靜地躺著,像睡著了一般副硅。 火紅的嫁衣襯著肌膚如雪姥宝。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,772評論 1 290
  • 那天恐疲,我揣著相機與錄音腊满,去河邊找鬼。 笑死培己,一個胖子當著我的面吹牛碳蛋,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播省咨,決...
    沈念sama閱讀 38,921評論 3 406
  • 文/蒼蘭香墨 我猛地睜開眼肃弟,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了零蓉?” 一聲冷哼從身側(cè)響起笤受,我...
    開封第一講書人閱讀 37,688評論 0 266
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎壁公,沒想到半個月后感论,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 44,130評論 1 303
  • 正文 獨居荒郊野嶺守林人離奇死亡紊册,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,467評論 2 325
  • 正文 我和宋清朗相戀三年比肄,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片囊陡。...
    茶點故事閱讀 38,617評論 1 340
  • 序言:一個原本活蹦亂跳的男人離奇死亡芳绩,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出撞反,到底是詐尸還是另有隱情妥色,我是刑警寧澤,帶...
    沈念sama閱讀 34,276評論 4 329
  • 正文 年R本政府宣布遏片,位于F島的核電站嘹害,受9級特大地震影響撮竿,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜笔呀,卻給世界環(huán)境...
    茶點故事閱讀 39,882評論 3 312
  • 文/蒙蒙 一幢踏、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧许师,春花似錦房蝉、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,740評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至逞盆,卻和暖如春檀蹋,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背云芦。 一陣腳步聲響...
    開封第一講書人閱讀 31,967評論 1 265
  • 我被黑心中介騙來泰國打工续扔, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人焕数。 一個月前我還...
    沈念sama閱讀 46,315評論 2 360
  • 正文 我出身青樓纱昧,卻偏偏與公主長得像,于是被迫代替她去往敵國和親堡赔。 傳聞我的和親對象是個殘疾皇子识脆,可洞房花燭夜當晚...
    茶點故事閱讀 43,486評論 2 348

推薦閱讀更多精彩內(nèi)容