From:M_Studio Unity2D教程《Robbie》

导入预制素材

导入Assets文件夹

_Extended Addons Gizmos VFX
额外扩展包,包含预制(Prefabs) 2D插件 CinemaChine摄像机插件 视觉特效

Tilemap

TilePalette

  1. windows -> 2D -> TilePalette(瓦片调色)
  1. Hierarchy窗口显示-> 2D -> Tilemap

  2. 在对应 Tilemap 上绘制需要 TilePalette 对应绘画内容 Active Tilemap

  3. 背景 Tilemap 叠加透明 Tilemap 需要Active Tilemap对应层(如 platform 上叠加 shadow)

Shorting Layer

选中Hierarchy窗口中Tilemap对象,Inspector -> Tilemap Renderer

Order in Layer:同一个Shorting Layer,数值越大越在前面

2

Tilemap(Rule Tile)

  1. Tiles 文件夹 create -> Tile插件 -> Rule Tile

  2. 新建的 Rule Tile(BG Details)拖至 TilePalette,绘制背景可以自动填充

  • 转角的设置x的位置就是转角位置

    绘制:B

    擦除:连按shift

3

  • 瓦片随机分布

4

自制笔刷

  1. Prob文件夹 -> 右键新建Brushes -> Prefab Brush

  2. 修改 Tile Palette 的笔刷,可以将自制图形按网格刷到图中

5


物理碰撞

产生碰撞的条件

  • 双方有碰撞器(Collider),一方有刚体(Rigidbody)
  • 触发器(Is Trigger)是Collider的一个属性,Is Trigger = true 发生碰撞

Collider

碰撞器,用于定义需要进行物理碰撞的GameObject的形状

Tilemap具有自己的碰撞器 Tilemap collider 2D,Inspector -> Component

Composite Collider

6

  • player逐一碰撞Platform的Collider,会卡在Collider之间,通过将Platform合并成一个碰撞器解决:

    Tilemap Collider 勾选 Used By Composite,添加 Composite Collider 组件,

    Rigidbody 设置 Static

    Inspector 顶部信息勾选 Static

7

Rigidbody

  • Body Type

    | Dynamic(动态刚体) | Kinematic(运动学刚体) | Static(静止刚体) |
    | :—————————————- | ———————————- | ————————— |
    | 用于模拟运动,具有质量和阻力 | 在无重力质量影响下移动 | 静止刚体,无位移 |

  • 属性设定:collision Detection 选择 Continuous 连续判断是否碰撞

    ​ Interpolate 选择 Interpolate 碰撞产生微小形变

    8

​ Constraints z轴固定使碰撞后z轴不发生变化

碰撞摩擦力

Collider 添加物理材质,可以修改物理材质的摩擦力数值,置0为无摩擦力

9


图层

用于代码判断Player、Enemy、Platform等,注意修改

10


移动

  1. 获取刚体、碰撞体

  2. 控制地上移动的函数,按键—左右:Horizontal

  3. 角色面向:vilocity scale

  4. 参数:角色移动速度—数值,数值基本都是浮点型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// 地面移动
void GroundMovent()
{
// x轴方向(左右) -1f 1f,键盘无输入时归0,不会滑动
xVelocity = Input.GetAxis("Horizontal");

// x轴上移动,y轴不变
rb.velocity = new Vector2(xVelocity * Speed, rb.velocity.y);
}

