Unity3D学习——牧师与恶魔过河游戏(组合模式、单实例模式)

成品展示

通过鼠标点击实现对象移动
牧师与恶魔游戏演示

原游戏Priests and Devils传送门,有兴趣的可以去玩下:
http://www.flash-game.net/game/2535/priests-and-devils.html

游戏制作

如果需要,可以按如下步骤先设置好预设后,再把文章最后的代码复制进脚本,即可运行察看游戏效果,所用unity版本为5.5

预制与脚本的挂载


预制
脚本


空对象挂脚本
相机挂GUI

下面预制中的牧师与恶魔我加了上两个Tag,以方便把它们分类统计。

牧师
魔鬼
岸
船

游戏设计以及代码分析

采用MVC结构,单实例模式,类的UML图如下


UML
组合模式产生动作,单实例模式方便管理

各类说明

  • 导演类:管理游戏全局状态,获取当前游戏的场景,协调各个类之间的通讯,是单实例类,不被Unity内存管理所管理
  • 界面:负责与用户交互
  • 场记:管理本次场景所有的游戏对象,协调游戏对象(预制件级别)之间的通讯,响应外部输入事件,管理本场次的规则,杂务
  • 动作管理者:被场记调用,为场景中的对象设计具体动作并执行
  • 红色方框中为基本的动作类

各类具体代码

  • 导演类 SSDirector
using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;


public enum State { WIN, LOSE, PAUSE, CONTINUE , START };
public class SSDirector : System.Object
{
    //计时器
    public int totalSeconds = 60;
    public int leaveSeconds;
    public bool onCountDown = false;
    public string countDownTitle = "Start";

    public  State state { get; set; }
    public static SSDirector _instance;
    public ISceneController currentScenceController { get; set; }
    public bool running { get; set; }


    public static SSDirector getInstance()
    {
        if (_instance == null)
        {
            _instance = new SSDirector();
        }
        return _instance;
    }

    public int getFPS()
    {
        return Application.targetFrameRate;
    }

    public void setFPS(int fps)
    {
        Application.targetFrameRate = fps;
    }

    public void NextScene()
    {
        Debug.Log("抱歉,没下一个场景了");
    }

    public IEnumerator DoCountDown()
    {
        while (leaveSeconds > 0)
        {
            yield return new WaitForSeconds(1f);
            leaveSeconds--;
        }
    }
}
  • 界面交互 UserGUI
using System.Collections;
using System.Collections.Generic;
using UnityEngine;



/**
 * 玩家动作接口,鼠标点击是唯一动作
 */
public interface IUserAction
{
    void clickOne();
}

public class UserGUI : MonoBehaviour
{
    private IUserAction action;
    float width, height;

    void Start()
    {
        action = SSDirector.getInstance().currentScenceController as IUserAction;
    }

    float castw(float scale)
    {
        return (Screen.width - width) / scale;
    }

    float casth(float scale)
    {
        return (Screen.height - height) / scale;
    }

    void OnGUI()
    {
        width = Screen.width / 12;
        height = Screen.height / 12;

        GUI.Label(new Rect(castw(2f)+20, casth(6f) - 20, 50, 50), SSDirector.getInstance().leaveSeconds.ToString());// 倒计时秒数 //  


        if (SSDirector.getInstance().state != State.WIN && SSDirector.getInstance().state != State.LOSE
            && GUI.Button(new Rect(10, 10, 80, 30), SSDirector.getInstance().countDownTitle))
        {
            if (SSDirector.getInstance().countDownTitle == "Start")
            {

                SSDirector.getInstance().currentScenceController.Resume();
                SSDirector.getInstance().countDownTitle = "Pause";
                SSDirector.getInstance().onCountDown = true;
                StartCoroutine(SSDirector.getInstance().DoCountDown());
            }
            else
            {
                SSDirector.getInstance().currentScenceController.Pause();
                SSDirector.getInstance().countDownTitle = "Start";
                SSDirector.getInstance().onCountDown = false;
                StopAllCoroutines();
            }
        }

        if (SSDirector.getInstance().state == State.WIN)//胜利
        {
            StopAllCoroutines();
            if (GUI.Button(new Rect(castw(2f), casth(6f), Screen.width / 8, height), "Win!"))
            {
                SSDirector.getInstance().currentScenceController.Restart();
            }
        }
        else if (SSDirector.getInstance().state == State.LOSE)//失败
        {
            StopAllCoroutines();
            if (GUI.Button(new Rect(castw(2f), casth(6f), Screen.width / 7, height), "Lose!"))
            {
                SSDirector.getInstance().currentScenceController.Restart();
            }
        }
    }

