打飞碟

@[toc]

游戏规则与要求

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

项目地址与演示视频

项目地址 ->传送门?

视频连接 -> 传送门?

具体实现

动作管理的大部分代码延用上一次作业,需要实现的就只有一个飞碟的飞行动作。

  • FlyActionManager

    飞碟的动作管理类,当场景控制器需要发射飞碟时就调用DiskFly使飞碟飞行。

    1. using System.Collections;
    2. using System.Collections.Generic;
    3. using UnityEngine;
    4. public class FlyActionManager : SSActionManager {
    5. public DiskFlyAction fly;
    6. public FirstController scene_controller;
    7. protected void Start() {
    8. scene_controller = (FirstController)SSDirector.GetInstance().CurrentScenceController;
    9. scene_controller.action_manager = this;
    10. }
    11. //飞碟飞行
    12. public void DiskFly(GameObject disk, float angle, float power) {
    13. int lor = 1;
    14. if (disk.transform.position.x > 0) lor = -1;
    15. fly = DiskFlyAction.GetSSAction(lor, angle, power);
    16. this.RunAction(disk, fly, this);
    17. }
    18. }
  • DiskFlyAction

    通过位置变换和角度变换模拟飞碟的飞行,也可以使用刚体组件(Rigidbody)实现。当飞碟的高度在摄像机观察范围之下时则动作停止。

    1. using System.Collections;
    2. using System.Collections.Generic;
    3. using UnityEngine;
    4. public class DiskFlyAction : SSAction {
    5. public float gravity = -5; //向下的加速度
    6. private Vector3 start_vector; //初速度向量
    7. private Vector3 gravity_vector = Vector3.zero; //加速度的向量,初始时为0
    8. private Vector3 current_angle = Vector3.zero; //当前时间的欧拉角
    9. private float time; //已经过去的时间
    10. private DiskFlyAction() { }
    11. public static DiskFlyAction GetSSAction(int lor, float angle, float power) {
    12. DiskFlyAction action = CreateInstance<DiskFlyAction>();
    13. if (lor == -1) {
    14. action.start_vector = Quaternion.Euler(new Vector3(0, 0, -angle)) * Vector3.left * power;
    15. }
    16. else {
    17. action.start_vector = Quaternion.Euler(new Vector3(0, 0, angle)) * Vector3.right * power;
    18. }
    19. return action;
    20. }
    21. public override void Update() {
    22. time += Time.fixedDeltaTime;
    23. gravity_vector.y = gravity * time;
    24. transform.position += (start_vector + gravity_vector) * Time.fixedDeltaTime;
    25. current_angle.z = Mathf.Atan((start_vector.y + gravity_vector.y) / start_vector.x) * Mathf.Rad2Deg;
    26. transform.eulerAngles = current_angle;
    27. //如果物体y坐标小于-10,动作就做完了
    28. if (this.transform.position.y < -10) {
    29. this.destroy = true;
    30. this.callback.SSActionEvent(this);
    31. }
    32. }
    33. public override void Start() { }
    34. }

然后是游戏对象部分,本次游戏中的对象就只有飞碟,为了节约资源需要使用工厂模式对飞碟进行管理。

  • Disk

    Disk中保留飞碟类型type,分数score,颜色color。然后原本还有一个控制缩放的public Vector3 scale = new Vector3(1, 0.25f, 1),我认为没有实际用处,因为碰撞检测器的大小会随着拉伸到比飞碟更大。

