欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

3D游戏编程作业7

程序员文章站 2022-07-13 09:57:02
...

作业目标

智能巡逻兵:

  • 提交要求:
  • 游戏设计要求:
    • 创建一个地图和若干巡逻兵(使用动画);
    • 每个巡逻兵走一个3~5个边的凸多边型,位置数据是相对地址。即每次确定下一个目标位置,用自己当前位置为原点计算;
    • 巡逻兵碰撞到障碍物,则会自动选下一个点为目标;
    • 巡逻兵在设定范围内感知到玩家,会自动追击玩家;
    • 失去玩家目标后,继续巡逻;
    • 计分:玩家每次甩掉一个巡逻兵计一分,与巡逻兵碰撞游戏结束;
  • 程序设计要求:
    • 必须使用订阅与发布模式传消息
      • subject:OnLostGoal
      • Publisher: ?
      • Subscriber: ?
    • 工厂模式生产巡逻兵
  • 友善提示1:生成 3~5个边的凸多边型
    • 随机生成矩形
    • 在矩形每个边上随机找点,可得到 3 - 4 的凸多边型
    • 5 ?
  • 友善提示2:参考以前博客,给出自己新玩法

游戏规则

  • 使用wsad或者方向键控制玩家上下左右移动;
  • 当玩家靠近巡逻兵一定距离时,巡逻兵会主动追击玩家;
  • 若玩家摆脱巡逻兵,加一分;
  • 若巡逻兵碰撞到玩家时,玩家死亡,游戏结束;

游戏设计

1、UML类图

3D游戏编程作业7

2、游戏预制

游戏预制中的玩家和巡逻兵都是我从资源商店中找的免费资源。

3D游戏编程作业7

  • 玩家:一只红皮独眼的小怪兽

3D游戏编程作业7

  • 巡逻兵:一只背壳带刺的独眼小怪兽

3D游戏编程作业7

  • 地图:由四面墙合围而成的封闭地图,其中有9个区域,各区域之间用栅栏隔开。

3D游戏编程作业7

3、游戏动画机制作

动画机之中的各个状态都是随游戏对象一起从资源商店获得的,在制作动画时将需要的动画拖到界面中然后配上一定的条件即可。

3D游戏编程作业7

  • 玩家动画:

3D游戏编程作业7

  • 巡逻兵动画:

3D游戏编程作业7

4、相关代码

大部分代码与之前的游戏设计大同小异,架构差不多。这次的主要不同之处在于碰撞通知。

4.1 巡逻兵工厂

巡逻兵工厂对巡逻兵进行实例化,设定好了巡逻兵的初始位置。

public class PropFactory : MonoBehaviour {
    private GameObject patrol = null; 
    private List<GameObject> used = new List<GameObject> (); 
    private Vector3[] vec = new Vector3[9];

    public FirstSceneController sceneControler; 

    public List<GameObject> GetPatrols () {
        int[] pos_x = {-6, 4, 13 };
        int[] pos_z = {-4, 6, -13 };
        int index = 0;

        for (int i = 0; i < 3; i++) {
            for (int j = 0; j < 3; j++) {
                vec[index] = new Vector3 (pos_x[i], 0.5f, pos_z[j]);
                index++;
            }
        }
        for (int i = 0; i < 9; i++) {
            patrol = Instantiate (Resources.Load<GameObject> ("Prefabs/Patrol"));
            patrol.transform.position = vec[i];
            patrol.GetComponent<PatrolData> ().sign = i + 1;
            patrol.GetComponent<PatrolData> ().start_position = vec[i];
            used.Add (patrol);
        }
        return used;
    }
    public void StopPatrol () {
        for (int i = 0; i < used.Count; i++) {
            used[i].gameObject.GetComponent<Animator> ().SetBool ("run", false);
        }
    }
}

4.2 场景控制器

场景控制器FirstSceneController继承了接口IUserAction和ISceneController,并对其声明的函数进行实现。