    /**
     * 检测用户的点击动作
     */
    void Update()
    {
        action.clickOne();
    }

}
  • 场景管理器 FirstSceneController
using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

/**
 * 场记的接口,每个场记都要实现
 * 加载场景资源、场景的暂停、恢复和重新布置
 */
public interface ISceneController
{
    void LoadResources();
    void Pause();
    void Resume();
    void Restart();
}

public class FirstSceneController : MonoBehaviour, IUserAction, ISceneController {

    public FirstSSActionManager actionManager;

    List<GameObject> LeftObjList = new List<GameObject>();//存储在左岸的对象
    List<GameObject> RightObjList = new List<GameObject>();//存储在右岸的对象
    GameObject[] boat = new GameObject[2];

    GameObject boat_obj, leftShore_obj, rightShore_obj;

    Vector3 LeftShorePos = new Vector3(-12, 0, 0);
    Vector3 RightShorePos = new Vector3(12, 0, 0);
    Vector3 BoatLeftPos = new Vector3(-4, 0, 0);
    Vector3 BoatRightPos = new Vector3(4, 0, 0);

    void Awake()
    {
        SSDirector director = SSDirector.getInstance();
        director.setFPS(60);
        director.currentScenceController = this;
        director.currentScenceController.LoadResources();
        director.leaveSeconds = director.totalSeconds;

    }

    void Start () {

        SSDirector.getInstance().state = State.PAUSE;
        SSDirector.getInstance().countDownTitle = "Start";
        actionManager = GetComponent<FirstSSActionManager>() as FirstSSActionManager;
    }

    void Update()
    {
        Judge();
    }

    public void LoadResources()
    {
        GameObject priest_obj, devil_obj;
        Camera.main.transform.position = new Vector3(0, 0, -20);

        /**
         * 加载两岸
         * 加载船
         */
        leftShore_obj = Instantiate(Resources.Load("Prefabs/Shore"), LeftShorePos, Quaternion.identity) as GameObject;
        rightShore_obj = Instantiate(Resources.Load("Prefabs/Shore"), RightShorePos, Quaternion.identity) as GameObject;
        leftShore_obj.name = "left_shore";
        rightShore_obj.name = "right_shore";

        boat_obj = Instantiate(Resources.Load("Prefabs/Boat"), BoatLeftPos, Quaternion.identity) as GameObject;
        boat_obj.name = "boat";

        /**
         * 一开始牧师、魔鬼、船都在左岸,注意:我是通过判断父对象决定物体在两岸还是船上的
         */
        boat_obj.transform.parent = leftShore_obj.transform;

        /**
         * 把牧师与恶魔摆在左岸初始位置
         */
        for (int i = 0; i < 3; ++i)
        {
            priest_obj = Instantiate(Resources.Load("Prefabs/Priest")) as GameObject;
            priest_obj.name = i.ToString();//牧师编号0 1 2
            priest_obj.transform.position = new Vector3(-16f + 1.5f * Convert.ToInt32(priest_obj.name), 2.7f, 0);
            priest_obj.transform.parent = leftShore_obj.transform;
            LeftObjList.Add(priest_obj);

            devil_obj = Instantiate(Resources.Load("Prefabs/Devil")) as GameObject;
            devil_obj.name = (i + 3).ToString();//魔鬼编号3 4 5
            devil_obj.transform.position = new Vector3(-16f + 1.5f * Convert.ToInt32(devil_obj.name), 2.7f, 0);
            devil_obj.transform.parent = leftShore_obj.transform;
            LeftObjList.Add(devil_obj);
        }
    }

    /**
     * 实现用户动作接口中的点击对象函数
     * 利用射线获取对象
     *   对象名字 0—2代表牧师
     *            3—5代表魔鬼
     *            boat代表船
     * 若对象名字为其他,或者没点中对象则不处理
     */
    public void clickOne()
    {
        GameObject gameObj = null;

        if (Input.GetMouseButtonDown(0) && 
            (SSDirector.getInstance().state == State.START || SSDirector.getInstance().state == State.CONTINUE))
        {
            Ray ray = Camera.main.ScreenPointToRay(Input.mousePosition);
            RaycastHit hit;
            if (Physics.Raycast(ray, out hit))
            {
                gameObj = hit.transform.gameObject;
            }
        }

        if (gameObj == null) return;
        else if (gameObj.name == "0" || gameObj.name == "1" || gameObj.name == "2"
            || gameObj.name == "3" || gameObj.name == "4" || gameObj.name == "5")
        {
            MovePeople(gameObj);
        }
        else if(gameObj.name == "boat")
        {
            MoveBoat();
        }
    }

