五、与游戏世界交互

本文详细介绍了一款使用MVC模式设计的打飞碟游戏,涉及Model(GameObject管理)、View(用户界面与事件处理)和Controller(动作管理与规则判定)。游戏随机性增强,难度逐轮提升,通过Singleton模式管理DiskFactory。关键组件如Ruler、ScoreRecorder和ActionManager详细阐述了各自职责。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

编写一个简单的鼠标打飞碟(Hit UFO)游戏
游戏内容要求:
游戏有 n 个 round,每个 round 都包括10 次 trial;
每个 trial 的飞碟的色彩、大小、发射位置、速度、角度、同时出现的个数都可能不同。它们由该 round 的 ruler 控制;
每个 trial 的飞碟有随机性,总体难度随 round 上升;
鼠标点中得分,得分规则按色彩、大小、速度不同计算,规则可自由设定。
游戏的要求:
使用带缓存的工厂模式管理不同飞碟的生产与回收,该工厂必须是场景单实例的!具体实现见参考资源 Singleton 模板类
尽可能使用前面 MVC 结构实现人机交互与游戏模型分离

在这里插入图片描述

程序设计

MVC结构

在这里插入图片描述

模型(Model)

场景中的所有GameObject

UserGUI(View)

显示模型,将人机交互事件交给控制器处理

  • 处收 Input 事件
    private void Update()
    {
        if (Input.GetButtonDown("Fire1"))
        {
            //Debug.Log("Fire1");
            Vector3 pos = Input.mousePosition;
            action.hit(pos);
        }
    }
  • 渲染 GUI ,接收事件
        void OnGUI()
        {
            GUI.skin.label.font = blue_font;
            GUI.Label(new Rect(Screen.width / 2 - 50, 20, 180, 50), "     Hit UFO     ");
            GUI.Label(new Rect(Screen.width / 2 - 30, 50, 180, 50), "score: " + score.ToString());
            GUI.Label(new Rect(Screen.width / 2 + 60, 50, 180, 50), "goal: " + targetThisRound.ToString());
            if (round != -1)
            {
                GUI.Label(new Rect(Screen.width / 2 - 120, 50, 100, 50), "Round: " + round.ToString());
            }
            else if (round == -1)
            {
                GUI.Label(new Rect(Screen.width / 2 - 120, 50, 100, 50), "You Lose!");
            }

            if (GUI.Button(new Rect(Screen.width / 2 - 40, 240, 70, 30), "Restart"))
            {
                action.restart();
            }
        }

Controller

与上两个作业一样。

  • 使用单例模式,由Director类统筹整个游戏
  • 由场记XXXSceneController来管理本次场景所有的游戏对象,响应外部输入事件
  • UserAction接口定义了用户行为
    唯一的改动是玩家与游戏交互的接口,包含玩家的两个动作:
    public interface UserAction
    {
        void hit(Vector3 pos);
        void restart();
    }

FirstController

加载资源并控制整个游戏的流程。
加载资源

    public void LoadResources()
    {
        actionManager = gameObject.AddComponent<CCActionManager>() as CCActionManager;
        this.gameObject.AddComponent<DiskFactory>();
        ruler = new Ruler();
        scoreRecorder = new ScoreRecorder();
    }

在Update函数中,设定每隔一段时间就抛出一个disk,每个round总共抛出10个Disk;同时判断是否到达晋级下一轮的条件或是否失败。

    void Update()
    {
        if (round != -1&&ruler.enterNextRound(round, scoreRecorder.score))
        {
            round++;
            trial = 0;
            getDisksForNextRound();
            userGUI.score = this.score = 0;
            scoreRecorder.reset();
            userGUI.targetThisRound = ruler.getTargetThisRound(round);
        }
        else if (round != -1&&!ruler.enterNextRound(round, scoreRecorder.score) && trial == 11)
        {
            round = -1;
        }

        if (this.round >= 1)
        {
            if (interval > ruler.setInterval(round))
            {
                if (trial < 10)
                {
                    throwDisk();
                    interval = 0;
                    trial++;
                }
                else if (trial == 10)
                {
                    trial++;
                }
            }
            else
            {
                interval += Time.deltaTime;
            }
        }

        userGUI.round = this.round;
    }

每一轮的开始,FirstController会从diskFactory中一次性拿够10个Disk放入queue中。

    public void getDisksForNextRound()
    {
        DiskFactory diskFactory = Singleton<DiskFactory>.Instance;
        int numDisk = 10;
        for (int i = 0; i < numDisk; i++)
        {
            GameObject disk = diskFactory.GetDisk(round);
            disksQueue.Enqueue(disk);
        }
    }

抛出时,从queue中拿出一个,通过ruler为其设定好属性,然后将其交给动作管理器抛出。

    public void throwDisk()
    {
        if (disksQueue.Count != 0)
        {
            GameObject disk = disksQueue.Dequeue();
            ruler.setDiskProperty(disk, round);
            disk.SetActive(true);
            actionManager.diskFly(disk, disk.GetComponent<DiskData>().angle, disk.GetComponent<DiskData>().power);
        }
    }

动作管理器

在这里插入图片描述

