Unity实现打飞碟小游戏

Unity实现打飞碟小游戏

项目地址

演示视频

适配器模式概述

定义

适配器模式将某个类的接口转换成客户端期望的另一个接口表示,主的目的是兼容性,让原本因接口不匹配不能一起工作的两个类可以协同工作。其别名为包装器(Wrapper),属于结构型模式。

为表述方便,定义:

  • 需要被适配的类、接口、对象,简称 src(source)
  • 最终需要的输出,简称 dst (destination,即Target)
  • 适配器称之为 Adapter 。

一句话描述适配器模式的感觉: src->Adapter->dst,即src以某种形式(三种形式分别对应三种适配器模式)给到Adapter里,最终转化成了dst。

使用场景

  • 系统需要使用现有的类,而这些类的接口不符合系统的需要。
  • 想要建立一个可以重复使用的类,用于与一些彼此之间没有太大关联的一些类,包括一些可能在将来引进的类一起工作。
  • 需要一个统一的输出接口,而输入端的类型不可预知。

分类

  • 类适配器,以类给到,在Adapter里,就是将src当做类,继承,
  • 对象适配器,以对象给到,在Adapter里,将src作为一个对象,持有。
  • 接口适配器,以接口给到,在Adapter里,将src作为一个接口,实现。

项目要求

编写一个简单的鼠标打飞碟(Hit UFO)游戏

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

项目配置

  • 新建项目,将Assets文件替换为我项目中的Assets文件,由于第五章、第六章作业合并,故基础版和运动与物理兼容版文件夹下各有一个Assets
  • 将Assets/Resources/Fantasy Skybox FREE/Materials/Classic中的第一个天空盒FS000_Day_01拖到Scene里
  • 将Assets/Resources/Scripts中的DiskFactory、RoundController、ScoreRecorder拖到Main Camera上
  • 编译运行,开始游戏

核心算法分析

基础版(仅使用射线,动作管理)

项目框架如下:

Unity实现打飞碟小游戏_第1张图片

DiskData.cs规定了飞碟有如下属性:

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

MyDiskEditor.cs是用来制作预制的,实现飞碟属性图形化界面编辑:

Unity实现打飞碟小游戏_第2张图片

具体代码如下:

using UnityEngine;
using UnityEditor;
using System.Collections;
[CustomEditor(typeof(DiskData))]
[CanEditMultipleObjects]
public class MyDEditor : Editor
{
    SerializedProperty score;                              //分数
    SerializedProperty color;                              //颜色
    SerializedProperty scale;                              //大小

    void OnEnable()
    {
        //序列化对象后获得各个值
        score = serializedObject.FindProperty("score");
        color = serializedObject.FindProperty("color");
        scale = serializedObject.FindProperty("scale");
    }

    public override void OnInspectorGUI()
    {
        //开启更新
        serializedObject.Update();
        //设置滑动条
        EditorGUILayout.IntSlider(score, 0, 5, new GUIContent("score"));
  
        if (!score.hasMultipleDifferentValues)
        {
            //显示进度条
            ProgressBar(score.intValue / 5f, "score");
        }
        //显示值
        EditorGUILayout.PropertyField(color);
        EditorGUILayout.PropertyField(scale);
        //应用更新
        serializedObject.ApplyModifiedProperties();
    }
    private void ProgressBar(float value, string label)
    {
        Rect rect = GUILayoutUtility.GetRect(18, 18, "TextField");
        EditorGUI.ProgressBar(rect, value, label);
        //中间留一个空行
        EditorGUILayout.Space();
    }
}

RoundController.cs是MVC结构里的控制器,是场景控制器,有以下几个成员变量

    public FlyActionManager action_manager;
    public DiskFactory disk_factory;
    public UserGUI user_gui;
    public ScoreRecorder score_recorder;

    private Queue disk_queue = new Queue();          //游戏场景中的飞碟队列
    private List disk_notshot = new List();          //没有被打中的飞碟队列
    private int round = 1;                                                   //回合
    private int trial = 1;
    private float interval = 2.1f;                                                //发射一个飞碟的时间间隔
    private bool playing_game = false;                                       //游戏中
    private bool game_over = false;                                          //游戏结束
    private bool game_start = false;                                         //游戏开始

实现了以下几个方法

//初始化
void Start ();
//每帧更新,定时调用LoadResources函数通知飞碟加工厂生产飞碟,调用发射飞碟函数
void Update ();
//更新trial和round和生成飞碟间隔
public void UpdateTrial();
//通知飞碟加工厂生产飞碟并加入飞碟队列
public void LoadResources();
//发射飞碟
private void SendDisk();
//处理点击事件
public void Hit(Vector3 pos);
//获得分数
public int GetScore();
//获得round
public int GetRound();
//获得trial
public int GetTrail();
//重新开始
public void ReStart();
//设定游戏结束
public void GameOver();
//暂停几秒后回收飞碟
IEnumerator WaitingParticle(float wait_time, RaycastHit hit, DiskFactory disk_factory, GameObject obj);
//开始游戏
public void BeginGame();