    /**
     * 人的移动分四种情况
     * 上船:左岸上左船,右岸上右船
     * 上岸:左船上左岸、右船上右岸
     * 其他情况人都不会动
     */
    void MovePeople(GameObject people)
    {
        int shoreNum, seatNum;//0为左,1为右,作为参数传给动作管理员

        if (people.transform.parent == boat_obj.transform.parent && (boat[0] == null || boat[1] == null))//物体和船都在同一个岸且船有空位才能上船
        {
            seatNum = boat[0] == null ? 0 : 1;
            if (people.transform.parent == leftShore_obj.transform)
            {
                shoreNum = 0;
                for (int i = 0; i < LeftObjList.Count; i++)
                {
                    if (people.name == LeftObjList[i].name)
                    {
                        actionManager.getOnBoat(people, shoreNum, seatNum);//让动作管理员执行上船动作
                        LeftObjList.Remove(LeftObjList[i]);
                    }
                }
            }
            else
            {
                shoreNum = 1;
                for (int i = 0; i < RightObjList.Count; i++)
                {
                    if (people.name == RightObjList[i].name)
                    {
                        actionManager.getOnBoat(people, shoreNum, seatNum);
                        RightObjList.Remove(RightObjList[i]);
                    }
                }
            }
            boat[seatNum] = people;
            people.transform.parent = boat_obj.transform;
        }
        else if (people.transform.parent == boat_obj.transform)//若物体在船上就选择上岸
        {
            shoreNum = boat_obj.transform.parent == leftShore_obj.transform ? 0 : 1;
            seatNum = (boat[0] != null && boat[0].name == people.name) ? 0 : 1;

            actionManager.getOffBoat(people, shoreNum);//动作管理员去执行上岸动作

            boat[seatNum] = null;
            if(shoreNum == 0)
            {
                people.transform.parent = leftShore_obj.transform;
                LeftObjList.Add(people);
            }
            else
            {
                people.transform.parent = rightShore_obj.transform;
                RightObjList.Add(people);
            }
        }
    }

    /**
     * 若船上有人,则船可驶向对岸
     * 让动作管理器去负责移动小船
     */
    void MoveBoat()
    {
        if(boat[0]!=null || boat[1] != null)
        {
            actionManager.moveBoat(boat_obj);

            boat_obj.transform.parent = boat_obj.transform.parent == leftShore_obj.transform ? 
                rightShore_obj.transform : leftShore_obj.transform;
        }
    }

    /**
     * 判断游戏输赢状态并告知导演
     * 计算分三步,先统计左岸的牧师、恶魔
     *             然后统计右岸的牧师、恶魔
     *             再判断船在左岸还是右岸,把船上恶魔与牧师加到对应岸的数量上
     *  若某一岸恶魔 > 牧师,则游戏失败
     *  若全到右岸,则胜利
     *  否则游戏继续
     */
    public void Judge()
    {
        int left_d = 0, left_p = 0, right_d = 0, right_p = 0;

        foreach (GameObject element in LeftObjList)
        {
            if (element.tag == "Priest") left_p++;
            if (element.tag == "Devil") left_d++;
        }

        foreach (GameObject element in RightObjList)
        {
            if (element.tag == "Priest") right_p++;
            if (element.tag == "Devil") right_d++;
        }

        for (int i = 0; i < 2; i++)
        {
            if (boat[i] != null && boat_obj.transform.parent == leftShore_obj.transform)//船在左岸
            {
                if (boat[i].tag == "Priest") left_p++;
                else left_d++;
            }
            if (boat[i] != null && boat_obj.transform.parent == rightShore_obj.transform)//船在右岸
            {
                if (boat[i].tag == "Priest") right_p++;
                else right_d++;
            }
        }

        if ((left_d > left_p && left_p != 0) || (right_d > right_p && right_p != 0) || SSDirector.getInstance().leaveSeconds == 0)
        {
            SSDirector.getInstance().state = State.LOSE;
        }
        else if (right_d == right_p && right_d == 3)//全过河,赢了
        {
            SSDirector.getInstance().state = State.WIN;
        }
    }

    public void Pause()
    {
        SSDirector.getInstance().state = State.PAUSE;
    }

    public void Resume()
    {
        SSDirector.getInstance().state = State.CONTINUE;
    }

    public void Restart()
    {
        Application.LoadLevel(Application.loadedLevelName);
        SSDirector.getInstance().state = State.START;
    }
}
  • 动作管理者 FirstSSActionManager
    我把三个基本动作的类SSAction、MoveToAction、 SequenceAction也一起放到了这里,然后SSActionManager是对基本动作的管理。
    再写一个FirstSSActionManager继承SSActionManager,专门实现对FirstSceneController场景里对象的动作设计,这样场记可以更方便地让FirstActonManager执行动作。
