Unity学习记录——与游戏世界交互

Unity学习记录——与游戏世界交互

前言

​ 本文是中山大学软件工程学院2020级3d游戏编程与设计的作业5

编程题:简单打飞碟

1. 题目要求

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

2. 基本介绍

​ 本次简单打飞碟小游戏使用了工厂模式管理不同飞碟的生产与回收。

​ **工厂模式(Factory Pattern)**是 游戏开发中最常用的设计模式之一。该模式将实例化对象的代码提取出来,放到一个类中统一管理和维护,达到和主项目的依赖关系的解耦。从而提高项目的扩展和维护性。也因此,在工厂模式中,我们在创建对象时不会对用户暴露创建逻辑,并且是通过使用一个共同的接口来指向新创建的对象。

3.代码解读

​ 本次代码仍旧使用了MVC模式,同时进行了动作分离。

​ 由于动作分离中动作基类等代码基本一致,本次编程题中,一部分代码使用了上次作业(牧师与魔鬼动作分离版本)中的代码,复用的代码分别为:SSAction(),SSActionManager(),CCSequenceAction()三个类。

​ 同时,Interface()SSDirector()两个类的设计在这三次游戏编程中也是基本一致,此处不进行介绍。

​ 以下为代码解读:

FlyAction

​ 飞碟的动作类。控制飞碟的飞行。实现的逻辑为根据飞碟的位置决定飞碟的运动,当飞碟位置在一定范围内时进行我们所设计的模拟位移,超出一定范围之后就停止位移,等待工厂的回收。

public class FlyAction : SSAction
{
    public float gravity = -1;                                 //向下的加速度
    private Vector3 start_vector;                              //初速度向量
    private Vector3 gravity_vector = Vector3.zero;             //加速度的向量,初始时为0
    private Vector3 current_angle = Vector3.zero;              //当前时间的欧拉角
    private float time;                                        //已经过去的时间

    private FlyAction() { }
    public static FlyAction GetSSAction(int lor, float angle, float power)
    {
        //初始化物体将要运动的初速度向量
        FlyAction action = CreateInstance();
        if (lor == -1)
        {
            action.start_vector = Quaternion.Euler(new Vector3(0, 0, -angle)) * Vector3.left * power;
        }
        else
        {
            action.start_vector = Quaternion.Euler(new Vector3(0, 0, angle)) * Vector3.right * power;
        }
        return action;
    }

    public override void Update()
    {
        //计算物体的向下的速度,v=at
        time += Time.fixedDeltaTime;
        gravity_vector.y = gravity * time;

        //位移模拟
        transform.position += (start_vector + gravity_vector) * Time.fixedDeltaTime;
        current_angle.z = Mathf.Atan((start_vector.y + gravity_vector.y) / start_vector.x) * Mathf.Rad2Deg;
        transform.eulerAngles = current_angle;

        //如果物体y坐标小于-10,动作就做完了
        if (this.transform.position.y < -10)
        {
            this.deleted = true;
            this.callback.SSActionEvent(this);
        }
    }

    public override void Start() { }
}

FlyActionManager

​ 飞碟的动作管理类。通过调用FlyAction控制飞碟的运动。

public class FlyActionManager : SSActionManager
{
    public FlyAction fly;
    public FirstController scene_controller;
    protected void Start()
    {
        scene_controller = (FirstController)SSDirector.GetInstance().CurrentSceneController;
        scene_controller.action_manager = this;
    }
    public void DiskFly(GameObject disk, float angle, float power)
    {
        int loc = disk.transform.position.x < 0 ? 1 : -1;
        fly = FlyAction.GetSSAction(loc, angle, power);
        this.RunAction(disk, fly, this);
    }
}

Judge

​ 裁判类。由于游戏到达第三回合之后就直接停止,裁判类无需再判断游戏的终止条件,只是进行游戏分数的记录。

