交互入门——基于鼠标控制的射击飞碟小游戏

文章目录

游戏要求

游戏内容要求:

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

自定义规则(游戏制作思路)

  • 本次游戏设置了3个round,虽然round比较少,但是难度递增明显,而且有一定运气成分,也就是说即使到了最后一个round,也有可能出现比较简单的trial。
  • 游戏难度主要由飞碟飞行速度(不同颜色代表不同属性),飞碟同时出现个数决定,大致由round控制,但是带有随机性。
  • 每个trial之间间隔1.5秒左右,且不带提示信息。
  • 飞碟得分分别为1分、2分、3分对应3种速度的飞碟。

游戏制作代码

重复用到了之前实验时候的一些类,这里就不再列出代码了。

  • 导演类,跟之前一样负责管理场景控制器,是单实例
  • 动作基类、动作管理器基类,跟之前一样,只是子类的实现方式不同。

用户交互接口

public interface Interaction
{
	void hit(Vector3 pos);
    int GetScore();
    int getState();
	void changeState(int a);
	void reset();
}
  •  

只要用于用户点击的时候判断碰撞,记录并显示分数、记录状态、改变状态(根据round、trial、漏掉的飞碟数等等决定),重置。这个需要场景控制器实现。

单例模板

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

这个类可以用来实现各个类型的单例模式,使得每个类型有且仅有一个实例,与Director类似,不同的是这个类具有通用性,可以用于其他类型,只需更改模板的类型并且使用Instance的get函数来获取实例。

飞碟工厂

首先需要定义一个存储飞碟信息的结构,将其与飞碟实例绑定起来。

public class DiskInfo : MonoBehaviour {
    public Vector3 pos; //初始位置
    public Color color; //颜色,代表不同难度
    public float speed; // 初速度
    public Vector3 target; // 初速度方向
    public int hit = 0; // 是否已经被击中一次,防止重复击中而多计分
}
  •  

下面就是工厂的类:

public class DiskFactory : MonoBehaviour
{
    public GameObject disk = null;
    private List<DiskInfo> activeList = new List<DiskInfo>();
    private List<DiskInfo> freeList = new List<DiskInfo>();
    int rand1 = 6, rand2 = 9, rand3 = 13;
    public int difficulty = 0;
    public GameObject GetDisk(int round)
    {
        GameObject newDisk = null;
        if (freeList.Count > 0)
        {
            newDisk = freeList[0].gameObject;
            freeList.Remove(freeList[0]);
        }
        else
        {
            newDisk = Instantiate(Resources.Load<GameObject>("Prefabs/Disk"), Vector3.zero, Quaternion.identity);
            
            newDisk.AddComponent<DiskInfo>();
        }

        switch(round) {
            case 1: {
                difficulty = Random.Range(0, rand1);
                break;
            }
            case 2: {
                difficulty = Random.Range(0, rand2);
                break;
            }
            case 3: {
                difficulty = Random.Range(0, rand3);
                break;
            }
        }

        if (difficulty < rand1 - 1) {
            newDisk.GetComponent<DiskInfo>().color = Color.white;
            newDisk.GetComponent<DiskInfo>().speed = 5.0f;
            float RanX = Random.Range(-1f, 1f) < 0 ? -2 : 2;
            newDisk.GetComponent<DiskInfo>().target = new Vector3(RanX, 1, 0);
            newDisk.GetComponent<Renderer>().material.color = Color.white;
        }
        else if (difficulty < rand2 - 1) {
            newDisk.GetComponent<DiskInfo>().color = Color.red;
            newDisk.GetComponent<DiskInfo>().speed = 7.0f;
            float RanX = Random.Range(-1f, 1f) < 0 ? -2 : 2;
            newDisk.GetComponent<DiskInfo>().target = new Vector3(RanX, 1, 0);
            newDisk.GetComponent<Renderer>().material.color = Color.red;
        }
        else {
            newDisk.GetComponent<DiskInfo>().color = Color.black;
            newDisk.GetComponent<DiskInfo>().speed = 9.0f;
            float RanX = Random.Range(-1f, 1f) < 0 ? -2 : 2;
            newDisk.GetComponent<DiskInfo>().target = new Vector3(RanX, 1, 0);
            newDisk.GetComponent<Renderer>().material.color = Color.black;
        }

        activeList.Add(newDisk.GetComponent<DiskInfo>());
        return newDisk;
    }
    public void freeDisk(GameObject disk) {
        DiskInfo tmp = null;
        foreach (DiskInfo i in activeList)
        {
            if (disk.GetInstanceID() == i.gameObject.GetInstanceID())
            {
                tmp = i;
                break;
            }
        }
        if (tmp != null) {
            tmp.gameObject.SetActive(false);
            tmp.hit = 0;
            freeList.Add(tmp);
            activeList.Remove(tmp);
        }
    }
}
  •  
  • 每次外界访问GetDisk函数的时候,都会传入参数round,来决定生产(或者重用)哪一个类型的飞碟,这个带有随机性,难度随round增大而增大。
  • 需要维护两个飞碟实例队列,当飞碟被击中或者落地的时候,可以重复使用该飞碟实例,将其加入空闲队列,让其在下一回合有需要的时候以新的形式(改变位置或者颜色等)出现,重新回到使用队列。
  • 根据随机产生的难度,改变飞碟实例的属性,并且将其加入使用队列中。如果空闲队列有对象,直接重用,否则新实例化一个(减少实例化带来的性能损耗)。