场景控制器是订阅与发布模式中的订阅者,可以将自身相应事件处理函数提交给消息处理器,在相应事件发生时自动调用。

public class FirstSceneController : MonoBehaviour, IUserAction, ISceneController {
    public PropFactory patrol_factory;
    public ScoreRecorder recorder;
    public PatrolActionManager action_manager;
    public int wall_sign = -1;
    public GameObject player;
    public Camera main_camera;
    public float player_speed = 5;
    private List<GameObject> patrols;
    private bool game_over = false;

    void Update () {
        for (int i = 0; i < patrols.Count; i++) {
            patrols[i].gameObject.GetComponent<PatrolData> ().wall_sign = wall_sign;
        }
    }
    void Start () {
        SSDirector director = SSDirector.GetInstance ();
        director.CurrentScenceController = this;
        patrol_factory = Singleton<PropFactory>.Instance;
        action_manager = gameObject.AddComponent<PatrolActionManager> () as PatrolActionManager;
        LoadResources ();
        main_camera.GetComponent<CameraFlow> ().follow = player;
        recorder = Singleton<ScoreRecorder>.Instance;
    }

    public void LoadResources () {
        Instantiate (Resources.Load<GameObject> ("Prefabs/Plane"));
        player = Instantiate (Resources.Load ("Prefabs/Player"), new Vector3 (0, 0, 0), Quaternion.identity) as GameObject;
        patrols = patrol_factory.GetPatrols ();

        for (int i = 0; i < patrols.Count; i++) {
            action_manager.GoPatrol (patrols[i]);
        }
    }

    public void MovePlayer (float translationX, float translationZ) {
        if (!game_over) {
            if (translationX != 0 || translationZ != 0) {
                player.GetComponent<Animator> ().SetBool ("run", true);
            } else {
                player.GetComponent<Animator> ().SetBool ("run", false);
            }
            player.transform.Translate(translationX * player_speed * Time.deltaTime, 0, 0);
            player.transform.Translate (0, 0, translationZ * player_speed * Time.deltaTime);

            if (player.transform.localEulerAngles.x != 0 || player.transform.localEulerAngles.y !=0 || player.transform.localEulerAngles.z != 0) {
                player.transform.localEulerAngles = new Vector3 (0, 0, 0);
            }
            player.GetComponent<Rigidbody>().velocity = new Vector3(0, -0.5f, 0);
        }
    }

    public int getScore () {
        return recorder.getScore ();
    }

    public bool GetGameover () {
        return game_over;
    }

    public void Restart () {
        SceneManager.LoadScene ("Scenes/game");
    }

    void OnEnable () {
        GameEventManager.ScoreChange += addScore;
        GameEventManager.GameoverChange += Gameover;
    }

    void OnDisable () {
        GameEventManager.ScoreChange -= addScore;
        GameEventManager.GameoverChange -= Gameover;
    }

    void addScore () {
        recorder.addScore ();
    }

    void Gameover () {
        game_over = true;
        patrol_factory.StopPatrol ();
        action_manager.DestroyAllAction ();
    }
}

4.3 事件管理器

事件管理器继承了是订阅与发布模式中的中继者,消息的订阅者通过与管理器中相应的事件委托绑定,在相应的函数被发布者调用。

此处一共有两个事件管理器:SSActionManager和其子类PatrolActionManager。

SSActionManager: 包括RunAction和SSActionEvent,在SSActionEvent中委托了巡逻兵跟随玩家和巡逻兵巡逻两个事件。

public class SSActionManager : MonoBehaviour, ISSActionCallback {
    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 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 (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, int intParam = 0, GameObject objectParam = null) {
        if (intParam == 0) {
            PatrolFollowAction follow = PatrolFollowAction.GetSSAction (objectParam.gameObject.GetComponent<PatrolData> ().player);
            this.RunAction (objectParam, follow, this);
        } else {
            GoPatrolAction move = GoPatrolAction.GetSSAction (objectParam.gameObject.GetComponent<PatrolData> ().start_position);
            this.RunAction (objectParam, move, this);

            Singleton<GameEventManager>.Instance.PlayerEscape ();
        }
    }