动作管理器可以在上一次作业的基础上进行修改。

  • ISSActionCallback为动作接口
  • SSAction为动作父类,规定所有Action的属性和方法
  • DiskFlyAction继承自SSAction,规定了一个Disk的动作,动作即飞碟的移动,由两个参数决定:受力角度和受力大小。构造函数初始化初始速度,Update函数模拟物体的坐标移动过程,当disk在画面之外(通过坐标判定),就callback通知动作做完。
        public override void Update()
        {
            time += Time.fixedDeltaTime;

            gravity_vector.y = 0;  //gravity * time;   

            transform.position += (start_vector + gravity_vector) * Time.fixedDeltaTime;
            current_angle.z = Mathf.Atan((start_vector.y + gravity_vector.y) / start_vector.x) * Mathf.Rad2Deg;
            transform.eulerAngles = current_angle;

            //动作做完
            if (this.transform.position.y < -10 || this.transform.position.y > 10)
            {
                this.destroy = true;
                this.callback.SSActionEvent(this);
            }
        }

  • SSActionManager函数。动作管理器,管理Action List中的每个Action(不用关心是单一Action还是连续Action)。
  • CCActionManager。SSActionManager的子类,封装了操作Action的相关类的函数,使得FirstController调用起来更简洁。
public void diskFly(GameObject disk, float angle, float power)
{
       fly = DiskFlyAction.GetSSAction(angle, power); //disk.GetComponent<Disk>().direction, angle, power);
       this.RunAction(disk, fly, this);
}

DiskData

简单地记录飞碟的属性,作为一个组件添加到具体的GameObject上

public class DiskData : MonoBehaviour
{
    public float size;
    public Color color;
    //move
    public float angle;
    public float power;
}

DiskFactory

  • 一个单实例类,用前面场景单实例创建
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;
		}
	}
}

然后使用代码
Singleton<DiskFactory>.Instance获得该对象

  • 负责飞碟的生产和回收。
    public GameObject GetDisk(int round)
    {
        GameObject newDisk = null;
        if (free.Count > 0)
        {
            newDisk = free[0].gameObject;
            free.Remove(free[0]);
        }
        else
        {
            newDisk = GameObject.Instantiate<GameObject>(diskPrefab, Vector3.zero, Quaternion.identity);
            newDisk.name = nameIndex.ToString();
            nameIndex++;
        }
        used.Add(newDisk);
        return newDisk;
    }

    public void FreeDisk(GameObject usedDisk)
    {
        if (usedDisk != null)
        {
            usedDisk.SetActive(false);
            used.Remove(usedDisk);
            free.Add(usedDisk);
        }
    }
  • 管理两个列表,记录已使用和空闲的飞碟数据。
    private List<GameObject> used = new List<GameObject>();   //正在使用
    private List<GameObject> free = new List<GameObject>();     //使用过已被释放的,可以重复使用
  • 使用模板模式根据预制和规则制作飞碟(diskPrefab)
    private void Awake()
    {
        diskPrefab = GameObject.Instantiate<GameObject>(Resources.Load<GameObject>("Prefabs/disk"), Vector3.zero, Quaternion.identity);
        diskPrefab.name = "prefab";
        diskPrefab.AddComponent<DiskData>();
        diskPrefab.SetActive(false);
        nameIndex = 0;
    }
    newDisk = GameObject.Instantiate<GameObject>(diskPrefab, Vector3.zero, Quaternion.identity);

Ruler

Ruler根据Round设置每个 trial 的飞碟的大小、速度、出现的时间间隔。
色彩、发射位置、角度则与Round无关。

public class Ruler
{     
    public void setDiskProperty(GameObject disk, int round)
    {
        disk.transform.position = this.setRandomInitPos();
        disk.GetComponent<Renderer>().material.color = setRandomColor();
        disk.transform.localScale = setScale(round);
        disk.GetComponent<DiskData>().angle = setRandomAngle();
        disk.GetComponent<DiskData>().power = setPower(round);
    }

    public Vector3 setRandomInitPos()
    {
        float x = Random.Range(-10f, 10f);
        float y = Random.Range(-1f, 5f);
        float z = Random.Range(-3f, 3f);
        return new Vector3(x, y, z);
    }

    public Vector4 setRandomColor()
    {
        int r = Random.Range(0f, 1f) > 0.5 ? 255 : 0;
        int g = Random.Range(0f, 1f) > 0.5 ? 255 : 0;
        int b = Random.Range(0f, 1f) > 0.5 ? 255 : 0;
        return new Vector4(r, g, b, 1);
    }

    public Vector3 setScale(int round)
    {
        float x = Random.Range((float)(1 - 0.1 * round), (float)(2 - 0.1 * round));
        float y = Random.Range((float)(1 - 0.1 * round), (float)(2 - 0.1 * round));
        float z = Random.Range((float)(1 - 0.1 * round), (float)(2 - 0.1 * round));
        return new Vector3(x, y, z);
    }

    public float setRandomAngle()
    {
        return Random.Range(-360f, 360f);
    }

    public float setPower(int round)
    {
        return round;
    }

    public float setInterval(int round)
    {
        return (float)(2 - 0.2 * round);
    }

    public int getTargetThisRound(int round)
    {
        if (round != -1)
        {
            return 5 + round > 10 ? 10 : 5 + round;
        }
        return 0;
    }

    public bool enterNextRound(int round,int score)
    {
        if (round != -1 && score >= (5 + round > 10 ? 10 : 5 + round))
        {
            return true;
        }
        return false;
    }
}

另外,Ruler还负责判断游戏是否可以进入下一轮/失败,round越大,要求进入下一轮的分数也越大。一轮10个trial,最高10分。

        public bool enterNextRound(int round)
        {
            if (round != -1 && this.score[round - 1] >= (5 + round > 10 ? 10 : 5 + round))
            {
                return true;
            }
            return false;
        }

ScoreRecorder

记分员保存分数值,按飞碟的数据计分,重置将分数值清零

public class ScoreRecorder
{
    public int score;
    public void record(GameObject disk)
    {
        int s = 1;
        if (disk.GetComponent<Renderer>().material.color == new Color(255, 0, 0, 1)) s += 1;
        score+=s;
    }
    public void reset()
    {
        score = 0;
    }
}

效果展示

在这里插入图片描述

代码传送门

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值