动作相关类

除了以前的动作基类和动作管理器基类之外,还需要新建飞碟运动的子类去继承

飞碟飞行类:

public class FlyAction : SSAction{
    public float g = 9.8f;
    public Vector3 to;//初速度方向
    public float v; //初速度
    public float v_down = 0;
    public float time;
    private FlyAction() {}

    public static FlyAction GetSSAction(Vector3 target, float speed) {
        FlyAction action = ScriptableObject.CreateInstance<FlyAction>();
        action.to = target;
        action.v = speed;
        return action;
    }

    public override void Update() {
        time += Time.fixedDeltaTime;
        this.transform.position += Vector3.down * (float)(v_down*Time.fixedDeltaTime+0.5*g*(Time.fixedDeltaTime)*(Time.fixedDeltaTime));
        this.transform.position += to * v * Time.fixedDeltaTime;
        v_down = time * g;

        if (this.transform.position.y <= -5) {
            this.destroy = true;
            if (this.transform.position.y > -15) {
                Singleton<Judger>.Instance.Miss();
            }
            this.callBack.SSActionEvent(this);
            
        }
    }
    public override void Start() {

    }
}
  •  
  • 需要实现GetSSAction的方法,便于外界获取动作对象。其中需要传入两个参数初速度和初速度方向,这就是飞行动作的关键(模拟重力的方向和大小是默认的)。
  • 每次更新的时候都将物体移动一小段距离,这个移动根据两个方向运动进行叠加,模拟一个类抛物线运动的过程。
  • 当运动到某个位置(摄像机视角以下看不见的位置,就停止运动,并回调),由于被击中后的飞碟也是设定到某个下方的位置,所以需要一个小小判断来区分飞碟是被击中还是自由落地的。

飞行动作管理器

public class FlyActionManager : ActionManager, ISSActionCallback
{
    FlyAction UFOAction;
    Controller controller;

    private void Start()
    {
        controller = Director.getInstance().currentSceneController as Controller;
        controller.actionManager = this;
    }

    public void flyUFO(GameObject disk, Vector3 target, float speed) {
        UFOAction = FlyAction.GetSSAction(target, speed);
        this.RunAction(disk, UFOAction, this);
    }

    public void SSActionEvent(SSAction action){
        Singleton<DiskFactory>.Instance.freeDisk(action.gameObject);
    }
}
  •  
  • 这个类做的不多,就是使得创建一个飞行动作实例,并且将其绑定到特定的对象上,执行动作。
  • 回调函数则是在运动完成后,利用飞碟工厂回收飞碟实例。

Controller场景控制器

public class Controller : MonoBehaviour, SceneController, ISSActionCallback, Interaction {
    public FlyActionManager actionManager;
    public DiskFactory diskFactory;

    public Judger judger;
    public UserUI ui;
    public int trial = 10;
    public float time = 0;
    public int round = 1;
    public int n = 3;
    public int state = 0;

    public Queue<GameObject> diskQueue = new Queue<GameObject>();

