06 打飞碟游戏

1. 游戏要求

  • 分多个 round , 每个 round 都是 n 个 trail
  • 每个 trail 的飞碟的色彩,大小;发射位置,速度,角度,每次发射飞碟数量不一
  • 鼠标击中得分,得分按色彩、大小、速度不同计算,计分规则自由定
  • 使用带缓存的工厂模式管理不同飞碟的生产与回收,该工厂必须是场景单实例的!具体实现见参考资源 Singleton 模板类
  • 近可能使用前面 MVC 结构实现人机交互与游戏模型分离
  • 必须使用对象池管理飞碟对象
  • 建议使用 ScriptableObject 配置不同的飞碟
  • 建议使用物理引擎管理飞碟飞行路径

2.游戏规则

游戏每round有10个disk抛出,round完成后玩家可以选择进入下一round。round=3时,游戏结束,得分>60即为游戏胜利。

积分规则:disk有三种颜色,白色、灰色、黑色,对应的分数为1,2,4。

3.前置知识

工厂模式Factory Pattern

工厂模式 | 菜鸟教程 (runoob.com)

工厂模式提供了一种将对象的实例化过程封装在工厂类中的方式。通过使用工厂模式,可以将对象的创建与使用代码分离,提供一种统一的接口来创建不同类型的对象。

在工厂模式中,我们在创建对象时不会对客户端暴露创建逻辑,并且是通过使用一个共同的接口来指向新创建的对象。

简单工厂又称为工厂方法,即类一个方法能够得到一个对象实例,使用者不需要知道该实例如何构建、初始化等细节。

  • 游戏对象的创建与销毁高成本,必须减少销毁次数。如:游戏中子弹
  • 屏蔽创建与销毁的业务逻辑,使程序易于扩展

优点: 1、一个调用者想创建一个对象,只要知道其名称就可以了。 2、扩展性高,如果想增加一个产品,只要扩展一个工厂类就可以。 3、屏蔽产品的具体实现,调用者只关心产品的接口。 

在 Unity 中,工厂方法 + 单实例 + 对象池 通常都是同时一起使用。

单实例

单例模式 | 菜鸟教程 (runoob.com)

这种类型的设计模式属于创建型模式,它提供了一种创建对象的最佳方式。

这种模式涉及到一个单一的类,该类负责创建自己的对象,同时确保只有单个对象被创建。这个类提供了一种访问其唯一的对象的方式,可以直接访问,不需要实例化该类的对象。

单例模式是一种创建型设计模式,它确保一个类只有一个实例,并提供了一个全局访问点来访问该实例。

  • 单例类只能有一个实例。
  • 单例类必须自己创建自己的唯一实例。
  • 单例类必须给所有其他对象提供这一实例。

优点:

  • 在内存里只有一个实例,减少了内存的开销,尤其是频繁的创建和销毁实例(比如管理学院首页页面缓存)。
  • 避免对资源的多重占用(比如写文件操作)。

对象池

对象池(Object Pool) - 知乎 (zhihu.com)

当创建对象时,对象池将对象放入池管理的某种内存连续的数据结构中(数组或者栈等)。当不需要对象时,对象池并不销毁对象,而是将对象回收到池中,下次需要的时候再次从池中拿出来。
因为,对象储存在内存连续的数据结构中,所以解决了内存碎片的问题。
因为,对象每次用完以后就放回池中循环利用而不是再次创建和销毁,这样就解决了频繁的内存分配和销毁的问题。

4.游戏设计

场景控制类

  • 导演类Director,单例模式,继承System.Object(会不被Unity内存管理,但所有Scene都能访问到它),主要控制场景切换(虽然现在只有一个场景)。
  • 接口场景类ISceneController,负责指明具体实现的场景类要实现的方法,而且便于更多的类能通过接口来访问场景类,由FirstSceneController具体场景实现类来实现。

工厂模式

  • 飞碟数据类DiskData,说明当前飞碟的状态,用于描述飞碟。
  • 模板类Singleton,用于给需要的类生成一个唯一的实例。
  • 飞碟工厂类DiskFactory,用于制造和销毁飞碟的工厂。

