Unity2D项目:Ruby's Adventure
  Wd9zM9Ft6LTY 2023年11月02日 85 0

2D开发

1. 内容简介

  • 使用2D资源
  • 2D RPG游戏开发流程
  • 创建并控制角色(脚本开发)
  • 设置动态精灵
  • 学习简单特效(粒子效果)

2. 内容详情

2.1 创建角色

Sprite:Unity中2D素材的默认存在形式,是Unity中的2D图形对象

PNG ---> Sprite ---> GameObject

2.2 移动角色

2D游戏,X、Y定位元素位置,X增大元素向移动,Y增大元素向移动

对象的X、Y的信息存放在Transform组件的Position属性中

// 每帧向右移动0.01米
void Update()
{
    Vector2 pos = this.transform.position;
    pos.x += 0.01f;
    this.transform.position = pos;
}

注:transform.position的类型为Vector3,结构体类型,该值仅为其内存的拷贝,无法对其进行直接修改

2.3 控制角色

控制角色,最基础的用户交互

2.3.1 控制方式
  • 鼠标键盘
  • 手柄
  • 手机触屏、陀螺仪
  • 体感
  • 可穿戴设备(VR、AR、瞳孔捕捉)
  • 声控
2.3.2 键盘控制

Input类:交互接口类,用以获取交互信息

// 获取水平输入
float horizontal = Input.GetAxis("Horizontal");
// 获取垂直输入
float vertical = Input.GetAxis("Vertical");

按左键,获取-1.0f,按右键,获取1.0f

按上键,获取1.0f,按下键,获取-1.0f

// 根据输入修改坐标
pos.x = pos.x + horizontal * step;
pos.y = pos.y + vertical * step;

Unity中默认使用Input Manager中设置项目的输入管理,可进行配置或自定义

负值键触发后设置值为-1,正值键触发后设置值为1

注:也可使用GetKey等方法获取用户输入,但局限性强不推荐

2.3.3 时间与帧率

Update事件按帧率触发,该触发次数受硬件配置的影响,会造成不同用户间的体验效果有所不同

解决方案Ⅰ:锁帧(垂直同步)

// 开启垂直同步
QualitySettings.vSyncCount = 0;
// 锁帧为60帧
Application.targetFrameRate = 60;

锁帧会降低画面效果(帧数降低)

解决方案Ⅱ:时间计算

单位/秒为单位计算位移,而非单位/帧

Time.deltaTime:每帧的时间间隔,浮点型

将移动速度乘以每帧的时间,则移动速度会以秒表示

// 根据输入修改坐标
pos.x = pos.x + horizontal * step * Time.deltaTime;
pos.y = pos.y + vertical * step * Time.deltaTime;

2.4 瓦片地图

2.4.1 概念

Tilemap是2D游戏中构建世界的工具

该系统可存储和操作Tile资源,并将信息传输给其他组件(如Tilemap Renderer和Tilemap Collider 2D)

创建瓦片地图时,会默认创建一个Grid组件,作为瓦片地图的父级,并在瓦片布置时作参考

相关概念:

  • Sprite:纹理的容器
  • Tile:包含一个Sprite,以及两个属性,颜色和碰撞体类型
  • Tile Pattle:瓦片调色板
  • Brush:调色板的笔刷
  • Tilemap:瓦片地图,可以在Tilemap上增加Tile
  • Grid:Tilemap的父对象
  • Tilemap Renderer:Tilemap的一部分,控制Tile在Tilemap上的渲染
2.4.2 Tilemap设置

Hierarchy --> Scene ---> GameObject ---> 2D Object ---> Tilemap

Tilemap设置后默认生成Tile Palette,Tile Palette内可创建Palette,在调色板中可进行Tile的相关操作

2D_TilePalette.png

2.4.3 Tile设置

Sprite拖拽至Palettle可自动创建Tile

注:Sprite原文件设置Pixels Per Unit(单元格绘制大小)

可进入Sprite Editor中切割精灵图

2D_SpriteEditor.png

