3D游戏编程与设计 HW 5 与游戏世界交互

Hit UFO 完整游戏过程可见以下视频:
https://www.bilibili.com/video/BV1ni4y1E7gq/

Hit UFO 完整代码可见以下仓库:
https://gitee.com/beilineili/game3-d

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

① 游戏内容要求

  • 游戏有 n 个 round,每个 round 都包括10 次 trial;
  • 每个 trial 的飞碟的色彩、大小、发射位置、速度、角度、同时出现的个数都可能不同。它们由该 round 的 ruler 控制;
  • 每个 trial 的飞碟有随机性,总体难度随 round 上升;
  • 鼠标点中得分,得分规则按色彩、大小、速度不同计算,规则可自由设定。

② 游戏的要求

  • 使用带缓存的工厂模式管理不同飞碟的生产与回收,该工厂必须是场景单实例的!具体实现见参考资源 Singleton 模板类
  • 尽可能使用前面 MVC 结构实现人机交互与游戏模型分离

2、游戏设计

  • 游戏界面
    在这里插入图片描述

① 游戏玩法

  • 通过用鼠标点击飞出来的UFO来得分,其中绿色飞碟为1分,黄色飞碟为2分,红色飞碟为3分
  • 分为四个模式,分为简单,普通,困难和无限模式。其中开始时游戏默认是 Normal 模式
  • 除了无限模式有无限轮次的飞碟外,其他模式都是有10轮,所有飞碟发射完毕时游戏结束

② 设计模式 – 工厂方法

  • 简单工厂又称为工厂方法,即类一个方法能够得到一个对象实例,使用者不需要知道该实例如何构建、初始化等细节。
  • 使用工厂方法的原因:
  • 游戏对象的创建与销毁高成本,必须减少销毁次数。如:游戏中子弹
  • 屏蔽创建与销毁的业务逻辑,使程序易于扩展

③ 设计代码

一、Controller

在这里插入图片描述

1) SSDirector.cs
  • 导演类
public class SSDirector : System.Object
{
    //单实例
    private static SSDirector instance;
    public ISceneController CurrentScenceController { get; set; }

    //在任何时候任何地方获得实例
    public static SSDirector GetInstance(){
        if (instance == null){
            instance = new SSDirector();
        }
        return instance;
    }
}
2) SSAction.cs
  • 动作基类
public class SSAction : ScriptableObject
{
    public bool enable = true; //是否进行
    public bool destroy = false; //是否删除
    //需要进行运动的游戏对象
    public GameObject gameObject { get; set; }
    public Transform transform { get; set; }
    //动作执行完后要通知的对象
    public ISSActionCallback callback { get; set; }

    protected SSAction()
    {

    }
    // 申明虚方法,通过重写实现多态,由继承者来明确行为
    public virtual void Start()
    {
        throw new System.NotImplementedException();
    }

    public virtual void Update()
    {
        throw new System.NotImplementedException();
    }
}
3) SSActionManager.cs
  • 动作管理者的基类
public class SSActionManager : MonoBehaviour
{
    //动作集
    private Dictionary<int, SSAction> actions = new Dictionary<int, SSAction>();
    //等待执行的动作的list
    private List<SSAction> waitingTodo = new List<SSAction>();
    //等待删除的动作的key list
    private List<int> waitingDelete = new List<int>();

    protected void Update()
    {
        //将等待执行list中的动作保存进动作集
        foreach (SSAction ac in waitingTodo)
            actions[ac.GetInstanceID()] = ac;
        waitingTodo.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();
            }
        }

        //销毁等待删除list中的动作
        foreach (int key in waitingDelete)
        {
            SSAction ac = actions[key];
            actions.Remove(key);
            Destroy(ac);
        }
        waitingDelete.Clear();
    }

    //准备执行一个动作,将动作初始化,并加入到等待执行list
    public void RunAction(GameObject gameObject, SSAction action, ISSActionCallback manager)
    {
        action.gameObject = gameObject;
        action.transform = gameObject.transform;
        action.callback = manager;
        waitingTodo.Add(action);
        action.Start();
    }
    
    protected void Start()
    {

    }
}
4) ISSActionCallback.cs
  • 回调函数接口
public enum SSActionEventType : int { Started, Competed }
public interface ISSActionCallback
{
    //回调函数
    void SSActionEvent(SSAction source,
                       SSActionEventType events = SSActionEventType.Competed,
                       int intParam = 0,
                       string strParam = null,
                       Object objectParam = null);
}
5) ISceneController.cs
  • 场景控制类接口
public interface ISceneController
{
    void LoadResources();
}
6) IUserAction.cs
  • 用户动作接口,有点击,重新开始和选择游戏模式三个函数的接口