下图为拉伸之后的飞碟: \[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-g29drGwn-1570591537310)(T:\TH\大三上\3D游戏设计\5与游戏世界交互\打飞碟\image\lashen.jpg)\]

  1. public class Disk : MonoBehaviour {
  2. public int type = 1;
  3. public int score = 1;
  4. public Color color = Color.white;
  5. }
  • DiskFactory

    维护两个列表,一个是正在使用的飞碟,一个是空闲飞碟。当场景控制器需要获取一个飞碟时,先在空闲列表中寻找可用的空闲飞碟,如果找不到就根据预制重新实例化一个飞碟。回收飞碟的逻辑为遍历使用列表,当有飞碟已经完成了所有动作,即位置在摄像机之下,则回收。

    1. using System.Collections;
    2. using System.Collections.Generic;
    3. using UnityEngine;
    4. public class DiskFactory : MonoBehaviour {
    5. private List<Disk> used = new List<Disk>();
    6. private List<Disk> free = new List<Disk>();
    7. public GameObject GetDisk(int round) {
    8. GameObject disk_prefab;
    9. //寻找空闲飞碟,如果无空闲飞碟则重新实例化飞碟
    10. if (free.Count>0) {
    11. disk_prefab = free[0].gameObject;
    12. free.Remove(free[0]);
    13. } else {
    14. disk_prefab = Instantiate(
    15. Resources.Load<GameObject>("Prefabs/disk"),
    16. new Vector3(0, -10f, 0), Quaternion.identity);
    17. disk_prefab.GetComponent<Renderer>().material.color = disk_prefab.GetComponent<Disk>().color;
    18. disk_prefab.transform.localScale = disk_prefab.GetComponent<Disk>().scale;
    19. }
    20. used.Add(disk_prefab.GetComponent<Disk>());
    21. disk_prefab.SetActive(true);
    22. return disk_prefab;
    23. }
    24. public void FreeDisk() {
    25. for(int i=0; i<used.Count; i++) {
    26. if (used[i].gameObject.transform.position.y <= -10f) {
    27. free.Add(used[i]);
    28. used.Remove(used[i]);
    29. }
    30. }
    31. }
    32. public void Reset() {
    33. FreeDisk();
    34. }
    35. }
  • UserGUI

    比较简单,将分数,Round,Trial显示出来,有按钮控制游戏开始和重新开始即可。比较重要的一点时使用Input.GetButtonDown("Fire1")检测鼠标左键的点击。

    1. if (Input.GetButtonDown("Fire1")) {
    2. Vector3 pos = Input.mousePosition;
    3. action.Hit(pos);
    4. }
  • FirstController

    最重要的部分是场景控制器,游戏开始之后,设置一个定时器,每隔一定时间从飞碟工厂中获取一个飞碟并发射,检测用户点击发送的射线是否与飞碟发生碰撞,有则通知记分员加分并且通知工厂回收飞碟。部分重要函数代码如下。

    Update函数每一帧检测鼠标点击,并根据round调整规则。

    1. void Update () {
    2. if(running) {
    3. count++;
    4. //检测鼠标点击
    5. if (Input.GetButtonDown("Fire1")) {
    6. //Debug.Log("sdfsdfdf");
    7. Vector3 pos = Input.mousePosition;
    8. Hit(pos);
    9. }
    10. //ruler
    11. switch (round) {
    12. case 1: {
    13. if (count >= 150) {
    14. count = 0;
    15. SendDisk(1);
    16. trial += 1;
    17. if (trial == 10) {
    18. round += 1;
    19. trial = 0;
    20. }
    21. }
    22. break;
    23. }
    24. case 2: {
    25. if (count >= 100) {
    26. count = 0;
    27. if (trial % 2 == 0) SendDisk(1);
    28. else SendDisk(2);
    29. trial += 1;
    30. if (trial == 10) {
    31. round += 1;
    32. trial = 0;
    33. }
    34. }
    35. break;
    36. }
    37. case 3: {
    38. if (count >= 50) {
    39. count = 0;
    40. if (trial % 3 == 0) SendDisk(1);
    41. else if(trial % 3 == 1) SendDisk(2);
    42. else SendDisk(3);
    43. trial += 1;
    44. if (trial == 10) {
    45. running = false;
    46. }
    47. }
    48. break;
    49. }
    50. default:break;
    51. }
    52. disk_factory.FreeDisk();
    53. }
    54. }

    SendDisk从工厂中拿飞碟并根据种类设置发射参数,然后调用动作管理器执行动作。师兄博客中提及的点一下会执行两次加分是因为将检测鼠标点击,即Input.GetButtonDown("Fire1")在UserGUI的OnGUI中实现,OnGUI再通知场景控制器执行Hit,调试了很久发现点一下调用了两次OnGUI,所以调用了两次Hit。并且我认为检测碰撞不应该由GUI实现所以在场景控制器中检测。

    1. private void SendDisk(int type) {
    2. //从工厂中拿一个飞碟
    3. GameObject disk = disk_factory.GetDisk(type);
    4. //飞碟位置
    5. float ran_y = 0;
    6. float ran_x = Random.Range(-1f, 1f) < 0 ? -1 : 1;
    7. //飞碟初始所受的力和角度
    8. float power = 0;
    9. float angle = 0;
    10. //根据飞碟种类不同设置不同的发射位置和速度
    11. if (type == 1) {
    12. ran_y = Random.Range(1f, 5f);
    13. power = Random.Range(5f, 7f);
    14. angle = Random.Range(25f,30f);
    15. }
    16. else if (type == 2) {
    17. ran_y = Random.Range(2f, 3f);
    18. power = Random.Range(10f, 12f);
    19. angle = Random.Range(15f, 17f);
    20. }
    21. else {
    22. ran_y = Random.Range(5f, 6f);
    23. power = Random.Range(15f, 20f);
    24. angle = Random.Range(10f, 12f);
    25. }
    26. disk.transform.position = new Vector3(ran_x*16f, ran_y, 0);
    27. action_manager.DiskFly(disk, angle, power);
    28. }

    Hit函数检测射线与飞碟是否碰撞,如碰撞则计分并回收飞碟。

    1. public void Hit(Vector3 pos) {
    2. Ray ray = Camera.main.ScreenPointToRay(pos);
    3. RaycastHit[] hits;
    4. hits = Physics.RaycastAll(ray);
    5. for (int i = 0; i < hits.Length; i++) {
    6. RaycastHit hit = hits[i];
    7. if (hit.collider.gameObject.GetComponent<Disk>() != null) {
    8. score_recorder.Record(hit.collider.gameObject);
    9. hit.collider.gameObject.transform.position = new Vector3(0, -10, 0);
    10. }
    11. }
    12. }

总结

本次作业主要使用了工厂模式复用已经实例化的对象节约资源,以及鼠标点击事件的检测,射线与游戏对象的碰撞。

参考资料

师兄博客1

师兄博客2

官方文档