    private void Awake()
    {
        Director director = Director.getInstance();
        director.currentSceneController = this;
        actionManager = gameObject.AddComponent<FlyActionManager>() as FlyActionManager;
        this.gameObject.AddComponent<DiskFactory>();
        this.gameObject.AddComponent<Judger>();
        diskFactory = Singleton<DiskFactory>.Instance;
        ui = gameObject.AddComponent<UserUI>() as UserUI;
        judger = Singleton<Judger>.Instance;
    }

    private void Update()
    {   
        if (state <= 0 || state == 2) {
            return;
        }
        if (trial == 0 && round >= n) {
            time += Time.deltaTime;
            if (time > 3) {
                changeState(-2);
                time = 0;
            }
            return;
        }
        if (trial == 0 && state == 1)
        {
            state = 2;
            
        }
 
        if (trial == 0 && state == 3)
        {
                round = (round + 1);
                if (round > n) {
                    return;
                }
                trial = 10;
                state = 1;
        }

        if (time > 1.5)
        {   
            if (Singleton<Judger>.Instance.checkGame() == false) {
                changeState(-1);
                return;
            }
            ThrowDisk();
            time = 0;
        }
        else
        {
            time += Time.deltaTime;
        }
    }

    
    public void ThrowDisk() {
        int tmp = Random.Range(0, round);
        int num = 0;
        if (tmp < 0.9) {
            diskQueue.Enqueue(diskFactory.GetDisk(round));
            num = 1;
        }
        else if (tmp < 2) {
            diskQueue.Enqueue(diskFactory.GetDisk(round));
            diskQueue.Enqueue(diskFactory.GetDisk(round));
            num = 2;
        }
        else
        {
            diskQueue.Enqueue(diskFactory.GetDisk(round));
            diskQueue.Enqueue(diskFactory.GetDisk(round));
            diskQueue.Enqueue(diskFactory.GetDisk(round));
            num = 3;
        }
        for(int i = 0; i < num; i ++) {
            GameObject disk =  diskQueue.Dequeue();
            Vector3 position = new Vector3(0, 0, 0);
            float y = UnityEngine.Random.Range(-3f, 2f);
            position = new Vector3(-disk.GetComponent<DiskInfo>().target.x * 7, y, 0);
            disk.transform.position = position;
 
            disk.SetActive(true);
            
            actionManager.flyUFO(disk, disk.GetComponent<DiskInfo>().target,disk.GetComponent<DiskInfo>().speed);
        }
        trial --;
    }

    public void hit(Vector3 pos)
    {
        Ray ray = Camera.main.ScreenPointToRay(pos);
 
        RaycastHit[] hits;
        hits = Physics.RaycastAll(ray);
        for (int i = 0; i < hits.Length; i++)
        {
            RaycastHit hit = hits[i];
 
            if (hit.collider.gameObject.GetComponent<DiskInfo>() != null && hit.collider.gameObject.GetComponent<DiskInfo>().hit != 1)
            {
                hit.collider.gameObject.GetComponent<DiskInfo>().hit = 1;
                judger.hit(hit.collider.gameObject);
                
                hit.collider.gameObject.transform.position = new Vector3(0, -20, 0);
                return;
            }
 
        }
    }
    public void loadResources() {

    }
    public void SSActionEvent(SSAction action) {

    }
    public int GetScore() {
        return Singleton<Judger>.Instance.getScore();
    }
    //游戏结束
    public int getState(){
        return state;
    }
    //游戏重新开始
    public void changeState(int a){
        state = a;
    }

    public void reset() {
        trial = 10;
        round = 1;
        time = 0;
    }   
}
  •  

这个类的代码比较多,稍微总结一下:

  • 初始状态,将各个部件实例化并添加到自身
  • Update状态,每一帧都判断当前状态(游戏结束或进行中,round和trial进行到哪一步)并作出相应状态变化。还有每隔一段事件就执行丢飞碟的函数,使得飞碟飞出。
  • 丢飞碟函数,就是从工厂里获取实例对象(数量根据round随机),设置一下初始位置。然后执行它们的飞行动作。
  • hit函数,只要处理用户鼠标点击的交互,判断上是否点击到了飞碟,需要与UI类协同。
  • 状态分为几个:round结束,游戏结束(分为正常结束和中途死亡)、游戏运行中。不同状态下对应不同的状态转换。

裁判类

