3D游戏编程与设计实验八——打飞碟游戏

一. 游戏要求:

  • 分多个 round , 每个 round 都是 n 个 trail;
  • 每个 trail 的飞碟的色彩,大小;发射位置,速度,角度,每次发射飞碟数量不一,轮次越大飞碟数量越多;。
  • 鼠标击中得分,得分按色彩、大小、速度不同计算,计分规则自由定
  • 使用ScriptableObject文件定制属性配置
  • 记分员负责记录得分

二. 对象池

什么是对象池:

游戏中会经常用到很多个相同类型的物体,比如子弹特效。如果每次要用的时候都去创建用完就删除掉,会造成频繁的资源回收(GC),部分游戏玩着玩着就卡顿就源于此。对象池是一种设计模式,是一种游戏经常用到的脚本类型,了解对象池对游戏性能优化非常有帮助。

核心思想:

预先初始化一组可重用的实体,而不是按需销毁然后重建。也就是说,每次需要实体时就去对象池中获取,如果有空闲的实体就直接使用,如果没有再创建从而减少频繁创建销毁的过程。

伪代码实例(基本原理):

// 定义一个对象池类
public class ObjectPool
{
    private List<GameObject> pool; // 对象池列表
    private GameObject prefab; // 对象的预制体
    private int poolSize; // 对象池的大小

    // 构造函数,初始化对象池
    public ObjectPool(GameObject prefab, int poolSize)
    {
        this.prefab = prefab;
        this.poolSize = poolSize;
        pool = new List<GameObject>();

        // 在对象池中创建指定数量的对象
        for (int i = 0; i < poolSize; i++)
        {
            GameObject obj = Instantiate(prefab);
            obj.SetActive(false);
            pool.Add(obj);
        }
    }

    // 从对象池中获取一个对象
    public GameObject GetObject()
    {
        // 遍历对象池,找到一个未激活的对象并返回
        foreach (GameObject obj in pool)
        {
            if (!obj.activeInHierarchy)
            {
                obj.SetActive(true);
                return obj;
            }
        }

        // 如果没有未激活的对象,创建一个新的对象并返回
        GameObject newObj = Instantiate(prefab);
        newObj.SetActive(true);
        pool.Add(newObj);
        return newObj;
    }

    // 将对象放回对象池
    public void ReturnObject(GameObject obj)
    {
        obj.SetActive(false);
    }
}

// 使用对象池
public class ObjectPoolExample : MonoBehaviour
{
    public GameObject prefab; // 预制体
    public int poolSize; // 对象池的大小
    private ObjectPool objectPool; // 对象池实例

    private void Start()
    {
        objectPool = new ObjectPool(prefab, poolSize);
    }

    private void Update()
    {
        // 按下空格键从对象池中获取一个对象,并将其放置在鼠标点击的位置
        if (Input.GetKeyDown(KeyCode.Space))
        {
            GameObject obj = objectPool.GetObject();
            obj.transform.position = Input.mousePosition;
        }
    }
}

 打飞碟游戏中的设计:

此游戏中游戏工厂就是使用了对象池的思想,每次创建飞碟都从工厂中获取实体对象,然后工厂就会检查它所维护的空闲队列看是否有空闲对象,有的话就返回;每次对象使用完后工厂就会把对象恢复预制状态然后添加到空闲队列去。

三. 游戏效果:

由于使用的是刚体所以飞碟也会与其他飞碟碰撞,增加了玩家游戏的难度,受动力学影响,飞碟的轨迹无法预测。

B站视频链接如下:3D游戏编程与设计实验七之打飞碟_哔哩哔哩_bilibili

视频如下:

3D游戏编程与设计实验七之打飞碟

四. 具体实现:

下面按照MVC架构进行代码思路解释

Model——Singleton类和DiskFactory工厂类

Singleton类:

 该类用于单实例化飞碟工厂,使用了对象池的思想,通过使用Singleton模式,可以避免多个实例的创建和资源的浪费,同时也能确保实例的唯一性和数据的一致性。

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

DiskFactory工厂类:

此类用于生成和回收飞碟游戏对象。

1. 在Start()方法中,初始化了两个列表used和free,用于存储已使用和空闲的飞碟对象,以及一个随机数生成器rand。

2. GetDisk(int round)方法用于获取一个飞碟对象,在主控制器中调用此函数来为对应的轮次生产飞碟。在生产飞碟的时候,它首先检查是否有空闲的飞碟对象,如果有,则从空闲列表中取出一个飞碟对象,否则创建一个新的飞碟对象。

3. 根据传入的round参数设置飞碟的属性,包括分数、速度、颜色和大小,由分数和轮次来共同决定飞碟的大小和速度,这样难度就会逐渐增加。随机生成一个1-4的随机数,根据随机数的值飞碟会从左上、左下、右上、右下四个方向随机进入游戏场景中。

4. 设置飞碟的属性后,将飞碟对象添加到used列表中,并激活飞碟对象。

6. FreeDisk(GameObject disk)方法用于回收一个飞碟对象。首先将飞碟对象设置为非激活状态,并重置位置和大小。检查used列表中是否包含要回收的飞碟对象,如果不包含,则抛出自定义的异常MyException。从used列表中移除飞碟对象,并将其添加到free列表中。

此工厂类可以生产不同的飞碟并通过维护两个列表来管理飞碟对象的使用和回收,实现了飞碟对象的复用和属性设置。

public class MyException : System.Exception
{
    public MyException() { }
    public MyException(string message) : base(message) { }
}
public class DiskAttributes : MonoBehaviour
{
    //public GameObject gameobj;
    public int score;
    public float speedX;
    public float speedY;
}
public class DiskFactory : MonoBehaviour
{
    
    List<GameObject> used;
    List<GameObject> free;
    System.Random rand;

    // Start is called before the first frame update
    void Start()
    {
        
        used = new List<GameObject>();
        free = new List<GameObject>();
        rand = new System.Random();
        //Disk disk = GetDisk(1); 
    }

    // Update is called once per frame
    void Update()
    {
        
    }

    public GameObject GetDisk(int round) {
        GameObject disk;
        if (free.Count != 0) {
            disk = free[0];
            //used.Add(free[0]);
            free.Remove(disk);
            //disk.SetActive(true);
        }
        else {
            disk = GameObject.Instantiate(Resources.Load("Prefabs/disk", typeof(GameObject))) as GameObject;
            disk.AddComponent<DiskAttributes>();
            //used.Add(disk.GetComponent<DiskAttributes>());
        }
        
        //根据不同round设置diskAttributes的值

        //随意的旋转角度
        disk.transform.localEulerAngles = new Vector3(-rand.Next(20,40),0,0);

        DiskAttributes attri = disk.GetComponent<DiskAttributes>();
        attri.score = rand.Next(1,4);
        //由分数来决定速度、颜色、大小
        attri.speedX = (rand.Next(20,30) + attri.score + round) * 0.2f;
        attri.speedY = (rand.Next(5,10) + attri.score + round) * 0.2f;
        
        
        if (attri.score == 3) {
            disk.GetComponent<Renderer>().material.color = Color.red;
            disk.transform.localScale += new Vector3(-0.5f,0,-0.5f);
        }
        else if (attri.score == 2) {
            disk.GetComponent<Renderer>().material.color = Color.green;
            disk.transform.localScale += new Vector3(-0.2f,0,-0.2f);
        }
        else if (attri.score == 1) {
            disk.GetComponent<Renderer>().material.color = Color.blue;
            
        }
        
        //飞碟可从四个方向飞入(左上、左下、右上、右下)
        int direction = rand.Next(1,5);

        //左上
        if (direction == 1) {
            disk.transform.Translate(Camera.main.ScreenToWorldPoint(new Vector3(0, Camera.main.pixelHeight * 1.5f, 8)));
            attri.speedY *= 0;
        }
        //左下
        else if (direction == 2) {
            disk.transform.Translate(Camera.main.ScreenToWorldPoint(new Vector3(0, Camera.main.pixelHeight * 0f, 8)));
            attri.speedY =rand.Next(10,15);
        }
        //右上
        else if (direction == 3) {
            disk.transform.Translate(Camera.main.ScreenToWorldPoint(new Vector3(Camera.main.pixelWidth, Camera.main.pixelHeight * 1.5f, 8)));
            attri.speedX *= -1;
            attri.speedY =0 ;
        }
        //右下
        else if (direction == 4) {
            disk.transform.Translate(Camera.main.ScreenToWorldPoint(new Vector3(Camera.main.pixelWidth, Camera.main.pixelHeight * 0f, 8)));
            attri.speedX *= -1;
            attri.speedY =rand.Next(10,15);
        }
        used.Add(disk);
        disk.SetActive(true);
        Debug.Log("generate disk");
        return disk;
    }

    public void FreeDisk(GameObject disk) {
        disk.SetActive(false);
        //将位置和大小恢复到预制,这点很重要!
        disk.transform.position = new Vector3(0, 0,0);
        disk.transform.localScale = new Vector3(2f,0.1f,2f);
        if (!used.Contains(disk)) {
            throw new MyException("Try to remove a item from a list which doesn't contain it.");
        }
        Debug.Log("free disk");
        used.Remove(disk);
        free.Add(disk);
    }
}

View——UserGUI类

UserGUI类

该类用于显示用户界面,以便与玩家进行交互。

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

public class UserGUI : MonoBehaviour
{
    public int mode;
    public int score;
    public int round;
    public string gameMessage;
    private IUserAction action;
    public GUIStyle bigStyle, blackStyle, smallStyle;//自定义字体格式
    public Font pixelFont;
    private int menu_width = Screen.width / 5, menu_height = Screen.width / 10;//主菜单每一个按键的宽度和高度
    // Start is called before the first frame update
    void Start()
    {
        mode = 0;
        gameMessage = "";
        action = SSDirector.getInstance().currentSceneController as IUserAction;

        
        //大字体初始化
        bigStyle = new GUIStyle();
        bigStyle.normal.textColor = Color.white;
        bigStyle.normal.background = null;
        bigStyle.fontSize = 50;
        bigStyle.alignment=TextAnchor.MiddleCenter;

        //black
        blackStyle = new GUIStyle();
        blackStyle.normal.textColor = Color.black;
        blackStyle.normal.background = null;
        blackStyle.fontSize = 50;
        blackStyle.alignment=TextAnchor.MiddleCenter;

        //小字体初始化
        smallStyle = new GUIStyle();
        smallStyle.normal.textColor = Color.white;
        smallStyle.normal.background = null;
        smallStyle.fontSize = 20;
        smallStyle.alignment=TextAnchor.MiddleCenter;

        
        
    }

    // Update is called once per frame
    void Update()
    {
        
    }

    void OnGUI() {
        //GUI.skin.button.font = pixelFont;
        GUI.skin.button.fontSize = 35;
        switch(mode) {
            case 0:
                mainMenu();
                break;
            case 1:
                GameStart();
                break;
            
        }       
    }

    void mainMenu() {
        GUI.Label(new Rect(Screen.width / 2 - menu_width * 0.5f, Screen.height * 0.1f, menu_width, menu_height), "Hit UFO", bigStyle);
        bool button = GUI.Button(new Rect(Screen.width / 2 - menu_width * 0.5f, Screen.height * 3 / 7, menu_width, menu_height), "Start");
        if (button) {
            mode = 1;
        }
        
    }
    void GameStart() {
        GUI.Label(new Rect(300, 60, 50, 200), gameMessage, bigStyle);
        GUI.Label(new Rect(0,0,100,50), "Score: " + score, smallStyle);
        GUI.Label(new Rect(560,0,100,50), "Round: " + round, smallStyle);
    }
}

Action——CCFlyAction类,SSActionManager,CCActionManager类

SSActionManager类:

1. 该类新增了一个RemainActionCount()函数,用于获取当前剩余动作数量(飞碟数量),当飞碟数量为0的时候才进入下一轮(否则几轮的飞碟就混在一起了)

2. 两个列表(waitingAdd和waitingDelete)用来存储待添加和待删除的动作。

3. RunAction()函数用于运行一个动作。首先将动作的gameObject、transform和callback属性设置为传入的参数。然后将动作添加到waitingAdd列表中,并调用动作的Start()方法进行初始化。

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>(); 
    // Start is called before the first frame update
    protected void Start()
    {
        
    }

    // Update is called once per frame
    protected void Update()
    {
        //Debug.Log("ssactionmanager update");
        foreach (SSAction ac in waitingAdd) {
            actions[ac.GetInstanceID()] = ac;
        }
        waitingAdd.Clear();
        //Debug.Log(actions.Count);
        foreach(KeyValuePair<int, SSAction> kv in actions) {
            SSAction ac = kv.Value;
            if (ac.destroy) {
                waitingDelete.Add(ac.GetInstanceID());
            } 
            else if (ac.enable) {
                //Debug.Log("ssactionmanager update");
                ac.Update();
            }
        }

        foreach(int key in waitingDelete) {
            SSAction ac = actions[key];
            actions.Remove(key);
            Destroy(ac);
        }
        waitingDelete.Clear();
    }

    public void RunAction(GameObject gameObject, SSAction action, IActionCallback manager) {
        //Debug.Log("run action");
        action.gameObject = gameObject;
        action.transform = gameObject.transform;
        action.callback = manager;
        waitingAdd.Add(action);
        action.Start();
    }

    public int RemainActionCount() {
        return actions.Count;
    }
}

