与游戏世界交互

与游戏世界交互

这是3D游戏编程的第五次作业

说明文档

本次实验完成了所有基本要求,尽量将步骤展示出。
闪光点:
附有详细类图以及详细的代码注释

作业内容

1、编写一个简单的鼠标打飞碟(Hit UFO)游戏

  • 游戏内容要求:
    • 游戏有 nround,每个 round 都包括10trial
    • 每个 trial 的飞碟的色彩、大小、发射位置、速度、角度、同时出现的个数都可能不同。它们由该 roundruler 控制;
    • 每个 trial 的飞碟有随机性,总体难度随 round 上升;
    • 鼠标点中得分,得分规则按色彩、大小、速度不同计算,规则可自由设定。
  • 游戏的要求:
    • 使用带缓存的工厂模式管理不同飞碟的生产与回收,该工厂必须是场景单实例的!具体实现见参考资源 Singleton模板类
    • 尽可能使用前面MVC 结构实现人机交互与游戏模型分离
效果展示
  • 正常游戏:
    在这里插入图片描述

  • 正确切换模式或重启游戏(注意回收正在运动的飞碟):
    在这里插入图片描述

  • 正确结束游戏:
    在这里插入图片描述
    注意,这里细节在于不能以发送完飞碟作为游戏结束标志,而是要以所有运动的飞碟都离开屏幕后才结束游戏。

需求分析
  • 本次实验的要求中有一点值得注意的就是,游戏中会出现很多的飞碟,而游戏对象的创建与销毁高成本,必须减少销毁次数。于是就引入了工厂对象

    简单工厂又称为工厂方法,即类一个方法能够得到一个对象实例,使用者不需要知道该实例如何构建、初始化等细节。

  • 使用工厂方法的优势在于:

    • 减少了对象的销毁次数,工厂管理可复用的游戏对象,仅在有需要的时候才去创建对象以及销毁对象。
    • 屏蔽创建与销毁的业务逻辑,使程序易于扩展。
  • 这里就延续前两次练习的习惯,先分析如何将新的结构融合进入我们的MVC主架构中:

    • 整个游戏剧组:

      • 游戏由导演、场记、运动管理师、演员构成。
      • 新游戏中,场记请了记分员、飞碟管理员,由于这两个工作并不复杂,则间接合并到场景控制器的角色中。
      • 飞碟管理员(也就是工厂)管理飞碟的发放与回收,自己有个小仓库管理这些飞碟
      • 记分员(整合到了主场景控制器中)按飞碟的数据计分,记分员拥有计分规则
      • 场记只需要管理出飞碟规则与管理碰撞就可以了
    • 设计模式解读:

      • DiskFactory 类是一个单实例类,用前面场景单实例创建
      • DiskFactory 类有工厂方法 GetDisk 产生飞碟,有回收方法 Free(Disk)
      • DiskFactory 使用模板模式根据预制和规则制作飞碟
      • 对象模板包括飞碟对象与飞碟数据
设计与实现

