欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

我的第一个Unity的2D小游戏(Flappy Bird)

程序员文章站 2022-07-13 08:34:24
...

前言

兜兜转转跑来学习unity了,学习利用的是unity2017.2版本,在看过网上所谓的一堆零基础入门的视频后(确实0基础,无外乎都从界面开始介绍,然后是脚本基础几个API的介绍,然后讲解了下UGUI的一些应用,然后就没了),终于选定 Flappy Bird 作为第一个熟悉界面操作的作品。
根据视频内容分为以下

  1. 场景构建
  2. 根据需求添加相应的组件
  3. 功能脚本的分析及编写

过程

1.场景需求及构建

我的第一个Unity的2D小游戏(Flappy Bird)
最终结果大概如图,该过程通过构建使人熟悉了对于GameObject类的创建,以及稍微科普了下贴图与材质球的关系。以及通过对物体的position的z轴移动调整物体的的一个视觉关系,让人初步了解关于创建之后的视角关系。在创建之后,根据场景的从属关系或者同质关系将object进行层级的从属管理,以*的场景做最上级元素,将从属于场景的地面、水管等元素作为子类拖曳在场景下方,又由于水管为多个同质存在,通过创建空的GameObject类做父类进行统一管理。

2.根据需求添加相应的组件

在场景构建完毕后,由于构建基础只是在3D Object的Squd上涂上一层材质,并未具有更多的物理属性以支持我们进行对应事件的触发,我们要分析出最基本的碰撞事件会在什么之间发生,然后是他们的碰撞会触发什么后果,哪些物体需要刚体属性。
当物体需要到重力或者外力因素的时候即考虑使用riligbody组件,而如果只是需要使用其进行碰撞判定,则只添加collider组件即可,根据object的种类及需求选择,这里选择使用box collider来做碰撞判定。有如下分析:

  1. Bird——玩家操作本身,具有碰撞特性、受重力影响的特性,不能作为触发器使用;
  2. Pipe——游戏中的障碍,具有碰撞特性,却并不需要刚体特性,由于每过一根柱子我们要进行一次计分,而pipe本身为一个组合(上下两根),因此在该元素组合下增添一个empty object,添加碰撞特性做触发器,以触发事件;
  3. Land——游戏中的土地,在Bird坠落地面后不至于由于重力因素无限向下坠落,同样的只需要一个碰撞组件以承载Bird,并不需要接受外力。

    3.功能脚本的分析及编写

    在组件的添加的时候就考虑过对应要实现的相应功能,在根据原视频的教导下,基本能实现到这个小游戏需要的功能,但是代码却并不具备着良好的概念,这里写一下自己的体会,在过去的java学习中,遵循MVC模式,视图层可以直接调用改变模型业务层数据,而为了保持一个整洁清晰的开发过程,MVP模式的概念更适用于unity开发之中,通过编程过程中对象的泛型化、工厂化的形式进行编写,以节省更多的效率。

场景块管理类概念

设计场景管理模块的目的是在无较大程度视觉差异,不影响真实性效果的情况下人为的减少三角面数量,从而保证场景渲染的实时性,流畅性。

即通过更底层的功能拆分进行代码编写,在上级场景的管理类中进行整合,增大代码的复用性。

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
/// <summary>
/// 关于物体图片的材质球平移造成连续播放效果
/// </summary>
public class MainTexOffset : MonoBehaviour {

    private int frameNum;//帧率
    private float timer; //计数器
    private int frameCounter =0;//当前图片帧数
    public int frameTotal;//图片总帧数
    private float perFramePosition;//每帧的移动幅度
    [SerializeField]
    private MeshRenderer renderer;
    private float count;
    private Vector2 vec;

    private void Start()
    {
        Init();
    }