using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

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

/**
 * SSAction是动作的基类,保存要移动的游戏对象属性
 */
public class SSAction : ScriptableObject
{

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

    public GameObject gameObject { get; set; }
    public Transform transform { get; set; }
    public ISSActionCallback callback { get; set; }

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

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

/**
 * MoveToAction是SSAction的一个子类,代表平移的动作。 
 */
public class MoveToAction : SSAction
{
    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.callback.actionDone(this);
        }
    }

    public override void Start() {}

}

/**
 * SequenceAction是SSAction的另一个子类,它代表一系列组合动作。
 */
public class SequenceAction : SSAction, ISSActionCallback
{
    public List<SSAction> sequence;
    public int repeat = -1; //-1表示无限循环,0表示只执行一遍,repeat> 0 表示重复repeat遍
    public int currentAction = 0;//当前动作列表里,执行到的动作序号

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

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

    public void actionDone(SSAction source)
    {
        source.destroy = false;
        this.currentAction++;
        if (this.currentAction >= sequence.Count)
        {
            this.currentAction = 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统筹上面三个动作类
 */
public class SSActionManager : MonoBehaviour
{
    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 RunAction(GameObject gameObject, SSAction action, ISSActionCallback whoToNotify)
    {
        action.gameObject = gameObject;
        action.transform = gameObject.transform;
        action.callback = whoToNotify;
        waitingToAdd.Add(action);
        action.Start();
    }

}

/**
 * 专门设计动作并执行
 */
public class FirstSSActionManager : SSActionManager, ISSActionCallback
{
    public FirstSceneController scene;
    public MoveToAction action1, action2;
    public SequenceAction saction;
    float speed = 30f;

    /**
     * 为小船设置水平移动动作
     */
    public void moveBoat(GameObject boat)
    {
        action1 = MoveToAction.getAction((boat.transform.position ==  new Vector3(4, 0, 0)? new Vector3(-4, 0, 0) : new Vector3(4, 0, 0)), speed);
        this.RunAction(boat, action1, this);
    }

    /**
     * 人的上船动作
     * 为人设置水平和垂直两个分解动作,然后组合起来执行
     */
    public void getOnBoat(GameObject people, int shore, int seat)
    {
        if (shore == 0 && seat == 0)
        {
            action1 = MoveToAction.getAction(new Vector3(-5f, 2.7f, 0), speed);//右移
            action2 = MoveToAction.getAction(new Vector3(-5f, 1.2f, 0), speed);//下移
        }
        else if(shore==0 && seat == 1)
        {
            action1 = MoveToAction.getAction(new Vector3(-3f, 2.7f, 0), speed);
            action2 = MoveToAction.getAction(new Vector3(-3f, 1.2f, 0), speed);
        }
        else if (shore == 1 && seat == 0)
        {
            action1 = MoveToAction.getAction(new Vector3(3f, 2.7f, 0), speed);
            action2 = MoveToAction.getAction(new Vector3(3f, 1.2f, 0), speed);
        }
        else if (shore == 1 && seat == 1)
        {

            action1 = MoveToAction.getAction(new Vector3(5f, 2.7f, 0), speed);
            action2 = MoveToAction.getAction(new Vector3(5f, 1.2f, 0), speed);
        }

        SequenceAction saction = SequenceAction.getAction(0, 0, new List<SSAction> { action1, action2 });//组合动作
        this.RunAction(people, saction, this);
    }

    /**
     * 人的下船动作
     */
    public void getOffBoat(GameObject people, int shoreNum)
    {
        action1 = MoveToAction.getAction(new Vector3(people.transform.position.x, 2.7f, 0), speed);//上移

        if (shoreNum == 0)  action2 = MoveToAction.getAction(new Vector3(-16f + 1.5f * Convert.ToInt32(people.name), 2.7f, 0), speed);//左移
        else action2 = MoveToAction.getAction(new Vector3(16f - 1.5f * Convert.ToInt32(people.name), 2.7f, 0), speed);//右移

        SequenceAction saction = SequenceAction.getAction(0, 0, new List < SSAction >{ action1, action2});//组合动作
        this.RunAction(people, saction, this);
    }

    protected void Start()
    {
        scene = (FirstSceneController)SSDirector.getInstance().currentScenceController;
        scene.actionManager = this;
    }

    protected new void Update()
    {
        base.Update();
    }

    public void actionDone(SSAction source)
    {
        Debug.Log("Done");
    }
}

对类之间的操作还不熟练,自己设计得很粗糙,有出错的地方请大家指正TAT。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值