注:切割前将精灵图的SpriteMode修改为Mulitple

2.4.4 场景地图排序

渲染器的Order in Layer属性设置先后关系

伪透视图:仿照3D视图的深度效果,实现2D元素间的遮罩关系

设置伪透视图:在y轴(y值小遮盖,y值大被遮盖)基于精灵的位置来绘制精灵(Unity的Project Manager的Graphic中设置 )

同时修改精灵图的锚点位置为底层,并将游戏对象的SpriteSortPoint属性修改为Pivot,以实现效果(Unity基于中心点判断对象的位置)

2D_伪透视图.png

2.5 2D物理系统

Unity提供了一套完整的物理系统,以计算物体间的移动与碰撞,为对象添加Rigidbody2D后对象可进行物理计算

2.5.1 碰撞
  1. 添加Rigidbody2D组件和Collider2D组件

    注:碰撞是相互的,故碰撞双方均需添加;游戏中的大型不可移动物体无需添加刚体组件,因为它们不实现移动逻辑

  2. 调整碰撞体边界

    注:注意考虑透视效果

  3. 锁定z轴旋转

  4. 碰撞抖动bug

    产生原因:Update函数中transform更新角色移动至碰撞体内,物理引擎检测生效并将角色设置到碰撞体外,进而产生抖动的现象

    解决方法:利用刚体属性的transform值进行移动

    // 获取刚体组件
    rigidbody2d = GetComponent<Rigidbody2D>();    
    
    // 获取当前位置
    Vector2 pos = this.transform.position;
    
    // 根据输入修改坐标
    pos.x = pos.x + horizontal * step * Time.deltaTime;
    pos.y = pos.y + vertical * step * Time.deltaTime;
    
    // 赋值新坐标
    rigidbody2d.position = pos;
    

    注:物理引擎相关操作需放在FixedUpdate函数中

  5. 为Tilemap添加Tilemap碰撞体,并根据需求修改tile的碰撞关系

  6. 为Tilemap添加整合碰撞体,以优化计算性能

    注:在Tilemap碰撞体内勾选使用整合选项,并修改刚体为静态,以提升性能

2.5.2 触发
  • 可收集对象
  1. 添加生命统计功能

    // 声明生命值
    public int maxHp = 10;
    private int curHp;
    
    void Start()
    {
     // 初始化当前生命值
     curHp = maxHp;
    }
    
    // 修改血量函数
    private void ChangeHp(int amount)
    {
     curHp = Mathf.Clamp(curHp + amount, 0, maxHp);
    }
    

    注:Clamp方法用以限定取值范围

  2. 添加触发器

触发器:一种特殊的碰撞体,不会阻止物体移动,但会检查碰撞,并触发事件(void OnTriggerEnter2D(Collider2D other))

  1. 添加触发逻辑

    private void OnTriggerEnter2D(Collider2D collision)
    {
     // 获取Ruby对象的脚本对象
     RubyController rubyController = collision.GetComponent<RubyController>();
     if (rubyController != null)
     {
         rubyController.ChangeHp(amount);
         Destroy(this.gameObject);
     }
    }
    
  2. 优化逻辑

    满血不吃血瓶

    else if (rubyController.GetCurHp() == rubyController.maxHp)
     Debug.Log("full hp");
    

    封装curHp为属性

    // 当前生命值
    private int _curHp;
    public int CurHp { get => _curHp; }
    
  • 伤害区域
  1. 添加触发器

  2. 添加触发逻辑(OnTriggerStay2D回调)

  3. 设置Sleeping Mode

    定义游戏对象如何在处于静止状态时“睡眠”以节省处理器时间

    NeverSleep:永不睡眠

    StartAwake:最初处于唤醒

    StartAsleep:最初处于睡眠,碰撞以唤醒

  4. 设置无敌时间

    // 无敌时间
    public float InvincibleTime = 2.0f;
    // 无敌状态
    private bool IsInvincible = false;
    // 无敌时间计时器
    private float InvincibleTimer;
    
    void Update()
    {
     // 无敌状态,计时器倒数
     if (IsInvincible)
     {
         InvincibleTimer -= Time.deltaTime;
         // 倒计时结束,取消无敌状态
         if (InvincibleTimer <= 0)
         {
             IsInvincible = false;
             InvincibleTimer = InvincibleTime;
         }
     }
    }
    
    public void ChangeHp(int amount)
    {
     // 无敌状态不伤血
     if (amount < 0 && IsInvincible) 
         return;
     _curHp = Mathf.Clamp(CurHp + amount, 0, maxHp);  // 限制方法
     // 受伤,进入无敌状态
     if (amount < 0)
         IsInvincible = true;
     Debug.Log($"hp = {CurHp}/{maxHp}");
    }
    
  5. 平铺区域

    1. scale复原
    2. DrawMode改为Tile
    3. 素材的MeshType改为Full Rect