    // Update is called once per frame
    void Update () {


        PlayThePages();

    }
    /// <summary>
    /// 初始化程序
    /// </summary>
    public void Init()
    {
        frameTotal = 3;
        frameNum = 10;
        count = 1.0f / frameNum;
        vec = new Vector2(0.3333333f * frameCounter, 0);
        perFramePosition = 1 / frameTotal;
    }
    /// <summary>
    /// 播放图片的方法
    /// </summary>
    void PlayThePages()
    {
        timer += Time.deltaTime;
        if (timer >= count)
        {
            frameCounter++;
            frameCounter %= 3;//图片层数
            timer -= count;
            renderer.material.mainTextureOffset = vec;
        }
    }
}
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
/// <summary>
/// 二维物体的移动工具
/// </summary>
public class 2DMove : MonoBehaviour {
    [SerializeField]
    private Rigidbody rb;
    private float speedX;//移动速度X轴
    private float speedY;//移动速度Y轴
    private bool triggerType;//触发类型

    // Use this for initialization
    void Start () {
        Init();
    }

    public void Init()
    {

        if (null == rb)  rb = GetComponent<Rigidbody>(); //检测要是没被创建就自己实例一个
        rb.velocity = new Vector2(2.0f, 0);//给予初始速度
        triggerType = Input.GetMouseButtonDown(0);//给予触发事件
    }
    /// <summary>
    /// 触发后事件
    /// </summary>
    // Update is called once per frame
    void Update () {
        if (triggerType)
        {
            rb.velocity = new Vector2(2.0f, 2.0f);
        }
    }
}

至此Bird相关的脚本已经编写完毕,实现了其图片的播放及移动的功能。然后是场景的初始化事件——柱子的随机生成。

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

public class RandomApp : MonoBehaviour {

    [SerializeField]
    private GameObject pipe;
    private Transform tf;

    // Use this for initialization
    void Start () {
        if (pipe == null) pipe = GetComponent<GameObject>();
        tf = GetComponent<Transform>();
    }

    /// <summary>
    /// 场景初始化引用
    /// </summary>


    public void RandomY()
    {
        float ranY =  Random.Range(-0.18f, 0.065f);
        tf.localPosition = new Vector2(transform.localPosition.x,ranY);
    }
}

这里设置了一个固定X轴在一定范围随机移动Y轴的类,但并没有在start函数调用时添加,他仅作为提供参数的类,在其上级管理场景中统一调用:

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

public class PipesMan : MonoBehaviour {
    [SerializeField]
    private RandomApp [] ras;

    // Use this for initialization
    void Start () {
        RandomAllAppPos();

    }

    // Update is called once per frame
    void Update () {

    }

    public void RandomAllAppPos()
    {
        for (int i = 0; i < ras.Length; i++)
        {
            ras[i].RandomY();
        }
    }
}

然后是场景的变化,在视频中,通过将场景作为预设物prefab,在通过对应位置的触发器后进行Instantiate生成,但是考虑到无限生成的场景将极大考验电脑的运算能力,也没法通过Destory方法指定删除某一个场景,作为熟悉预设物的生成的练习还凑合,但是考虑到实用性就是极为不智的,于是有了以下的场景管理代码:

public class blockS : MonoBehaviour {

    [SerializeField]
    private PipesMan rapps;
    [SerializeField]
    private Transform tf;
    // Use this for initialization

    void Start () {
        if (tf == null) tf = gameObject.GetComponent<Transform>();
        if (rapps == null) Debug.Log("there is no Management");
    }

    /// <summary>
    /// 移动场景块到下一个位置
    /// </summary>
    public void Move()
    {
        tf.position += new Vector3(54f, 0, 0);
        rapps.RandomAllAppPos();
    }
}

在移动的过程中再度调用随机生成功能,以作为一个新生成的关卡。而move方法则通过绑定触发器使用,触发器获取当前所在的上级元素tag来获取位置,进行对指定目标的移动。

后续还有计分、游戏结束等,通过简单的判定及api调用即可完成。