unity3d课后作业(七)

智能巡逻兵

游戏设计要求

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

程序设计要求

  1. 必须使用订阅与发布模式传消息
  2. 工厂模式生产巡逻兵

游戏玩法说明:

玩家通过上下左右箭头控制角色的移动;
地图上每个隔间有一个巡逻兵,巡逻轨迹为随机生成的,巡逻范围是整个隔间;
当玩家控制的角色进入某一隔间,该隔间的巡逻兵就会来追捕玩家;
追捕上了,则游戏结束;如果玩家成功逃离(进入另一隔间),则获得一分。

游戏效果图:
在这里插入图片描述
首先,制作预制。可以在Asset Store中搜索我们想要的免费资源~
在本游戏中,地图预制如下:
在这里插入图片描述
共有六个隔间,每个隔间将会有一个巡逻兵。

巡逻兵预制如下(是不是看起来就很像反派):
在这里插入图片描述
玩家控制的英雄预制如下:
在这里插入图片描述
除此之外,为了便于分数的显示,我将Text也做成了预制。
接下来是代码的设计过程。

· PatrolFactory
巡逻兵工厂,延续前几次游戏的设计思路,使用工厂模式来管理游戏对象的生成,以此达到良好的管理资源的目的。工厂是单例模式,只进行一次实例化。
巡逻兵初始的位置是设定好的,游戏开始后,他们开始做随机巡逻。

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

    private Vector3[] PatrolPosSet = new Vector3[] { new Vector3(-6, 0, 16), new Vector3(-1, 0, 19),
        new Vector3(6, 0, 16), new Vector3(-5, 0, 7), new Vector3(0, 0, 7), new Vector3(6, 0, 7)};

    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 PatrolPosSet;
    }
}

· FirstController
本类主要负责:加载游戏场景(产生玩家控制的英雄以及巡逻兵)、玩家控制英雄移动、巡逻兵巡逻。这里涉及到了本次游戏的精髓——巡逻兵的巡逻设计。
巡逻兵有两种巡逻模式:

  1. 当游戏玩家没有出现在巡逻范围内,做随机方向的巡逻;
  2. 当英雄出现在巡逻范围内了,开始追捕英雄。

先来看随机方向的巡逻。实现的方法是,用addRandomMovement方法给巡逻兵添加随机方向动作,但是,我们需要确保两个条件:

  1. 巡逻兵不会走出自己的巡逻范围;
  2. 巡逻兵不会卡在围墙上,即撞到墙后需要变换方向继续巡逻。

每个巡逻兵当前只有一个动作,加入新的动作之前,需要销毁当前的动作。
控制巡逻范围的方法是,给每个巡逻兵编号(一共有六个),给六个隔间也编号,因为我们知道隔间的坐标信息,以此来限制巡逻兵就可以了。

控制撞墙返回的方法是,检测巡逻兵是否发生Collision,如果检测到,则销毁当前的动作,增加一个新的随机巡逻动作。
核心代码:

//FirstController.cs
...
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);
        }
    }
        
    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;
}

检测撞墙:

//PatrolBehavior.cs
//挂载在Patrol预设上

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

其中,SSActionManager类负责管理动作的创建和销毁:

//SSActionManager.cs

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();
    }
}

再来看巡逻兵发现英雄后的追捕活动。检测英雄是否进入巡逻区域很简单:在英雄预制上面挂载一个脚本,可以返回英雄当前走到的隔间编号:

//HeroStatus.cs
...
public int standOnArea = -1;
...
void modifyStandOnArea() {
    float posX = this.gameObject.transform.position.x;
    float posZ = this.gameObject.transform.position.z;
    if (posZ >= FenchLocation.FenchHori) {
        if (posX < FenchLocation.FenchVertLeft)
            standOnArea = 0;
        else if (posX > FenchLocation.FenchVertRight)
            standOnArea = 2;
        else
            standOnArea = 1;
    }
    else {
        if (posX < FenchLocation.FenchVertLeft)
            standOnArea = 3;
        else if (posX > FenchLocation.FenchVertRight)
            standOnArea = 5;
        else
            standOnArea = 4;
    }
}