对象平铺设置.png

2.6 敌人

2.6.1 敌人游荡

敌人按一定速率、在一定范围直线往返移动

[System.Serializable]
public enum Dirction
{
    HORIZONTAL,
    VERTICAL,
}

public class BotController : MonoBehaviour
{
    // 速度
    public float speed = 3;
    // 位移距离
    public float distance = 4;
    // 移动方向
    public Dirction moveDirction = Dirction.VERTICAL;
    // 记录移动距离
    private float moveRecord = 0;
    // 方向转换
    private int dirction = 1;
    // 刚体
    private new Rigidbody2D rigidbody;

    void Start()
    {
        rigidbody = this.GetComponent<Rigidbody2D>();
    }

    void FixedUpdate()
    {
        // 不在移动范围内
        if (moveRecord < 0 || moveRecord > distance)
            dirction = -dirction;

        // 获取当前位置
        Vector2 pos = this.transform.position;
        // 移动
        float moveDistance = speed * dirction * Time.deltaTime;
        if (moveDirction == Dirction.HORIZONTAL)
            pos.x += moveDistance;
        else
            pos.y += moveDistance;
        rigidbody.position = pos;

        // 更新位置记录
        moveRecord += moveDistance;
    }
}
2.6.2 敌人伤害

OnCollisionEnter2D:当刚体和某对象碰撞后调用,参数提供碰撞信息

void OnCollisionEnter2D(Collision2D collision)
{
    // 获取ruby的脚本
    var rubyController = collision.gameObject.GetComponent<RubyController>();
    if (rubyController != null)
        rubyController.ChangeHp(damage);
}

2.7 动画

2.7.1 Unity动画工作流程

动画剪辑(Animation Clip):动画的基本组成部分,包含游戏对象的位置变化、旋转或其他相关信息

动画剪辑一般和不同的状态相挂接,比如“静止状态”挂接站立不动的动画、“奔跑状态”挂接跑动的动画......

Animator Controller用以调度动画的播放及切换

Unity中通过Animator组件进行操作

2.7.2 Animator挂接

游戏对象添加Animator组件 >>> 创建Animator Controller >>> Controller挂接到Animator组件

Animator挂接.png

2.7.3 动画剪辑录制

选取待录制的游戏对象 >>> 选取Animation面板(Ctrl+6) >>> 创建Animation >>> 选取素材片段并拖入动画时间窗中 >>> 添加相应属性调整动画

Unity默认动画每秒渲染60次

2.7.4 构建动画关系
  1. 进入Animator面板

    动画状态机(state machine):每一个动画剪辑,以及一段动画剪辑与另一段动画剪辑的连接

    Layers:将动画用于角色的不同位置,可用用于3D

    Parameters:由脚本向Controller提供信息

    混合树(Blend Tree):根据参数来混合多段动画

  2. 添加并设置Blend Tree

    添加参数 >>> 添加Motion

BlendTree设置.png

  1. 用脚本为Animator Controller传值
// 动画控制器
private Animator animator;

animator = this.GetComponent<Animator>();

// 设置移动动画
animator.SetFloat("MoveX", dircX);
animator.SetFloat("MoveY", dircY);
2.7.5 基础动画总结

