1. 程式人生 > >Unity3d學習之路-牧師與魔鬼V2(動作分離版)

Unity3d學習之路-牧師與魔鬼V2(動作分離版)

Unity3d學習之路-牧師與魔鬼V2(動作分離版)


  • 該版本改進的目的

    1. 把每個需要移動的遊戲物件的移動方法提取出來,建立一個動作管理器來管理不同的移動方法。
    2. 對於上一個版本,每一個可移動的遊戲物件的元件都有一個Move指令碼,當遊戲物件需要移動時候,遊戲物件自己呼叫Move指令碼中的方法讓自己移動。而動作分離版,則剝奪了遊戲物件自己呼叫動作的能力,建立一個動作管理器,通過場景控制器(在我的遊戲中是Controllor)把需要移動的遊戲物件傳遞給動作管理器,讓動作管理器去移動遊戲物件。
    3. 當動作很多或是需要做同樣動作的遊戲物件很多的時候,使用動作管理器可以讓動作很容易管理,也提高了程式碼複用性。
  • 改進後動作管理部分類圖如下(Controllor類只寫了新增的函式和屬性)
    UML類圖

動作管理類圖描述與實現

  • ISSActionCallback(動作事件介面)
    作為動作和動作管理者的介面(組合動作也可以是動作管理者),動作管理者繼承這個介面,並且實現介面的方法。當動作完成的時候,動作會呼叫這個介面,傳送訊息告訴動作管理者物件,這個動作做完啦,然後管理者會對下一個動作進行處理。
public enum SSActionEventType : int { Started, Competeted }  

public interface ISSActionCallback  
{  
    void
SSActionEvent(SSAction source, SSActionEventType events = SSActionEventType.Competeted, int intParam = 0, string strParam = null, Object objectParam = null); }
  • SSActionManager(動作管理基類)
    管理SequenceAction和SSAction,可以給它們傳遞遊戲物件,讓遊戲物件做動作或是一連串的動作,控制動作的切換。SSActionManager繼承了ISSActionCallback介面,通過這個介面,當動作做完或是連續的動作做完時候會告訴SSActionManager,然後SSActionManager去決定如何執行下一個動作。
public class SSActionManager: MonoBehaviour, ISSActionCallback
{  
    private Dictionary<int, SSAction> actions = new Dictionary<int, SSAction>();    //將執行的動作的字典集合,int為key,SSAction為value
    private List<SSAction> waitingAdd = new List<SSAction>();                       //等待去執行的動作列表
    private List<int> waitingDelete = new List<int>();                              //等待刪除的動作的key                

    protected void Update() 
    {
        foreach(SSAction ac in waitingAdd)                                
        {
            actions[ac.GetInstanceID()] = ac;                                      //獲取動作例項的ID作為key
        }
        waitingAdd.Clear();

        foreach(KeyValuePair<int, SSAction> kv in actions) 
        {
            SSAction ac = kv.Value;
            if (ac.destroy)          //如果動作被標記摧毀,那麼加入等待刪除列表
            {
                waitingDelete.Add(ac.GetInstanceID());
            } 
            else if (ac.enable) 
            {
                ac.Update();
            }
        }

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

    public void RunAction(GameObject gameobject, SSAction action, ISSActionCallback manager) 
    {
        action.gameobject = gameobject;
        action.transform = gameobject.transform;
        action.callback = manager;                                               
        waitingAdd.Add(action);                                           
        action.Start();
    }

    public void SSActionEvent(SSAction source, SSActionEventType events = SSActionEventType.Competeted,  
        int intParam = 0, string strParam = null, Object objectParam = null)
    {
        //牧師與魔鬼的遊戲物件移動完成後就沒有下一個要做的動作了,所以回撥函式為空
    }
}
  • SequenceAction(組合動作實現)
    SequenceAction繼承了ISSActionCallback,因為組合動作是每一個動作的順序完成,它管理這一連串動作中的每一個小的動作,所以當小的動作完成的時候,也要發訊息告訴它,然後它得到訊息後去處理下一個動作。SequenceAction也繼承了SSAction,因為成個組合動作也需要遊戲物件,也需要標識是否摧毀,也會有一個組合動作的管理者的介面,組成動作也是動作的子類,只不過是讓具體的動作組合起來做罷了。
public class SequenceAction: SSAction, ISSActionCallback
{           
    public List<SSAction> sequence;    //動作的列表
    public int repeat = -1;            //-1就是無限迴圈做組合中的動作
    public int start = 0;              //當前做的動作的索引