addDirectMovement方法来实现巡逻兵的追捕:

//FirstController.cs
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;
    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);
}

· CCMoveToAction

public class CCMoveToAction: SSAction {
    public Vector3 target;
    public float speed;
    public bool isCatching;    //追捕或巡逻

    public static CCMoveToAction CreateSSAction(Vector3 _target, float _speed, bool _isCatching) {
        CCMoveToAction action = ScriptableObject.CreateInstance<CCMoveToAction>();
        action.target = _target;
        action.speed = _speed;
        action.isCatching = _isCatching;
        return action;
    }

    public override void Start() {
        
    }

    public override void Update() {
        this.transform.position = Vector3.MoveTowards(this.transform.position, target, speed);
        if (this.transform.position == target) {
            this.destroy = true;
            if (!isCatching)    
                this.callBack.SSActionEvent(this);
            else
                this.callBack.SSActionEvent(this, SSActionEventType.Completed, SSActionTargetType.Catching);
        }
    }
}

· UserInterface
接收用户的“上下左右”输入,通知FirstController来控制英雄的移动:

//UserInterface.cs
void detectKeyInput() {
    if (Input.GetKey(KeyCode.UpArrow)) {
        action.heroMove(Diretion.UP);
    }
    if (Input.GetKey(KeyCode.DownArrow)) {
        action.heroMove(Diretion.DOWN);
    }
    if (Input.GetKey(KeyCode.LeftArrow)) {
        action.heroMove(Diretion.LEFT);
    }
    if (Input.GetKey(KeyCode.RightArrow)) {
        action.heroMove(Diretion.RIGHT);
    }
}
//FirstController.cs
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;
    }
}

本次游戏项目中,要求使用订阅/发布模式来传递信息,定义如下:

订阅者把自己想订阅的事件注册到调度中心,当该事件触发时候,发布者发布该事件到调度中心(顺带上下文),由调度中心统一调度订阅者注册到调度中心的处理代码。

UML图:
在这里插入图片描述
具体实现如下:
首先来考虑所有的游戏事件,其实只有两个:

  1. 英雄从一个隔间进入另一个隔间,加一分;
  2. 英雄被巡逻兵追上,游戏结束。

我们可以用一个GameEventManager来管理这两个事件,它就可以作为信息的发布者(Publisher),来告知订阅了信息的其他类(Subscriber)。比如说管理分数的类GameStatusText,如果GameEventManager发布了英雄进入另一隔间的消息,则显示的分数+1;如果GameEventManager发布了英雄被巡逻兵追上的消息,则显示“Game Over”字样。
这样就可以在触发玩家得分、游戏结束时,改变显示的文本内容,并且有效地降低类之间的耦合度。


实现到这一步,简单的巡逻兵游戏已经完成了,但是我们还可以有改进,比如加入动画。正好,我使用的人物模型自带动画(可以在Assets Store搜索warrior,免费素材噢),下面是添加奔跑动画的过程:

  1. 首先,我们可以先看看下载的素材都有哪些动画效果:
    在这里插入图片描述
    以我的Hero模型为例,进入animations目录,可以看到有以下动画:
    在这里插入图片描述
    在这里插入图片描述
  2. 然后,在Assets目录下新建文件夹Animator,新建Animator Controller文件并命名(比如Hero)。再找到动画文件,拖入Hero的Base Layer中,如图所示:
    在这里插入图片描述
  3. 做完这些,给做好的Hero预制添加Animator组件,将刚才完成的Animator Controller文件(Hero)拖入组件的"Controller"部分;再找到游戏模型的目录下的Avatr文件,拖入组件的"Avatr"部分,动画的植入就完成啦。

以上是巡逻兵游戏的主要实现过程,完整的代码见Github传送门

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值