相关术语

术语 定义
动画剪辑(Animation Clip) 角色的简单动画数据,是控制动画的素材
Animator组件 用以管理动画素材,对模型动画化
Animator Controller 通过控制动画状态机和混合树来控制动画
状态机 一种用于控制动画状态交互情况的图
混合树 根据浮点动画参数在类似动画剪辑之间进行连续混合

动画工作流程

Animation创建动画剪辑 => Animator控制动画的播放与转换 => 脚本控制Animator Controller的参数

2.8 飞弹

// 子弹预制件脚本
public class BulletController : MonoBehaviour
{
    // 刚体
    private new Rigidbody2D rigidbody;

    void Start()
    {
        rigidbody = GetComponent<Rigidbody2D>();
    }

    // 发生碰撞
    void OnCollisionEnter2D(Collision2D collision)
    {
        Debug.Log($"Get Object => {collision.gameObject}");
        Destroy(this.gameObject);
    }

    /// <summary>
    /// 子弹移动
    /// </summary>
    /// <param name="dirc">发射方向</param>
    /// <param name="force">发射力</param>
    public void Launch(Vector2 dirc, float force)
    {
        // 对当前子弹施加一个力
        rigidbody.AddForce(dirc * force);
    }
}

// Ruby脚本
public class RubyController : MonoBehaviour
{
    // 子弹预制件
    public GameObject bulletPrefeb;
    
    void Update()
    {
        // 获取发射子弹输入  J键发射
        if (Input.GetKeyDown(KeyCode.J))
            Launch();
    }
    
    /// <summary>
    /// Ruby发射子弹
    /// </summary>
    private void Launch()
    {
        // 在指定位置创建子弹对象
        GameObject bullet = Instantiate(bulletPrefeb, 
            rigidbody2d.position + Vector2.up * 0.5f,
            Quaternion.identity);
        // 获取子弹脚本
        BulletController bulletController = bullet.GetComponent<BulletController>();
        // 发射子弹
        bulletController.Launch(staticDirction);
        // 发射子弹动画
        animator.SetTrigger("Launch");
    }    
}

相应问题:

  1. 通过Instantiate函数创建的游戏对象不会执行Start函数,故rigidbody组件获取异常,应将获取组件的代码写入Awake函数
  2. 子弹直接与玩家碰撞产生逻辑错误,应通过调整图层解决问题
  3. 未触发的子弹无法销毁,会影响游戏性能,可用计时器逻辑进行控制

2.9 玩家攻击

// Bot脚本
public class BotController : MonoBehaviour
{
    /// <summary>
    /// 修复机器人
    /// </summary>
    public void Fix()
    {
        // 转换修复状态
        isFixed = true;
        // 机器人无伤害
        damage = 0;
        // 播放修复动画
        animator.SetBool("IsFix", true);
    }
}

// 子弹脚本
public class BulletController : MonoBehaviour
{
    // 发生碰撞
    void OnTriggerEnter2D(Collider2D collision)
    {
        // 获取Bot脚本
        BotController botController = collision.GetComponent<BotController>();
        if (botController != null)
        {
            botController.Fix();
        }
        Destroy(this.gameObject);
    }
}

2.10 摄像头移动

  • 脚本控制Camera组件移动

  • 第三方组件Cinemachine

    1. 安装Cinemachine插件(已集成至Unity)
    2. 添加2D Cinemachine
    3. 更改虚拟摄像机的设置(实际摄像机会复制虚拟摄像机的设置)
    4. 添加跟随
    5. 添加Confiner组件以限定边界

2.11 Unity视觉特效

2.11.1 粒子系统

模拟渲染许多称为粒子的小图像或网格以产生视觉效果。系统中的每个粒子代表效果中的单个图形元素。系统共同模拟每个粒子产生的完整效果的印象。

如:火、烟、液体等,通过网格(3D)或精灵(2D)素材很难描绘这些对象,而粒子系统则可实现。