public interface IUserAction
{
    void Hit(Vector3 position); //点击
    void Restart(); //重新开始
    void SetMode(int mode); //选择游戏模式
}
7) FlyAction.cs
  • 将飞行运动拆分成水平和竖直两个方向,其中水平方向速度恒定,竖直方向添加重力加速度
  • 当飞碟到达底部时,即飞碟的高度在摄像机观察范围之下时,动作结束,将进行回调
public class FlyAction : SSAction
{
    float gravity;          //重力加速度
    float speed;            //初始速度
    float time;             //时间
    Vector3 direction;      //初始飞行方向

    public static FlyAction GetSSAction(Vector3 direction, float speed)
    {
        FlyAction action = ScriptableObject.CreateInstance<FlyAction>();
        action.gravity = 9.8f;
        action.time = 0;
        action.speed = speed;
        action.direction = direction;
        return action;
    }

    public override void Start()
    {
        
    }

    public override void Update()
    {
        time += Time.deltaTime;
        //竖直方向上有重力加速度
        transform.Translate(Vector3.down * gravity * time * Time.deltaTime);
        //初始飞行方向匀速运动
        transform.Translate(direction * speed * Time.deltaTime);
        //飞碟到达画面底部,动作结束,进行回调
        if (this.transform.position.y < -6) 
        {
            this.destroy = true;
            this.enable = false;
            this.callback.SSActionEvent(this);
        }
        
    }
}
8) ActionManager.cs
  • 飞行动作管理类,用于生成飞碟飞行动作,和接受飞行动作的回调信息,回收飞碟
public class ActionManager : SSActionManager, ISSActionCallback
{
    //飞行动作
    public FlyAction flyAction;
    //控制器
    public FirstController controller;

    protected new void Start()
    {
        controller = (FirstController)SSDirector.GetInstance().CurrentScenceController;
        controller.actionManager = this;
    }

    public void Fly(GameObject disk, float speed, Vector3 direction)
    {
        flyAction = FlyAction.GetSSAction(direction, speed);
        RunAction(disk, flyAction, this);
    }

    //回调函数
    public void SSActionEvent(SSAction source,
                              SSActionEventType events = SSActionEventType.Competed,
                              int intParam = 0,
                              string strParam = null,
                              Object objectParam = null)
    {
        //飞碟结束飞行后工厂进行回收
        controller.diskFactory.FreeDisk(source.gameObject);
    }
}
9) FirstController.cs
  • 场景控制器,负责游戏逻辑
  • 发送飞碟 SendDisk – 从工厂获得一个飞碟并设置初始位置和速度,飞行动作
  • 点击判断 Hit – 处理用户点击动作,将被点击到的飞碟回收,计算得分
    在游戏中,玩家通过鼠标点击飞碟,从而得分。这当中涉及到一个点击判断的问题。我们调用 ScreenPointToRay 方法,构造由摄像头和屏幕点击点确定的射线,与射线碰撞的游戏对象即为玩家点击的对象
public class FirstController : MonoBehaviour, ISceneController, IUserAction
{
    public ActionManager actionManager;                   //动作管理者
    public DiskFactory diskFactory;                       //飞碟工厂
    int[] roundDisks;           //对应轮次的飞碟数量
    int mode;                   //当前模式,简单-0,正常-1,困难-2,或者无限模式-3
    int points;                 //当前分数
    int round;                  //当前轮次
    int sendCount;              //当前已发送的飞碟数量
    float sendTime;             //发送时间

    void Start()
    {
        LoadResources();
    }

    public void LoadResources()
    {
        SSDirector.GetInstance().CurrentScenceController = this;
        gameObject.AddComponent<DiskFactory>();
        gameObject.AddComponent<ActionManager>();
        gameObject.AddComponent<UserGUI>();
        diskFactory = Singleton<DiskFactory>.Instance;
        points = 0;
        round = 1;
        sendCount = 0;
        sendTime = 0;
        mode = 1;
        roundDisks = new int[] { 1, 1, 2, 3, 5, 8, 13, 21, 34, 55 };
    }
    //用于发射一个飞碟
    public void SendDisk(int mode)
    {
        //生成一个飞碟
        GameObject disk = diskFactory.GetDisk(round-1);
        //设置飞碟的初始位置
        disk.transform.position = new Vector3(-disk.GetComponent<DiskData>().direction.x * 7, UnityEngine.Random.Range(0f, 8f), 0);
        disk.SetActive(true);
        //设置飞碟的飞行动作
        if (mode == 0) {
            actionManager.Fly(disk, disk.GetComponent<DiskData>().speed*0.5f, disk.GetComponent<DiskData>().direction);
        }
        else if (mode == 2) {
            actionManager.Fly(disk, disk.GetComponent<DiskData>().speed*1.5f, disk.GetComponent<DiskData>().direction);
        }
        else {
            actionManager.Fly(disk, disk.GetComponent<DiskData>().speed, disk.GetComponent<DiskData>().direction);
        }
        
    }
    
