3Dgame_homework5

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

内容要求

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

游戏要求

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

参考:
弹药和敌人:减少,重用和再利用
Unity对象池(Object Pooling)理解与简单应用

实现

游戏规则

根据游戏的内容要求,可以有以下游戏规则:

  1. 每次游戏有3次 round,各个 round 都可积累下来分数,每个 round 都包括10 次 trial;
  2. 游戏中出现的飞碟的大小、颜色都相同,击中飞碟可获得1分,漏掉飞碟不扣分;
  3. 每个飞碟的发射位置、速度都不同;
  4. 游戏结束后可以选择重新开始,此时分数也会归零。
动作表(规则表)
动作描述
玩家点击飞碟改变分数,被点击(击中)的飞碟消失
飞碟管理员产生新的飞碟颜色、位置、速度按一定比例随机出现
记分员加分/减分玩家击中不同的飞碟,会导致不同的加分/减分
玩家开始/重新开始分数归零,重新开始游戏
相关概念与实现思路

参考课程网站:与游戏世界交互

工厂对象:

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

  • 游戏对象的创建与销毁高成本,必须减少销毁次数(如游戏中的子弹);
  • 屏蔽创建与销毁的业务逻辑,使程序易于扩展;
  • 在 Unity 中,【工厂方法 + 单实例 + 对象池】通常都是同时在一起使用的。

UML图:

设计师读图:

  1. 游戏由导演、场记、运动管理师、演员构成;
  2. 新游戏中,场记请了记分员、飞碟管理员;
  3. 飞碟管理员管理飞碟的发放与回收,自己有个小仓库管理这些飞碟;
  4. 记分员按飞碟的数据计分,记分员拥有计分规则;
  5. 场记只需要管理出飞碟规则与管理碰撞就可以了。

设计模式解读:

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

优势:

应对规则、地图等变化,让设计者最低代价管理规则变化。例如:

道具工厂通过场景单实例,构建了方便可取获取DISK的类,包装了复杂的 Disk 生产与回收逻辑,易于使用,包含 Disk 产生规则(控制每个 round 的难度),可以积极应对未来游戏规则的变化,减少维护成本;
记分员包装了计分规则(控制业务均衡),提供了简单的对外业务接口。

对象池的实现:(伪代码)

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
实现与代码

制备预制体作为飞碟:

用 Sphere 和 Capsule 来制备飞碟,将 Sphere 命名为disk,并将 Capsule 作为其子对象,通过调整其缩放比例形成飞碟的形状。



代码:

DiskFactory.cs: 飞碟的工厂类,用于制造和销毁飞碟的工厂;

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

Director.cs: 导演类,单例模式,继承System.Object,主要控制场景切换;

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

ISceneController.cs: 接口场景类,负责指明具体实现的场景类要实现的方法,便于更多的类能通过接口来访问场景类,由 FirstSceneController 具体场景实现类来实现;

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public interface ISceneControl {
    void loadResources();
}

IUserAction.cs: 接口类,负责指明由用户行为引发的变化的方法,由 FirstSceneController 这个最高级的控制类来实现;

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
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);
}

DiskData.cs: 飞碟数据类,说明当前飞碟的状态,描述飞碟;

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

FirstSceneController.cs: 场景控制,最高级的控制类,负责加载资源,底层数据与用户操作的GUI的交互,实现 ISceneControl 和 IUserAction;

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

UserGUI.cs: 场景控制,用户界面类,负责生成界面交于用户操作,显示按钮,分数等;

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.red;
        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() >= 30 ? "You win" : "You fail", red);
            if(GUI.Button(new Rect(Screen.width / 2 - 60, Screen.height / 2 - 50, 120, 40), "Restart", black))
            {
                SceneManager.LoadScene("DiskAttack");
            }
            return;
        }
        Rect rect = new Rect(Screen.width / 2 - 100, 0, 200, 40);
        Rect rect2 = new Rect(Screen.width / 2 - 45, 60, 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;
            GUI.Label(new Rect(Screen.width / 2 - 250, 120, 500, 200), "There are 3 rounds, every round has 10 disk " +
                "whose color is different.\nIf you attack the white one, you will get 1 score. And you will get 2 score\n" +
                "if you attack the gray one. Finally, if you can attack the black and most\nfast one, you will get 4 " +
                "score. Once you get 30 scores, you win!", blackLabel);
        }
        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(rect2, "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);
        }
    }
}

SSAction.cs: 动作管理,是所有动作的基础类,用于规定所有动作的基础规范,通过实现 SSAciton 来指定不同的动作,继承 ScriptableObject;

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class SSAction : ScriptableObject {
    public bool enable = false;
    public bool destroy = false;
    public GameObject gameObject { set; get; }
    public Transform transform { set; get; }
    public ISSActionCallback callback { set; get; }
    protected SSAction() { }
    public virtual void Start()
    {
        throw new System.NotImplementedException();
    }
    public virtual void Update()
    {
        throw new System.NotImplementedException();
    }
    public void reset()
    {
        enable = false;
        destroy = false;
        gameObject = null;
        transform = null;
        callback = null;
    }
}