动作分离和动作控制类

  • 接口类IUserAction,负责指明由用户行为引发的变化的方法,由FirstSceneController这个最高级的控制类来实现。
  • 所有动作的基础类SSAction,用于规定所有动作的基础规范,继承ScriptableObject(ScriptableObject是不需要绑定GameObject对象的可编程基类,这些基类受Unity引擎场景管理)。
  • 飞碟飞行动作类CCFlyAction。
  • 组合动作管理类SSActionManager,用于管理一系列的动作,负责创建和销毁它们。

事件和逻辑控制类

  • 最高级的控制类FirstSceneController,负责底层数据与用户操作的GUI的交互,实现ISceneControl和IUserAction。
  • 动作事件接口类ISSActionCallback,定义了事件处理的接口,事件管理器必须实现它。
  • 事件管理类CCActionManager,继承了SSActionManager,实现了ISSActionCallback,负责事件的处理。
  • 用户界面类UserGUI,负责生成界面交于用户操作。
  • 记分员ScoreRecorder,用于给用户计分。

 5.代码

CCAction类

  • sceneControlFirstSceneControl 类型的变量,用于管理场景控制。
  • flysCCFlyAction 动作的列表。
  • diskNumber 跟踪当前场景中的飞行盘数量。
  • SSActionEvent 方法处理动作事件,特别是当动作类型为 CCFlyAction 时。它减少 diskNumber 计数,释放对应的飞行盘对象,并调用 freeSSAction 方法释放动作。
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class CCActionManager : SSActionManager, ISSActionCallback {
    private FirstSceneControl sceneControl;
    private List<CCFlyAction> flys = new List<CCFlyAction>();
    private int diskNumber = 0;

    private List<SSAction> used = new List<SSAction>();
    private List<SSAction> free = new List<SSAction>();

    public void setDiskNumber(int dn)
    {
        diskNumber = dn;
    }

    public int getDiskNumber()
    {
        return diskNumber;
    }

    public SSAction getSSAction()
    {
        SSAction action = null;
        if(free.Count > 0)
        {
            action = free[0];
            free.Remove(free[0]);
        }
        else
        {
            action = ScriptableObject.Instantiate<CCFlyAction>(flys[0]);
        }
        used.Add(action);
        return action;
    }

    public void freeSSAction(SSAction action)
    {
        foreach(SSAction a in used)
        {
            if(a.GetInstanceID() == action.GetInstanceID())
            {
                a.reset();
                free.Add(a);
                used.Remove(a);
                break;
            }
        }
    }

    protected void Start()
    {
        sceneControl = (FirstSceneControl)Director.getInstance().current;
        sceneControl.actionManager = this;
        flys.Add(CCFlyAction.getCCFlyAction());
    }

    private new void Update()
    {
        if (sceneControl.getGameState() == GameState.RUNNING)
            base.Update();
    }

    public void SSActionEvent(SSAction source, SSActionEventType events = SSActionEventType.Completed, int intPram = 0
        , string strParm = null, Object objParm = null)
    {
        if(source is CCFlyAction)
        {
            diskNumber--;
            Singleton<DiskFactory>.Instance.freeDisk(source.gameObject);
            freeSSAction(source);
        }
    }

    public void startThrow(Queue<GameObject> diskQueue)
    {
        foreach(GameObject i in diskQueue)
        {
            runAction(i, getSSAction(), (ISSActionCallback)this);
        }
    }
}

 CCFlyAction类

  • getCCFlyAction 方法是一个静态方法,用于获取 CCFlyAction 的实例。它通过调用 ScriptableObject.CreateInstance<CCFlyAction>() 来创建一个新的实例,并返回该实例。
    • 如果飞行盘的 Y 坐标小于 -4,表示飞行盘已经超出视野,触发销毁逻辑。设置 destroy 为 true,禁用该动作,并通过 callback.SSActionEvent(this) 触发动作完成事件。
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class CCFlyAction : SSAction
{
    float acceleration;
    float horizontalSpeed;
    Vector3 direction;
    float time;

    public static CCFlyAction getCCFlyAction()
    {
        CCFlyAction action =  ScriptableObject.CreateInstance<CCFlyAction>();
        return action;
    }

    public override void Start()
    {
        enable = true;
        acceleration = 9.8f;
        time = 0;
        horizontalSpeed = gameObject.GetComponent<DiskData>().getSpeed();
        direction = gameObject.GetComponent<DiskData>().getDirection();
    }

    public override void Update()
    {
        if (gameObject.activeSelf)
        {
            time += Time.deltaTime;
            transform.Translate(Vector3.down * acceleration * time * Time.deltaTime);
            transform.Translate(direction * horizontalSpeed * Time.deltaTime);
            if(this.transform.position.y < -4)
            {
                this.destroy = true;
                this.enable = false;
                this.callback.SSActionEvent(this);
            }
        }
    }
}