Unity的粒子系统的选择:

  • 内置粒子系统 => CPU渲染
  • Visual Effect Graph => GPU渲染
2.11.2 粒子系统的构建
  1. 准备粒子的精灵图素材

  2. 创建particle effect游戏对象

  3. 将精灵图绑定至粒子对象上

  4. 配置粒子系统

    • Texture Sheet Animation渲染粒子播放的相关逻辑,如播放精灵图、播放次序等
    • Shape设定粒子的生成形状,如生成半径、角度等
    • 基础设置控制粒子的播放速度、生命周期、尺寸等
    • Cover over time设定粒子结束播放的颜色和透明度变化
    • Size over time设定粒子结束播放的大小变化
  5. 将粒子预制件与游戏对象预制件绑定为一个新预制件,即粒子作为游戏对象的子预制件

  6. 脚本控制烟雾

    // 清除冒烟特效
    if (smokeEffect != null)
     smokeEffect.Stop();
    
  7. 脚本控制子弹碰撞特效

    // 子弹脚本
    public class BulletController : MonoBehaviour
    {
     void OnTriggerEnter2D(Collider2D collision)
    	{
     	// 生成子弹击中特效
     	Instantiate(hitEffectPrefeb, this.transform.position, Quaternion.identity);
    	}
    }
    
    // 子弹特效脚本
    public class HitEffect : MonoBehaviour
    {
     private float destroyTime = 1.0f;
    
     private void Update()
     {
         // 子弹特效的销毁
         if (destroyTime < 0)
             Destroy(this.gameObject);
         else
             destroyTime -= Time.deltaTime;
     }
    }
    

2.12 UI系统

User Interface,用于用户与游戏的信息交互与反馈

2.12.1 Unity UI分类
  • UI toolkit:Unity最新的UI系统,优化跨平台性能,基于标准Web技术,但暂时缺少部分功能
  • Unity UI(UGUI):大多数商业软件沿用的UI技术,基于游戏对象开发
  • NGUI:第三方UI系统,UGUI的前身
  • IMGUI:纯代码驱动的UI工具包,一般用于构建Unity编辑器内的UI界面
2.12.2 UGUI
  1. 添加Canvas

    UGUI的UI组件均存放在**画布(Canvas)**上,可设置画布的渲染模式

  2. 添加Image

    锚点枢轴的相对距离不变,通过设置锚点位置保证UI的位置在不同分辨率下保持一致

  3. 添加遮罩

    遮罩用于控制图像的显示范围

    一般先建立遮罩层,再将Image作为遮罩层的子元素进行控制

    注:调节锚点位置以保证

  4. 脚本控制遮罩

    // ImageExtension
    internal static class ImageExtension
    {
     // 获取遮罩长度
     public static float GetRectWidth(this Image image) => image.rectTransform.rect.width;
    }
    
    // 生命条UI脚本
    public class HealthBarController : MonoBehaviour
    {
     // 静态生命条实例
     public static HealthBarController Instance { get; private set; }
     // UI图形对象
     public Image mask;
     // 初始遮罩层长度
     private float originLength;
    
     void Awake()
     {
         // 获得唯一实例
         Instance = this;
     }
    
     void Start()
     {
         // 获取遮罩层图像的宽度值
         originLength = mask.GetRectWidth();
     }
    
     /// <summary>
     /// 设置遮罩长度
     /// </summary>
     /// <param name="percentage">血量修改百分比</param>
     public void SetRectWidth(float percentage) =>
         mask.rectTransform.SetSizeWithCurrentAnchors(RectTransform.Axis.Horizontal, originLength * percentage);
    }
    
    // Ruby脚本
    public class RubyController : MonoBehaviour
    {
     public void ChangeHp(int amount)
    	{
         // UI显示
         Debug.Log($"hp = {CurHp}/{maxHp}");
         HealthBarController.Instance.SetRectWidth(CurHp / (float)maxHp);  
    	}
    }
    

2.13 剧情系统

