[Unity3D课堂作业] 巡逻兵 GetAwayFromPatrols

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/qq_33000225/article/details/70045292

Attention:

由于此次作业代码量与平时作业相比稍多_(:зゝ∠)_为了避免篇幅过长,所以下文仅介绍实现难点,而不考虑po出所有的代码。但是可以访问我的github查看完整项目代码和资源哦(可以两边参照着看 or 下载下来跑哦~~):

https://github.com/MarkMoHR/GetAwayFromPatrols


游戏规则:



游戏效果:



游戏UML类图:



游戏难点实现:

1、巡逻兵(Patrol)的行走模式,如何实现循环走动

        @答:简单来说就是“动作结束后,在动作结束回调函数里再添加动作”

        @详解:从“巡逻兵交互”部分看到有个IAddAction接口,提供两个方法:addRandomMovement(当巡逻兵为非追捕状态时,添加随机 移动动作)、addDirectMovement(当巡逻兵为追捕状态时,添加指向玩家的移动动作)。

        addRandomMovement的实现就是添加随机方向动作、addDirectMovement的实现就是用巡逻兵和玩家的位置相减得到移动方

向。注意的是巡逻兵不能走出自己的区域,代码里也有根据位置进行判断。

        巡逻兵的行走利用了面向对象的动作管理(上面UML图右下红框部分)。当巡逻兵初始化时候,加随机动作。由于动作结束时候会回调接口函数SSActionEvent(在GameModel里实现),此时再根据传回参数(传参下面会说)继续调用添加动作的方法addRandomMovement或addDirectMovement。

==> GameModel.cs

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using Com.Patrols;

public class GameModel : SSActionManager, ISSActionCallback {
    public GameObject PatrolItem, HeroItem, sceneModelItem, canvasItem;