Director类

控制场景的切换

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class Director : System.Object {
    public ISceneControl current { set; get; }

    private static Director _Instance;

    public static Director getInstance()
    {
        return _Instance ?? (_Instance = new Director());
    }
}

IUserAction类

public enum GameState { ROUND_START, ROUND_FINISH, RUNNING, PAUSE, START, FUNISH}
 
public interface IUserAction{
    GameState getGameState();
    void setGameState(GameState gameState);
    int getScore();
    void hit(Vector3 pos);
    void reset();
}

DiskData类

  • size: 用于存储飞行盘的大小(Vector3 类型)。
  • color: 用于存储飞行盘的颜色。
  • speed: 用于存储飞行盘的速度。
  • direction: 用于存储飞行盘的飞行方向。
  • getSize(): 获取飞行盘的大小。
  • getSpeed(): 获取飞行盘的速度。
  • getDirection(): 获取飞行盘的飞行方向。
  • getColor(): 获取飞行盘的颜色。
  • setDiskData(Vector3 size, Color color, float speed, Vector3 direction): 设置飞行盘的属性。通过传递参数来初始化飞行盘的大小、颜色、速度和飞行方向。
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class DiskData : MonoBehaviour {
    private Vector3 size;
    private Color color;
    private float speed;
    private Vector3 direction;

    public DiskData() { }

    public Vector3 getSize()
    {
        return size;
    }

    public float getSpeed()
    {
        return speed;
    }

    public Vector3 getDirection()
    {
        return direction;
    }

    public Color getColor()
    {
        return color;
    }

    public void setDiskData(Vector3 size, Color color, float speed, Vector3 direction)
    {
        this.size = size;
        this.color = color;
        this.speed = speed;
        this.direction = direction;
    }
}

