unity编程实践-HitUFO改进

作业要求

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

游戏的要求

  • 使用带缓存的工厂模式管理不同飞碟的生产与回收,该工厂必须是场景单实例的!具体实现见参考资源 Singleton 模板类
  • 尽可能使用前面 MVC 结构实现人机交互与游戏模型分离
  • 按 adapter模式 设计图修改飞碟游戏
  • 使它同时支持物理运动与运动学(变换)运动

部署工作

使用天空盒TGU Skybox Pack构建场景,并构建三个飞碟预制,在飞碟上分别添加diskdata类用来初始化飞碟大小等参数

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

public class DiskData : MonoBehaviour{
    public int score = 1;                               //射击此飞碟得分
    public Color color = Color.white;                   //飞碟颜色
    public Vector3 direction;                           //飞碟初始的位置
    public Vector3 scale = new Vector3( 1 ,0.25f, 1);   //飞碟大小
}

游戏规则

游戏规则大体如下,游戏总共有3关,每关有10个trial,trial13只会同时出现1个飞碟,trial46会同时出现2个飞碟,trial7~10会同时出现3个飞碟。不同关卡飞碟的大小和速度会有区别。不同飞碟的得分会有区别。每局游戏一共有10条命,当没有成功击落飞碟会扣除一条命,生命值为0时游戏结束。

重要代码分析

飞碟的生产与回收

首先是实现用工厂模式管理不同飞碟的生产与回收。

public GameObject disk_prefab = null;                 //飞碟预制体
private List<DiskData> used = new List<DiskData>();   //正在被使用的飞碟列表
private List<DiskData> free = new List<DiskData>();   //空闲的飞碟列表

了函数GetDisk,控制器通过把当前关卡传给这个函数并带有随机性质地返回相应的飞碟,如果空闲列表中存在有飞碟则取空闲列表中的,否则将从预制件中实例化一个新的飞碟并返回。

public GameObject GetDisk(int round){
    int choice = 0;
    int scope1 = 1, scope2 = 4, scope3 = 7;           //随机的范围
    float start_y = -10f;                             //刚实例化时的飞碟的竖直位置
    string name;
    disk_prefab = null;

    //根据回合,随机选择要飞出的飞碟
    if (round == 1) choice = Random.Range(0, scope1);
    else if(round == 2) choice = Random.Range(0, scope2);
    else choice = Random.Range(0, scope3);

    //将要选择的飞碟的name
    if(choice <= scope1) name = "disk1";
    else if(choice <= scope2 && choice > scope1) name = "disk2";
    else name = "disk3";

    //寻找相同name的空闲飞碟
    for(int i=0;i<free.Count;i++) if(free[i].name == name){
        disk_prefab = free[i].gameObject;
        free.Remove(free[i]);
        break;
    }
    //如果空闲列表中没有,则重新实例化飞碟
    if(disk_prefab == null){
        if (name == "disk1") disk_prefab = Instantiate(Resources.Load<GameObject>("Prefabs/disk1"), new Vector3(0, start_y, 0), Quaternion.identity);
        else if (name == "disk2") disk_prefab = Instantiate(Resources.Load<GameObject>("Prefabs/disk2"), new Vector3(0, start_y, 0), Quaternion.identity);
        else disk_prefab = Instantiate(Resources.Load<GameObject>("Prefabs/disk3"), new Vector3(0, start_y, 0), Quaternion.identity);

        //给新实例化的飞碟赋予其他属性
        float ran_x = Random.Range(-1f, 1f) < 0 ? -1 : 1;
        disk_prefab.GetComponent<Renderer>().material.color = disk_prefab.GetComponent<DiskData>().color;
        disk_prefab.GetComponent<DiskData>().direction = new Vector3(ran_x, start_y, 0);
        disk_prefab.transform.localScale = disk_prefab.GetComponent<DiskData>().scale;
    }
    //添加到使用列表中
    used.Add(disk_prefab.GetComponent<DiskData>());
    return disk_prefab;
}

当飞碟完成操作(被击落或是飞出屏幕)后需要回收飞碟

//回收飞碟
public void FreeDisk(GameObject disk){
    for(int i = 0;i < used.Count; i++) if (disk.GetInstanceID() == used[i].gameObject.GetInstanceID()){
        used[i].gameObject.SetActive(false);
        free.Add(used[i]);
        used.Remove(used[i]);
        break;
    }
}