// 移动时产生面向调整
void FilpDirection()
{
// 面向朝左,localscale设置-1
if (xVelocity < 0)
transform.localScale = new Vector2(-1, 1);
// 面向朝右,localscale设置1
if (xVelocity > 0)
transform.localScale = new Vector2(1, 1);
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// 有判断条件的地面移动
void GroundMovent()
{
// 优先判断是否下蹲
if (Input.GetButton("Crouch") && !isCrouch && isOnGround)
Crouch();
// 未按下Crouch按键且处于下蹲状态/或在地面上就要起立
else if (!Input.GetButtonDown("Crouch") && isCrouch && !isHeadBlocked)
StandUp();
else if (!isCrouch && isOnGround)
StandUp();
// 若下蹲,需要先确定移动速度
if (isCrouch)
xVelocity /= crouchSpeedDivisor;

// x轴方向(左右) -1f 1f,键盘无输入时归0,不会滑动
xVelocity = Input.GetAxis("Horizontal");

// 开始在x轴上移动,y轴不变
rb.velocity = new Vector2(xVelocity * Speed, rb.velocity.y);
}

下蹲

  1. InputManager—按 Jump 模板新建下蹲系统按键设置
  2. 下蹲的移动速度—减慢

  3. 下蹲状态碰撞体 y 轴减半

11

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
   // 原始碰撞体尺寸
colliderStandSize = coll.size;
colliderStandOffset = coll.offset;
// 下蹲碰撞体尺寸
colliderCrouchSize = new Vector2(coll.size.x, colliderStandSize.y / 2f);
colliderCrouchOffset = new Vector2(coll.offset.x, colliderStandOffset.y / 2f);

// 下蹲动作
void Crouch()
{
isCrouch = true;

coll.size = colliderCrouchSize;
coll.offset = colliderCrouchOffset;
}

// 下蹲恢复站立
void StandUp()
{
isCrouch = false;
coll.size = colliderStandSize;
coll.offset = colliderStandOffset
}


跳跃

  • 修改重力参数为-50 消除重力影响

12

按键参数设置

  • 单次按 /长按 /下蹲跳
1
2
3
4
5
      // 按键放入Updata方便调用
jumpPressed = Input.GetButtonDown("Jump"); // 按下即触发一次
jumpHeld = Input.GetButton("Jump"); // 按下且一直延续
crouchHeld = Input.GetButton("Crouch");
crouchPressed = Input.GetButtonDown("Crouch");
  • 判断是否在地面才能跳跃 ,需环境监测参数

    Update:每一帧执行一次,不同设备每秒执行的帧数不一样

    FixedUpdate:固定时间执行,默认每秒执行50次。与物理效果相关。

1
2
3
// 由于Updata帧数不稳定,FixUpdata为50帧,对于跳跃判断不一致,设置帧数为50解决跳跃键无效果问题
// 实测 Build 后仍存在跳跃无效问题,将跳跃按键和跳跃函数均放到 Updata
Application.targetFrameRate = 50;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
// 跳跃动作
void MidAirMovement()
{
// 单次按跳跃 且在地面上 且不在跳跃状态--跳跃状态即添加跳跃力
if (jumpPressed && isOnGround && !isJump)
{
// 下蹲跳跃
if (isCrouch && isOnGround)
{
StandUp();
rb.AddForce(new Vector2(0f, crouchJumpBoost), ForceMode2D.Impulse);
}

isOnGround = false;
isJump = true;

// Time.time记录当前游戏开始后的时间,增加加持续按键时间为本次跳跃时间
jumpTime = Time.time + jumpHoldDuration;

// 添加力的方法:rigidbody.Addforce,ForceMode2D.Impulse模型为突然增加这个力
rb.AddForce(new Vector2(0f, jumpForce), ForceMode2D.Impulse);
}

// 跳跃状态下,如果长按则持续增加跳跃力
else if(isJump)
{
if (jumpHeld)
rb.AddForce(new Vector2(0f, jumpHoldForce), ForceMode2D.Impulse);
// 停止跳跃
// 记录的本次跳跃时间jumpTime最终会小于持续增加的游戏时间
if (jumpTime < Time.time)
isJump = false;
}
}

射线判断碰撞体

需要判断双腿分别是否在地面上,仅用一个GameObject触碰点判断难以实现,设置两个过于繁琐

射线

射线函数

1
2
// 起点(GameObject位置+偏移),方向,距离,图层
RaycastHit2D hit = Physics2D.Raycast(pos + offset, rayDirection, length, layer);

画射线

1
Debug.DrawRay(pos + offset, Vector2.down, Color.red, 0.2f);

射线功能重载

1
2
3
4
5
6
7
8
9
10
11
12
// 射线功能重载
RaycastHit2D Raycast(Vector2 offset, Vector2 rayDirection, float length, LayerMask layer)
{
Vector2 pos = transform.position;
// 起点(GameObject位置+偏移),方向,距离,图层
RaycastHit2D hit = Physics2D.Raycast(pos + offset, rayDirection, length, layer);

// 画线颜色根据不同颜色变化,三目运算
Color color = hit ? Color.red : Color.green;
Debug.DrawRay(pos + offset, rayDirection * length, color);
return hit;
}

物理环境判断

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 物理环境判断
void PhysicsCheck()
{
// 判断是否在地面上(是否触碰图层coll.IsTouchingLayers())
RaycastHit2D leftCheck = Raycast(new Vector2(-footOffset, 0f), Vector2.down, groundDistance, groundLayers);
RaycastHit2D rightCheck = Raycast(new Vector2(footOffset, 0f), Vector2.down, groundDistance, groundLayers);
if (leftCheck || rightCheck)
isOnGround = true;
else isOnGround = false;

// 判断头顶是否碰撞
RaycastHit2D headCheck = Raycast(new Vector2(0f, coll.size.y), Vector2.up, groundDistance, groundLayers);
if (headCheck)
isHeadBlocked = true;
else
isHeadBlocked = false;
}

悬挂

悬挂条件

  • 头顶无额外平台— !blockedCheck
  • 眼前有其他墙壁— wallCheck
  • 角色离墙壁近— wallCheck.distance
  • 头di顶超过上方平台— ledgeCheck

13

悬挂射线检测

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// 悬挂射线--角色面向控制
float direction = transform.localScale.x;
// 射线发射方向(二维向量)
Vector2 grabDir = new Vector2(direction, 0f);
// 头顶左上右上射线
RaycastHit2D blockedCheck = Raycast(new Vector2(footOffset * direction, playerHeight), grabDir, grabDistance, groundLayers);
// 眼睛前方射线
RaycastHit2D wallCheck = Raycast(new Vector2(footOffset * direction, eyeHeight), grabDir, grabDistance, groundLayers);
// 头顶检测
RaycastHit2D ledgeCheck = Raycast(new Vector2(reachOffset * direction, playerHeight), Vector2.down, grabDistance, groundLayers);

// 不在地面上 向上跳到头 上方有plantform 前面有wall 头顶无墙
if (!isOnGround && rb.velocity.y < 0f && ledgeCheck && wallCheck && !blockedCheck)
{
// 每次挂壁高度一样
Vector3 pos = transform.position;
// x固定与墙面的距离,留出手臂距离
pos.x += (wallCheck.distance-0.05f) * direction;
// 头顶与墙平行
pos.y -= ledgeCheck.distance;
transform.position = pos;
rb.bodyType = RigidbodyType2D.Static; // 修改为静止状态
isHanging = true;
}

悬挂后动作

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 悬挂状态二次跳
if (isHanging)
{
// 按跳跃上平台
if (jumpPressed)
{
rb.bodyType = RigidbodyType2D.Dynamic;
rb.velocity = new Vector2(rb.velocity.x, hangingJumpForce);
isHanging = false;
}
// 按下蹲下去
if (crouchPressed)
{
rb.bodyType = RigidbodyType2D.Dynamic;
isHanging = false;
}
}

摄像机(Cinemachine)

  • Camera->Background:摄像机拍到初游戏区域以外的背景颜色

    Camera->Projection:Orthographic 矩形摄像机无透视效果/ Perspective 透视摄像机(近实远虚)

14

  • Tilemap图层前后关系的设置可以通过transform->position->z轴

15

  • 相机跟随,添加摄像机插件:Windows -> Package Manager -> Cinemachine

    Cinemachine->Body属性设置镜头大小跟踪大小

  • 摄像机边界:

    Add Extension - Cinemachine Confiner | 新建GameObject

    添加 Polygon Collider 设置为 is Trigger

    摄像机焦点会在Polygon Collider边界上移动,大小为Camera Distance

    16


2D灯光效果(法线贴图)

  • 为对象增加材质(纹理,法线贴图),使其可以接受光照

    角色法线贴图替换原本GameObject,调整图层

  • 创建点光源:Hierarchy -> create -> light ->pointlight

  • 环境光设置:Windows -> Rendering -> Lighting窗口 -> Environment


角色动画(Blend Tree)

Animator -> Blend Tree

28

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
public class PlayAnimation : MonoBehaviour
{
Animator anim;
// 具有材质的角色继承原本2D Ribbie的动作代码,PlayerMovement类
PlayerMovement movement;
Rigidbody2D rb;

// 状态编号
int groundID;
int hangingID;
int crouchID;
int speedID;
int fallID;

void Start()
{
anim = GetComponent<Animator>();
movement = GetComponentInParent<PlayerMovement>(); // 继承父级
rb = GetComponentInParent<Rigidbody2D>(); // 继承父级

// 动画名状态传递字符型变为数值型,防止字符型传递出问题
groundID = Animator.StringToHash("isOnGround");
hangingID = Animator.StringToHash("isHanging");
crouchID = Animator.StringToHash("isCrouching");
speedID = Animator.StringToHash("speed");
fallID = Animator.StringToHash("verticalVelocity");
}

void Update()
{
// anim.SetFloat("speed", Mathf.Abs(movement.xVelocity));
// anim.SetBool("isOnGround", movement.isOnGround);
// 将父级的isOnGround状态传递进来
anim.SetBool(groundID, movement.isOnGround);
anim.SetFloat(speedID, Mathf.Abs(movement.xVelocity));
anim.SetBool(hangingID, movement.isHanging);
anim.SetBool(crouchID, movement.isCrouch);
anim.SetFloat(fallID, rb.velocity.y); // rb.velocity.y纵向跳跃速度
}


音效控制(Audio Manager)

Audio Manager

建立AudioManager GameObject和对应脚本控制音效

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
public class AudioManager : MonoBehaviour
{
// 静态音效管理,不可更改
static AudioManager current;
public AudioClip ambientClip; // 环境音效,AudioClip 加载音效

AudioSource ambientSource;
private void Awake()
{
// Singleton 内部创建一个实例,通过实例来访问,变量就是脚本本身。不摧毁会重复添加
if (current != null)
{
Destroy(gameObject);
return;
}
current = this;
DontDestroyOnLoad(gameObject); // 关卡加载不会销毁音效

ambientSource = gameObject.AddComponent<AudioSource>();
StartLevelAudio();
}

// BGM
void StartLevelAudio()
{
current.ambientSource.clip = current.ambientClip;
current.ambientSource.loop = true;
current.ambientSource.Play();
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
public class AudioManager : MonoBehaviour
{
// 静态音效管理,不可更改
static AudioManager current;

[Header("环境声音")]
public AudioClip ambientClip; // 环境音效,AudioClip 加载音效
public AudioClip musicClip; // 背景音
[Header("Robbie音效")]
public AudioClip[] walkStepClips; // 声音片段数组存放走路片段
public AudioClip[] crouchStepClips; // 声音片段数组存放下蹲走路片段
public AudioClip jumpClip; // 跳跃声音片段

public AudioClip jumpVoiceClip;

// 直接创建AudioSource播放音效
AudioSource ambientSource;
AudioSource musicSource;
AudioSource fxSource;
AudioSource playSource;
AudioSource voiceSource;


private void Awake()
{
// Singleton 内部创建一个实例,通过实例来访问,变量就是脚本本身。不摧毁会重复添加
if (current != null)
{
Destroy(gameObject);
return;
}
current = this;

DontDestroyOnLoad(gameObject); // 关卡加载不会销毁音效

ambientSource = gameObject.AddComponent<AudioSource>();
musicSource = gameObject.AddComponent<AudioSource>();
fxSource = gameObject.AddComponent<AudioSource>();
playSource = gameObject.AddComponent<AudioSource>();
voiceSource = gameObject.AddComponent<AudioSource>();

StartLevelAudio();
}

// BGM
void StartLevelAudio()
{
current.ambientSource.clip = current.ambientClip;
current.ambientSource.loop = true;
current.ambientSource.Play();

current.musicSource.clip = current.musicClip;
current.musicSource.loop = true;
current.musicSource.Play();
}

// 脚步声7个片段,随机选取一个作为当前脚步声音效
static public void PlayFootstepAudio()
{
int index = Random.Range(0, current.walkStepClips.Length); // 随机获取片段

current.playSource.clip = current.walkStepClips[index];
current.playSource.Play(); // 播放
}

// 下蹲移动同上
static public void PlayCrouchFootstepAudio()
{
int index = Random.Range(0, current.crouchStepClips.Length); // 随机获取片段

current.playSource.clip = current.crouchStepClips[index];
current.playSource.Play(); // 播放
}

// 跳跃音效
static public void PlayJumpAudio()
{
current.playSource.clip = current.jumpClip;
current.playSource.Play();

current.voiceSource.clip = current.jumpVoiceClip;
current.voiceSource.Play();
}
}
  • 音效延迟播放

    1
    current.fxSource.PlayDelayed(1f)

Audio Mixer

  • 调整每个声音的效果 window -> Audio -> Audio Mixer
1
2
3
4
   // Audio Mix
public AudioMixerGroup ambientGroup;

ambientSource.outputAudioMixerGroup = ambientGroup;

17

  • play过程保存设置:Edit in Play Mode

18

  • 用代码编辑volume

19


视觉效果(Post Processing)

  • Main Camera -> Post-processing Layers 在所选 layers 上增加特效

20

  • 新建一个 GameObject 作为 Layer,增加 Post-processing Volume

    并修改layer Profile选择特效

    21

  • 添加后无效果解决方法:

    Edit > Projec Settings / Player / Other Settings 下的 Color Space 设置为 Linear

  • 导出到移动设置时,Post Processing 会影响效果,有Fast Mode适用于移动设备

22


相机抖动(Camera Shake)

  1. 每次收集宝珠执行震动(player 碰撞 orb)

  2. Camera 添加 Cinemachine Impulse Linster

  3. Orb 添加 Cinemachine Collision Impulse Source 并添加Explosion Shake参数

23


死亡机制(Spikes&Death)

  • 触碰尖刺
1
2
// 一个碰撞体2D进入触发器
void OnTriggerEnter2D(Collider2D collision)
  • 触发烟雾效果替换player位置
1
Instantiate(gameObject, position, rotation)
  • player消失
1
gameObject.SetActive(false);
  • 死亡后场景重置
1
2
using UnityEngine.SceneManager
SceneManager.LoadScene(SceneManager.GetActiveScene().buildIndex当前编号)

24

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
public class PlayHealth : MonoBehaviour
{
public GameObject deathVFXPrefab; // 添加死亡烟雾特效(死亡残影效果同理)
public GameObject deathShadow;
int trapslayer; // 陷阱图层

void Start()
{
trapslayer = LayerMask.NameToLayer("Traps");
}

private void OnTriggerEnter2D(Collider2D collision)
{
// layer为int型,用NametoLayer转换
if (collision.gameObject.layer == trapslayer)
{
// 烟雾效果替换角色,指定项目放置到指定位置和角度
Instantiate(deathVFXPrefab, transform.position, transform.rotation);
// 残影效果,旋转角度调整变化
Instantiate(deathVFXPrefab, transform.position, Quaternion.Euler(0, 0, Random.Range(-45, 90)));

// 触碰到trapslayer 角色消失
gameObject.SetActive(false);

// 死亡音效
AudioManager.PlayDeathAudio();

SceneManager.LoadScene(SceneManager.GetActiveScene().buildIndex);
}

}
}
  • 其他陷阱:

    设置范围 两个Collider,触碰到外部Collider尖刺弹出

25


收集物品(Collection Orb)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public class Orb : MonoBehaviour
{
int player;
public GameObject explosionVFXPrefab;

void Start()
{
player = LayerMask.NameToLayer("Player");
}


private void OnTriggerEnter2D(Collider2D collision)
{
if (collision.gameObject.layer == player)
{
// 爆炸烟雾特效替换宝珠位置和角度
Instantiate(explosionVFXPrefab, transform.position, transform.rotation);
gameObject.SetActive(false);

AudioManager.PlayOrbAudio();
}
}
}


GameManager

  • 统筹管理游戏,创建一个新的GameObject挂载脚本

  • GameManager的思想:一个功能(类)将自己注册给管理器(传参进去),管理器就可以使用这个类的方法(函数),从而其他类(例如状态)可以直接调用管理器里面的功能实现

    例如:

    • GameManager 类生成静态实例 instance,通过调用 instance 可以调用自己的函数

      1
      static GameManager instance;    // 单例模式 生成静态类(实例) 可以直接访问
    • instance 包含了 SceneFader 类,SceneFader 类的实例 fader 通过RegisterSceneFader(SceneFader obj) 赋值

      1
      2
      # GameManager
      SceneFader fader; // 场景切换Fader动画
      1
      2
      3
      4
      5
      // 播放切换场景动画UI:SceneFader调用Manager的RegisterSceneFader函数注册
      public static void RegisterSceneFader(SceneFader obj)
      {
      instance.fader = obj;
      }
    • SceneFader类注册 GameManager.RegisterSceneFader(this)

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      # SceneFader
      // 控制SceneFader的UI动画效果
      public class SceneFader : MonoBehaviour
      {
      Animator anim;
      int faderID;

      public void Start()
      {
      anim = GetComponent<Animator>();

      faderID = Animator.StringToHash("Fade");

      // 在GameManager里面注册this使用了RegisterSceneFader函数,传参进obj
      GameManager.RegisterSceneFader(this);
      }

      // 触发动画效果
      public void FadeOut()
      {
      anim.SetTrigger(faderID);
      }
      }
    • instance 可以使用 fader 的函数 FadeOut() 实现 PlayerDied() 状态场景切换动画

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      # GameManager    
      // 场景恢复: 角色死亡 通过gamemanager设置延迟和效果
      public static void PlayerDied()
      {
      // SceneFader注册后(传参进obj)通过GameManager实现Fader动画
      instance.fader.FadeOut();
      instance.deathNum++;
      UIManager.UpdateDeathUI(instance.deathNum);
      instance.Invoke("RestartScene", 1.5f);
      }
    • PlayHealth 类调用 PlayerDied() 函数

      1
      2
      # PlayHealth
      GameManager.PlayerDied();
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
public class GameManager : MonoBehaviour
{
// GameManager里面的变量是通过调用GameManager时赋值传进去的
static GameManager instance; // 单例模式 生成静态类(实例) 可以直接访问
SceneFader fader; // 场景切换Fader动画
List<Orb> orbs; // 宝珠列表,生成一个列表,元素都是orb类
Door lockedDoor; // 门

float gameTime; // 游戏时间
bool gameIsOver = false;

// public int orbNum; // 场景宝珠数量
public int deathNum; // 死亡次数

private void Awake()
{
// 单例模式在场景重置会重新生成,多余生成的删除
if (instance != null)
{
Destroy(gameObject);
return;
}
instance = this;

orbs = new List<Orb>();
// 场景重置不删除
DontDestroyOnLoad(this);
}

private void Update()
{
if (gameIsOver) return;
// 当前场景中宝珠数量,list.count
// orbNum = instance.orbs.Count;
gameTime += Time.deltaTime; // 稳定统计时间,传到UI timetext
UIManager.UpdateTimeUI(gameTime);
}

// 门类注册
public static void RegisterDoor(Door door)
{
instance.lockedDoor = door;
}

// 播放切换场景动画UI:SceneFader调用Manager的RegisterSceneFader函数注册
public static void RegisterSceneFader(SceneFader obj)
{
instance.fader = obj;
}

// orb运行就添加到函数中
public static void RegisterOrb(Orb orb)
{
if (instance == null) return;
// list.contains 包含,list.add 添加进列表
if (!instance.orbs.Contains(orb))
{
instance.orbs.Add(orb);
}

UIManager.UpdateOrbUI(instance.orbs.Count); // UI直接显示数量
}

// 移除吃掉的宝珠,list.remove
public static void PlayerGrabbedOrb(Orb orb)
{
if (!instance.orbs.Contains(orb)) // 不包含宝珠直接返回
{
return;
}
instance.orbs.Remove(orb);

// 收集齐场上的宝珠门打开
if (instance.orbs.Count == 0)
instance.lockedDoor.Open();

UIManager.UpdateOrbUI(instance.orbs.Count);
}

// 通关,游戏结束
public static void PlayerWon()
{
instance.gameIsOver = true;
// UI gameover
UIManager.DisplayGameOver();
AudioManager.PlayerWonAudio();
}
//通关后人物不再移动
public static bool GameOver()
{
return instance.gameIsOver;
}


// 场景恢复: 角色死亡 通过gamemanager设置延迟和效果
public static void PlayerDied()
{
// SceneFader注册后(传参进obj)通过GameManager实现Fader动画
instance.fader.FadeOut();
instance.deathNum++;
UIManager.UpdateDeathUI(instance.deathNum);
instance.Invoke("RestartScene", 1.5f);
}

// 重新加载场景函数
void RestartScene()
{
instance.orbs.Clear(); // 重新加载宝珠列表清0
SceneManager.LoadScene(SceneManager.GetActiveScene().buildIndex);
}

}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// 控制SceneFader的UI动画效果
public class SceneFader : MonoBehaviour
{
Animator anim;
int faderID;

public void Start()
{
anim = GetComponent<Animator>();

faderID = Animator.StringToHash("Fade");

// 在GameManager里面注册this使用了RegisterSceneFader函数,传参进obj
GameManager.RegisterSceneFader(this);
}

// 触发动画效果
public void FadeOut()
{
anim.SetTrigger(faderID);
}
}


UIManager

  • 场景切换UI动画

    Hierarchy -> UI -> image 添加效果和脚本

​ 场景切换动画先出现,重新加载场景重新出现,cull Transparent mesh关闭

  • UI中属性显示的数量都是text类型,数值转文本,不同text接受不同数值输入

26

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
using TMPro;    // text mesh pro 文本框渲染

public class UIManager : MonoBehaviour
{
static UIManager instance; // 单例模式获得text文本框变量

// 获取文本
public TextMeshProUGUI orbText, timeText, deathText, gameOverText;

public void Awake()
{
if (instance != null)
{
Destroy(gameObject);
return;
}
instance = this;
DontDestroyOnLoad(this);
}

public static void UpdateOrbUI(int orbCout) // 宝珠数值
{
// 数值转字符
instance.orbText.text = orbCout.ToString();
}

public static void UpdateDeathUI(int deathCout) // 死亡次数
{
instance.deathText.text = deathCout.ToString();
}

public static void UpdateTimeUI(float time) // 直接传float小数点位数不合适
{
int minuts = (int)(time / 60); // 取整做分钟,强制转换int
float seconds = time % 60; // 119/60 1余59

instance.timeText.text = minuts.ToString("00") + ":" + seconds.ToString("00"); // 00显示两位
}

public static void DisplayGameOver()
{
instance.gameOverText.enabled = true; // 启动gameover UI
}
}
  • 时间控制特殊格式
1
2
3
4
5
6
7
public static void UpdateTimeUI(float time)     // 直接传float小数点位数不合适
{
int minuts = (int)(time / 60); // 取整做分钟,强制转换int
float seconds = time % 60; // 119/60 1余59

instance.timeText.text = minuts.ToString("00") + ":" + seconds.ToString("00"); // 00显示两位
}

导出

  • File -> Buile Setting -> build

    导出为Mac平台需要修改版本

27