3D Computer Game Programming-Note 5

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

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

【游戏设计】

  1. 游戏规则:
  2. · 在飞碟飞过时点击鼠标左键,尽可能击中更多的飞碟
  3. · 游戏共分四个轮次,随着游戏的进行难度逐渐增加,具体表现为飞碟飞行速度的提升和飞行角度的变化
  4. · 不同颜色的飞碟得分不同,击中难度也不同

【结构设计】

  参考游戏牧师与魔鬼(动作分离版)的 MVC 架构,保留SSDirector,SSAction和SSActionManager 等,重复部分略过不表。

  使用带缓存的工厂模式管理飞碟,要求:

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

  借助课程主页的设计图,设计程序的 UML图:

Unity-与游戏世界交互 - 图1

【编程实现】

  • Singleton.cs: 模板类,可以为每个 MonoBehaviour 子类创建一个对象的实例
  1. public class Singleton<T> : MonoBehaviour where T : MonoBehaviour {
  2. protected static T instance;
  3. public static T Instance {
  4. get {
  5. if (instance == null) {
  6. instance = (T)FindObjectOfType(typeof(T));
  7. if (instance == null) {
  8. Debug.LogError("An instance of " + typeof(T) +
  9. " is needed in the scene, but there is none.");
  10. }
  11. }
  12. return instance;
  13. }
  14. }
  15. }
  • DiskData.cs: 记录飞碟数据,包括飞碟的类型、对应得分等,挂载到预制上时可以实现自定义组件
  1. public class DiskData : MonoBehaviour {
  2. public int type = 1;
  3. public int score = 1;
  4. public Color color = Color.white;
  5. }
  • DiskFactory.cs: 提供工厂方法 GetDisk 产生飞碟以及 FreeDisk 回收飞碟
  1. public class DiskFactory : MonoBehaviour {
  2. private List<DiskData> used;
  3. private List<DiskData> free;
  4. void Start() {
  5. used = new List<DiskData>();
  6. free = new List<DiskData>();
  7. }
  8. public GameObject GetDisk(int type) {
  9. GameObject disk = null;
  10. if (free.Count > 0) {
  11. for (int i = 0; i < free.Count; i++) {
  12. if (free[i].type == type) {
  13. disk = free[i].gameObject;
  14. free.Remove(free[i]);
  15. break;
  16. }
  17. }
  18. }
  19. float random_y = Random.Range(-3f, 1f);
  20. float random_x = Random.Range(-1f, 1f) < 0 ? -1 : 1;
  21. if (disk == null)
  22. disk = Instantiate(Resources.Load<GameObject>("Prefabs/disk"+type), new Vector3(0, -20f, 0), Quaternion.identity);
  23. disk.transform.position = new Vector3(random_x * 20f, random_y, 0);
  24. disk.GetComponent<Renderer>().material.color = disk.GetComponent<DiskData>().color;
  25. used.Add(disk.GetComponent<DiskData>());
  26. disk.SetActive(true);
  27. return disk;
  28. }
  29. public void FreeDisk(GameObject disk) {
  30. for (int i = 0; i < used.Count; ++i) {
  31. if (disk.GetInstanceID() == used[i].gameObject.GetInstanceID()) {
  32. used[i].gameObject.SetActive(false);
  33. used.Remove(used[i]);
  34. free.Add(used[i]);
  35. break;
  36. }
  37. }
  38. }
  39. public void Reset() {
  40. for (int i = 0; i < used.Count; i++) {
  41. if (used[i].gameObject.transform.position.y <= -20f) {
  42. free.Add(used[i]);
  43. used.Remove(used[i]);
  44. }
  45. }
  46. }
  47. }
  • FlyActionManager.cs: 管理飞碟的飞行,场景控制器可以通过该管理器使飞碟按照一定的角度和速度飞行
  1. public class FlyActionManager : SSActionManager {
  2. public DiskFlyAction flyAction;
  3. public FirstController sceneController;
  4. protected void Start() {
  5. sceneController = (FirstController)SSDirector.GetInstance().CurrentSceneController;
  6. sceneController.actionManager = this;
  7. }
  8. public void DiskFly(GameObject disk, float angle, float speed) {
  9. int direction = (disk.transform.position.x > 0) ? -1 : 1;
  10. flyAction = DiskFlyAction.GetSSAction(direction, angle, speed);
  11. this.RunAction(disk, flyAction, this);
  12. }
  13. }
  • DiskFlyAction.cs: 使用旋转实现飞行。当飞碟到达指定的水平面时动作结束
  1. public class DiskFlyAction : SSAction {
  2. public float gravity = -0.2f;
  3. private Vector3 speed;
  4. private Vector3 gravityVector = Vector3.zero;
  5. private Vector3 angle = Vector3.zero;
  6. private float time;
  7. private DiskFlyAction() { }
  8. public static DiskFlyAction GetSSAction(int direction, float angle, float speed) {
  9. DiskFlyAction action = ScriptableObject.CreateInstance<DiskFlyAction>();
  10. if (direction == -1) {
  11. action.speed = Quaternion.Euler(new Vector3(0, 0, -angle)) * Vector3.left * speed;
  12. }
  13. else {
  14. action.speed = Quaternion.Euler(new Vector3(0, 0, angle)) * Vector3.right * speed;
  15. }
  16. return action;
  17. }
  18. public override void Start() { }
  19. public override void Update() {
  20. time += Time.deltaTime;
  21. gravityVector.y = gravity * time;
  22. transform.position += (speed + gravityVector) * Time.fixedDeltaTime;
  23. angle.z = Mathf.Atan((speed.y + gravityVector.y) / speed.x) * Mathf.Rad2Deg;
  24. transform.eulerAngles = angle;
  25. if (this.transform.position.y < -20f) {
  26. this.destroy = true;
  27. this.callback.SSActionEvent(this);
  28. }
  29. }
  30. }
  • ScoreRecorder.cs: 记分员,根据飞碟的数据计分,提供对计分表的读写
  1. public class ScoreRecorder : MonoBehaviour {
  2. private int score;
  3. void Start () {
  4. score = 0;
  5. }
  6. public void Record(GameObject disk) {
  7. score += disk.GetComponent<DiskData>().score;
  8. }
  9. public int GetScore() {
  10. return score;
  11. }
  12. public void Reset() {
  13. score = 0;
  14. }
  15. }
  • IUserAction.cs: 提供用户交互事件的接口,包括击中飞碟、重新开始游戏等
  1. public interface IUserAction
  2. {
  3. void Hit(Vector3 position);
  4. void ReStart();
  5. int GetScore();
  6. int GetRound();
  7. bool isGameOver();
  8. void GameOver();
  9. }
  • FirstController.cs: 场景控制器,管理飞碟和计分表等对象及相关事件
  1. public class FirstController : MonoBehaviour, ISceneController, IUserAction {
  2. public FlyActionManager actionManager;
  3. public DiskFactory diskFactory;
  4. public ScoreRecorder scoreRecorder;
  5. private int round = 1;
  6. private int trial = 0;
  7. private bool running = false;
  8. private int count = 0;
  9. void Start () {
  10. SSDirector.GetInstance().CurrentSceneController = this;
  11. diskFactory = Singleton<DiskFactory>.Instance;
  12. scoreRecorder = Singleton<ScoreRecorder>.Instance;
  13. gameObject.AddComponent<FlyActionManager>();
  14. gameObject.AddComponent<UserGUI>();
  15. }
  16. void Update () {
  17. if(running) {
  18. count++;
  19. //用户按下鼠标左键时进行hit事件处理
  20. if (Input.GetButtonDown("Fire1")) {
  21. Hit(Input.mousePosition);
  22. }
  23. //根据游戏轮次设计飞碟的类型和发射速率
  24. int speed = 300 - round * 50;
  25. if (count >= speed)
  26. {
  27. count = 0;
  28. float rand;
  29. switch (round)
  30. {
  31. case 1:
  32. SendDisk(1);
  33. break;
  34. case 2:
  35. rand = Random.Range(0, 1f);
  36. if (rand < 0.6f)
  37. SendDisk(1);
  38. else
  39. SendDisk(2);
  40. break;
  41. case 3:
  42. rand = Random.Range(0, 1f);
  43. if (rand < 0.4f)
  44. SendDisk(1);
  45. else if (rand < 0.8f)
  46. SendDisk(2);
  47. else
  48. SendDisk(3);
  49. break;
  50. case 4:
  51. rand = Random.Range(0, 1f);
  52. if (rand < 0.2f)
  53. SendDisk(1);
  54. else if (rand < 0.5f)
  55. SendDisk(2);
  56. else
  57. SendDisk(3);
  58. break;
  59. default:
  60. break;
  61. }
  62. trial += 1;
  63. if (trial == 10)
  64. {
  65. if (round == 4)
  66. {
  67. running = false;
  68. }
  69. else
  70. {
  71. round += 1;
  72. trial = 0;
  73. }
  74. }
  75. }
  76. diskFactory.Reset();
  77. }
  78. }
  79. public void LoadResources() {
  80. diskFactory.GetDisk(round);
  81. diskFactory.Reset();
  82. }
  83. private void SendDisk(int type) {
  84. GameObject disk = diskFactory.GetDisk(type);
  85. float speed = 0;
  86. float angle = 0;
  87. if (type == 1)
  88. {
  89. speed = Random.Range(5f, 10f);
  90. angle = Random.Range(10f, 14f);
  91. }
  92. else if (type == 2)
  93. {
  94. speed = Random.Range(8f, 13f);
  95. angle = Random.Range(12f, 16f);
  96. }
  97. else
  98. {
  99. speed = Random.Range(13f, 18f);
  100. angle = Random.Range(16f, 20f);
  101. }
  102. actionManager.DiskFly(disk, angle, speed);
  103. }
  104. //处理hit事件
  105. public void Hit(Vector3 position) {
  106. Ray ray = Camera.main.ScreenPointToRay(position);
  107. RaycastHit[] hits;
  108. hits = Physics.RaycastAll(ray);
  109. for (int i = 0; i < hits.Length; i++) {
  110. RaycastHit hit = hits[i];
  111. if (hit.collider.gameObject.GetComponent<DiskData>() != null) {
  112. scoreRecorder.Record(hit.collider.gameObject);
  113. hit.collider.gameObject.transform.position = new Vector3(0, -20f, 0);
  114. }
  115. }
  116. }
  117. public int GetScore() {
  118. return scoreRecorder.GetScore();
  119. }
  120. public int GetRound() {
  121. return round;
  122. }
  123. public bool isGameOver()
  124. {
  125. return round == 4 && trial == 10;
  126. }
  127. public void ReStart() {
  128. running = true;
  129. scoreRecorder.Reset();
  130. diskFactory.Reset();
  131. round = 1;
  132. trial = 1;
  133. }
  134. public void GameOver() {
  135. running = false;
  136. }
  137. }

【游戏效果】

Unity-与游戏世界交互 - 图2

Unity-与游戏世界交互 - 图3

Unity-与游戏世界交互 - 图4

【动态展示】

  🔗视频链接

2、编写一个简单的自定义 Component (选做)

  • 用自定义组件定义几种飞碟,做成预制
    • 实现自定义组件,编辑并赋予飞碟一些属性

  使用 Sphere 和 Capsule 组合制作预制如下:

  1. ![](https://cdn.nlark.com/yuque/0/2020/png/2589459/1603882158743-1a47a714-13b2-486f-8fa1-630ac70e2bf9.png#align=left&display=inline&height=127&margin=%5Bobject%20Object%5D&originHeight=127&originWidth=287&size=0&status=done&style=none&width=287)

  在预制上挂载 DiskData.cs,即可编辑飞碟的属性:类型、得分和颜色:

  1. ![](https://cdn.nlark.com/yuque/0/2020/png/2589459/1603882158561-1b2d1a6a-4318-4a1a-b922-67b7f98a2421.png#align=left&display=inline&height=463&margin=%5Bobject%20Object%5D&originHeight=463&originWidth=596&size=0&status=done&style=none&width=596)

  🔗项目地址