    //处理用户的点击动作
    public void Hit(Vector3 position)
    {
        Camera ca = Camera.main;
        Ray ray = ca.ScreenPointToRay(position);

        RaycastHit[] hits;
        hits = Physics.RaycastAll(ray);

        for (int i = 0; i < hits.Length; i++)
        {
            RaycastHit hit = hits[i];
            //如果用户点击到飞碟
            if (hit.collider.gameObject.GetComponent<DiskData>() != null)
            {
                //将飞碟移至底部,触发飞行动作的回调
                hit.collider.gameObject.transform.position = new Vector3(0, -7, 0);
                //分数增加
                points += hit.collider.gameObject.GetComponent<DiskData>().points;
                //更新GUI
                gameObject.GetComponent<UserGUI>().points = points;
            }
        }
    }
    
    //重新开始
    public void Restart()
    {
        gameObject.GetComponent<UserGUI>().result = "";
        round = 1;
        sendCount = 0;
        points = 0;
        gameObject.GetComponent<UserGUI>().points = points;
        gameObject.GetComponent<UserGUI>().round = round;
        gameObject.GetComponent<UserGUI>().mode = mode;
    }
    
    //设置模式
    public void SetMode(int mode)
    {
        this.mode = mode;
    }

    //发射飞碟
    void Update()
    {
        sendTime += Time.deltaTime;
        //每1s发送一次飞碟
        if (sendTime > 1)
        {
            sendTime = 0;
            //每次发送至多5个飞碟,且不能多于当前 round 的最大飞碟数量
            for (int i = 0; i < 5 && sendCount < roundDisks[round-1]; i++)
            {
                sendCount++;
                SendDisk(mode);
            }
            //过了10 round,判断是否是无限模式,不是则输出游戏结束
            if (sendCount == roundDisks[round-1] && round == roundDisks.Length)
            {
                if (mode == 3)
                {
                    round = 1;
                    sendCount = 0;
                    gameObject.GetComponent<UserGUI>().result = "";
                }
                else
                {
                    gameObject.GetComponent<UserGUI>().result = "Game Over!";
                }
            }
            //一轮发送的飞碟数量够了,更新轮次
            if (sendCount == roundDisks[round-1] && round < roundDisks.Length)
            {
                sendCount = 0;
                round++;
                gameObject.GetComponent<UserGUI>().round = round;
            }
        }
    }
}

二、Model

在这里插入图片描述

1) 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;
        }
    }
}
2) DiskData.cs
  • 飞碟数据类,包含飞碟的初始速度、击中时的得分、以及初始飞行方向
public class DiskData : MonoBehaviour
{
    public float speed;         //初始速度
    public int points;          //击中的得分
    public Vector3 direction;   //初始移动方向
}
3) DiskFactory.cs
  • DiskFactory 类是一个单实例类,用单实例创建
  • DiskFactory 类有工厂方法 GetDisk 产生飞碟,有回收方法 FreeDisk;使用两个列表来分别维护正在使用和未被使用的飞碟对象
  • DiskFactory 使用模板模式根据预制和规则制作飞碟

  在游戏中,对象的新建和销毁的开销是巨大的,是不可忽视的。对于频繁出现的游戏对象,我们应该使用对象池技术缓存,从而降低对象的新建和销毁开销。在本游戏中,飞碟是频繁出现的游戏对象,我们使用带缓存的工厂模式管理不同飞碟的生产和回收。对于该飞碟工厂,我们使用单例模式

对象池的实现(伪代码)

getDisk(ruler) 
BEGIN
	IF (free list has disk) THEN
	a_disk = remove one from list
	ELSE
	a_disk = clone from Prefabs
	ENDIF
	Set DiskData of a_disk with the ruler
	Add a_disk to used list
	Return a_disk
END
FreeDisk(disk)
BEGIN
	Find disk in used list
	IF (not found) THEN THROW exception
	Move disk from used to free list
END

DiskFactory

//带缓存的工厂模式管理不同飞碟的生产与回收
public class DiskFactory : MonoBehaviour
{
    public GameObject diskPrefab;              //飞碟预制
    //生产含有基本属性的飞碟
    private List<DiskData> use;                //正被使用的飞碟
    private List<DiskData> free;                //空闲的飞碟

