Unity3d学习之路-简单打靶游戏

简单打靶游戏

游戏规则与游戏要求

  • 规则
    使用WSAD键或者上下左右键移动弓箭,鼠标点击射箭。每一关有十支箭,需要用这十支箭打靶,达到目标分数即可进入下一关,每次射出箭后会变换一次风向。

  • 要求:

    • 靶对象为 5 环,按环计分
    • 箭对象,射中后要插在靶上,射中后,箭对象产生颤抖效果,持续0.8秒
    • 添加一个风向和强度标志,提高难度

游戏UML类图

这次游戏相比原来的框架多了主副相机相关的类和靶的碰撞器检测类

UML

游戏实现

靶创建部分

靶子是由5个圆柱体组成,每个圆柱体上添加一个Mesh Collider组件官方文档介绍,勾选isTrigger做触发器使用。而且挂载了CollisionDetectionRingData脚本。RingData脚本上只有一个分数属性,代表了这一环的分数。(如果觉得环应该有其他属性,可以使用之前的序列化自定义编辑器组件)。5个圆柱体作为一个空对象的孩子。最后预制体图如下。

预制体

弓移动部分

这部分实现了当用户按下键盘的WSAD或上下左右键时,弓箭移动,并且保持相机与弓箭的相对位置不变,实现相机跟随弓箭的效果,并且弓箭移动范围不是无限的,所以限定了一个移动范围

  • UserGUI

在UserGUI的Update中,当游戏进行时每一帧获取是否按下方向键,使用Input.GetAxis官方文档介绍,得到按下方向键后的虚拟轴中的值,然后通过接口去调用场景控制器中移动弓的方法。相关代码如下。

void Update()
{
    if(game_start && !action.GetGameover())
    {
        if (Input.GetButtonDown("Fire1"))
        {
            action.Shoot();
        }
        //获取方向键的偏移量
        float translationY = Input.GetAxis("Vertical");
        float translationX = Input.GetAxis("Horizontal");
        //移动弓箭
        action.MoveBow(translationX, translationY);
    }
}
  • FirstSceneController

场景的控制器,继承了IUserAction接口,并实现其方法,其中MoveBow方法实现了根据获取的虚拟轴的值移动弓。相关代码如下。

public void MoveBow(float offsetX, float offsetY)
{
    //游戏未开始时候不允许移动弓
    if (game_over || !game_start)
    {
        return;
    }
    //弓是否超出限定的移动范围
    if (bow.transform.position.x > 5)
    {
        bow.transform.position = new Vector3(5, bow.transform.position.y, bow.transform.position.z);
        return;
    }
    else if(bow.transform.position.x < -5)
    {
        bow.transform.position = new Vector3(-5, bow.transform.position.y, bow.transform.position.z);
        return;
    }
    else if (bow.transform.position.y < -3)
    {
        bow.transform.position = new Vector3(bow.transform.position.x, -3, bow.transform.position.z);
        return;
    }
    else if (bow.transform.position.y > 5)
    {
        bow.transform.position = new Vector3(bow.transform.position.x, 5, bow.transform.position.z);
        return;
    }

    //弓箭移动
    offsetY *= Time.deltaTime;
    offsetX *= Time.deltaTime;
    bow.transform.Translate(0, -offsetX, 0);
    bow.transform.Translate(0, 0, -offsetY);
}
  • CameraFlow

挂载在主相机上,根据初始的时候与弓的偏移量,在弓的位置变化的时候,保持偏移量不变,从而产生跟随效果。这里的bow在场景控制器中设置。

public class CameraFlow : MonoBehaviour
{
    public GameObject bow;               //跟随的物体
    public float smothing = 5f;          //相机跟随的速度
    Vector3 offset;                      //相机与物体相对偏移位置

    void Start()
    {
        offset = transform.position - bow.transform.position;
    }

    void FixedUpdate()
    {
        Vector3 target = bow.transform.position + offset;
        //摄像机自身位置到目标位置平滑过渡
        transform.position = Vector3.Lerp(transform.position, target, smothing * Time.deltaTime);
    }
}

箭飞行部分

这部分实现了用户按下鼠标左键,然后箭实现飞行动作

  • ArrowFlyAction

