Unity3D——简单巡逻兵游戏

一、游戏要求

  • 提交要求:

  • 游戏设计要求:

    • 创建一个地图和若干巡逻兵(使用动画);
    • 每个巡逻兵走一个3~5个边的凸多边型,位置数据是相对地址。即每次确定下一个目标位置,用自己当前位置为原点计算;
    • 巡逻兵碰撞到障碍物,则会自动选下一个点为目标; 巡逻兵在设定范围内感知到玩家,会自动追击玩家;
    • 失去玩家目标后,继续巡逻;
    • 计分:玩家每次甩掉一个巡逻兵计一分,与巡逻兵碰撞游戏结束;
  • 程序设计要求:

    • 必须使用订阅与发布模式传消息
    • subject:OnLostGoal
    • Publisher: ?
    • Subscriber: ?
    • 工厂模式生产巡逻兵
  • 友善提示1:

    • 生成 3~5个边的凸多边型
    • 随机生成矩形
    • 在矩形每个边上随机找点,可得到 3 - 4 的凸多边型
    • 5 ?
  • 友善提示2:

    • 参考以前博客,给出自己新玩法

二、UML图

根据发布者接收者模式,我设计的程序UML图如下:
这里写图片描述
在UML图形中,GameEventManager是发布者,GameStatus是信息的接收者。而在SSActionManager这部分中,主要是实现对动作的管理。在PatrolBehavior中是实现巡逻兵的动作交互的。
接下来是重点程序的举例。

三、程序源码及说明

巡逻兵部分:

1、巡逻兵工厂模式:

巡逻兵工厂模式同时也是单例模式,而且在实现的时候,要生成九个不同位置上的巡逻兵,以及生成巡逻兵实例。具体代码如下:

public class PatrolFactory : System.Object
{
    private static PatrolFactory instance;
    private GameObject PatrolItem;

    //九个方格中的巡逻兵初始位置
    private Vector3[] PatrolPos = new Vector3[] {new Vector3(9, 0, 9), new Vector3(0, 0, 9),
        new Vector3(-9, 0, 9), new Vector3(0, 0, 0), new Vector3(9, 0, 0), new Vector3(-9, 0, 0),
        new Vector3(9, 0, -9), new Vector3(0, 0, -9), new Vector3(-9, 0, -9)};

    public static PatrolFactory getInstance()
    {
        if (instance == null)
            instance = new PatrolFactory();
        return instance;
    }

    public void initItem(GameObject _PatrolItem)
    {
        PatrolItem = _PatrolItem;
    }

    public GameObject getPatrol()
    {
        GameObject newPatrol = Camera.Instantiate(PatrolItem);
        return newPatrol;
    }

    public Vector3[] getPosSet()
    {
        return PatrolPos;
    }
}

2、巡逻兵动作管理:

巡逻兵的本身的动作主要有两个,一个是当游戏玩家Hero进入当前巡逻兵的范围的时候,要变化本身的运动轨迹;另外一个动作是当游戏玩家Hero与巡逻兵相撞的时候,判定游戏结束。具体代码如下:

public class PatrolBehavior : MonoBehaviour {
    private IAddAction addAction;
    private IGameStatusOp gameStatus;
    public int index;
    public bool isCatching;
    private float CATCH_RADIUS = 3.0f;

    // Use this for initialization
    void Start () {
        addAction = SceneController.getInstance() as IAddAction;
        gameStatus = SceneController.getInstance() as IGameStatusOp;
        index = getOwnIndex();
        isCatching = false;
    }

    // Update is called once per frame
    void Update () {
        checkNearByHero();
    }

    int getOwnIndex()
    {
        string name = this.gameObject.name;
        char cindex = name[name.Length - 1];
        int result = cindex - '0';
        return result;
    }

    //检测进入自己区域的hero
    void checkNearByHero()
    {
        if (gameStatus.getHeroStandOnArea() == index)
        {    //只有当走进自己的区域
            if (!isCatching)
            {
                isCatching = true;
                addAction.addDirectMovement(this.gameObject);
            }
        }
        else
        {
            if (isCatching)
            {    //刚才为捕捉状态,但此时hero已经走出所属区域
                gameStatus.heroEscapeAndScore();
                isCatching = false;
                addAction.addRandomMovement(this.gameObject, false);
            }
        }
    }