DiskFactory类

  • diskPrefab: 预制飞行盘的游戏对象,用于实例化新的飞行盘。
  • used: 保存正在使用中的飞行盘的列表。
  • free: 保存可重用的飞行盘的列表。
  • getDisk 方法用于获取一个新的飞行盘对象。如果有可重用的飞行盘,则从 free 列表中获取;否则,通过实例化 diskPrefab 来创建一个新的飞行盘对象。根据传入的 round 参数,设置飞行盘的大小、颜色、速度、方向等属性。将新创建或重新使用的飞行盘对象加入到 used 列表,并设置其名字和缩放。
  • freeDisk 方法用于释放一个飞行盘对象。通过传入的 disk 参数查找对应的 DiskData 对象,并将其从 used 列表移到 free 列表中,同时设置其状态为不激活。
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class DiskFactory : MonoBehaviour {
    public GameObject diskPrefab;
    public List<DiskData> used = new List<DiskData>();
    public List<DiskData> free = new List<DiskData>();

    private void Awake()
    {
        diskPrefab = GameObject.Instantiate<GameObject>(Resources.Load<GameObject>("Prefabs/disk"), Vector3.zero, Quaternion.identity);
        diskPrefab.SetActive(false);
    }

    public GameObject getDisk(int round)
    {
        GameObject disk = null;
        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>();
        }

        int start;
        switch (round)
        {
            case 0: start = 0; break;
            case 1: start = 100; break;
            default: start = 200; break;
        }
        int selectColor = Random.Range(start, round * 499);
        round = selectColor / 250;
        DiskData diskData = disk.GetComponent<DiskData>();
        Renderer renderer = disk.GetComponent<Renderer>();
        Renderer childRenderer = disk.transform.GetChild(0).GetComponent<Renderer>();
        float ranX = Random.Range(-1, 1) < 0 ? -1.2f : 1.2f;
        Vector3 direction = new Vector3(ranX, 1, 0);
        switch (round)
        {
            case 0:
                diskData.setDiskData(new Vector3(1.35f, 1.35f, 1.35f), Color.white, 4.0f, direction);
                renderer.material.color = Color.white;
                childRenderer.material.color = Color.white;
                break;
            case 1:
                diskData.setDiskData(new Vector3(1f, 1f, 1f), Color.gray, 6.0f, direction);
                renderer.material.color = Color.gray;
                childRenderer.material.color = Color.gray;
                break;
            case 2:
                diskData.setDiskData(new Vector3(0.7f, 0.7f, 0.7f), Color.black, 8.0f, direction);
                renderer.material.color = Color.black;
                childRenderer.material.color = Color.black;
                break;
        }
        used.Add(diskData);
        diskData.name = diskData.GetInstanceID().ToString();
        disk.transform.localScale = diskData.getSize();

        return disk;
    }

    public void freeDisk(GameObject disk)
    {
        DiskData temp = null;
        foreach (DiskData i in used)
        {
            if (disk.GetInstanceID() == i.gameObject.GetInstanceID())
            {
                temp = i;
            }
        }
        if (temp != null)
        {
            temp.gameObject.SetActive(false);
            free.Add(temp);
            used.Remove(temp);
        }
    }
}

 FirstSceneControl类

  • actionManager: 用于管理动作的管理器,包括飞行盘的发射和动作状态。
  • scoreRecorder: 记录游戏得分的类。
  • diskQueue: 一个队列,用于存储飞行盘对象。
  • diskNumber: 一个表示每轮生成的飞行盘数量的变量。
  • currentRound: 表示当前游戏轮数的变量。
  • time: 用于计时,控制飞行盘的发射间隔。
  • gameState: 表示当前游戏状态的枚举变量。
  • Update 方法中,根据不同的游戏状态执行相应的逻辑。
  • 如果当前动作管理器中的飞行盘数量为0,且游戏状态为 RUNNING,则表示一轮飞行盘已经全部飞出,将游戏状态设为 ROUND_FINISH。如果当前轮数为2,表示游戏结束,将游戏状态设为 FUNISH
  • 如果当前动作管理器中的飞行盘数量为0,且游戏状态为 ROUND_START,表示上一轮飞行盘已经全部飞出,进入下一轮的准备。增加当前轮数,调用 nextRound 方法准备下一轮的飞行盘,同时将动作管理器中的飞行盘数量设置为10,将游戏状态设为 RUNNING
  • 如果计时器超过1秒,且游戏状态不是 PAUSE,则调用 throwDisk 方法抛出飞行盘,并将计时器重置为0。否则,继续递增计时器。
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class FirstSceneControl : MonoBehaviour, ISceneControl, IUserAction {
    public CCActionManager actionManager { set; get; }
    public ScoreRecorder scoreRecorder { set; get; }
    public Queue<GameObject> diskQueue = new Queue<GameObject>();
    private int diskNumber = 0;
    private int currentRound = -1;
    private float time = 0;
    private GameState gameState = GameState.START;

    void Awake()
    {
        Director director = Director.getInstance();
        director.current = this;
        diskNumber = 10;
        this.gameObject.AddComponent<ScoreRecorder>();
        this.gameObject.AddComponent<DiskFactory>();
        scoreRecorder = Singleton<ScoreRecorder>.Instance;
        director.current.loadResources();
    }

    public void loadResources()
    {

    }

    private void Update()
    {
        if(actionManager.getDiskNumber() == 0 && gameState == GameState.RUNNING)
        {
            gameState = GameState.ROUND_FINISH;
            if(currentRound == 2)
            {
                gameState = GameState.FUNISH;
                return;
            }
        }
        if(actionManager.getDiskNumber() == 0 && gameState == GameState.ROUND_START)
        {
            currentRound++;
            nextRound();
            actionManager.setDiskNumber(10);
            gameState = GameState.RUNNING;
        }
        if(time > 1 && gameState != GameState.PAUSE)
        {
            throwDisk();
            time = 0;
        }
        else
        {
            time += Time.deltaTime;
        }
    }

    private void nextRound()
    {
        DiskFactory diskFactory = Singleton<DiskFactory>.Instance;
        for(int i = 0; i < diskNumber; i++)
        {
            diskQueue.Enqueue(diskFactory.getDisk(currentRound));
        }
        actionManager.startThrow(diskQueue);
    }

    void throwDisk()
    {
        if(diskQueue.Count != 0)
        {
            GameObject disk = diskQueue.Dequeue();
            Vector3 pos = new Vector3(-disk.GetComponent<DiskData>().getDirection().x * 10, Random.Range(0f, 4f), 0);
            disk.transform.position = pos;
            disk.SetActive(true);
        }
    }

    public int getScore()
    {
        return scoreRecorder.score;
    }

    public GameState getGameState()
    {
        return gameState;
    }

    public void setGameState(GameState gameState)
    {
        this.gameState = gameState;
    }

    public void hit(Vector3 pos)
    {
        RaycastHit[] hits = Physics.RaycastAll(Camera.main.ScreenPointToRay(pos));
        for(int i = 0; i < hits.Length; i++)
        {
            RaycastHit hit = hits[i];
            if(hit.collider.gameObject.GetComponent<DiskData>() != null)
            {
                scoreRecorder.record(hit.collider.gameObject);
                hit.collider.gameObject.transform.position = new Vector3(0, -5, 0);
            }
        }
    }

    public void reset(){
        this.currentRound=-1;
        scoreRecorder.reset();
    }
}