    public void DestroyAll () {
        foreach (KeyValuePair<int, SSAction> kv in actions) {
            SSAction ac = kv.Value;
            ac.destroy = true;
        }
    }
}

PatrolActionManager:

public class PatrolActionManager : SSActionManager {
    private GoPatrolAction go_patrol;

    public void GoPatrol (GameObject patrol) {
        go_patrol = GoPatrolAction.GetSSAction (patrol.transform.position);
        this.RunAction (patrol, go_patrol, this);
    }

    public void DestroyAllAction () {
        DestroyAll ();
    }
}

4.4 游戏界面

实现了虚拟轴的获取、游戏分数显示、游戏提示界面以及结束时重新开始的按钮。

public class UserGUI : MonoBehaviour {

    private IUserAction action;
    private GUIStyle score_style = new GUIStyle ();
    private GUIStyle text_style = new GUIStyle ();
    private GUIStyle over_style = new GUIStyle ();
    public int show_time = 8; 
    void Start () {
        action = SSDirector.GetInstance ().CurrentScenceController as IUserAction;
        text_style.normal.textColor = new Color (0, 0, 0, 1);
        text_style.fontSize = 16;
        score_style.normal.textColor = new Color (1, 0.92f, 0.016f, 1);
        score_style.fontSize = 16;
        over_style.fontSize = 25;
        StartCoroutine (ShowTip ());
    }

    void Update () {
        float translationX = Input.GetAxis ("Horizontal");
        float translationZ = Input.GetAxis ("Vertical");
        action.MovePlayer (translationX, translationZ);
    }
    
    private void OnGUI () {
        GUI.Label (new Rect (10, 5, 200, 50), "分数:", text_style);
        GUI.Label (new Rect (55, 5, 200, 50), action.getScore ().ToString (), score_style);
        if (action.GetGameover () ) {
            GUI.Label (new Rect (Screen.width / 2 - 50, Screen.width / 2 - 250, 100, 100), "游戏结束", over_style);
            if (GUI.Button (new Rect (Screen.width / 2 - 50, Screen.width / 2 - 200, 100, 50), "重新开始")) {
                action.Restart ();
                return;
            }
        } 
        if (show_time > 0) {
            GUI.Label (new Rect (Screen.width / 2 - 80, 10, 100, 100), "按WSAD或方向键移动", text_style);
            GUI.Label (new Rect (Screen.width / 2 - 87, 30, 100, 100), "成功躲避巡逻兵追捕加1分", text_style);
        }
    }

    public IEnumerator ShowTip () {
        while (show_time >= 0) {
            yield return new WaitForSeconds (1);
            show_time--;
        }    
    }
}

4.5 碰撞通知

碰撞通知包括3个部分:玩家与游戏区域的碰撞、玩家与巡逻兵巡逻区域之间的碰撞以及玩家与巡逻兵的碰撞。

  • 玩家与游戏区域的碰撞结果是记录玩家在哪个区域,从而唤醒该区域巡逻兵的跟随功能。
public class AreaCollide : MonoBehaviour {
    public int sign = 0;
    FirstSceneController sceneController;

    private void Start () {
        sceneController = SSDirector.GetInstance ().CurrentScenceController as FirstSceneController;
    }
    
    void OnTriggerEnter (Collider collider) {
        if (collider.gameObject.tag == "Player") {
            sceneController.wall_sign = sign;
        }
    }
}
  • 玩家与巡逻兵巡逻区域的碰撞目的是使得巡逻兵可以跟随玩家、追踪玩家,以及在玩家逃离巡逻区域时停止追击。