本次实践实现了飞碟模型与动作的分离,与上一次实践类似,只需要继承实现好的三个类:SSActionSSActionManagerSequenceAction即动作类,动作管理器类,以及对于连续动作序列的类。

动作类

对于动作类,需要实现UFO飞行的动作逻辑。要使它同时支持物理运动与运动学(变换)运动,需要设定重力和给定一个初速度

public class PhyUFOFlyAction : SSAction{
    private Vector3 start_vector;                              //初速度向量
    public float power;
    private PhyUFOFlyAction() {}
    public static PhyUFOFlyAction GetSSAction(Vector3 direction, float angle, float power){
        //初始化物体将要运动的初速度向量
        PhyUFOFlyAction action = CreateInstance<PhyUFOFlyAction>();
        if (direction.x == -1) action.start_vector = Quaternion.Euler(new Vector3(0, 0, -angle)) * Vector3.left * power;
        else action.start_vector = Quaternion.Euler(new Vector3(0, 0, angle)) * Vector3.right * power;
        action.power = power;
        return action;
    }

    public override void Update() { 
        if (this.transform.position.y < -10){
            this.destroy = true;
            this.callback.SSActionEvent(this);
        }
    }
    public override void Start(){
        //使用重力以及给一个初速度
        gameobject.GetComponent<Rigidbody>().velocity = power / 13 * start_vector;
        gameobject.GetComponent<Rigidbody>().useGravity = true;
    }
}
动作管理类

对于动作管理类,只需要简单建立起动作和管理器之间的联系并作为中间人调用动作类即可。

本实践事实上不需要实现动作序列,因为动作类足以描述飞碟运动全过程。

public class PhyActionManager : SSActionManager{
    public PhyUFOFlyAction fly;
    protected void Start(){  
    }
    //飞碟飞行
    public void UFOFly(GameObject disk, float angle, float power){
        fly = PhyUFOFlyAction.GetSSAction(disk.GetComponent<DiskData>().direction, angle, power);
        this.RunAction(disk, fly, this);
    }

}
全局控制器类(FirstController)

首先实现了Start函数,定义了计分规则,实例化了相关联的类。

void Start (){
    //初始化参数 一共3级 1级10层
    trial = new int[3][];                                                
    score_round = new int[3];                                           
    int tmp=0;
    for (int i=0;i<3;i++){
        trial[i]= new int[10];
        for (int j=0;j<10;j++){
            trial[i][j]=tmp;
            tmp+=2*(i+1);
        }
        score_round[i]=trial[i][0];
    }

    SSDirector director = SSDirector.GetInstance();     
    director.CurrentScenceController = this;             
    disk_factory = Singleton<DiskFactory>.Instance;
    score_recorder = Singleton<ScoreRecorder>.Instance;
    action_manager = gameObject.AddComponent<FlyActionManager>() as FlyActionManager;
    user_gui = gameObject.AddComponent<UserGUI>() as UserGUI;

}

其中,disk_factoryscore_recorder即飞碟工厂和计分员都是单实例的,根据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;
        }
    }
}

然后是Update函数,它根据当前游戏的情况来确定使用飞碟的逻辑

void Update (){
    if(game_start){
        //游戏结束,取消定时发送飞碟
        if (game_over) CancelInvoke("LoadResources");

        //设定一个定时器,发送飞碟,游戏开始
        if (!playing_game){
            InvokeRepeating("LoadResources", 1f, speed);
            playing_game = true;
        }

        //发送飞碟
        SendDisk();

        //回合升级
        if (score_recorder.score >= score_round[1] && round == 0){
            round = 1;
            //缩小飞碟发送间隔
            speed = speed - 0.6f;
            CancelInvoke("LoadResources");
            playing_game = false;
        }
        else if (score_recorder.score >= score_round[2] && round == 1){
            round = 2;
            speed = speed - 0.5f;
            CancelInvoke("LoadResources");
            playing_game = false;
        }
    }
}

其中的LoadResources函数实现了访问工厂使用飞碟

public void LoadResources(){
    disk_queue.Enqueue(disk_factory.GetDisk(round+1)); 
    if (score_recorder.score>=trial[round][3]) 
        disk_queue.Enqueue(disk_factory.GetDisk(round+1)); 
    if (score_recorder.score>=trial[round][6])
        disk_queue.Enqueue(disk_factory.GetDisk(round+1)); 
}