    public void Start()
    {
        use = new List<DiskData>();
        free = new List<DiskData>();
        diskPrefab = GameObject.Instantiate<GameObject>(Resources.Load<GameObject>("Prefabs/Disk"), Vector3.zero, Quaternion.identity);
        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<DiskData>();
        }

        //飞碟的等级 = 0~2之间的随机数 * 轮次数
        //轮次 round 越高,出的飞碟分数越高,速度也越快
        float level = UnityEngine.Random.Range(0, 2f) * (round + 1);
        if (level < 4)
        {
            disk.GetComponent<DiskData>().points = 1;
            disk.GetComponent<DiskData>().speed = 4.0f;
            disk.GetComponent<DiskData>().direction = new Vector3(UnityEngine.Random.Range(-1f, 1f) > 0 ? 2 : -2, 1, 0);
            disk.GetComponent<Renderer>().material.color = Color.green; //绿色飞碟,速度最慢
        }
        else if (level >= 4 && level < 7)
        {
            disk.GetComponent<DiskData>().points = 2;
            disk.GetComponent<DiskData>().speed = 6.0f;
            disk.GetComponent<DiskData>().direction = new Vector3(UnityEngine.Random.Range(-1f, 1f) > 0 ? 2 : -2, 1, 0);
            disk.GetComponent<Renderer>().material.color = Color.yellow; //黄色飞碟,速度中等
        }
        else
        {
            disk.GetComponent<DiskData>().points = 3;
            disk.GetComponent<DiskData>().speed = 8.0f;
            disk.GetComponent<DiskData>().direction = new Vector3(UnityEngine.Random.Range(-1f, 1f) > 0 ? 2 : -2, 1, 0);
            disk.GetComponent<Renderer>().material.color = Color.red; //红色飞碟,速度最快
        }

        use.Add(disk.GetComponent<DiskData>());

        return disk;
    }
  
    //回收飞碟
    public void FreeDisk(GameObject disk)
    {
        //找到使用中的飞碟,将其回收
        foreach (DiskData diskData in use)
        {
            if (diskData.gameObject.GetInstanceID() == disk.GetInstanceID())
            {
                disk.SetActive(false);
                free.Add(diskData);
                use.Remove(diskData);
                break;
            }

        }
    }
}

三、View
  • UserGUI 界面类,用于构建 UI 和捕捉用户动作,将分数,Round,游戏模式显示出来

在这里插入图片描述

public class UserGUI : MonoBehaviour
{
    private IUserAction userAction;
    public string result;
    public int points;
    public int round;
    public int mode;
    void Start()
    {
        points = 0;
        round = 1;
        mode = 1;
        result = "";
        userAction = SSDirector.GetInstance().CurrentScenceController as IUserAction;
    }
    
    //打印和用户交互提示界面
    void OnGUI()
    {
        GUIStyle titleStyle = new GUIStyle();
        titleStyle.normal.textColor = Color.black;
        titleStyle.fontSize = 50;

        GUIStyle style = new GUIStyle();
        style.normal.textColor = Color.white;
        style.fontSize = 30;

        GUIStyle resultStyle = new GUIStyle();
        resultStyle.normal.textColor = Color.red;
        resultStyle.fontSize = 50;

        GUI.Label(new Rect(600, 30, 50, 200), "Hit UFO", titleStyle);
        GUI.Label(new Rect(20, 10, 100, 50), "Points: " + points, style);
        GUI.Label(new Rect(220, 10, 100, 50), "Round: " + round, style);
        GUI.Label(new Rect(800, 100, 50, 200), result, resultStyle);

        if (GUI.Button(new Rect(1300, 100, 100, 50), "Restart"))
        {
            userAction.Restart();
        }
        //简单模式
        if (GUI.Button(new Rect(1300, 200, 100, 50), "Easy Mode"))
        {
            userAction.SetMode(0);
        }
        //通常模式
        if (GUI.Button(new Rect(1300, 300, 100, 50), "Normal Mode"))
        {
            userAction.SetMode(1);
        }
        //困难模式
        if (GUI.Button(new Rect(1300, 400, 100, 50), "Hard Mode"))
        {
            userAction.SetMode(2);
        }
        //无限模式
        if (GUI.Button(new Rect(1300, 500, 100, 50), "Infinite Mode"))
        {
            userAction.SetMode(3);
        }
        string[] s = new string[]{"Easy","Normal","Hard","Infinite"};
        GUI.Label(new Rect(600, 100, 100, 50),  s[mode] + " Mode", style);

        //捕捉鼠标点击
        if (Input.GetButtonDown("Fire1"))
        {
            userAction.Hit(Input.mousePosition);
        }
    }
}


3. 游戏画面

  • 简单模式

在这里插入图片描述

  • 困难模式下游戏结束

在这里插入图片描述

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值