2.13.1 创建NPC
  1. 创建游戏对象
  2. 创建并调整动画的播放,调整透视关系
  3. 创建碰撞体
  4. 设定图层为NPC图层
2.13.2 射线投射

将射线投射到场景中并检查该射线是否与碰撞体相交的行为,用于游戏对象的检测

var hit = Physics2D.Raycast(OffsetPoint(0.2f), staticDirction, 1.5f, LayerMask.GetMask("NPC"));
// 检测碰撞
if (hit.collider != null)
{
    Debug.Log($"raycast get collider {hit.collider.gameObject}");
    // 检测交互  "E"键
    if (Input.GetKeyDown(KeyCode.E))
    {
        Debug.Log("Text here...");
    }
}

设置Layer可以限制投射的生效层

2.13.3 对话系统
  1. 为NPC添加Canvas并设置

  2. 为Canvas添加外框(Image)

  3. 为外框添加Text文本

    第一次使用TMP组件需要导入组件至项目中

  4. NPC脚本控制Text的显示

void Update()
{
    // 开始计时
    if (displayTimer >= 0)
    {
        displayTimer -= Time.deltaTime;
    }
    // 超时,文本框隐藏
    else
    {
        dialogBox.SetActive(false);
    }
}

/// <summary>
/// 显示文本框
/// </summary>
public void ShowDialogBox()
{
    displayTimer = displayTime;
    dialogBox.SetActive(true);
}
2.13.4 Text Mash Pro
  1. 中文字体问题
  • 动态字体

下载中文开源字体,直接再project中将字体转换为字体资源即可使用

  • 静态字体

window >>> TextMeshPro >>> Font Asset Creator >>> 将已有的常用字体套用字体文件的字体

注:静态字体性能高于动态字体,针对文本量大的游戏(AVG游戏)应优先使用静态字体

  1. 多页显示
  1. Overflow设置为Page
  2. 设置脚本控制翻页逻辑
// 获取总页数(start函数无法获取)
totalPage = tmp.textInfo.pageCount;
// 开始计时
if (hintBtnRange.isInRange)
{
    // 文本对话翻页   "空格"翻页
    if (Input.GetKeyUp(KeyCode.Space))
    {
        // 翻到最后一页回到第一页,否则继续翻页
        curPage = (curPage == totalPage) ? 1 : curPage + 1;
        // 根据当前页tmp显示
        tmp.pageToDisplay = curPage;
    }
}

2.14 音频系统

2.14.1 音频基础关系

Unity中通过Audio Source(音源)和Audio Linsener(音频接收者)构建基础的音频系统

Audio Source的资源来自于Audio Clip(音频剪辑),Unity可接受多数音频格式文件(如WAV、MP3、Ogg等)

2.14.2 音频系统定义
  • Audio Clip是Unity的实体发声文件,可导入.wav, .mp3, .ogg等音频文件,也可导入.mod, .xm等音轨模块
  • Audio Source用以控制音频剪辑的播放,类似于动画系统的Animator
  • Audio Lisener用以接收Audio Source发出的声音
2.14.3 工作流程
  1. 游戏对象绑定音频

创建固定的游戏对象,绑定固定的音源播放音频

  1. 代码控制音频
/// <summary>
/// 播放音频
/// </summary>
/// <param name="audioClip">待播放的音频剪辑</param>
public void PlayAudio(AudioClip audioClip, float volume = 1.0f)
{
    audioSource.PlayOneShot(audioClip, volume);
}
2.14.4 空间音频

根据Audio Lisener的位置与Audio Source的位置播放音频,在Spatial Blend和3D Sound Setting中调整

2.15 打包发布

  1. 游戏信息配置:在Project Setting的Player中设置开发者信息
  2. 发布配置:在Build Setting设置发布信息
【版权声明】本文内容来自摩杜云社区用户原创、第三方投稿、转载,内容版权归原作者所有。本网站的目的在于传递更多信息,不拥有版权,亦不承担相应法律责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@moduyun.com

  1. 分享:
最后一次编辑于 2023年11月08日 0

暂无评论