其中的SendDisk函数将从工厂获得的飞碟经过处理(随机地给飞碟力与角度使其能实现斜抛运动)传递给动作管理器类实现飞碟运动,并对飞碟飞出后的情况(被击落或是飞出屏幕)进行处理。

private void SendDisk(){
    float position_x = 16;                       
    if (disk_queue.Count != 0){
        GameObject disk = disk_queue.Dequeue();
        disk_notshot.Add(disk);
        disk.SetActive(true);
        //设置被隐藏了或是新建的飞碟的位置
        float ran_y = Random.Range(1f, 4f);
        float ran_x = Random.Range(-1f, 1f) < 0 ? -1 : 1;
        disk.GetComponent<DiskData>().direction = new Vector3(ran_x, ran_y, 0);
        Vector3 position = new Vector3(-disk.GetComponent<DiskData>().direction.x * position_x, ran_y, 0);
        disk.transform.position = position;
        //设置飞碟初始所受的力和角度
        float power = Random.Range(10f, 15f);
        float angle = Random.Range(15f, 28f);
        action_manager.UFOFly(disk,angle,power);
    }

    for (int i = 0; i < disk_notshot.Count; i++){
        GameObject temp = disk_notshot[i];
        //飞碟飞出摄像机视野也没被打中
        if (temp.transform.position.y < -10 && temp.gameObject.activeSelf == true){
            disk_factory.FreeDisk(disk_notshot[i]);
            disk_notshot.Remove(disk_notshot[i]);
            //玩家血量-1
            user_gui.ReduceBlood();
        }
    }
}

需要实现击落飞碟的逻辑,通过GUI类接受Fire1(默认为单击鼠标左键)来访问控制器实现对飞碟的打击,记录下点击的射线然后进行判断,如果击中了则实现记分员记分,飞碟爆炸,回收飞碟逻辑。

public void Hit(Vector3 pos){
    Ray ray = Camera.main.ScreenPointToRay(pos);
    RaycastHit[] hits;
    hits = Physics.RaycastAll(ray);
    bool not_hit = false;
    for (int i = 0; i < hits.Length; i++){
        RaycastHit hit = hits[i];
        //射线打中物体
        if (hit.collider.gameObject.GetComponent<DiskData>() != null){
            //射中的物体要在没有打中的飞碟列表中
            for (int j = 0; j < disk_notshot.Count; j++)
                if (hit.collider.gameObject.GetInstanceID() == disk_notshot[j].gameObject.GetInstanceID()) not_hit = true;

            if(!not_hit) return;

            disk_notshot.Remove(hit.collider.gameObject);
            //记分员记录分数
            score_recorder.Record(hit.collider.gameObject);
            //显示爆炸粒子效果
            Transform explode = hit.collider.gameObject.transform.GetChild(0);
            explode.GetComponent<ParticleSystem>().Play();
            //执行回收飞碟
            StartCoroutine(WaitingParticle(0.02f, hit, disk_factory, hit.collider.gameObject));
            break;
        }
    }
}
Adapt模式

用Adapter模式写一个适配器,使飞碟能选择运动学和物理运动来实现飞碟的飞行。

public class Adapter : MonoBehaviour,AdaActionmanager {
    public FlyActionManager fly_action_manager;
    public PhyActionManager phy_action_manager;
    public void UFOFly(GameObject disk,float angle,float power,bool op){
        if (op) phy_action_manager.UFOFly(disk,angle,power);
        else fly_action_manager.UFOFly(disk,angle,power);
    }
    void Start(){
        fly_action_manager=gameObject.AddComponent<FlyActionManager>() as FlyActionManager;
        phy_action_manager=gameObject.AddComponent<PhyActionManager>() as PhyActionManager;
    }
}

在上述代码中,当op=true时选择物理运动实现,op=false时选择运动学实现。

最后我们只要将FirstController中所有调用运动学的部分改成调用这个Adapter就可以了。

view

GUI类显示基本的游戏情况,包括分数,生命,记录玩家射击的过程并传递给控制器,当游戏结束时出现重新游戏按钮以及记录最高分并显示。逻辑比较简单。

具体代码:https://gitee.com/zipzhou/hit-ufo

演示视频链接:https://www.bilibili.com/video/BV1JP4y1L7R5?spm_id_from=333.999.0.0

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值