public class Judge : MonoBehaviour
{
    private float score;
    void Start()
    {
        score = 0;
    }
    public void Record(GameObject disk)
    {
        score += disk.GetComponent().score;
    }
    public float GetScore()
    {
        return score;
    }
    public void Reset()
    {
        score = 0;
    }
}

Singleton

​ 场景单实例类。当所需的实例第一次被需要时,在场景内搜索该实例,下一次使用时不需要搜索直接返回。

public class Singleton : 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;
        }
    }
}

FirstController

​ 负责游戏第一个场景的搭载。

​ 在FirstController之中还编写了获取相应数值的GetXXX(),重新开始游戏的实现游戏的ReStart(),游戏结束的GameOver()等小部件函数。

​ 而更为重要的函数为Update()Hit()SendDisk()三个函数。

Update()检测每一帧的鼠标点击是否命中目标,同时根据此时的回合数设置不同的游戏难度。此处变量count用来控制飞碟的发射速度以及游戏回合切换的暂停时间:每经过一帧count加1,当count到达相应数值就发射飞碟,当回合结束时,使count在一定数值的帧内不再增加(此处引入变量hold来进行此操作),即可暂停发射飞碟并在此时插入切换回合的UI。

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

SendDisk()从工厂中拿飞碟并根据种类设置一定范围内的随机发射参数,然后调用动作管理器执行发射飞碟的动作。

public class FirstController : MonoBehaviour, ISceneController, IUserAction
{
    public FlyActionManager action_manager;
    public DiskFactory disk_factory;
    public UserGUI user_gui;
    public Judge judge;
    private int round = 1;
    private int trial = 0;
    private bool running = false;
    private int count = 0;
    public bool stop = false;
    public int hold = 0;

    void Start()
    {
        SSDirector director = SSDirector.GetInstance();
        director.CurrentSceneController = this;
        disk_factory = Singleton.Instance;
        judge = Singleton.Instance;
        action_manager = gameObject.AddComponent() as FlyActionManager;
        user_gui = gameObject.AddComponent() as UserGUI;
    }


    void Update()
    {
        if (running)
        {
            count++;
            if (Input.GetButtonDown("Fire1"))
            {
                Vector3 pos = Input.mousePosition;
                Hit(pos);
            }
            switch (round)
            {
                case 1:
                    {
                        if (stop)
                        {
                            count = 0;
                            hold++;
                            if (hold == 300)
                            {
                                stop = false;
                                hold = 0;
                            }
                        }
                        if (count >= 180)
                        {
                            count = 0;
                            SendDisk(1);
                            trial += 1;
                            if (trial == 10)
                            {
                                stop = true;
                                hold = 0;
                                round += 1;
                                trial = 0;
                            }
                        }
                        break;
                    }
                case 2:
                    {
                        if (stop)
                        {
                            count = 0;
                            hold++;
                            if (hold == 600)
                            {
                                stop = false;
                                hold = 0;
                            }
                        }
                        if (count >= 120)
                        {
                            count = 0;
                            if (trial % 2 == 0) SendDisk(1);
                            else SendDisk(2);
                            trial += 1;
                            if (trial == 10)
                            {
                                stop = true;
                                hold = 0;
                                round += 1;
                                trial = 0;
                            }
                        }
                        break;
                    }
                case 3:
                    {
                        if (stop)
                        {
                            count = 0;
                            hold++;
                            if (hold == 600)
                            {
                                stop = false;
                                hold = 0;
                            }
                        }
                        if (count >= 60)
                        {
                            count = 0;
                            if (trial % 3 == 0) SendDisk(1);
                            else if (trial % 3 == 1) SendDisk(2);
                            else SendDisk(3);
                            trial += 1;
                            if (trial == 10)
                            {
                                stop = true;
                                hold = 0;
                            }
                        }
                        break;
                    }
                default:
                    {
                        if (stop)
                        {
                            hold++;
                            if (hold == 300)
                            {
                                running = false;
                                stop = false;
                                hold = 0;
                            }
                        }
                        break;
                    }
            }
            disk_factory.FreeDisk();
        }
    }