一些关键函数实现如下

//每帧更新,定时调用LoadResources函数通知飞碟加工厂生产飞碟,调用发送飞碟函数
void Update ()
{
    if(game_start)
    {
        //游戏结束,取消定时发送飞碟
        if (game_over)
        {
            CancelInvoke("LoadResources");//取消调用LoadResources
        }
        //设定一个定时器,发送飞碟,游戏开始
        if (!playing_game)
        {
            InvokeRepeating("LoadResources", 1f, interval);//1秒后调用LoadResources,每speed秒调用一次
            playing_game = true;
        }
        //发射飞碟
        SendDisk();
    }
}
//发射飞碟
private void SendDisk()
{
    float position_x = 9;                       
    if (disk_queue.Count != 0)
    {
        GameObject disk = disk_queue.Dequeue();
        disk_notshot.Add(disk);
        disk.SetActive(true);
        //设置被隐藏了或是新建的飞碟的位置
        float ran_y = Random.Range(3f, 6f);
        float ran_x = Random.Range(-1f, 1f) < 0 ? -1 : 1;
        disk.GetComponent().direction = new Vector3(ran_x, ran_y, 0);
        Vector3 position = new Vector3(-disk.GetComponent().direction.x * position_x, ran_y, 0);
        disk.transform.position = position;
        //设置飞碟初始所受的力和角度,飞碟速度总体随round增大而加快。
        float power = Random.Range(0.9f + 0.1f * round, 1.35f + 0.15f * round);
        float angle = Random.Range(15f, 28f);
        action_manager.UFOFly(disk,angle,power,round);
    }

    for (int i = 0; i < disk_notshot.Count; i++)
    {
        GameObject temp = disk_notshot[i];
        //飞碟飞出摄像机视野也没被打中
        if (temp.transform.position.y < -3 && temp.gameObject.activeSelf == true)
        {
            if(user_gui.life>1)UpdateTrial();
            disk_factory.FreeDisk(disk_notshot[i]);
            disk_notshot.Remove(disk_notshot[i]);
            //玩家血量-1
            user_gui.ReduceBlood();
        }
    }
}
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() != 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;
            }
            UpdateTrial();
            disk_notshot.Remove(hit.collider.gameObject);
            //记分员记录分数
            score_recorder.Record(hit.collider.gameObject);
            //显示爆炸粒子效果
            Transform explode = hit.collider.gameObject.transform.GetChild(0);
            explode.GetComponent().Play();
            //等0.1秒后执行回收飞碟
            StartCoroutine(WaitingParticle(0.08f, hit, disk_factory, hit.collider.gameObject));
            break;
        }
    }
}
//暂停几秒后回收飞碟
IEnumerator WaitingParticle(float wait_time, RaycastHit hit, DiskFactory disk_factory, GameObject obj)
{
    yield return new WaitForSeconds(wait_time);
    //等待之后执行的动作  
    hit.collider.gameObject.transform.position = new Vector3(0, -9, 0);
    disk_factory.FreeDisk(obj);
}

DiskFactory.cs是飞碟工厂,实现了生产飞碟和回收飞碟两个方法,具体实现如下:

public class DiskFactory : MonoBehaviour
{
    public GameObject disk_prefab = null;                 //飞碟预制体
    private List used = new List();   //正在被使用的飞碟列表
    private List free = new List();   //空闲的飞碟列表

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

        //随机选择要飞出的飞碟
        choice = Random.Range(0, scope3);
        //将要选择的飞碟的tag
        if(choice <= scope1)
        {
            tag = "disk1";
        }
        else if(choice <= scope2 && choice > scope1)
        {
            tag = "disk2";
        }
        else
        {
            tag = "disk3";
        }
        //寻找相同tag的空闲飞碟
        for(int i=0;i("Prefabs/disk1"), new Vector3(0, start_y, 0), Quaternion.identity);
            }
            else if (tag == "disk2")
            {
                disk_prefab = Instantiate(Resources.Load("Prefabs/disk2"), new Vector3(0, start_y, 0), Quaternion.identity);
            }
            else
            {
                disk_prefab = Instantiate(Resources.Load("Prefabs/disk3"), new Vector3(0, start_y, 0), Quaternion.identity);
            }
            //给新实例化的飞碟赋予其他属性
            float ran_x = Random.Range(-1f, 1f) < 0 ? -1 : 1;
            disk_prefab.GetComponent().material.color = disk_prefab.GetComponent().color;
            disk_prefab.GetComponent().direction = new Vector3(ran_x, start_y, 0);
            disk_prefab.transform.localScale = disk_prefab.GetComponent().scale;
        }
        //添加到使用列表中
        used.Add(disk_prefab.GetComponent());
        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;
            }
        }
    }
}