箭的飞行动作,在Start中为箭的Rigidbody设置一个初始力,让箭射出,在FixedUpdate中给箭一个持续的风力。(在开始时候给箭的预制体添加Rigidbody组件,不使用重力,并且预制体的Collider勾选isTrigger选项,作为触发器使用)

public class ArrowFlyAction : SSAction
{
    public Vector3 force;                      //初始时候给箭的力
    public Vector3 wind;                       //风方向上的力
    private ArrowFlyAction() { }
    public static ArrowFlyAction GetSSAction(Vector3 wind)
    {
        ArrowFlyAction action = CreateInstance<ArrowFlyAction>();
        //给予箭z轴方向的力
        action.force = new Vector3(0, 0, 20);
        action.wind = wind;
        return action;
    }

    public override void Update(){}

    public override void FixedUpdate()
    {
        //风的力持续作用在箭身上
        this.gameobject.GetComponent<Rigidbody>().AddForce(wind, ForceMode.Force);

        //检测是否被击中或是超出边界
        if (this.transform.position.z > 30 || this.gameobject.tag == "hit")
        {
            this.destroy = true;
            this.callback.SSActionEvent(this,this.gameobject);
        }
    }
    public override void Start()
    {
        gameobject.transform.parent = null;
        gameobject.GetComponent<Rigidbody>().velocity = Vector3.zero;
        gameobject.GetComponent<Rigidbody>().AddForce(force, ForceMode.Impulse);
    }
}
  • ArrowFactory

箭的工厂,在场景控制器需要箭的时候,从空闲的箭队列拿箭或者实例化新的箭放在场景中。

public class ArrowFactory : MonoBehaviour {

    public GameObject arrow = null;                             //弓箭预制体
    private List<GameObject> used = new List<GameObject>();     //正在被使用的弓箭
    private Queue<GameObject> free = new Queue<GameObject>();   //空闲的弓箭队列
    public FirstSceneController sceneControler;                 //场景控制器

    public GameObject GetArrow()
    {
        if (free.Count == 0)
        {
            arrow = Instantiate(Resources.Load<GameObject>("Prefabs/arrow"));
        }
        else
        {
            arrow = free.Dequeue();
            //如果是曾经射出过的箭
            if(arrow.tag == "hit")
            {
                arrow.GetComponent<Rigidbody>().isKinematic = false;
                //箭头设置为可见
                arrow.transform.GetChild(0).gameObject.SetActive(true);
                arrow.tag = "arrow";
            }
            arrow.gameObject.SetActive(true);
        }

        sceneControler = (FirstSceneController)SSDirector.GetInstance().CurrentScenceController;
        Transform temp = sceneControler.bow.transform.GetChild(2);
        //设置新射出去的箭的位置在弓箭上
        arrow.transform.position = temp.transform.position;
        arrow.transform.parent = sceneControler.bow.transform;
        used.Add(arrow);
        return arrow;
    }

    //回收箭
    public void FreeArrow(GameObject arrow)
    {
        for (int i = 0; i < used.Count; i++)
        {
            if (arrow.GetInstanceID() == used[i].gameObject.GetInstanceID())
            {
                used[i].gameObject.SetActive(false);
                free.Enqueue(used[i]);
                used.Remove(used[i]);
                break;
            }
        }
    }
}
  • FirstSceneController

用户按下鼠标左键后,场景控制器从箭工厂得到箭,然后生成一个风力,传递给动作管理器,让箭飞行,并开启副相机。相关代码如下。

public void Shoot()
{
    if((!game_over || game_start) && arrow_num <= 10)
    {
        arrow = arrow_factory.GetArrow();
        arrow_queue.Add(arrow);
        //风方向
        Vector3 wind = new Vector3(wind_directX, wind_directY, 0);
        //动作管理器实现箭飞行
        action_manager.ArrowFly(arrow, wind);
        //副相机开启
        child_camera.GetComponent<ChildCamera>().StartShow();
        arrow_num++;
    }
}
  • ChildCamera

在设定的时间内,显示副相机,可以让用户看清楚射出去箭在靶上的位置,挂载在副相机上。(初始设定副相机不显示)

public class ChildCamera : MonoBehaviour
{   
    public bool isShow = false;                   //是否显示副摄像机
    public float leftTime;                        //显示时间

    void Update()
    {
        if (isShow)
        {
            leftTime -= Time.deltaTime;
            if (leftTime <= 0)
            {
                this.gameObject.SetActive(false);
                isShow = false;
            }
        }
    }

    public void StartShow()
    {
        this.gameObject.SetActive(true);
        isShow = true;
        leftTime = 2f;
    }
}

箭中靶后部分

当箭射中靶子之后,会出现箭颤抖的效果,并且检测箭射中哪一环

  • CollisionDetection

箭分为两个部分,箭头和箭身,当箭头进入每一环碰撞器的时候,会消失。然后根据触发了哪一环的碰撞器,来计分。

public class CollisionDetection : MonoBehaviour
{
    public FirstSceneController scene_controller;         //场景控制器
    public ScoreRecorder recorder;                        //记录员

    void Start()
    {
        scene_controller = SSDirector.GetInstance().CurrentScenceController as FirstSceneController;
        recorder = Singleton<ScoreRecorder>.Instance;
    }

    void OnTriggerEnter(Collider arrow_head)
    { 
        //得到箭身
        Transform arrow = arrow_head.gameObject.transform.parent;
        if (arrow == null)
        {
            return;
        }
        if(arrow.tag == "arrow")
        {
            //箭身速度为0,不受物理影响
            arrow.GetComponent<Rigidbody>().velocity = new Vector3(0, 0, 0);
            arrow.GetComponent<Rigidbody>().isKinematic = true;
            recorder.Record(this.gameObject);
            //箭头消失
            arrow_head.gameObject.gameObject.SetActive(false); ;
            arrow.tag = "hit";
        }
    }
}
  • ArrowTremble

箭中靶后,通过回调函数告诉动作管理器,去执行箭颤抖动作。箭颤抖是通过短时间内上下快速移动实现的。

public class ArrowTremble : SSAction
{
    float radian = 0;                             // 弧度  
    float per_radian = 3f;                        // 每次变化的弧度  
    float radius = 0.01f;                         // 半径  
    Vector3 old_pos;                              // 开始时候的坐标  
    public float left_time = 0.8f;                 //动作持续时间

    private ArrowTremble() { }

    public override void Start()
    {
        //将最初的位置保存  
        old_pos = transform.position;             
    }

    public static ArrowTremble GetSSAction()
    {
        ArrowTremble action = CreateInstance<ArrowTremble>();
        return action;
    }
    public override void Update()
    {
        left_time -= Time.deltaTime;
        if (left_time <= 0)
        {
            //颤抖后回到初始位置
            transform.position = old_pos;
            this.destroy = true;
            this.callback.SSActionEvent(this);
        }

        // 弧度每次增加
        radian += per_radian;
        //y轴的位置变化,上下颤抖
        float dy = Mathf.Cos(radian) * radius; 
        transform.position = old_pos + new Vector3(0, dy, 0);
    }
    public override void FixedUpdate()
    {
    }
}
  • SSActionManager

之前的ISSActionCallback接口终于有用到了,在箭飞行后会执行一个回调函数SSActionEvent,传递现在中靶的GameObject。实现这个回调函数就可以让箭颤抖动作开始了。部分代码如下。

public void SSActionEvent(SSAction source, GameObject arrow = null)
{
    //回调函数,如果是箭飞行动作做完,则做箭颤抖动作
    if(arrow != null)
    {
        ArrowTremble tremble = ArrowTremble.GetSSAction();
        this.RunAction(arrow, tremble, this);
    }
    else
    {
        //场景控制器减少一支箭
        FirstSceneController scene_controller = (FirstSceneController)SSDirector.GetInstance().CurrentScenceController;
        scene_controller.ReduceArrow();
    }
}
  • FirstSceneController

从上面可以看到在箭颤抖动作做完后,动作管理器调用了场景控制器的ReduceArrow方法,弓箭减少一支,并且生成新的风向。(这部分在之后有修改,见补充改进部分),部分代码如下。

public void ReduceArrow()
{
    recorder.arrow_number--;
    if (recorder.arrow_number <= 0 && recorder.score < recorder.target_score)
    {
        game_over = true;
        return;
    }
    //生成新的风向
    wind_directX = Random.Range(-(round + 1), (round + 1));
    wind_directY = Random.Range(-(round + 1), (round + 1));
    CreateWind();
}

其他

  • FirstSceneController

GUI基本是延用了上次游戏的风格,增加了一个风向文本,通过判定风力值的大小来显示是哪个方向的风,风力几级。UserGUI可以通过IUserAction接口得到风力文本,实现是在场景控制器中。部分代码如下。

//根据风的方向生成文本
public void CreateWind()
{
    string Horizontal = "", Vertical = "", level = "";
    if (wind_directX > 0)
    {
        Horizontal = "西";
    }
    else if (wind_directX <= 0)
    {
        Horizontal = "东";
    }
    if (wind_directY > 0)
    {
        Vertical = "南";
    }
    else if (wind_directY <= 0)
    {
        Vertical = "北";
    }
    if ((wind_directX + wind_directY) / 2 > -1 && (wind_directX + wind_directY) / 2 < 1)
    {
        level = "1 级";
    }
    else if ((wind_directX + wind_directY) / 2 > -2 && (wind_directX + wind_directY) / 2 < 2)
    {
        level = "2 级";
    }
    else if ((wind_directX + wind_directY) / 2 > -3 && (wind_directX + wind_directY) / 2 < 3)
    {
        level = "3 级";
    }
    else if ((wind_directX + wind_directY) / 2 > -5 && (wind_directX + wind_directY) / 2 < 5)
    {
        level = "4 级";
    }

    wind = Horizontal + Vertical + "风" + " " + level;
}

实现效果

GIF

补充改进

正常游戏应该是射出箭之后立即弓箭数减少,等待中靶之后判断游戏是否应该继续或结束。在之前的版本中,判断游戏应该处于哪种状态我是写在FirstSceneController的Update中,现在我改为在箭颤抖动作做完之后再进行判断,这样实现的游戏效果就比较好了。修改FirstSceneController的ReduceArrowa函数,更名为CheckGamestatus。箭数量减少代码写在发射箭的函数中。

  • FirstSceneController(部分代码)
void Update ()
{
    if(game_start)
    {
        for (int i = 0; i < arrow_queue.Count; i++)
        {
            GameObject temp = arrow_queue[i];
            //场景中超过5只箭或者超出边界则回收箭
            if (temp.transform.position.z > 30 || arrow_queue.Count > 5)
            {
                arrow_factory.FreeArrow(arrow_queue[i]);
                arrow_queue.Remove(arrow_queue[i]);
            }
        }
    }
}
public void Shoot()
{
    if((!game_over || game_start) && arrow_num <= 10)
    {
        arrow = arrow_factory.GetArrow();
        arrow_queue.Add(arrow);
        //风方向
        Vector3 wind = new Vector3(wind_directX, wind_directY, 0);
        //动作管理器实现箭飞行
        action_manager.ArrowFly(arrow, wind);
        //副相机开启
        child_camera.GetComponent<ChildCamera>().StartShow();
        //用户能射出的箭数量减少
        recorder.arrow_number--;
        //场景中箭数量增加
        arrow_num++;
    }
}
public void CheckGamestatus()
{
    if (recorder.arrow_number <= 0 && recorder.score < recorder.target_score)
    {
        game_over = true;
        return;
    }
    else if (recorder.arrow_number <= 0 && recorder.score >= recorder.target_score)
    {
        round++;
        arrow_num = 0;
        if (round == 4)
        {
            game_over = true;
        }
        //回收所有的箭
        for (int i = 0; i < arrow_queue.Count; i++)
        {
            arrow_factory.FreeArrow(arrow_queue[i]);
        }
        arrow_queue.Clear();
        recorder.arrow_number = 10;
        recorder.score = 0;
        recorder.target_score = targetscore[round];
    }
    //生成新的风向
    wind_directX = Random.Range(-(round + 1), (round + 1));
    wind_directY = Random.Range(-(round + 1), (round + 1));
    CreateWind();
}

小结

本次游戏开始时候觉得很复杂,然后把游戏的每个部分分解来做,最后合起来的时候很幸运没有出现大的bug,都是按照预期来执行,之前的MVC架构扩展性果然很好。就算之后发现有些游戏逻辑实现的不太好,要修改也很方便。(ps:最后一关是需要50分,意味着十次都必须打中红心,其实很难的,毕竟不想做恭喜通关界面了。逃)

完整项目请点击传送门,Assets/Scenes/中的myScene是本次游戏场景

猜你喜欢

转载自blog.csdn.net/C486C/article/details/80058316