Singleton类

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

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;
        }
    }
}

UserGUI类

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.SceneManagement;

public class UserGUI : MonoBehaviour {
    private IUserAction action;
    bool isFirst = true;
    GUIStyle red;
    GUIStyle black;

    // Use this for initialization
    void Start () {
        action = Director.getInstance().current as IUserAction;
        black = new GUIStyle("button");
        black.fontSize = 20;
        red = new GUIStyle();
        red.fontSize = 30;
        red.fontStyle = FontStyle.Bold;
        red.normal.textColor = Color.yellow;
        red.alignment = TextAnchor.UpperCenter;
    }

    private void OnGUI()
    {
        if (action.getGameState() == GameState.FUNISH)
        {
            GUI.Label(new Rect(Screen.width / 2 - 100, Screen.height / 2 - 150, 200, 100), action.getScore() >= 60 ? "You win!" : "You fail!", red);
            if(GUI.Button(new Rect(Screen.width / 2 - 40, Screen.height / 2 - 40, 120, 40), "Restart", black))
            {
                // SceneManager.LoadScene("DiskAttack");
                action.setGameState(GameState.ROUND_START);
                action.reset();
               
            }
            return;
        }
        Rect rect = new Rect(Screen.width / 2 - 400, 0, 200, 40);
        Rect rect2 = new Rect(Screen.width / 2 + 245, 20, 120, 40);

        if (Input.GetButtonDown("Fire1") && action.getGameState() != GameState.PAUSE)
        {
            Vector3 pos = Input.mousePosition;
            action.hit(pos);
        }

        if (!isFirst)
        {
            GUI.Label(rect, "Your score: " + action.getScore().ToString(), red);
        }
        else
        {
            GUIStyle blackLabel = new GUIStyle();
            blackLabel.fontSize = 16;
            blackLabel.normal.textColor = Color.black;
            
        }

        if (action.getGameState() == GameState.RUNNING && GUI.Button(rect2, "Paused", black))
        {
            action.setGameState(GameState.PAUSE);
        }
        else if(action.getGameState() == GameState.PAUSE && GUI.Button(rect2, "Run", black))
        {
            action.setGameState(GameState.RUNNING);
        }

        if (isFirst && GUI.Button(new Rect(Screen.width / 2 -45 , 100, 120, 40), "Start", black))
        {
            isFirst = false;
            action.setGameState(GameState.ROUND_START);
        }

        if(!isFirst && action.getGameState() == GameState.ROUND_FINISH && GUI.Button(rect2, "Next Round", black))
        {
            action.setGameState(GameState.ROUND_START);
        }
    }
}

视频地址:打飞碟小游戏_哔哩哔哩_bilibili

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值