ScoreRecorder.cs是记分员,负责记录分数和重置分数,具体实现如下:

public class ScoreRecorder : MonoBehaviour
{
    public int score;                   //分数
    void Start ()
    {
        score = 0;
    }
    //记录分数
    public void Record(GameObject disk)
    {
        int temp = disk.GetComponent().score;
        score = temp + score;
    }
    //重置分数
    public void Reset()
    {
        score = 0;
    }
}

Singleton.csRoundController.cs中被使用,确保飞碟工厂和记分员是单例模式,具体实现如下:

public class Singleton : 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;
        }
    }
}

``
FlyActionManager.cs是飞碟飞行的动作管理器,利用辅助类UFOFlyAction.cs计算飞碟飞行轨迹,控制飞碟飞行,具体实现如下:

public class FlyActionManager : SSActionManager
{

    public UFOFlyAction fly;                            //飞碟飞行的动作
    public RoundController scene_controller;             //当前场景的场景控制器

    protected void Start()
    {
        scene_controller = (RoundController)SSDirector.GetInstance().CurrentScenceController;
        scene_controller.action_manager = this;     
    }
    //飞碟飞行
    public void UFOFly(GameObject disk, float angle, float power, int round)
    {
        fly = UFOFlyAction.GetSSAction(disk.GetComponent().direction, angle, power);
        fly.gravity = -0.04f - 0.01f * round;
        this.RunAction(disk, fly, this);
    }
}

public class UFOFlyAction : SSAction
{
    public float gravity =(float) -0.05;                                 //向下的加速度
    private Vector3 start_vector;                              //初速度向量
    private Vector3 gravity_vector = Vector3.zero;             //加速度的向量,初始时为0
    private float time;                                        //已经过去的时间
    private Vector3 current_angle = Vector3.zero;               //当前时间的欧拉角

    private UFOFlyAction() { }
    public static UFOFlyAction GetSSAction(Vector3 direction, float angle, float power)
    {
        //初始化物体将要运动的初速度向量
        UFOFlyAction action = CreateInstance();
        if (direction.x < 0)
        {
            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;
        }
        return action;
    }

    public override void Update()
    {
        //计算物体的向下的速度,v=at
        time += Time.fixedDeltaTime;
        gravity_vector.y = 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;

        //如果物体y坐标小于-3,动作就做完了
        if (this.transform.position.y < -3)
        {
            this.destroy = true;
            this.callback.SSActionEvent(this);      
        }
    }

    public override void Start() { }
}

适配器版(运动与物理兼容)

项目框架如下:

Unity实现打飞碟小游戏_第3张图片

