与游戏世界交互
这是
3D
游戏编程的第五次作业
说明文档
本次实验完成了所有基本要求,尽量将步骤展示出。
闪光点:
附有详细类图以及详细的代码注释
作业内容
1、编写一个简单的鼠标打飞碟(Hit UFO
)游戏
- 游戏内容要求:
- 游戏有
n
个round
,每个round
都包括10
次trial
; - 每个
trial
的飞碟的色彩、大小、发射位置、速度、角度、同时出现的个数都可能不同。它们由该round
的ruler
控制; - 每个
trial
的飞碟有随机性,总体难度随round
上升; - 鼠标点中得分,得分规则按色彩、大小、速度不同计算,规则可自由设定。
- 游戏有
- 游戏的要求:
- 使用带缓存的工厂模式管理不同飞碟的生产与回收,该工厂必须是场景单实例的!具体实现见参考资源
Singleton
模板类 - 尽可能使用前面
MVC
结构实现人机交互与游戏模型分离
- 使用带缓存的工厂模式管理不同飞碟的生产与回收,该工厂必须是场景单实例的!具体实现见参考资源
效果展示
-
正常游戏:
-
正确切换模式或重启游戏(注意回收正在运动的飞碟):
-
正确结束游戏:
注意,这里细节在于不能以发送完飞碟作为游戏结束标志,而是要以所有运动的飞碟都离开屏幕后才结束游戏。
需求分析
-
本次实验的要求中有一点值得注意的就是,游戏中会出现很多的飞碟,而游戏对象的创建与销毁高成本,必须减少销毁次数。于是就引入了工厂对象。
简单工厂又称为工厂方法,即类一个方法能够得到一个对象实例,使用者不需要知道该实例如何构建、初始化等细节。
-
使用工厂方法的优势在于:
- 减少了对象的销毁次数,工厂管理可复用的游戏对象,仅在有需要的时候才去创建对象以及销毁对象。
- 屏蔽创建与销毁的业务逻辑,使程序易于扩展。
-
这里就延续前两次练习的习惯,先分析如何将新的结构融合进入我们的MVC主架构中:
-
整个游戏剧组:
- 游戏由导演、场记、运动管理师、演员构成。
- 新游戏中,场记请了记分员、飞碟管理员,由于这两个工作并不复杂,则间接合并到场景控制器的角色中。
- 飞碟管理员(也就是工厂)管理飞碟的发放与回收,自己有个小仓库管理这些飞碟
- 记分员(整合到了主场景控制器中)按飞碟的数据计分,记分员拥有计分规则
- 场记只需要管理出飞碟规则与管理碰撞就可以了
-
设计模式解读:
DiskFactory
类是一个单实例类,用前面场景单实例创建DiskFactory
类有工厂方法GetDisk
产生飞碟,有回收方法Free(Disk)
DiskFactory
使用模板模式根据预制和规则制作飞碟- 对象模板包括飞碟对象与飞碟数据
-
设计与实现
经过需求分析,我们可以整合工厂到我们的MVC架构中,最后形成如下的结构,注意,这里尽力使用了MVC架构,并维持前两次练习中的要求(外加Action)。
-
类图解释
- 结合前面的分析,记分员的角色被整合到主场景控制器
FirstController
中,主要原因在于记分员的功能比较单一,毕竟游戏规则简单,所以没有考虑单独分出角色。 - 继续按照第四次作业的动作分离来设计,这里由于用户动作是鼠标输入,所以动作管理只需要管理飞碟的飞行,飞碟的飞行是普通的抛物线运动,由
CCFlyAction
表征,它将接入到其管理者SSActionMagager
中,并最终由CCActionManager
对外暴露方法来使得飞碟动作与场记分离,也即动作管理类的功能。 - 右下角显示前面分析提到的单例工厂,它管理着闲置以及正在使用的飞碟游戏对象,并对外提供
Get/Free
方法,并自己面向具体的数据模型Disk
,从而屏蔽了创建具体游戏对象的细节。 - 前两点提到的动作管理类和飞碟工厂均被场景控制器组合,从而调度各自的运行逻辑。
- 结合前面的分析,记分员的角色被整合到主场景控制器
-
文件结构
Model Controller View Action
-
详细细节
- 工厂部分:
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只,避免太过拥挤,当飞碟发射完毕后判断是否重置或者结束游戏。
- 工厂部分: