目录

内容要求

代码部分

Model

View

Controller

Actions

具体的控制类        

Singleton单实例

简单工场

回合控制和计分器

总结


内容要求

这次要制作的游戏是简单的打飞碟的游戏,游戏的内容要求是这样的 :

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

unity文字形状烟花_unity文字形状烟花

unity文字形状烟花_System_02

依旧遵循之前的MVC架构,下面就直接展示UML图和代码了:

unity文字形状烟花_System_03

代码部分

代码整体结构如下:

unity文字形状烟花_System_04

Model

        这次的数据模型只有一个,那就是飞碟的相关数据。有飞碟的得分、初始x方向速度和初始y方向速度。飞碟的大小在预制中已经做好,由于没有手动改变飞碟大小的功能,所以这里没有大小的数据。

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


public class DiskAttri: MonoBehaviour
{
    public int score;
    public float speedX;
    public float speedY;
}

View

        依旧是我们熟悉的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 = Director.getInstance().currentSceneController as IUserAction;
        
        //大字体初始化
        bigStyle = new GUIStyle();
        bigStyle.normal.textColor = Color.black;
        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.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(400, 60, 50, 200), gameMessage, bigStyle);
        GUI.Label(new Rect(0,0,100,50), "Score: " + score, smallStyle);
        GUI.Label(new Rect(750,0,100,50), "Round: " + round, smallStyle);
    }
}

Controller

         Controller作用于模型和视图上。它控制数据流向模型对象,并在数据变化时更新视图。它使视图与模型分离开。是游戏的控制逻辑实现的地方,可以说是游戏的主体部分。

Actions

        由于采用和之前的游戏相似的项目框架,所以保留了主体的动作基类SSAction动作管理基类SSActionManager和提供回调函数接口的动作事件接口IActionCallBack。这三个类实现的功能和之前类似,只在具体的动作方面和之前不同,这里命名为CCFlyAction。这一部分实现了飞碟在屏幕中“飞”的动作。

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

//飞碟从界面左右两侧飞入,离开界面时运动结束
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()
    {
        
    }

    // Update is called once per frame
    public override void Update()
    {
        if (this.transform.gameObject.activeSelf == false) {    //飞碟已经被"销毁"
            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) {
            this.destroy = true;
            this.callback.SSActionEvent(this);
            return;
        }
        transform.position += new Vector3(speedX, speedY, 0) * Time.deltaTime * 2;
    }
}

具体的控制类        

        依然有指挥管理整个游戏的导演类Director和场景控制接口ISceneController,和之前的内容、功能一样,这里不再详细介绍。

Singleton单实例

        Singleton模式指的是某个类只能生成一个实例,该类提供了一个全局访问点供外部获取该实例,其拓展是有限多例模式。

        这里使用场景单实例,当所需的实例第一次被需要时,在场景内搜索该实例,下一次使用时不需要搜索直接返回。场景单实例的使用很简单,只需要将 MonoBehaviour 子类对象挂载任何一个游戏对象上,然后在任意位置使用代码 Singleton<YourMonoType>.Instance 获得该对象。

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;  
		}  
	}
}
简单工场

        简单工厂又称为工厂方法,即类一个方法能够得到一个对象实例,使用者不需要知道该实例如何构建、初始化等细节。这个方法的使用是为了减少游戏对象创建和销毁的次数,并且屏蔽创建和销毁的具体逻辑,使程序易于拓展。简单工场就是生产和销毁游戏对象的工厂,对象的使用者需要游戏对象时向它要,用完后再扔给工厂,完全不用考虑它是怎么来的,怎么销毁的。工厂方法 + 单实例 + 对象池通常都是同时一起使用。

        这里的DiskFactory就是生产飞碟的工厂,它是一个单实例类,用前面场景单实例创建。它有工厂方法 GetDisk 产生飞碟,有回收方法 FreeDisk。在制作方面,它使用模板模式根据预制和规则制作飞碟,其中对象模板包括飞碟对象与飞碟数据。

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class MyException : System.Exception
{
    public MyException() { }
    public MyException(string message) : base(message) { }
}

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

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

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

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

        DiskAttri attri = disk.GetComponent<DiskAttri>();
        attri.score = rand.Next(1,4);
        //由分数来决定速度、颜色、大小
        attri.speedX = (rand.Next(1,5) + attri.score + round) * 0.2f;
        attri.speedY = (rand.Next(1,5) + 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,3);
        if (direction == 1) {   //左上
            disk.transform.Translate(Camera.main.ScreenToWorldPoint(new Vector3(0, Camera.main.pixelHeight, 8)));
            attri.speedY *= -1;
        }
        else if (direction == 2) {  //右上
            disk.transform.Translate(Camera.main.ScreenToWorldPoint(new Vector3(Camera.main.pixelWidth, Camera.main.pixelHeight, 8)));
            attri.speedX *= -1;
            attri.speedY *= -1;
        }
        
        used.Add(disk);
        disk.SetActive(true);
        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.");
        }
        used.Remove(disk);
        free.Add(disk);
    }
}
回合控制和计分器

        回合控制器,主要负责每回合游戏的主要逻辑,主要有下面三个部分:

  • gameOver:判断游戏是否结束
  • GetHit:用于获取并处理用户的点击动作,当用户用鼠标左键点击到某个飞碟对象时,交由计分器计分,并将用户点击到的飞碟移除
  • Update:用于产生飞碟与更新状态,飞碟每0.5s发射一次,每次发射10个,当所有回合都完成后结束游戏。
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.SceneManagement;

public class RoundController : MonoBehaviour, ISceneController, IUserAction
{
    int round = 0;
    int max_round = 5;
    float timer = 0.5f;
    GameObject disk;
    DiskFactory factory ;
    public CCActionManager actionManager;
    public ScoreRecorder scoreController;
    public UserGUI userGUI;
    // Start is called before the first frame update
    void Start()
    {
        
    }

    // 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() {
        Director director = Director.getInstance();
        director.currentSceneController = this;
        director.currentSceneController.LoadSource();
        gameObject.AddComponent<UserGUI>();
        gameObject.AddComponent<CCActionManager>();
        gameObject.AddComponent<ScoreRecorder>();
        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")) {
			//create ray, origin is camera, and direction to mousepoint
			Camera ca = Camera.main;
			Ray ray = ca.ScreenPointToRay(Input.mousePosition);

			//Return the ray's hit
			RaycastHit hit;
			if (Physics.Raycast(ray, out hit)) {
                scoreController.Record(hit.transform.gameObject);
                
                hit.transform.gameObject.SetActive(false);
			}
		}
    }
}

        计分器的功能很简单,就是加上点击到飞碟的分数:

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

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

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

总结

这次游戏的重点在于工厂方法 + 单实例 + 对象池,用类似工厂中流水线的方法来生产和销毁游戏对象,采用的整体框架和之前类似。上面的代码可以实现简单的打飞碟,但是还是有许多可以改进的地方:1.飞碟的运动是用坐标的移动实现的,真实感较差,可以用施加力并增加重力来让运动效果更逼真;2.飞碟数量较多时会重叠,这是因为它们没有碰撞体积,可以给飞碟增加刚体属性,设置碰撞效果;3.飞碟大小不可自定义;4.可以给点击飞碟添加击碎效果……