    public void LoadResources()
    {
        disk_factory.GetDisk(round);
        disk_factory.FreeDisk();
    }

    private void SendDisk(int type)
    {
        GameObject disk = disk_factory.GetDisk(type);

        float ran_y = 0;
        float ran_x = Random.Range(-1f, 1f) < 0 ? -1 : 1;

        float power = 0;
        float angle = 0;
        if (type == 1)
        {
            ran_y = Random.Range(2f, 5f);
            power = Random.Range(5f, 8f);
            angle = Random.Range(15f, 20f);
        }
        else if (type == 2)
        {
            ran_y = Random.Range(3f, 6f);
            power = Random.Range(10f, 13f);
            angle = Random.Range(10f, 15f);
        }
        else
        {
            ran_y = Random.Range(1f, 3f);
            power = Random.Range(15f, 18f);
            angle = Random.Range(5f, 10f);
        }
        disk.transform.position = new Vector3(ran_x * 16f, ran_y, 0);
        action_manager.DiskFly(disk, angle, power);
    }

    public void Hit(Vector3 pos)
    {
        Ray ray = Camera.main.ScreenPointToRay(pos);
        RaycastHit[] hits;
        hits = Physics.RaycastAll(ray);
        for (int i = 0; i < hits.Length; i++)
        {
            RaycastHit hit = hits[i];
            if (hit.collider.gameObject.GetComponent() != null)
            {
                judge.Record(hit.collider.gameObject);
                hit.collider.gameObject.transform.position = new Vector3(0, -10, 0);
            }
        }
    }
    public float GetScore()
    {
        return judge.GetScore();
    }

    public int GetRound()
    {
        return round;
    }

    public int GetTrial()
    {
        return trial;
    }

    public bool GetStop()
    {
        return stop;
    }

    public int GetHold()
    {
        return hold;
    }

    public void ReStart()
    {
        running = true;
        judge.Reset();
        disk_factory.Reset();
        round = 1;
        trial = 0;
        stop = true;
    }

    public void GameOver()
    {
        running = false;
    }
}

Disk

​ 包括了飞碟类型type,分数score,颜色color三个属性,可以通过这些属性设置飞碟预制件的属性

public class Disk : MonoBehaviour
{
    public int type = 1;
    public int score = 1;
    public Color color = Color.white;
}

DiskFactory

​ Disk的工厂。主要进行飞碟的生产与回收。

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

public class DiskFactory : MonoBehaviour
{
    private List used = new List();
    private List free = new List();

    public GameObject GetDisk(int type)
    {
        GameObject disk_prefab = null;
        //寻找空闲飞碟,如果无空闲飞碟则重新实例化飞碟
        if (free.Count > 0)
        {
            for (int i = 0; i < free.Count; i++)
            {
                if (free[i].type == type)
                {
                    disk_prefab = free[i].gameObject;
                    free.Remove(free[i]);
                    break;
                }
            }
        }
        if (disk_prefab == null)
        {
            if (type == 1)
            {
                disk_prefab = Instantiate(
                Resources.Load("Prefabs/disk1"),
                new Vector3(0, -10f, 0), Quaternion.identity);
            }
            else if (type == 2)
            {
                disk_prefab = Instantiate(
                Resources.Load("Prefabs/disk2"),
                new Vector3(0, -10f, 0), Quaternion.identity);
            }
            else
            {
                disk_prefab = Instantiate(
                Resources.Load("Prefabs/disk3"),
                new Vector3(0, -10f, 0), Quaternion.identity);
            }
            disk_prefab.GetComponent().material.color = disk_prefab.GetComponent().color;
        }
        used.Add(disk_prefab.GetComponent());
        disk_prefab.SetActive(true);
        return disk_prefab;
    }
    public void FreeDisk()
    {
        for (int i = 0; i < used.Count; i++)
        {
            if (used[i].gameObject.transform.position.y <= -10f)
            {
                free.Add(used[i]);
                used.Remove(used[i]);
            }
        }
    }
    public void Reset()
    {
        FreeDisk();
    }
}