    private SceneController scene;
    private GameObject myHero, sceneModel, canvasAndText;
    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);
    }

    protected new void Start () {
        scene = SceneController.getInstance();
        scene.setGameModel(this);

        genHero();
        genPatrols();
        sceneModel = Instantiate(sceneModelItem);
        canvasAndText = Instantiate(canvasItem);
    }

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

    //生产英雄
    void genHero() {
        myHero = Instantiate(HeroItem);
    }

    //生产巡逻兵
    void genPatrols() {
        PatrolSet = new List<GameObject>(6);
        PatrolLastDir = new List<int>(6);
        Vector3[] posSet = PatrolFactory.getInstance().getPosSet();
        for (int i = 0; i < 6; i++) {
            GameObject newPatrol = PatrolFactory.getInstance().getPatrol();
            newPatrol.transform.position = posSet[i];
            newPatrol.name = "Patrol" + i;
            PatrolLastDir.Add(-2);
            PatrolSet.Add(newPatrol);
            addRandomMovement(newPatrol, true);
        }
    }

    //hero移动
    public void heroMove(int dir) {
        myHero.transform.rotation = Quaternion.Euler(new Vector3(0, dir * 90, 0));
        switch (dir) {
            case Diretion.UP:
                myHero.transform.position += new Vector3(0, 0, 0.1f);
                break;
            case Diretion.DOWN:
                myHero.transform.position += new Vector3(0, 0, -0.1f);
                break;
            case Diretion.LEFT:
                myHero.transform.position += new Vector3(-0.1f, 0, 0);
                break;
            case Diretion.RIGHT:
                myHero.transform.position += new Vector3(0.1f, 0, 0);
                break;
        }
    }

    //动作结束后
    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);
    }

    //isActive说明是否主动变向(动作结束)
    public void addRandomMovement(GameObject sourceObj, bool isActive) {
        int index = getIndexOfObj(sourceObj);
        int randomDir = getRandomDirection(index, isActive);
        PatrolLastDir[index] = randomDir;

        sourceObj.transform.rotation = Quaternion.Euler(new Vector3(0, randomDir * 90, 0));
        Vector3 target = sourceObj.transform.position;
        switch (randomDir) {
            case Diretion.UP:
                target += new Vector3(0, 0, 1);
                break;
            case Diretion.DOWN:
                target += new Vector3(0, 0, -1);
                break;
            case Diretion.LEFT:
                target += new Vector3(-1, 0, 0);
                break;
            case Diretion.RIGHT:
                target += new Vector3(1, 0, 0);
                break;
        }
        addSingleMoving(sourceObj, target, PERSON_SPEED_NORMAL, false);
    }
    int getIndexOfObj(GameObject sourceObj) {
        string name = sourceObj.name;
        char cindex = name[name.Length - 1];
        int result = cindex - '0';
        return result;
    }
    int getRandomDirection(int index, bool isActive) {
        int randomDir = Random.Range(-1, 3);
        if (!isActive) {    //当碰撞时,不走同方向
            while (PatrolLastDir[index] == randomDir || PatrolOutOfArea(index, randomDir)) {
                randomDir = Random.Range(-1, 3);
            }
        }
        else {              //当非碰撞时,不走反方向
            while (PatrolLastDir[index] == 0 && randomDir == 2 
                || PatrolLastDir[index] == 2 && randomDir == 0
                || PatrolLastDir[index] == 1 && randomDir == -1
                || PatrolLastDir[index] == -1 && randomDir == 1
                || PatrolOutOfArea(index, randomDir)) {
                randomDir = Random.Range(-1, 3);
            }
        }
        //Debug.Log(isActive + " isActive " + "PatrolLastDir " + PatrolLastDir[index] + " -- randomDir " + randomDir);
        return randomDir;
    }
    //判定巡逻兵走出了自己的区域
    bool PatrolOutOfArea(int index, int randomDir) {
        Vector3 patrolPos = PatrolSet[index].transform.position;
        float posX = patrolPos.x;
        float posZ = patrolPos.z;
        switch (index) {
            case 0:
                if (randomDir == 1 && posX + 1 > FenchLocation.FenchVertLeft
                    || randomDir == 2 && posZ - 1 < FenchLocation.FenchHori)
                    return true;
                break;
            case 1:
                if (randomDir == 1 && posX + 1 > FenchLocation.FenchVertRight
                    || randomDir == -1 && posX - 1 < FenchLocation.FenchVertLeft
                    || randomDir == 2 && posZ - 1 < FenchLocation.FenchHori)
                    return true;
                break;
            case 2:
                if (randomDir == -1 && posX - 1 < FenchLocation.FenchVertRight
                    || randomDir == 2 && posZ - 1 < FenchLocation.FenchHori)
                    return true;
                break;
            case 3:
                if (randomDir == 1 && posX + 1 > FenchLocation.FenchVertLeft
                    || randomDir == 0 && posZ + 1 > FenchLocation.FenchHori)
                    return true;
                break;
            case 4:
                if (randomDir == 1 && posX + 1 > FenchLocation.FenchVertRight
                    || randomDir == -1 && posX - 1 < FenchLocation.FenchVertLeft
                    || randomDir == 0 && posZ + 1 > FenchLocation.FenchHori)
                    return true;
                break;
            case 5:
                if (randomDir == -1 && posX - 1 < FenchLocation.FenchVertRight
                    || randomDir == 0 && posZ + 1 > FenchLocation.FenchHori)
                    return true;
                break;
        }
        return false;
    }

    //追捕hero
    public void addDirectMovement(GameObject sourceObj) {
        int index = getIndexOfObj(sourceObj);
        PatrolLastDir[index] = -2;

        sourceObj.transform.LookAt(sourceObj.transform);
        Vector3 oriTarget = myHero.transform.position - sourceObj.transform.position;
        Vector3 target = new Vector3(oriTarget.x / 4.0f, 0, oriTarget.z / 4.0f);
        target += sourceObj.transform.position;
        //Debug.Log("addDirectMovement: " + target);
        addSingleMoving(sourceObj, target, PERSON_SPEED_CATCHING, true);
    }

    void addSingleMoving(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> acList = new List<SSAction>();
        for (int i = 0; i < target.Length; i++) {
            acList.Add(CCMoveToAction.CreateSSAction(target[i], speed[i], isCatching));
        }
        CCSequeneActions MoveSeq = CCSequeneActions.CreateSSAction(acList);
        this.runAction(sourceObj, MoveSeq, this);
    }

    //获取hero所在区域
    public int getHeroStandOnArea() {
        return myHero.GetComponent<HeroStatus>().standOnArea;
    }
}



2、巡逻兵添加动作后,移动过程中遇到障碍物怎么办?

        (我在编码的时候也遇到添加动作,但是遇到障碍物过不去,即巡逻兵不能到达指定目标位置,动作一直不能结束,不断产生抖动现象。怎么解决这个问题?)

        @答:利用巡逻兵Collision碰撞检测方法,检测到碰撞,发送信号,销毁现在的动作,再添加其他方向的动作。

        @详解:每个巡逻兵上面都挂载一个脚本PatrolBehaviour.cs,在碰撞检测函数里(注意这里用了OnCollisionStay函数,而不是Enter,原因是确保处于碰撞中必定能变换方向,而Enter只会触发进入碰撞那一瞬间),调用IAddAction接口函数添加随机动作。

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,游戏结束
    ……
}

3、上面的代码只是在碰撞时候添加动作,那原来还有的动作呢(一直不能结束)?

        @答:添加动作时候需要销毁该对象现有的动作,保证巡逻兵所有时刻都只有一个动作

        @详解:我把SSActionManager改写了一下,主要是runAction方法里添加了一部分代码,用于在某对象添加动作时,需先销毁属于他的现在的所有动作。

==> SSActionManager.cs

using UnityEngine;
using System.Collections;
using System.Collections.Generic;

//并发顺序
public class SSActionManager : MonoBehaviour {

    private Dictionary<int, SSAction> actions = new Dictionary<int, SSAction>();
    private List<SSAction> waitingAdd = new List<SSAction>();
    private List<int> waitingDelete = new List<int>();

    protected void Start () {
	
	}

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