    void OnCollisionStay(Collision e)
    {
        //撞击围栏,选择下一个点移动
        if (e.gameObject.name.Contains("Patrol") || e.gameObject.name.Contains("fence")
            || e.gameObject.tag.Contains("FenceAround"))
        {
            isCatching = false;
            addAction.addRandomMovement(this.gameObject, false);
        }

        //撞击hero,游戏结束
        if (e.gameObject.name.Contains("Hero"))
        {
            gameStatus.patrolHitHeroAndGameover();
            Debug.Log("Game Over!");
        }
    }
}

玩家Player部分

这部分的脚本是放在玩家Hero预制上的,所以主要的动作我设定的主要是,判定玩家所处的区域是哪块就行了。代码如下:

public class HeroBehavior : MonoBehaviour {
    public int standOnArea = -1;

    void Update () {
        modifyStandOnArea();
    }

    void modifyStandOnArea()
    {
        float posX = this.gameObject.transform.position.x;
        float posZ = this.gameObject.transform.position.z;
        if (posZ >= FenchLocation.FenchHoriUp)
        {
            if (posX < FenchLocation.FenchVertLeft)
                standOnArea = 0;
            else if (posX > FenchLocation.FenchVertRight)
                standOnArea = 2;
            else
                standOnArea = 1;
        }
        else if(posZ < FenchLocation.FenchHoriUp && posZ > FenchLocation.FenchHoriDown)
        {
            if (posX < FenchLocation.FenchVertLeft)
                standOnArea = 3;
            else if (posX > FenchLocation.FenchVertRight)
                standOnArea = 5;
            else
                standOnArea = 4;
        }
        else
        {
            if (posX < FenchLocation.FenchVertLeft)
                standOnArea = 6;
            else if (posX > FenchLocation.FenchVertRight)
                standOnArea = 8;
            else
                standOnArea = 7;
        }
    }
}

主体部分

1、SceneController部分

SceneController继承了三个接口类:IUserAction、IAddAction和IGameStatusOp。其中成员对象有SceneController的实例,GameModel和GameEventManager。用于控制游戏中的各类事件:游戏玩家逃离小兵得分,玩家的运动等等。主要代码如下:

public class SceneController : System.Object, IUserAction, IAddAction, IGameStatusOp
{
    private static SceneController instance;
    private GameModel myGameModel;
    private GameEventManager myGameEventManager;

    public static SceneController getInstance()
    {
        if (instance == null)
            instance = new SceneController();
        return instance;
    }

    internal void setGameModel(GameModel _myGameModel)
    {
        if (myGameModel == null)
        {
            myGameModel = _myGameModel;
        }
    }

    internal void setGameEventManager(GameEventManager _myGameEventManager)
    {
        if (myGameEventManager == null)
        {
            myGameEventManager = _myGameEventManager;
        }
    }

    //实现IUserAction接口
    public void heroMove(int dir)
    {
        myGameModel.HeroMove(dir);
    }

    //实现IAddAction接口
    public void addRandomMovement(GameObject sourceObj, bool isActive)
    {
        myGameModel.addRandomMovement(sourceObj, isActive);
    }

    public void addDirectMovement(GameObject sourceObj)
    {
        myGameModel.addDirectMovement(sourceObj);
    }

    //实现IGameStatusOp接口
    public int getHeroStandOnArea()
    {
        return myGameModel.GetHeroArea();
    }

    public void heroEscapeAndScore()
    {
        myGameEventManager.heroEscape();
    }

    public void patrolHitHeroAndGameover()
    {
        myGameEventManager.patrolHitHero();
    }
}

2、GameModel类