CCActionManager类:动作管理类,实现动作管理基类和回调函数

1. MoveDisk()函数用于移动飞碟。首先根据传入的飞碟对象获取其速度,并通过CCFlyAction的GetSSAction()方法创建一个CCFlyAction的实例。然后调用RunAction()方法来运行该动作,并将CCActionManager自身作为回调对象传入。

2. 动作结束时会调用动作管理者实现的回调函数,即IActionCallback接口中SSActionEventType,动作管理者将动作绑定的游戏对象(飞碟)销毁。

public class CCActionManager : SSActionManager, IActionCallback 
{
    public FirstController sceneController;
    public CCFlyAction action;
    public DiskFactory factory;
    
    // Start is called before the first frame update
    protected new void Start()
    {
        sceneController = (FirstController)SSDirector.getInstance().currentSceneController;
        sceneController.actionManager = this;
        factory = Singleton<DiskFactory>.Instance;
    }

    // Update is called once per frame
    // protected new void Update()
    // {
        
    // }

    public void SSActionEvent(SSAction source,
        SSActionEventType events = SSActionEventType.Completed,
        int intParam = 0,
        string strParam = null,
        Object objectParam = null) {
            factory.FreeDisk(source.transform.gameObject);
    }

    public void MoveDisk(GameObject disk) {
        action = CCFlyAction.GetSSAction(disk.GetComponent<DiskAttributes>().speedX, disk.GetComponent<DiskAttributes>().speedY);
        RunAction(disk, action, this);

    }
}

 CCFlyAction类:

这个类很关键,由于飞碟预制体是刚体属性,所以不需要设定它的飞行的动作轨迹什么的,给他一个初速度就可以按照动力学原理自己飞和与其他飞碟发生碰撞,所以这个类主要是判断飞碟飞行过程中是否出界,如果出界,则同样将该动作的destroy属性设置为true,并调用回调函数SSActionEvent()来处理动作事件,这样动作管理器才能判断在RemainActionCount函数正确获取每轮次剩余的飞碟数来判断是否进入下一轮次避免几轮的飞碟聚在一起。

在Start()函数中,首先获取飞碟的Rigidbody2D组件,并将其速度设置为(speedX, speedY)。然后将其重力系数设置为0,即取消重力的影响。接着创建一个向下的力,并通过AddForce()方法将其作用于飞碟上,这里额外创建向下的力是因为重力加速度太大,那样的话飞碟很快就消失了所以自己添加向下的力来模拟一下动力学效果。

public class CCFlyAction : SSAction
{
    public float speedX;
    public float speedY;
    public static CCFlyAction GetSSAction(float x, float y) {
        CCFlyAction action = ScriptableObject.CreateInstance<CCFlyAction>();
        action.speedX = x;
        action.speedY = y;
        return action;
    }
    // Start is called before the first frame update
    public override void Start()
    {
        Rigidbody2D rb = this.transform.gameObject.GetComponent<Rigidbody2D>();
        rb.velocity = new Vector2(speedX,speedY);
        rb.gravityScale = 0f;
        Vector2 force = new Vector2(0, -8); // 向下的力
        rb.AddForce(force, ForceMode2D.Impulse); // 添加一个冲量力
    }

    // Update is called once per frame
    public override void Update()
    {
        //Debug.Log("flyaction update");
        if (this.transform.gameObject.activeSelf == false) {//飞碟已经被"销毁"
            Debug.Log("1");
            this.destroy = true;
            this.callback.SSActionEvent(this);
            return;
        }
        
        Vector3 vec3 = Camera.main.WorldToScreenPoint (this.transform.position);
        //飞碟出界
        if (vec3.x < -100 || vec3.x > Camera.main.pixelWidth + 100 || vec3.y < -100 || vec3.y > Camera.main.pixelHeight + 100) {
            Debug.Log("2");
            this.destroy = true;
            this.callback.SSActionEvent(this);
            return;
        }
              
    }
}

Controll——FirstController类,IUserAction类 ,ScoreController类(仅介绍几个主要类)

IUserAction类:

用户动作的接口主要定义两个方法。


public interface IUserAction {
    
    void gameOver();
    void GetHit();
}