        foreach (KeyValuePair<int, SSAction> kv in actions) {
            SSAction ac = kv.Value;
            if (ac.destroy)
                waitingDelete.Add(kv.Key);
            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 gameObj, SSAction action, ISSActionCallback manager) {
        //先把该对象现有的动作销毁(与原来不同部分)
        for (int i = 0; i < waitingAdd.Count; i++) {
            if (waitingAdd[i].gameObject.Equals(gameObj)) {
                SSAction ac = waitingAdd[i];
                waitingAdd.RemoveAt(i);
                i--;
                DestroyObject(ac);
            }
        }
        foreach (KeyValuePair<int, SSAction> kv in actions) {
            SSAction ac = kv.Value;
            if (ac.gameObject.Equals(gameObj)) {
                ac.destroy = true;
            }
        }

        action.gameObject = gameObj;
        action.transform = gameObj.transform;
        action.callBack = manager;
        waitingAdd.Add(action);
        action.Start();
    }
}

4、巡逻兵如何 感知玩家,并进行追踪?如何 感知到玩家逃离,并继续巡逻?

        @答:玩家hero有个所在区域的变量信息,每个巡逻兵时刻检测该信息,来判断是否在自己区域,从而有对应行动。

        @详解:在玩家hero上添加一个脚本HeroStatus.cs,有个standOnArea的整形变量,时刻根据玩家位置改变该变量的值。巡逻兵在Update()方法里时刻检测该值来判断玩家是否进入自己区域。若是,添加跟踪动作addDirectMovement(在上面GameModel.cs 里实现),而原动作也自动销毁。

        巡逻兵上挂载的脚本PatrolBehaviour.cs里有个isCatching的变量,代表巡逻兵是否处于追捕状态。若处于追捕状态,而发现玩家已不在自己的区域,说明在刚一瞬间,玩家逃离了自己区域,此时会添加随机动作,即继续巡逻。

==> PatrolBehaviour.cs

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using Com.Patrols;

//----------------------------------
// 此脚本加在巡逻兵上
//----------------------------------

public class PatrolBehaviour : MonoBehaviour {
    private IAddAction addAction;
    private IGameStatusOp gameStatusOp;

    public int ownIndex;
    public bool isCatching;    //是否感知到hero

    private float CATCH_RADIUS = 3.0f;

    void Start () {
        addAction = SceneController.getInstance() as IAddAction;
        gameStatusOp = SceneController.getInstance() as IGameStatusOp;

        ownIndex = getOwnIndex();
        isCatching = false;
    }
	
	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 (gameStatusOp.getHeroStandOnArea() == ownIndex) {    //只有当走进自己的区域
            if (!isCatching) {
                isCatching = true;
                addAction.addDirectMovement(this.gameObject);
            }
        }
        else {
            if (isCatching) {    //刚才为捕捉状态,但此时hero已经走出所属区域
                gameStatusOp.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")) {
            gameStatusOp.patrolHitHeroAndGameover();
            Debug.Log("Game Over!");
        }
    }
}

5、如何利用 订阅和发布模式传信息(玩家得分、游戏结束信息)?

        @答:通过GameEventManager发布上述两信息。GameStatusText订阅信息。

        @详解:此模式的理解为:当玩家得分或游戏结束时,都会GameEventManager的得分/游戏结束方法,但是具体怎么实现不用管。因为有可能后期需求更改,得分/游戏结束后需要实现更多效果。而此时我们只需要让这些效果订阅GameEventManager发布的得分/游戏结束信息,即可产生作用。实现功能的分类,降低代码耦合。

        而由于此次作业的得分/游戏结束均只需改变得分数or显示"Game Over!",所以我在挂载在UI.text上的脚本GameStatusText订阅两种信息即可,这样就可以在触发玩家得分、游戏结束时,自己改变文字内容

==> GameEventManager.cs

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using Com.Patrols;

public class GameEventManager : MonoBehaviour {
    public delegate void GameScoreAction();
    public static event GameScoreAction myGameScoreAction;

    public delegate void GameOverAction();
    public static event GameOverAction myGameOverAction;

    private SceneController scene;

    void Start () {
        scene = SceneController.getInstance();
        scene.setGameEventManager(this);
    }
	
	void Update () {
		
	}

    //hero逃离巡逻兵,得分
    public void heroEscapeAndScore() {
        if (myGameScoreAction != null)
            myGameScoreAction();
    }

    //巡逻兵捕获hero,游戏结束
    public void patrolHitHeroAndGameover() {
        if (myGameOverAction != null)
            myGameOverAction();
    }
}


==> GameStatusText.cs

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;

//----------------------------------
// 此脚本加在text上
//----------------------------------

public class GameStatusText : MonoBehaviour {
    private int score = 0;
    private int textType;  //0为score,1为gameover

	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!";
    }
}


好啦,主要是实现起来有一点点麻烦的东西大致就是上面那些了。琐碎的那些代码没放出来哈。有需要的可以到我的github上看哦:

https://github.com/MarkMoHR/GetAwayFromPatrols




猜你喜欢

转载自blog.csdn.net/qq_33000225/article/details/70045292