    public static SequenceAction GetSSAcition(int repeat, int start, List<SSAction> sequence) 
    {
        SequenceAction action = ScriptableObject.CreateInstance<SequenceAction>();//讓unity自己建立一個SequenceAction例項
        action.repeat = repeat;
        action.sequence = sequence;
        action.start = start;
        return action;
    }

    public override void Update() 
    {
        if (sequence.Count == 0) return;
        if (start < sequence.Count) 
        {
            sequence[start].Update();     //一個組合中的一個動作執行完後會呼叫介面,所以這裡看似沒有start++實則是在回撥介面函式中實現
        }
    }

    public void SSActionEvent(SSAction source, SSActionEventType events = SSActionEventType.Competeted,  
        int intParam = 0, string strParam = null, Object objectParam = null)
    {
        source.destroy = false;     //先保留這個動作,如果是無限迴圈動作組合之後還需要使用
        this.start++;
        if (this.start >= sequence.Count) 
        {
            this.start = 0;
            if (repeat > 0) repeat--;
            if (repeat == 0) 
            {
                this.destroy = true;               
                this.callback.SSActionEvent(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() 
    {
        //如果組合動作做完第一個動作突然不要它繼續做了,那麼後面的具體的動作需要被釋放
    }
}
  • SSAction (動作基類)
    SSAction是所有動作的基類。SSAction繼承了ScriptableObject代表SSAction不需要繫結GameObject物件,且受Unity引擎場景管理。
public class SSAction : ScriptableObject            //動作
{
    public bool enable = true;                      //是否正在進行此動作
    public bool destroy = false;                    //是否需要被銷燬
    public GameObject gameobject;                   //動作物件
    public Transform transform;                     //動作物件的transform
    public ISSActionCallback callback;              //動作完成後的訊息通知者

    protected SSAction() {}                        //保證SSAction不會被new
    //子類可以使用下面這兩個函式
    public virtual void Start()                     
    {
        throw new System.NotImplementedException();
    }
    public virtual void Update()
    {
        throw new System.NotImplementedException();
    }
}

牧師與魔鬼動作管理實現

  • SSMoveToAction(移動動作實現)
    以speed的速度向target目的地移動
public class SSMoveToAction : SSAction                        //移動
{
    public Vector3 target;        //移動到的目的地
    public float speed;           //移動的速度

    private SSMoveToAction(){}
    public static SSMoveToAction GetSSAction(Vector3 target, float speed) 
    {
        SSMoveToAction action = ScriptableObject.CreateInstance<SSMoveToAction>();//讓unity自己建立一個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.callback.SSActionEvent(this);      //告訴動作管理或動作組合這個動作已完成
        }
    }

    public override void Start() 
    {
        //移動動作建立時候不做任何事情
    }
}
  • MySceneActionManager(移動動作管理實現)
    船的移動是一個SSMoveToAction動作就可以,而角色的移動需要兩個SSMoveToAction動作組合(先垂直後水平移動或先水平後垂直移動)。然後設定當前場景控制器的動作管理者為MySceneActionManager,這樣場景控制器就可以呼叫動作管理器的方法實現不同遊戲物件的移動啦。
public class MySceneActionManager:SSActionManager    
{ 
    public SSMoveToAction moveBoatToEndOrStart;     //移動船到結束岸,移動船到開始岸
    public SequenceAction moveRoleToLandorBoat;     //移動角色到陸地,移動角色到船上
    public Controllor sceneController;             //當前場景的場景控制器

    protected new void Start()
    {
        sceneController = (Controllor)SSDirector.GetInstance().CurrentScenceController;
        sceneController.actionManager = this;     //設定該場景控制器的動作管理者為自己
    }
    public void moveBoat(GameObject boat, Vector3 target, float speed)
    {
        moveBoatToEndOrStart = SSMoveToAction.GetSSAction(target, speed);
        this.RunAction(boat, moveBoatToEndOrStart, this);
    }

    public void moveRole(GameObject role, Vector3 middle_pos, Vector3 end_pos,float speed)
    {
        SSAction action1 = SSMoveToAction.GetSSAction(middle_pos, speed);
        SSAction action2 = SSMoveToAction.GetSSAction(end_pos, speed);
        moveRoleToLandorBoat = SequenceAction.GetSSAcition(1, 0, new List<SSAction>{action1, action2}); //1是做一次動作組合,0代表從action1開始
        this.RunAction(role, moveRoleToLandorBoat, this);
    }
}       
  • Controllor(當前場景控制器)
    最後Controllor接收到遊戲物件需要移動的時候,只要傳給MySceneActionManager相應的引數和遊戲物件就可以進行移動啦,修改了的部分程式碼如下:

SSActionManager繼承了MonoBehaviour。MonoBehaviour是每個指令碼的基類,又繼承於Behaviours。只有繼承了MonoBehaviour的類才可以作為元件掛載到遊戲物件上,在unity 中所有繼承MonoBehaviour的類是不可以例項化的,因為unity都會自動為其建立例項,所以我們需要呼叫該類的時候不使用new,而是使用AddComponent來呼叫。

public MySceneActionManager actionManager;
void Start ()
{
    SSDirector director = SSDirector.GetInstance();
    director.CurrentScenceController = this;
    user_gui = gameObject.AddComponent<UserGUI>() as UserGUI;
    LoadResources();

    actionManager = gameObject.AddComponent<MySceneActionManager>() as MySceneActionManager;    
}

public void MoveBoat()                  //移動船
{
    if (boat.IsEmpty() || user_gui.sign != 0) return;
    actionManager.moveBoat(boat.getGameObject(),boat.BoatMoveToPosition(),boat.move_speed);   //動作管理器實現移動船
    user_gui.sign = Check();
    if (user_gui.sign == 1)
    {
        for (int i = 0; i < 3; i++)
        {
            roles[i].PlayGameOver();
            roles[i + 3].PlayGameOver();
        }
    }
}
public void MoveRole(RoleModel role)    //移動角色
{
    if (user_gui.sign != 0) return;
    if (role.IsOnBoat())
    {
        LandModel land;
        if (boat.GetBoatSign() == -1)
            land = end_land;
        else
            land = start_land;
        boat.DeleteRoleByName(role.GetName());

        //動作分離版本改變
        Vector3 end_pos = land.GetEmptyPosition();                          
        Vector3 middle_pos = new Vector3(role.getGameObject().transform.position.x, end_pos.y, end_pos.z);  
        actionManager.moveRole(role.getGameObject(), middle_pos, end_pos, role.move_speed);  

        role.GoLand(land);
        land.AddRole(role);
    }
    else
    {                                
        LandModel land = role.GetLandModel();
        if (boat.GetEmptyNumber() == -1 || land.GetLandSign() != boat.GetBoatSign()) return;   //船沒有空位,也不是船停靠的陸地,就不上船
        land.DeleteRoleByName(role.GetName());

        //動作分離版本改變
        Vector3 end_pos = boat.GetEmptyPosition();  
        Vector3 middle_pos = new Vector3(end_pos.x, role.getGameObject().transform.position.y, end_pos.z); 
        actionManager.moveRole(role.getGameObject(), middle_pos, end_pos, role.move_speed);  

        role.GoBoat(boat);
        boat.AddRole(role);
    }
    user_gui.sign = Check();
    if (user_gui.sign == 1)
    {
        for (int i = 0; i < 3; i++)
        {
            roles[i].PlayGameOver();
            roles[i + 3].PlayGameOver();
        }
    }
}

skybox
天空盒是圍繞整個場景的包裝器,其實就是一個六面體盒子,這個盒子的六面是不同的Textrue,這樣就生成了類似真實世界的天空,天空盒裡面裝的是你的遊戲世界,這樣用相機觀察世界的時候就可以看到很好看的天空背景。官方描述:點選這裡

  • 製作
    首先新建一個Material,在Shader那裡選擇Skybox/6 Sided(版本2017.4),然後在每個Textrue那裡加入上下左右前後的圖片,這樣天空盒就做好啦
  • 掛載
    點選主相機,新增元件Skybox,然後把Material拖進去,在遊戲場景裡就可以看到製作好的天空了

遊戲實現截圖

game

小結
動作分離之後,增加了動作基類,組合動作,具體動作,動作事件介面,動作管理基類,具體動作管理。就從牧師與魔鬼這個遊戲來看,整個遊戲只有移動這個動作和折線移動這個組合動作,所以像動作事件介面就顯得沒有什麼作用,具體的動作管理似乎也顯得有些累贅。但是這個架構,當遊戲動作增加,遊戲複雜度增加的時候,整個結構可以方便擴充套件和管理,避免了控制類,動作類,遊戲模型之間的直接依賴。

unity初學階段,可能很多理解不到位或是錯誤的,歡迎指出和建議。
牧師與魔鬼V2(動作分離版)程式碼:傳送門