这个类是用来具体实现人物和巡逻兵运动的类。其中定义了游戏中所需要的各类对象和方法。对象包括PatrolItem,HeroItem,sceneController等。方法可以分为几大类:实例化游戏玩家和巡逻兵,实现巡逻兵绕一个凸多边形的边缘运动,实现对象向某一个规定的方向运动,以及游戏玩家的运动等。
具体的函数实现如下:

public class GameModel : SSActionManager, ISSActionCallback {
    public GameObject PatrolItem, HeroItem;
    private SceneController sceneController;
    private GameObject hero, sceneModel, canvas;
    private List<GameObject> patrolSet;
    private List<int> patrolLastDir;
    private const float PERSON_SPEED_NORMAL = 0.05f;
    private const float PERSON_SPEED_CATCHING = 0.06f;

    void Awake()
    {
        PatrolFactory.getInstance().initItem(PatrolItem);
    }

    // Use this for initialization
    protected new void Start () {
        sceneController = SceneController.getInstance();
        sceneController.setGameModel(this);
        GenHero();
        GenPatrols();
    }

    // Update is called once per frame
    protected new void Update()
    {
        base.Update();
    }

    //生产游戏玩家Hero
    void GenHero()
    {
        hero = Instantiate(HeroItem);
    }

    //生产巡逻兵
    void GenPatrols()
    {
        patrolSet = new List<GameObject>(9);
        patrolLastDir = new List<int>(9);
        Vector3[] posSet = PatrolFactory.getInstance().getPosSet();
        //从巡逻兵工厂得到九个位置,并生产出巡逻兵
        for(int i = 0; i < 9; i++)
        {
            GameObject newPatrol = PatrolFactory.getInstance().getPatrol();
            newPatrol.transform.position = posSet[i];
            newPatrol.name = "Patrol" + i;
            patrolLastDir.Add(-2);
            patrolSet.Add(newPatrol);
            addRandomMovement(newPatrol, true);
        }
    }

    //实现接口ISSActionCallback
    public void SSActionEvent(SSAction source,
        SSActionEventType eventType = SSActionEventType.Completed,
        SSActionTargetType intParam = SSActionTargetType.Normal,     //动作结束时回调,需要告知是哪种动作
        string strParam = null,
        Object objParam = null)
    {
        if (intParam == SSActionTargetType.Normal)
            addRandomMovement(source.gameObject, true);
        else
            addDirectMovement(source.gameObject);
    }

    //巡逻兵沿凸多边形边缘随意方向运动
    public void addRandomMovement(GameObject sourceObj, bool isActive)
    {
        int ind = GetIndex(sourceObj);
        int randomDir = GetRandomDirection(ind, isActive);
        patrolLastDir[ind] = randomDir;
        sourceObj.transform.rotation = Quaternion.Euler(new Vector3(0, randomDir * 90, 0));
        Vector3 target = sourceObj.transform.position;
        switch (randomDir)
        {
            case Direction.UP:
                target += new Vector3(0, 0, 1);
                break;
            case Direction.DOWN:
                target += new Vector3(0, 0, -1);
                break;
            case Direction.LEFT:
                target += new Vector3(-1, 0, 0);
                break;
            case Direction.RIGHT:
                target += new Vector3(1, 0, 0);
                break;
        }
        AddSingleMove(sourceObj, target, PERSON_SPEED_NORMAL, false);
    }

    //游戏对象往某一个规定的方向运动
    public void addDirectMovement(GameObject sourceObj)
    {
        int index = GetIndex(sourceObj);
        patrolLastDir[index] = -2;

        sourceObj.transform.LookAt(sourceObj.transform);
        Vector3 oriTarget = hero.transform.position - sourceObj.transform.position;
        Vector3 target = new Vector3(oriTarget.x / 4.0f, 0, oriTarget.z / 4.0f);
        target += sourceObj.transform.position;

        AddSingleMove(sourceObj, target, PERSON_SPEED_CATCHING, true);
    }

    //获取巡逻兵标号
    int GetIndex(GameObject sourceObj)
    {
        string name = sourceObj.name;
        char ind = name[name.Length - 1];
        int result = ind - '0';
        return result;
    }