UserGUI

​ UI代码,显示回合切换以及游戏开始结束、游戏进行时的UI。

public class UserGUI : MonoBehaviour
{
    private IUserAction action;
    //每个GUI的style
    GUIStyle bold_style = new GUIStyle();
    GUIStyle text_style = new GUIStyle();
    GUIStyle over_style = new GUIStyle();
    GUIStyle round_style = new GUIStyle();
    private bool game_start = false;
    private int wid = Screen.width;
    private int heig = Screen.height;
    void Start()
    {
        action = SSDirector.GetInstance().CurrentSceneController as IUserAction;
    }
    void OnGUI()
    {
        bold_style.normal.textColor = new Color(1, 0, 0);
        bold_style.fontSize = 16;
        text_style.normal.textColor = new Color(0, 0, 0, 1);
        text_style.fontSize = 16;
        over_style.normal.textColor = new Color(1, 0, 0);
        over_style.fontSize = 25;
        round_style.normal.textColor = new Color(1, 0, 0);
        round_style.fontSize = 70;
        if (game_start && !action.GetStop())
        {
            GUI.Label(new Rect(wid - 150, 5, 200, 50), "分数:" + action.GetScore().ToString(), text_style);
            GUI.Label(new Rect(100, 5, 50, 50), "Round:" + action.GetRound().ToString(), text_style);
            GUI.Label(new Rect(180, 5, 50, 50), "Trial:" + action.GetTrial().ToString(), text_style);
            if (action.GetRound() == 3 && action.GetTrial() == 10)
            {
                GUI.Label(new Rect(wid / 2 - 20, heig / 2 - 100, 100, 100), "游戏结束", over_style);
                GUI.Label(new Rect(wid / 2 - 10, heig / 2 - 50, 50, 50), "你的分数:" + action.GetScore().ToString(), text_style);
                if (GUI.Button(new Rect(wid / 2 - 20, heig / 2, 100, 50), "重新开始"))
                {
                    action.ReStart();
                    return;
                }
                action.GameOver();
            }
        }
        else if (game_start && action.GetStop())
        {
            if (action.GetHold() > 300 && action.GetRound() <= 3 && action.GetTrial() == 0)
            {
                GUI.Label(new Rect(wid / 2 - 110, heig / 2 - 60, 100, 100), "Round" + (action.GetRound()).ToString(), round_style);
            }
            if (action.GetRound() == 1)
            {
                GUI.Label(new Rect(wid / 2 - 110, heig / 2 - 60, 100, 100), "Round" + (action.GetRound()).ToString(), round_style);
            }
        }
        else
        {
            GUI.Label(new Rect(wid / 2 - 60, heig / 2 - 100, 100, 100), "简单打飞碟", over_style);
            GUI.Label(new Rect(wid / 2 - 50, heig / 2 - 50, 100, 100), "鼠标点击飞碟", text_style);
            if (GUI.Button(new Rect(wid / 2 - 50, heig / 2, 100, 50), "游戏开始"))
            {
                game_start = true;
                action.ReStart();
            }
        }
    }
}

4.演示

​ 演示gif图入下:

​ 提醒:本次游戏项目在运行时是默认打开垂直同步的,开关在如下图所示位置:

Unity学习记录——与游戏世界交互_第1张图片

​ 如果关闭了垂直同步,游戏帧率会变高,但是也会体验到地狱绘卷难度的打飞碟,如下所示:

Unity学习记录——与游戏世界交互_第2张图片

代码位置

​ 代码以及文档均已经上传至hw5 · XiaoChen04_3/3D_Computer_Game_Programming - gitee

你可能感兴趣的:(unity,学习,游戏)