public class PatrolCollide : MonoBehaviour {
    void OnTriggerEnter (Collider collider) {
        if (collider.gameObject.tag == "Player") {
            this.gameObject.transform.parent.GetComponent<PatrolData> ().follow_player = true;
            this.gameObject.transform.parent.GetComponent<PatrolData> ().player = collider.gameObject;
        }
    }
    
    void OnTriggerExit (Collider collider) {
        if (collider.gameObject.tag == "Player") {
            this.gameObject.transform.parent.GetComponent<PatrolData> ().follow_player = false;
            this.gameObject.transform.parent.GetComponent<PatrolData> ().player = null;
        }
    }
}
  • 玩家和巡逻兵的碰撞会使得巡逻兵攻击玩家,玩家死亡。
public class PlayerCollide : MonoBehaviour {

    void OnCollisionEnter (Collision other) {
        if (other.gameObject.tag == "Player") {
            other.gameObject.GetComponent<Animator> ().SetTrigger ("death");
            this.GetComponent<Animator> ().SetTrigger ("attack");
            Singleton<GameEventManager>.Instance.PlayerGameover ();
        }
    }
}

4.6 相机跟随

设置一个跟随玩家的摄像机,使得界面会随着玩家移动而移动。

这里一开始是用offset记录相机与玩家之间的距离差的:offset = this.transform.position - follow.transform.position,但是不知道什么原因,offset一直为0,最后只能给offset设定一个具体的方向向量值(-0.5f, 15f, -7.5f)。

public class CameraFlow : MonoBehaviour {
    public GameObject follow; 
    public float smothing = 5f; 
    Vector3 offset; 

    void Start () {
        offset = new Vector3(-0.5f, 15f, -7.5f);
    }

    void FixedUpdate () {
        Vector3 target = follow.transform.position + offset;
        transform.position = Vector3.Lerp (transform.position, target, smothing * Time.deltaTime);
    }
}

5、C#文件的挂载

  1. CameraFlow.cs挂载到Main Camera游戏对象上;
  2. 创建一个空的游戏对象,挂载GameEvent.cs, ScoreRecorder.cs, UserGUI.cs, FirstSceneController.csPropFactory.cs,并将FirstSceneControllerMain_camera设定为游戏界面的摄像机;
  3. 给巡逻兵Patrol的预制挂载PatrolData.csPlayerCollide.cs,并给巡逻兵的其中一个部件设定一个5*5的BoxCollider,勾选Is Trigger,挂载上PatrolCollide.cs
  4. 给地图Plane中的每个Trigger挂载AreaCollide.cs,并按规律设定sign值(1~9)。

游戏运行

点击Scenes下的game,加载游戏界面,然后点击运行,即可运行进入游戏界面。

3D游戏编程作业7

效果展示

游戏GIF截图过大,请查看仓库中的GIF.gif

游戏中遇到的问题

1、由于我选择的游戏对象都比较矮小,一开始参照往届师兄师姐们的设计设计了每个区域的Trigger时都是设计的比较高的box collide,使得玩家实际上在Trigger中,没有发生碰撞,所以一开始载入游戏时中间区域的巡逻兵不会追踪玩家,只有玩家跑出该区域再回来才能追踪。

后来将中间区域的Trigger的高度下调,使得玩家与其box collide有了交界,才解决了这个问题。

3D游戏编程作业7

2、玩家与巡逻兵无法碰撞。一开始以为是它们其中之一的刚体和碰撞器没有设置,检查后发现都有,排除了这方面的原因。

后来发现玩家并不是在平面上行走的,而是高了0.5,将巡逻兵的高度也调高了0.5,就能发生碰撞了。

但是有一点不太明白,玩家下面多的这0.5明明是胶囊碰撞体,也是可以接触与碰撞的,但是它就是不与巡逻兵发生碰撞,挺奇怪的。

3D游戏编程作业7

3、(未解决)当玩家在巡逻兵巡逻区域边缘疯狂试探或者玩家围着巡逻兵巡逻区域边缘转时,分数会疯狂增加。

仓库地址

传送门