FirstController类

该类主要用于连接用户与游戏 ,实现场景控制器的接口和用户操作的接口,将此脚本挂载在一个空物体上

1. 全局定义一个round参数用于记录打飞碟的轮次,获取DiskFactory的组件用于进行飞碟的生产,获取CCActionManager的组件用于对飞碟动作进行管理,还要获取计分器的组件用于统计玩家得分。

2. 每个轮次随机产生十个飞碟并用动作管理器控制飞碟的移动。

3. 实现用户操作接口的两个函数,并在Update()函数中调用来处理用户点击事件以及判断游戏是否结束。

4. 由于这里预制体增加了刚体(RigidBody 2D)的组件和碰撞器(Capsule Collider 2D)的组件,所以GetHit函数的射线也要设置成2D的才能获取碰撞到的飞碟。如果碰撞到了物体,就调用ScoreController的Record()方法记录得分,并将碰撞到的物体设置为非激活状态。

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

public class FirstController : MonoBehaviour, ISceneController, IUserAction
{
    int round = 0;
    int max_round = 5;
    float timer = 0.5f;
    GameObject disk;
    DiskFactory factory ;
    public CCActionManager actionManager;
    public ScoreController scoreController;
    public UserGUI userGUI;

    // Update is called once per frame
    void Update()
    {
        if (userGUI.mode == 0) return;
        GetHit();
        gameOver();
        if (round >= max_round) {
            return;
        }
        timer -= Time.deltaTime;
        if (timer <= 0 && actionManager.RemainActionCount() == 0) {
            //从工厂中得到10个飞碟,为其加上动作
            for (int i = 0; i < 10; ++i) {
                disk = factory.GetDisk(round);
                actionManager.MoveDisk(disk);
            }
            round += 1;
            if (round <= max_round) {
                userGUI.round = round;
            }
            timer = 4.0f;
        }
        
    }
    void Awake() {
        SSDirector director = SSDirector.getInstance();
        director.currentSceneController = this;
        director.currentSceneController.LoadSource();
        gameObject.AddComponent<UserGUI>();
        gameObject.AddComponent<CCActionManager>();
        gameObject.AddComponent<ScoreController>();
        gameObject.AddComponent<DiskFactory>();
        factory = Singleton<DiskFactory>.Instance;
        userGUI = gameObject.GetComponent<UserGUI>();
        
    }

    public void LoadSource() 
    {

    }

    public void gameOver() 
    {
        if (round >= max_round && actionManager.RemainActionCount() == 0)
            userGUI.gameMessage = "Game Over!";
    }

    public void GetHit() {
        if (Input.GetButtonDown("Fire1")) {
			Debug.Log ("Fired Pressed");
			// Debug.Log (Input.mousePosition);

			//Vector3 mp = Input.mousePosition; //get Screen Position

			//create ray, origin is camera, and direction to mousepoint
			Camera ca = Camera.main;
			Ray ray = ca.ScreenPointToRay(Input.mousePosition);

			//Return the ray's hit
			RaycastHit2D hit = Physics2D.Raycast(ray.origin, ray.direction);
			if (hit.collider != null) {
                scoreController.Record(hit.transform.gameObject);
                print("hit");
				print (hit.transform.gameObject.name);
				
                //Destroy (hit.transform.gameObject);
                hit.transform.gameObject.SetActive(false);
                
			}
		}
    }
}

ScoreController类

这是一个记分员,主要是记录玩家的得分,并将得分显示在用户界面上

1. 在start()函数中获取导演的实例,将当前对象赋给主控制器的ScoreController组件

2. Record函数用于记录玩家得分,玩家击中飞碟时通过传入飞碟对象获取飞碟的DiskAttributes组件,并从中获取飞碟的分数。然后将该分数加到score变量中,并将最新的得分赋值给userGUI的score属性,以更新用户界面上的得分显示。

public class ScoreController : MonoBehaviour
{
    int score;
    public FirstController roundController;
    public UserGUI userGUI;
    // Start is called before the first frame update
    void Start()
    {
        roundController = (FirstController)SSDirector.getInstance().currentSceneController;
        roundController.scoreController = this;
        userGUI = this.gameObject.GetComponent<UserGUI>();
    }

    public void Record(GameObject disk) {
        score += disk.GetComponent<DiskAttributes>().score;
        userGUI.score = score;
    }

    
}

五. 代码位置

gitee位置:代码位置

感谢师兄博客参考:师兄博客

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值