经过需求分析,我们可以整合工厂到我们的MVC架构中,最后形成如下的结构,注意,这里尽力使用了MVC架构,并维持前两次练习中的要求(外加Action)。

  • 类图解释

    在这里插入图片描述

    • 结合前面的分析,记分员的角色被整合到主场景控制器FirstController中,主要原因在于记分员的功能比较单一,毕竟游戏规则简单,所以没有考虑单独分出角色。
    • 继续按照第四次作业的动作分离来设计,这里由于用户动作是鼠标输入,所以动作管理只需要管理飞碟的飞行,飞碟的飞行是普通的抛物线运动,由CCFlyAction表征,它将接入到其管理者SSActionMagager中,并最终由CCActionManager对外暴露方法来使得飞碟动作与场记分离,也即动作管理类的功能。
    • 右下角显示前面分析提到的单例工厂,它管理着闲置以及正在使用的飞碟游戏对象,并对外提供Get/Free方法,并自己面向具体的数据模型Disk,从而屏蔽了创建具体游戏对象的细节。
    • 前两点提到的动作管理类和飞碟工厂均被场景控制器组合,从而调度各自的运行逻辑。
  • 文件结构
    ModelControllerViewAction
  • 详细细节
    • 工厂部分:
      • Singleton.cs
        public class Singleton<T> : MonoBehaviour where T : MonoBehaviour {
          protected static T instance;
        
          public static T Instance {
            get {
              if (instance == null) {
                instance = (T)FindObjectOfType(typeof(T));
                if (instance == null) {
                  Debug.LogError("An instance of " + typeof(T) + " is needed in the scene, but there is none");
                }
              }
              return instance;
            }
          }
        }
        
        场景单实例类,当所需的实例被请求时,第一次会在场景内搜索该实例,下一次使用时不需要搜索直接返回。
      • Disk.cs
        public class Disk : MonoBehaviour {
          public float speed;         //水平速度
          public int points;          //得分
          public Vector3 direction;   //初始方向
          public void Set(float speed, int points, Vector3 direction) {
            this.speed = speed;
            this.points = points;
            this.direction = direction;
          }
        }
        
        飞碟模型,含有飞碟的飞行速度、得分、以及初始飞行方向。
      • DiskFactory
        public class DiskFactory : MonoBehaviour {
        
          public GameObject diskPrefab; // 飞碟预制
        
          private List<Disk> used; // 正在使用的飞碟
          private List<Disk> free; // 空闲的飞碟
        
          // Start is called before the first frame update
          void Start() {
            used = new List<Disk>();
            free = new List<Disk>();
            diskPrefab = GameObject.Instantiate<GameObject>(Resources.Load<GameObject>("Prefabs/Disk"));
            diskPrefab.SetActive(false);
          }
        
          // 根据轮数获取对应难度的飞碟游戏对象
          public GameObject GetDisk(int round) {
            GameObject disk;
        
            if (free.Count > 0) {
              // 如果有空闲的飞碟,直接使用
              disk = free[0].gameObject;
              free.Remove(free[0]);
            } else {
              // 否则生成新的预制克隆
              disk = GameObject.Instantiate<GameObject>(diskPrefab, Vector3.zero, Quaternion.identity);
              disk.AddComponent<Disk>();
            }
        
            // 根据轮数设计飞碟的属性,对应不同难度
            // 照顾无限模式,不能一味增加难度,同时增加随机难度
            float hard = UnityEngine.Random.Range(0, 2f) * (round + 1);
            Vector3 randDire = new Vector3(UnityEngine.Random.Range(-1f, 1f) > 0 ? 2 : -2, 1, 0);
            if (hard < 4) {
              disk.GetComponent<Disk>().Set(2.0f, 1, randDire);
              disk.GetComponent<Renderer>().material.color = Color.red;
            } else if (hard < 8) {
              disk.GetComponent<Disk>().Set(3.0f, 2, randDire);
              disk.GetComponent<Renderer>().material.color = Color.blue;
            } else {
              disk.GetComponent<Disk>().Set(4.0f, 3, randDire);
              disk.GetComponent<Renderer>().material.color = Color.black;
            }
            // 添加到使用的飞碟
            used.Add(disk.GetComponent<Disk>());
            return disk;
          }
        
          public void FreeDisk(GameObject disk) {
            // 在used中找到该飞碟
            foreach (Disk diskModel in used) {
              if (diskModel.gameObject.GetInstanceID() == disk.GetInstanceID()) {
                disk.SetActive(false);
                free.Add(diskModel);
                used.Remove(diskModel);
                break;
              }
            }
          }
          // 快速回收,用于重启和设置模式时快重启
          public void FastClear() {
            foreach (Disk diskModel in used) {
              diskModel.gameObject.SetActive(false);
              free.Add(diskModel);
              // used.Remove(diskModel);
            }
            used.Clear();
          }
          // 全部都空闲才代表游戏结束
          public bool AllFree() {
            return used.Count == 0;
          }
        }
        
        飞碟工厂,使用数据结构列表来管理空闲与使用的飞碟,使用复用来降低创建的游戏对象的数目。它暴露的两个关键函数为:
        • GetDisk:用于生产飞碟,首先从free列表中查找是否有可用的飞碟,如果没有则新建一个飞碟。飞碟属性的设置依赖轮次数,使得轮次增大时,飞碟速度分数一起提到,循序渐进增加游戏难度。
        • FreeDisk:用于释放飞碟,将飞碟从used列表中移除并添加到free列表中。

    • 动作管理部分:
      • 动作基类SSAction.cs
        // 动作基类SSAction
        // ScriptableObject 是不需要绑定 GameObject 对象的可编程基类。
        // 这些对象受 Unity 引擎场景管理
        public class SSAction : ScriptableObject {
        
          public bool enable = true;
          public bool destroy = false;
        
          public GameObject gameObject { get; set; }
          public Transform transform { get; set; }
          // 利用接口(ISSACtionCallback)实现消息通知,
          // 避免与动作管理者直接依赖
          public ISSActionCallback callback { get; set; }
          // protected 防止用户自己 new 抽象的对象
          protected SSAction() { }
        
          // 使用 virtual 申明虚方法,通过重写实现多态。
          // 这样继承者就明确使用 Start 和 Update 编程游戏对象行为
          // Start is called before the first frame update
          public virtual void Start() {
            throw new System.NotImplementedException();
          }
        
          // Update is called once per frame
          public virtual void Update() {
            throw new System.NotImplementedException();
          }
        }
        
      • 飞碟简单行动类CCFlyAction
        public class CCFlyAction : SSAction {
          float speed;   // 水平速度
          float gravity; // 重力
          float time;    // 记录运动时间
          Vector3 direction; // 飞行方向
        
          // 生产函数
          public static CCFlyAction GetSSAction(float speed, Vector3 direction) {
            CCFlyAction action = ScriptableObject.CreateInstance<CCFlyAction>();
            action.speed = speed;
            action.direction = direction;
            action.time = 0;
            action.gravity = 2.5f;
            return action;
          }
        
          // Start is called before the first frame update
          public override void Start() {
        
          }
        
          // Update is called once per frame
          public override void Update() {
            time += Time.deltaTime;
            // 根据v = gt计算当前竖直方向速度
            // 随后以该瞬时速度移动
            transform.Translate(Vector3.down * gravity * time * Time.deltaTime);
            // 水平移动
            transform.Translate(direction * speed * Time.deltaTime);
            if(this.transform.position.y < -5) {
              this.destroy = true;
              this.enable = false;
              this.callback.SSActionEvent(this);
            }
          }
        }
        
      • 动作事件接口定义ISSActionCallback.cs
        接口作为接收通知对象(参数source)的抽象类型。
        // 定义事件类型
        public enum SSActionEventType : int { Started, Computed }
        public interface ISSActionCallback {
          // 事件处理接口,所有事件管理者都必须实现这个接口
          // 来实现事件调度。
          // 所以sequenceAction、ActionManager都必须实现它
          void SSActionEvent(SSAction source, SSActionEventType events = SSActionEventType.Computed, int intParam = 0, string strParam = null, Object objectParam = null);
        }
        
      • 动作管理基类SSActionManager.cs
        这是动作对象管理器的基类,实现了所有动作的基本管理。
        public class SSActionManager : MonoBehaviour {
        
          // 动作字典集
          public Dictionary<int, SSAction> actions = new Dictionary<int, SSAction>();
          // 等待被加入的动作队列,这是即将开始的动作
          private List<SSAction> waitingAdd = new List<SSAction>();
          // 等待被删除的动作队列,这是已经完成的动作
          private List<int> waitingDelete = new List<int>();
        
          // Start is called before the first frame update
          protected void Start() {
        
          }
        
          // Update is called once per frame
          protected void Update() {
            // 首先将waitingAdd中的动作保存
            foreach (SSAction action in waitingAdd) {
              actions[action.GetInstanceID()] = action;
            }
            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();
              }
            }
        
            // 删除waitingdelete中的动作
            foreach (int key in waitingDelete) {
              SSAction ac = actions[key];
              actions.Remove(key);
              Destroy(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();
          }
        }
        
      • 动作管理的总控类CCActionManager.cs
        public class CCActionManager : SSActionManager, ISSActionCallback {
        
          // 飞行动作基对象
          public CCFlyAction flyAction;
          // 场景控制器
          public FirstController controller;
          // Start is called before the first frame update
          protected new void Start() {
            controller = (FirstController)SSDirector.GetInstance().CurrentSceneController;
            controller.actionManager = this;
          }
        
          public void Fly(GameObject disk, float speed, Vector3 direction) {
            flyAction = CCFlyAction.GetSSAction(speed, direction);
            RunAction(disk, flyAction, this);
          }
        
          //回调函数
          public void SSActionEvent(SSAction source,
          SSActionEventType events = SSActionEventType.Computed,
          int intParam = 0,
          string strParam = null,
          Object objectParam = null) {
            //飞碟结束飞行后进行回收
            controller.diskFactory.FreeDisk(source.gameObject);
          }
        }
        
        飞行动作管理者,负责生成飞行动作,并接受飞行动作的回调信息,使飞碟被回收。

    • 场景控制部分:
      • IUserAction.cs
        public interface IUserAction {
          void Restart();
          void Hit(Vector3 position); // 点击飞碟打落
          void SetMode(bool isInfinite); // 设置模式:(普通/无限)
        }
        
        用户动作接口,提供打飞碟、重置游戏、选择模式(普通/无限)三个函数的接口。
      • 导演类SSDirector.cs
        public class SSDirector : System.Object {
          private static SSDirector _instance;
          public ISceneController CurrentSceneController { get; set; }
        
          public static SSDirector GetInstance() {
            if (_instance == null) {
              _instance = new SSDirector();
            }
            return _instance;
          }
        }
        
      • 主场景控制器FirstController.cs
        public class FirstController : MonoBehaviour, ISceneController, IUserAction {
        
          public CCActionManager actionManager;  //动作管理者
          public DiskFactory diskFactory;        //飞碟工厂
          int[] roundDisks;           //各轮次的飞碟数量
          bool isInfinite;            //当前模式
          int points;                 //当前分数
          int round;                  //当前轮次
          int sendCnt;                //当前轮次已经发出的飞碟数量
          float sendTime;             //计时器,用于定时发送飞碟
        
          public void Hit(Vector3 position) {
            // 获取主摄像头
            Camera ca = Camera.main;
            Ray ray = ca.ScreenPointToRay(position);
        
            // position是鼠标位置
            // 下面的到鼠标射线得到的碰撞物体
            RaycastHit[] hits;
            hits = Physics.RaycastAll(ray);
        
            for(int i = 0; i < hits.Length; i++) {
              RaycastHit hit = hits[i];
              if (hit.collider.gameObject.GetComponent<Disk>() != null) {
                Debug.Log("hit" +  hit.collider.transform.position);
                // 飞碟移动到底端,从而触发飞行动作的回调。
                hit.collider.gameObject.transform.position = new Vector3(0, -7, 0);
                // 算分
                points += hit.collider.gameObject.GetComponent<Disk>().points;
                // 更新显示
                gameObject.GetComponent<UserGUI>().points = points;
              }
            }
          }
        
          public void LoadResources() {
            SSDirector.GetInstance().CurrentSceneController = this;
            gameObject.AddComponent<DiskFactory>();
            gameObject.AddComponent<CCActionManager>();
            gameObject.AddComponent<UserGUI>();
            diskFactory = Singleton<DiskFactory>.Instance;
            sendCnt = 0;
            round = 0;
            sendTime = 0;
            points = 0;
            isInfinite = false;
            roundDisks = new int[] { 3, 5, 7, 9, 11, 13, 15 };
          }
        
          public void Restart() {
            round = 0;
            sendCnt = 0;
            points = 0;
            sendTime = 0;
            gameObject.GetComponent<UserGUI>().gameMessage = "";
            gameObject.GetComponent<UserGUI>().points = points;
            // 快速回收场上的飞碟
            diskFactory.FastClear();
          }
        
          public void SetMode(bool isInfinite) {
            this.isInfinite = isInfinite;
          }
        
          // Start is called before the first frame update
          void Start() {
            LoadResources();
          }
        
          public void SendDisk() {
            //从工厂生成一个飞碟
            GameObject disk = diskFactory.GetDisk(round);
            //设置飞碟的随机位置
            disk.transform.position = new Vector3(-disk.GetComponent<Disk>().direction.x * 7, UnityEngine.Random.Range(0f, 8f), 0);
            disk.SetActive(true);
            //设置飞碟的飞行动作
            actionManager.Fly(disk, disk.GetComponent<Disk>().speed, disk.GetComponent<Disk>().direction);
          }
        
        
          // Update is called once per frame
          void Update() {
            sendTime += Time.deltaTime;
            // 间隔1s发送飞碟
            if (sendTime >= 2) {
              sendTime = 0;
              // 每次至多发送5个飞碟
              for(int i = 0; i < 5 && sendCnt < roundDisks[round]; i++) {
                sendCnt++;
                SendDisk();
              }
              // 如果在最后一轮并且已经发送完所有该轮的所有飞碟
              if(round == roundDisks.Length - 1 && sendCnt == roundDisks[round]) {
                if(isInfinite) {
                  // 如果是无限模式
                  // 则重复循环
                  round = 0;
                  sendCnt = 0;
                  gameObject.GetComponent<UserGUI>().gameMessage = "";
                } else {
                  // 否则游戏结束,注意这里必须全部飞碟消失才是游戏结束
                  if (!diskFactory.AllFree())
                    return;
                  gameObject.GetComponent<UserGUI>().gameMessage = "Game Over!";
                }
              }
        
              // 如果发完本轮的飞碟,更新轮次
              if (sendCnt == roundDisks[round] && round < roundDisks.Length - 1) {
                sendCnt = 0;
                round++;
              }
            }
          }
        }
        
        主要函数:
        • SendDisk:用于发射一个飞碟。从工厂获得一个飞碟,再为其设置初始位置和飞行动作。
        • Hit:用于处理用户的点击动作。移除用户点击到的飞碟并更新分数。
        • Update:用于发射飞碟与更新状态,飞碟每2s发射一次,每次做多5只,避免太过拥挤,当飞碟发射完毕后判断是否重置或者结束游戏。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

CharlesKai

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值