public class Judger : MonoBehaviour {
    private int score;
    private int miss;
    void Start() {
        score = 0;
        miss = 0;
    }
    public int getScore() {
        return score;
    }
    public bool checkGame() {
        if (miss >= 10) {
            return false;
        }
        return true;
    }

    public void hit(GameObject disk) {
        if(disk.GetComponent<DiskInfo>().color == Color.white) {
            score += 1;
        }else if (disk.GetComponent<DiskInfo>().color == Color.red) {
            score += 2;
        }else {
            score += 3;
        }
    }
    public void Miss() {
        Debug.Log("Miss one!");
        miss += 1;
    }
    public void restart() {
        miss = 0;
        score = 0;

    }
}
  •  
  • 充当计分和判断输赢的角色,分别记录未打中飞碟数和分数
  • 根据打中飞碟的颜色不同加不同分数
  • 检查当前未击中数是否达到上限,宣布游戏结束

以上函数都需外界调用。根据不同情景来调用。

UI

public class UserUI : MonoBehaviour {
    private Interaction action;
    bool flag = true;
    GUIStyle style1;
    GUIStyle style2;
    GUIStyle style3;
    float time = 0;
    void Start ()
    {
        action = Director.getInstance().currentSceneController as Interaction;
        style1 = new GUIStyle("button");
		style1.fontSize = 25;

        style2 = new GUIStyle();
		style2.fontSize = 35;
		style2.alignment = TextAnchor.MiddleCenter;
        style3 = new GUIStyle();
        style3.fontSize = 25;
        style3.alignment = TextAnchor.MiddleCenter;
    }
    private void OnGUI() {
        if (action.getState() == -1) {
            GUI.Label(new Rect(Screen.width/2-50, Screen.height/2-105, 100, 50), "Game Over!", style2);
            GUI.Label(new Rect(Screen.width/2-50, Screen.height/2-55, 110, 40), "Your Score: "+Singleton<Judger>.Instance.getScore().ToString(), style3);
            if (GUI.Button(new Rect(Screen.width/2-70, Screen.height/2, 150, 70), "Play again", style1)){
                Singleton<Judger>.Instance.restart();
                action.reset();
                action.changeState(1);
            }
            return;
        }else if (action.getState() == -2) {
            GUI.Label(new Rect(Screen.width/2-50, Screen.height/2-105, 100, 50), "Finished!", style2);
            GUI.Label(new Rect(Screen.width/2-50, Screen.height/2-55, 110, 40), "Your Score: "+Singleton<Judger>.Instance.getScore().ToString(), style3);
            if (GUI.Button(new Rect(Screen.width/2-70, Screen.height/2, 150, 70), "Restart", style1)){
                Singleton<Judger>.Instance.restart();
                action.reset();
                action.changeState(1);
            }
            return;
        }
        if (Input.GetButtonDown("Fire1"))
        {
            Vector3 pos = Input.mousePosition;
            action.hit(pos);
        }
        GUI.Label(new Rect(5, 5, 100, 50), "Score: " +Singleton<Judger>.Instance.getScore().ToString(), style3);
 
        if (flag) {
            GUI.Label(new Rect(Screen.width/2-50, Screen.height/2-95, 100, 50), "Hit UFO!", style2);
            if(GUI.Button(new Rect(Screen.width/2-70, Screen.height/2, 150, 70), "Play", style1)) {
                flag = false;
                action.changeState(1);
            }
        }
 
        if (!flag && action.getState() == 2)
        {
            GUI.Label(new Rect(Screen.width/2-50, Screen.height/2-95, 100, 50), "Next Round!", style2);
            time += Time.deltaTime;
            if (time > 3.5) {
                action.changeState(3);
                time = 0;
            }
        }
    }   
}
  •  

获取Controller中的状态,并且对应显示不同的内容。

  • round结束,游戏未结束时,显示NextRound字样
  • 游戏刚开始(初始),显示游戏名字。判断是否按下开始按钮才开始游戏。
  • 游戏结束(分为中途死亡和正常结束)显示对应的字样,并设置按钮使其重玩。重新开始同时重置裁判类和状态。

逻辑:

  • 判断用户鼠标点击的位置,执行Controller的hit函数进一步判断是否击中目标。

游戏截图

在这里插入图片描述
在这里插入图片描述

实验到此结束!

猜你喜欢

转载自blog.csdn.net/weixin_40552127/article/details/112785235