    //获取任意方向
    int GetRandomDirection(int ind, bool isActive)
    {
        int randomDir = Random.Range(-1, 3);
        if (!isActive)
        {
            while(patrolLastDir[ind] == randomDir)
            {
                randomDir = Random.Range(-1, 3);
            }
        }
        else
        {
            while (patrolLastDir[ind] == 0 && randomDir == 2
                || patrolLastDir[ind] == 2 && randomDir == 0
                || patrolLastDir[ind] == 1 && randomDir == -1
                || patrolLastDir[ind] == -1 && randomDir == 1
                || PatrolOutOfArea(ind, randomDir))
            {
                randomDir = Random.Range(-1, 3);
            }
        }
        return randomDir;

    }

    //判定巡逻兵随意方向的下一步是否或出规定的区域
    bool PatrolOutOfArea(int ind, int randomDir)
    {
        Vector3 patrolPos = patrolSet[ind].transform.position;
        float posX = patrolPos.x;
        float posZ = patrolPos.z;
        switch (ind)
        {
            case 0:
                if (randomDir == 1 && posX + 1 > FenchLocation.FenchVertLeft
                    || randomDir == 2 && posZ - 1 < FenchLocation.FenchHoriUp)
                    return true;
                break;
            case 1:
                if (randomDir == 1 && posX + 1 > FenchLocation.FenchVertRight
                    || randomDir == -1 && posX - 1 < FenchLocation.FenchVertLeft
                    || randomDir == 2 && posZ - 1 < FenchLocation.FenchHoriUp)
                    return true;
                break;
            case 2:
                if (randomDir == -1 && posX - 1 < FenchLocation.FenchVertRight
                    || randomDir == 2 && posZ - 1 < FenchLocation.FenchHoriUp)
                    return true;
                break;
            case 3:
                if (randomDir == 1 && posX + 1 > FenchLocation.FenchVertLeft
                    || randomDir == 0 && posZ + 1 > FenchLocation.FenchHoriUp
                    || randomDir == 2 && posZ - 1 < FenchLocation.FenchHoriDown)
                    return true;
                break;
            case 4:
                if (randomDir == 1 && posX + 1 > FenchLocation.FenchVertRight
                    || randomDir == -1 && posX - 1 < FenchLocation.FenchVertLeft
                    || randomDir == 0 && posZ + 1 > FenchLocation.FenchHoriUp
                    || randomDir == 2 && posZ - 1 < FenchLocation.FenchHoriDown)
                    return true;
                break;
            case 5:
                if (randomDir == -1 && posX - 1 < FenchLocation.FenchVertRight
                    || randomDir == 0 && posZ + 1 > FenchLocation.FenchHoriUp
                    || randomDir == 2 && posZ - 1 < FenchLocation.FenchHoriDown)
                    return true;
                break;
            case 6:
                if (randomDir == 1 && posX + 1 > FenchLocation.FenchVertLeft
                    || randomDir == 0 && posZ + 1 > FenchLocation.FenchHoriDown)
                    return true;
                break;
            case 7:
                if (randomDir == 1 && posX + 1 > FenchLocation.FenchVertRight
                    || randomDir == -1 && posX - 1 < FenchLocation.FenchVertLeft
                    || randomDir == 0 && posZ + 1 > FenchLocation.FenchHoriDown)
                    return true;
                break;
            case 8:
                if (randomDir == -1 && posX - 1 < FenchLocation.FenchVertRight
                    || randomDir == 0 && posZ + 1 > FenchLocation.FenchHoriDown)
                    return true;
                break;
        }
        return false;
    }

    //对某一个确定的对象完成一步的运动
    void AddSingleMove(GameObject sourceObj, Vector3 target, float speed, bool isCatching)
    {
        this.runAction(sourceObj, CCMoveToAction.CreateSSAction(target, speed, isCatching), this);
    }

    //给某一个特定的对象完成某一系列的运动
    void AddCombinedMoving(GameObject sourceObj, Vector3[] target, float[] speed, bool isCatching)
    {
        List<SSAction> action = new List<SSAction>();
        for(int i = 0; i < target.Length; i++)
        {
            action.Add(CCMoveToAction.CreateSSAction(target[i], speed[i], isCatching));
        }
        CCSequeneActions moveSeq = CCSequeneActions.CreateSSAction(action);
        this.runAction(sourceObj, moveSeq, this);
    }