为实现适配器模式,对基础版做了如下修改:

  • 增加的脚本:
    • ActionManagerAdapter.cs负责接收动作管理器通知,选择采用运动学还是物理运动接口控制飞碟飞行,具体实现如下:
      public class ActionManagerAdapter : MonoBehaviour,IActionManager
      {
          public FlyActionManager action_manager;
          public PhysisFlyActionManager phy_action_manager;
          public void playDisk(GameObject disk, float angle, float power,int round,bool isPhy)
          {
              if(isPhy)
              {
                  phy_action_manager.UFOFly(disk, angle, power,round);
              }
              else
              {
                  action_manager.UFOFly(disk, angle, power,round);
              }
          }
          // Use this for initialization
          void Start ()
          {
              action_manager = gameObject.AddComponent() as FlyActionManager;
              phy_action_manager = gameObject.AddComponent() as PhysisFlyActionManager;
          }
      
      }
      
    • PhysisFlyActionManager.cs使用物理运动控制飞碟飞行,具体实现如下:
      public class PhysisFlyActionManager : SSActionManager
      {
      
          public PhysisUFOFlyAction fly;                            //飞碟飞行的动作
      
          protected void Start()
          {
          }
          //飞碟飞行
          public void UFOFly(GameObject disk, float angle, float power,int round)
          {
              fly = PhysisUFOFlyAction.GetSSAction(disk.GetComponent().direction, angle, power);
              fly.round=round;
              this.RunAction(disk, fly, this);
          }
      }
      
    • PhysisUFOFlyAction.cs,使用运动物理计算飞碟飞行轨迹,由于重力加速度太大游戏飞碟坠落太快难度太高,需要给飞碟加一个持续的竖直向上的加速度,改变这个加速度可以使飞碟有一个随round增大而增大的向下的加速度,具体实现如下:
      public class PhysisUFOFlyAction : SSAction
      {
          private Vector3 start_vector;                              //初速度向量
          public float power;
          public int round;
          private PhysisUFOFlyAction() { }
          public static PhysisUFOFlyAction GetSSAction(Vector3 direction, float angle, float power)
          {
              //初始化物体将要运动的初速度向量
              PhysisUFOFlyAction action = CreateInstance();
              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 FixedUpdate()
          {
              //判断是否超出范围
              if (this.transform.position.y < -3)
              {
                  this.destroy = true;
                  this.callback.SSActionEvent(this);
              }
          }
          public override void Update() { }
          public override void Start()
          {
              //使用重力加一个向上的加速度给飞碟一个随round增大而增大的向下的加速度
              gameobject.GetComponent().useGravity = true;
              gameobject.GetComponent().AddForce(new Vector3(0, 9.77f - 0.01f * round, 0),ForceMode.Acceleration);
              //给飞碟一个初速度
              gameobject.GetComponent().velocity = power * 7 * start_vector;
          }
      }
      
  • 修改的脚本:
    • FlyActionManager.cs去掉了对场景控制器的使用,修改后代码如下:
      public class FlyActionManager : SSActionManager
      {
      
          public UFOFlyAction fly;                            //飞碟飞行的动作
      
          protected void Start()
          {
          }
          //飞碟飞行
          public void UFOFly(GameObject disk, float angle, float power, int round)
          {
              fly = UFOFlyAction.GetSSAction(disk.GetComponent().direction, angle, power);
              fly.gravity = -0.04f - 0.01f * round;
              this.RunAction(disk, fly, this);
          }
      }
      
    • Interface.cs新增运动学与物理运动接口,具体实现如下:
      //运动学和物理运动接口
      public interface IActionManager
      {
          void playDisk(GameObject disk, float angle, float power,int round,bool isPhy);
      }
      
    • RoundController.cs增加新的成员变量isPhy并更改对动作管理器管理飞碟飞行方法的调用,注意要修改action_manager的类型为IActionManager,具体实现如下:
      public bool isPhy = true;                                               //是否使用物理运动
      
      //先将action_manager的类型修改为IActionManager,在修改实例化方法的调用
      action_manager = gameObject.AddComponent() as IActionManager;
      
      //动作管理器管理飞碟飞行方法调用
      action_manager.playDisk(disk, angle, power,round,isPhy);
      
    • SSActionManager.cs添加物理运动更新方法,具体实现如下
      protected void FixedUpdate()
      {
          foreach (SSAction ac in waitingAdd)
          {
              actions[ac.GetInstanceID()] = ac;
          }
          waitingAdd.Clear();
      
          foreach (KeyValuePair kv in actions)
          {
              SSAction ac = kv.Value;
              if (ac.destroy)
              {
                  waitingDelete.Add(ac.GetInstanceID());
              }
              else if (ac.enable)
              {
                  //物理运动更新
                  ac.FixedUpdate();
              }
          }
          foreach (int key in waitingDelete)
          {
              SSAction ac = actions[key];
              actions.Remove(key);
              DestroyObject(ac);
          }
          waitingDelete.Clear();
      }
      
    • UFOFlyAction.cs添加一个空的固定更新方法:
      public override void FixedUpdate() { }
      

效果展示

游戏规则

  • 游戏有无限多个round,每个round包括10次trial;
  • 鼠标点中飞碟得分,红色飞碟得3分,绿色飞碟得2分,紫色飞碟得1分,红飞碟体积最小,紫飞碟体积最大;
  • 每次trial得分不足该次trial出现飞碟总分数50%则生命-1,round结束后生命>0则进入下一个round,否则从round 1重新开始;
  • 每个trial的飞碟的色彩、大小、发射位置、速度、角度、同时出现的个数具有随机性,总体难度随round上升,即飞碟速度总体加快。

游戏截图

初始界面如下,展示了游戏规则:

Unity实现打飞碟小游戏_第4张图片

游戏截图如下,界面上方展示了分数、round数、trial数、生命值:

Unity实现打飞碟小游戏_第5张图片

击中飞碟有爆炸特效:

Unity实现打飞碟小游戏_第6张图片

生命值耗光,游戏结束,显示最高记录回合数和最高分数:

Unity实现打飞碟小游戏_第7张图片

心得体会

  • 加深了动作管理器的设计的理解
  • 加深了对工厂模式和单实例模式的理解
  • 进一步练习了使用MVC实现人机交互与游戏模型分离
  • 了解了适配器模式
  • 练习了刚体动力学物理引擎的使用

你可能感兴趣的:(unity,游戏引擎,c#)