CCFlyAction.cs: 动作管理 ,飞碟飞行动作类,管理飞碟的飞行动作;

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

SSActionManager.cs: 动作管理器,组合动作管理类,是所有动作管理器的基础类,负责管理一系列的动作,负责创建和销毁它们;

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class SSActionManager : MonoBehaviour {
    private Dictionary<int, SSAction> actions = new Dictionary<int, SSAction>();
    private List<SSAction> waitingAdd = new List<SSAction>();
    private List<int> waitingDelete = new List<int>();
    protected void Update()
    {
        foreach (SSAction action in waitingAdd)
        {
            actions[action.GetInstanceID()] = action;
        }
        waitingAdd.Clear();
        foreach(KeyValuePair<int, SSAction> i in actions)
        {
            SSAction value = i.Value;
            if (value.destroy)
            {
                waitingDelete.Add(value.GetInstanceID());
            }
            else if (value.enable)
            {
                value.Update();
            }
        }
        foreach(int i in waitingDelete)
        {
            SSAction ac = actions[i];
            actions.Remove(i);
            DestroyObject(ac);
        }
    }
    public void runAction(GameObject gameObject, SSAction action, ISSActionCallback manager)
    {
        action.gameObject = gameObject;
        action.transform = gameObject.transform;
        action.callback = manager;
        waitingAdd.Add(action);
        action.Start();
    }
}

CCActionManager.cs: 动作管理器,事件管理类,对 SSManager 进行加强,负责事件的处理,管理某个对象的具体动作,为了减小开销,CCActionManager 也作为一个工厂来管理 CCFlyAction,继承了 SSActionManager,实现了 ISSActionCallback;

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

ISSActionCallback.cs: 动作管理,动作事件接口类,定义了事件处理的接口,必须被事件管理器实现,作为 ActionManager 和 Action 之间的通信,规定动作执行完之后需要执行的行为;

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public enum SSActionEventType : int { Started, Completed}
public interface ISSActionCallback {
    void SSActionEvent(SSAction source, SSActionEventType events = SSActionEventType.Completed, int intPram = 0
        , string strParm = null, Object objParm = null);
}

ScoreRecorder.cs: 记分员,负责给用户计分;

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class ScoreRecorder : MonoBehaviour {
    public int score;
    private Dictionary<Color, int> scoreTable = new Dictionary<Color, int>();
    void Start()
    {
        score = 0;
        scoreTable.Add(Color.white, 1);
        scoreTable.Add(Color.gray, 2);
        scoreTable.Add(Color.black, 4);
    }
    public void reset()
    {
        score = 0;
    }
    public void record(GameObject disk)
    {
        score += scoreTable[disk.GetComponent<DiskData>().getColor()];
    }
}

Singleton.cs: Singleton模板类;
(单例模式:一种软件设计模式,其核心结构只包含一个被称为单例的特殊类。一个类只有一个对象实例,但如果经常被使用,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;
        }
    }
}
实现结果

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

用自定义组件定义几种飞碟,做成预制:

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
图像识别技术在病虫害检测中的应用是一个快速发展的领域,它结合了计算机视觉和机器学习算法来自动识别和分类植物上的病虫害。以下是这一技术的一些关键步骤和组成部分: 1. **数据收集**:首先需要收集大量的植物图像数据,这些数据包括健康植物的图像以及受不同病虫害影响的植物图像。 2. **图像预处理**:对收集到的图像进行处理,以提高后续分析的准确性。这可能包括调整亮度、对比度、去噪、裁剪、缩放等。 3. **特征提取**:从图像中提取有助于识别病虫害的特征。这些特征可能包括颜色、纹理、形状、边缘等。 4. **模型训练**:使用机器学习算法(如支持向量机、随机森林、卷积神经网络等)来训练模型。训练过程中,算法会学习如何根据提取的特征来识别不同的病虫害。 5. **模型验证和测试**:在独立的测试集上验证模型的性能,以确保其准确性和泛化能力。 6. **部署和应用**:将训练好的模型部署到实际的病虫害检测系统中,可以是移动应用、网页服务或集成到智能农业设备中。 7. **实时监测**:在实际应用中,系统可以实时接收植物图像,并快速给出病虫害的检测结果。 8. **持续学习**:随着时间的推移,系统可以不断学习新的病虫害样本,以提高其识别能力。 9. **用户界面**:为了方便用户使用,通常会有一个用户友好的界面,显示检测结果,并提供进一步的指导或建议。 这项技术的优势在于它可以快速、准确地识别出病虫害,甚至在早期阶段就能发现问题,从而及时采取措施。此外,它还可以减少对化学农药的依赖,支持可持续农业发展。随着技术的不断进步,图像识别在病虫害检测中的应用将越来越广泛。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值