    //游戏玩家运动
    public void HeroMove(int dir)
    {
        hero.transform.rotation = Quaternion.Euler(new Vector3(0, dir * 90, 0));
        switch (dir)
        {
            case Direction.UP:
                hero.transform.position += new Vector3(0, 0, 0.1f);
                break;
            case Direction.DOWN:
                hero.transform.position += new Vector3(0, 0, -0.1f);
                break;
            case Direction.LEFT:
                hero.transform.position += new Vector3(-0.1f, 0, 0);
                break;
            case Direction.RIGHT:
                hero.transform.position += new Vector3(0.1f, 0, 0);
                break;
        }
    }

    //获取此时游戏玩家所在的方格区域
    public int GetHeroArea()
    {
        return hero.GetComponent<HeroBehavior>().standOnArea;
    }
}

3、订阅者发布者模式

发布者:

我这里的发布者是GameEventManager,用于发布游戏对象Hero逃跑得分以及Hero碰到巡逻兵游戏结束的消息。具体代码如下:

public class GameEventManager : MonoBehaviour {
    public delegate void GameScoreAction();
    public static event GameScoreAction gameScoreAction;
    public delegate void GameOverAction();
    public static event GameOverAction gameOverAction;
    private SceneController sceneController;

    // Use this for initialization
    void Start () {
        sceneController = SceneController.getInstance();
        sceneController.setGameEventManager(this);
    }

    // Update is called once per frame
    void Update () { }

    //得分事件发布
    public void heroEscape()
    {
        if (gameScoreAction != null)
            gameScoreAction();
    }

    //游戏结束事件发布
    public void patrolHitHero()
    {
        if (gameOverAction != null)
            gameOverAction();
    }
}
订阅者

这里的订阅者是GameStatusText,通过接收事件,来调节游戏中用到的text的不同。当游戏继续或者结束的时候,显示不同的信息。具体代码如下:

public class GameStatusText : MonoBehaviour {
    private int score = 0;
    private int textType;  
    //0为得分,1为游戏结束

    void Start () {
        distinguishText();
    }

    void Update () { }

    void distinguishText() {
        if (gameObject.name.Contains("Score"))
            textType = 0;
        else
            textType = 1;
    }

    void OnEnable() {
        GameEventManager.myGameScoreAction += gameScore;
        GameEventManager.myGameOverAction += gameOver;
    }

    void OnDisable() {
        GameEventManager.myGameScoreAction -= gameScore;
        GameEventManager.myGameOverAction -= gameOver;
    }

    //得到游戏玩家得分事件
    void gameScore() {
        if (textType == 0) {
            score++;
            this.gameObject.GetComponent<Text>().text = "Score: " + score;
        }
    } 

    //得到游戏结束事件
    void gameOver() {
        if (textType == 1)
            this.gameObject.GetComponent<Text>().text = "Game Over!";
    }
}

主摄像机

在完成游戏主体代码之后,我发现当固定游戏的主摄像头时,游戏体验不好,看到的玩家比较小。所以参照其他博客,我给主摄像机加上了一个脚本,让他能随着游戏玩家运动。具体代码如下:

public class CameraBehavior : MonoBehaviour {

    public GameObject target;
    public float followingSpeed = 0.06f;
    Vector3 distance;

    // Use this for initialization
    void Start () {
        distance = transform.position - target.transform.position;
    }

    // Update is called once per frame
    void Update () {
        transform.position = Vector3.Lerp(transform.position, target.transform.position + distance, followingSpeed * Time.deltaTime);
    }
}

游戏效果

这里写图片描述

这里写图片描述
游戏视频链接如下:https://github.com/EmilyBlues/Unity-learning/blob/master/HW6/PatrolsGame.mp4

猜你喜欢

转载自blog.csdn.net/emilybluse/article/details/80284580