10 2D灯光效果(法线贴图)
在这一节中,我们为场景添加一些装饰物件以及对应的灯光效果
添加火炬以及灯光
打开Props资源文件夹,找到我们需要的火炬预制体 Wall Torch,将父级火炬本身以及子物体所带的粒子效果的 Sorting Layer 属性修改到 Background 层级
将火炬放置在场景的适宜位置,可以看到火炬的效果:
此时的火炬只有本身的特效播放,但并没有对应的灯光效果。我们需要为之添加 Point Light
为场景添加材质
但在设置了点光源后,我们的场景并没有被光线照亮,仅有火炬本身有点光源的效果。这是因为场景没有赋予材质来实现与光线的交互。因此我们需要为场景添加法线贴图
我们可以在 Level -> Materials中找到已经制作完成的材质球(也可也新建材质球从Texture文件夹中选取对应 normal map 自行制作),然后对应赋给Tilemap
可以看到场景可以和我们设置的点光源发生交互了,一些材质也有了特殊的发光效果
添加更多种类的灯光
我们可以在Props中添加另一种光源 Hanging Lamp ,这是一种墙壁挂灯,配置了左右晃动的动画效果,自带的点光源添加了Cookie而实现了十字遮蔽的灯光
(值得一提的是,这个遮蔽效果会和其他光源产生冲突)
使用 Lighting Settings 设置调整整个场景亮度
从菜单栏的 Window -> Rendering -> Lighting Settings 唤出 Lighting Settings窗口,通过 Environment Lighting 下的 Ambient Color 属性来调整整个场景的曝光度和色调
补充:
1.我们可以在 Scipts 文件夹中找到 EmissionPulse 脚本,将之挂载到 Background Details 上,启动游戏后可以看到场景中的部分物件自带的光亮渐隐渐现
(假装有GIF)
11 角色动画(Blend Tree)
在这一节中,我们为角色的动作添加动画效果
导入角色模型
在 Robbie 文件夹中,我们可以找到预制体 Robbie_Body ,这是已经准备好了材质和动画状态机的角色模型,包含有身体各个部分的骨骼。我们将模型拖入场景中原先用于替代角色的 Robbie 贴图下,取消 Robbie 的 Sprite Renderer 组件勾选,即可将贴图替换为导入的模型使用了
这里需要注意,角色模型的各个部分的 Sorting Layer 默认为 Default ,如果要角色正确的显示,则需要统一修改到 Player 或调整 Default 的层级
添加动画效果
我们需要打开 Animation 窗口和 Animator 窗口来为角色添加动画
在Animator中,我们可以看到需要的参数和动画的切换条件,在Animation窗口中也可以看到准备好的各个动画
我们只需要完成控制动画切换的代码就可以使角色动画正常播放
通过 Blend Tree 控制跳跃动画
角色的跳跃动画并未被添加到状态机中。这部分需要我们手动完成
打开 Robbie -> Animation -> Mid Air 文件夹,可以看到角色的跳跃动画一共有七个片段,分别对应跳跃过程中的不同阶段
在状态机中,我们使用 **Blend Tree(混合树)**以及一个 Float 的 Parameter ,verticalVelocity,即竖直方向上的速度,来控制其所处的动画阶段
打开混合树 Mid Air ,在右侧将我们需要的动画片段添加进去,并设置好 verticalVelocity 作切换判据:
这一系列动画中,1到3是跳起时的动画,5到7为下落时的动画,我们据此设置每个片段的 threshold
动画切换控制
新建脚本 PLayerAnimation ,挂载在 Robbie_Body 下
具体代码如下:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class PlayerAnimation : MonoBehaviour
{
Animator anim;
PlayerMovement movement;
Rigidbody2D rb;
//通过Parameter ID进行赋值(部分平台使用字符型传递时可能出现问题)
int speedID;
int groundID;
int crouchID;
int hangingID;
int fallID;
private void Start()
{
anim = GetComponent<Animator>();
movement = GetComponentInParent<PlayerMovement>();
rb = GetComponentInParent<Rigidbody2D>();
//获取Parameter ID
groundID = Animator.StringToHash("isOnGround");
hangingID = Animator.StringToHash("isHanging");
crouchID = Animator.StringToHash("isCrouching");
speedID = Animator.StringToHash("speed");
fallID = Animator.StringToHash("verticalVelocity");
}
private void Update()
{
//anim.SetFloat("speed", Mathf.Abs(movement.xVelocity));
anim.SetFloat(speedID, Mathf.Abs(movement.xVelocity));
anim.SetBool(groundID, movement.isGround);
anim.SetBool(hangingID, movement.isHanging);
anim.SetBool(crouchID, movement.isCrouch);
anim.SetFloat(fallID, rb.velocity.y);
}
}
( PlayerMovement 脚本中的 vVelocity 需要修改为 public 变量)
补充:
先前在 PlayerMovement 脚本中,控制人物翻转的代码使用的是 Vector2 ,修改时会导致角色z轴值变化为0,有时会影响角色贴图。可以在这里更正为 Vector3
12 音效控制(Audio Manager)
在这一节中,我们将为游戏添加音效,并实现音效的控制
动画事件音效
在上一节我们添加的角色动画中,如果打开 Animation 窗口查看,可以看到一些动画在中间插入了事件
这些事件是为动作音效所预留的,可以更好地让音效和动画贴合。我们需要实现事件所需要的方法,因此在 PLayerAnimation 中添加动画需要的事件方法:
声音管理
我们希望所有的声音资源可以得到统一的管理,因此我们创建(Audio文件夹中已经提供了该预制)AudioManager 以及相应的代码来实现这一功能
其代码如下:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Audio;
public class AudioManager : MonoBehaviour
{
static AudioManager current;
[Header("环境声音")]
public AudioClip AmbientClip;
public AudioClip MusicClip;
[Header("Robbie音效")]
public AudioClip[] WalkStepClip; //行走脚步音效
public AudioClip[] CrouchStepClips; //蹲行脚步音效
public AudioClip JumpClip; //跳跃音效
public AudioClip JumpVoiceClip; //跳跃人声
//使用脚本管理AudioSiurce
AudioSource ambientSource;
AudioSource musicSource;
AudioSource fxSource;
AudioSource playerSource;
AudioSource voiceSource;
private void Awake()
{
current = this;
DontDestroyOnLoad(gameObject);//在重新载入或切换场景时仍保留当前对象
ambientSource = gameObject.AddComponent<AudioSource>();
musicSource = gameObject.AddComponent<AudioSource>();
fxSource = gameObject.AddComponent<AudioSource>();
playerSource = gameObject.AddComponent<AudioSource>();
voiceSource = gameObject.AddComponent<AudioSource>();
}
void StratLevelAudio()
{
//环境音效播放
current.ambientSource.clip = current.AmbientClip;
current.ambientSource.loop = true;//设置循环播放
current.ambientSource.Play();
//背景音乐播放
current.musicSource.clip = current.MusicClip;
current.musicSource.loop = true;
current.musicSource.Play();
}
public static void PlayFootstepAudio()//播放正常行走音效
{
int index = Random.Range(0, current.WalkStepClip.Length);//根据行走的多个声音片段获取毅哥随机的索引数
current.playerSource.clip = current.WalkStepClip[index];//根据随机索引数随机选取需要播放的声音片段
current.playerSource.Play();
}
public static void PlayCrouchFootstepAudio()//播放下蹲行走音效
{
int index = Random.Range(0, current.CrouchStepClips.Length);
current.playerSource.clip = current.CrouchStepClips[index];
current.playerSource.Play();
}
public static void PlayJumpAudio()
{
current.playerSource.clip = current.JumpClip;
current.playerSource.Play();
current.voiceSource.clip = current.JumpVoiceClip;
current.voiceSource.Play();
}
}
我们需要在游戏中完成基本变量的赋值
也需要在相应的代码处调用 AudioManager 中的函数
完成了上面的代码和设置后,启动游戏,就可以体验有这部分音效加持的游戏效果了
(部分音效在之后补充)
补充:
1.代码中写好的 StartLevelAudio 方法需要在 Awake 时调用以启动音效,在此更正
13 死亡机制(Spikes & Death)
这一节中,我们来进一步的完善游戏机制——添加角色死亡的相关内容
我们需要使用 Props 文件夹中的预制体 Spikes(尖刺),当角色在场景中触碰到尖刺后,即受到伤害/死亡,游戏场景重置(角色回到初始位置或安全的存档点)
使用 Tilemap Brushes 添加机关(Spikes)
通常来说,我们需要在场景中放置一系列的 Spikes 形成陷阱地形,但 Spikes 作为 Sprite 贴图,单个直接置入场景不易调整位置,多个 Spikes 之间手动调整位置也十分繁复,为了简便地将尖刺放置在合适的位置,我们通过 Tilemap 规范地“绘制” Spikes
我们使用 Extra 拓展包强化的 Tilemap Brushes 功能:
首先,我们在文件夹中创建插件提供的 Brushes 文件,并将 Spikes 预制体拖入对应的位置中:
接着,我们就可以在 Palette 窗口下选择我们创建的“笔刷”在场景中绘制 Spikes (注意选择好要绘制到的 Tilemap )
实现死亡机制( PlayerHealth 脚本)
在场景中适当位置添加了 Spikes 后,接下来我们通过代码实现角色的死亡机制
具体的代码如下:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.SceneManagement;
public class PlayerHealth : MonoBehaviour
{
public GameObject Prefab_deathVFX; //角色死亡视觉特效引用
int trapsLayer; //Traps Layer 编号引用
private void Start()
{
trapsLayer = LayerMask.NameToLayer("Traps");
}
private void OnTriggerEnter2D(Collider2D collision)
{
if (collision.gameObject.layer == trapsLayer)
{
Instantiate(Prefab_deathVFX, transform.position, transform.rotation);
gameObject.SetActive(false);
AudioManager.PlayDeathAudio();
SceneManager.LoadScene(SceneManager.GetActiveScene().buildIndex);
}
}
}
其中,关于死亡音效的添加如下:
(这里的FX音效是角色死亡后失去收集品的音效,收集内容在下一节中添加)
14 收集物品(Collection Orb)
这一节我们继续完善游戏内容——添加游戏收集物 “Orb(宝珠)”
添加收集品
我们可以在 Props 文件夹中找到相应的预制体: Shrine、 Orb 和 Pedestal
其中, Pedestal 是收集品 Orb 的基座,而文件夹中已经提供了将两者组合好的预制体:Shrine,我们使用 Tilemap Brushes 在场景中的适当位置添加收集品(注意绘制在 Platform 上,Layer选择 Ground )
收集物品代码
Shrine下的Orb中我们需要为其挂载脚本来实现收集的相关功能,集体代码如下:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class Orb : MonoBehaviour
{
int playerLayer;
public GameObject Prefab_ExplosionVFX;
private void Start()
{
playerLayer = LayerMask.NameToLayer("Player");
}
private void OnTriggerEnter2D(Collider2D collision)
{
if (collision.gameObject.layer == playerLayer)
{
Instantiate(Prefab_ExplosionVFX, transform.position, transform.rotation);
gameObject.SetActive(false);
AudioManager.PlayOrbAudio();
}
}
}
添加音效和特效
在上面的代码中,包含了收集宝珠所需要的特效和音效,我们需要相应的引用:
首先,在 VFX 文件夹中找到宝珠收集后的爆炸特效,拖拽到我们代码引用中:
然后,我们在 AudioManager中新增所需的两种音效,并补充在 Orb中引用的播放音效方法:
15 视觉效果 & 相机抖动(Post Processing && Camera Shake)
这一节中,我们使用一些插件来提高游戏的视觉效果
Post Processing
这是一款 Unity 的后期处理组件,可以通过应用在摄像机上的全屏滤镜和特效完成各种后期处理功能。在这里我们从 Package Manager 中导入这个插件,使用其中的部分功能
摄像机是我们承载视觉效果最主要的部件,因此我们首先在 Camera 中完成 Post Processing 的相关设置:
在 Camera 上添加组件 Post Processing Layer
我们需要调整 Layer 属性选择某个 Layer 作为可观察的视觉效果(在这里不能选择 Everything ,我们选择下面新建的 Layer )
我们在场景中创建一个空物体来承载视觉效果的调整,并将其设置为新增的 Layer " Post Processing "以单独区分
在 Global Post Processing 中添加组件 Post Processing Volume ,在 VFX -> Profiles 中找到设置完备的后期处理文件拖拽到 Volume 组件中:
可以看到游戏画面有所变化:
(可以在 Lighting Setting 中调整曝光进一步改善游戏画面)
相机抖动效果
游戏中的某些时刻为相机添加震动效果能够进一步的提高游戏性。在这里,我们可以借助 Cinemachine 中的组件来实现这个功能
在 Cinemachine 摄像机中,我们添加额外的一个组件 Cinemachine Impulse Listener,这一组件可以接受场景中相应对象所发出的脉冲效果来实现相机的震动。我们保持默认属性不变
我们为角色收集品 Orb 添加这样的震动效果:
首先,为 Shrine 预制中 的 Orb 添加组件 Cinemachine Impulse Collision Source,这一组件可以在挂载对象发生碰撞时发送指定的脉冲信号。我们在 VFX -> Profiles 文件夹中找到制作好的脉冲信号文件拖拽到相应位置:
此外,还需要下面的属性中设置与哪一个 Layer 发生碰撞时生效:
也可以修改下面两个属性的数值来调整震动效果(分别是幅度和频率)
完成了上面的设置后,启动游戏收集宝珠,就会有相应的震动效果了
补充:
游戏角色多次死亡后,AudioManager会重复被创建而导致声音越来越大。因此我们需要调整代码来避免这个问题:
16 GameManager
为了便于项目的统筹管理和功能拆分,我们使用一个脚本 “GameManager” 来统筹游戏中的部分功能。为了方便其他脚本调用这些功能,我们使用单例模式来编写 GameManager
接下来,我们为 GameManager 添加相关的功能
角色死亡的场景重载和动画切换效果
使用 GameManager 控制场景重载并添加延迟
我们让 GameManager 来控制角色死亡后具体的场景重载等效果(而不是通过之前的 PlayerHealth来控制角色死亡的逻辑和动画特效的演示,PlayerHealth中只需调用方法即可)
(加入1.5秒的延迟提升游戏体验)
使用Fader预制和脚本控制屏幕遮挡效果
此外,在 UI 文件夹中,有一个 Fader 预制:
这是一个半透明的 UI 组件,用于角色死亡和重载游戏时的屏幕遮挡,带有三组动画效果,可以闭合和打开屏幕。我们利用这一组件进一步完善角色死亡相关的游戏效果
新建脚本 SceneFader 来控制Fader的播放效果,代码如下:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class SceneFader : MonoBehaviour
{
Animator anim;
int fadeID;//动画切换标签ID引用
private void Start()
{
anim = GetComponent<Animator>();
fadeID = Animator.StringToHash("Fade");
}
public void FadeOut()
{
anim.SetTrigger(fadeID);
}
}
将脚本挂载到预制上,再将 Fader 拖拽到场景中,并且在 Material 位置添加准备好的材质 “Fade”,这时调整 Fader 的 Image 组件中的颜色透明度,可以看到下面的游戏效果(Fader的动画效果即改变透明度):
接下来,我们在 GameManager 中添加 Fader 的有关内容:
(Fader 上 SceneFader 的脚本引用)
(Fader 的注册方法)
(在 SceneFader中添加注册语句)
实现了上述代码后,这个屏幕效果就可以在游戏中正常播放了
物品收集的统计
我们同样可以将场景中设置的 Orb 注册到 GameManager中,来实现诸如计数等的功能
由于一个场景中可能存在多个 Orb,我们创建一个 List 来存放 Orb 脚本的引用:
然后创建方法来注册场景中全部的 Orb ,以及在玩家收集到相应的 Orb 以后在 List 中移除:
(然后在 Orb 中的相应位置完成注册以及移除)
同时,我们可以在 GameManager 中新增一个 int 变量用于表示当前场景还存在的 Orb 数量(通过 List 获取),我们可以在 Updata 中动态更新这个数值
此外,也可以设置一个变量统计角色的死亡次数,每当角色死亡方法被调用,这一变量自增一次
这两个变量的设置可以服务与后续的游戏性设计
补充:
1.场景重载后列表 orbs 需要归零,因此我们添加语句:
17 UI Manager全部代码完成(Door & Win zone)
UI Manager
除了对游戏进程以及影响游戏进程的各个组件的统管的 GameManager 外,我们还需要创建 UI Manager 对游戏进程中的 UI 内容进行管理。在 UI 文件夹下,以及准备好了 UI Manager 的预制:
可以看到,其本身是一个简单的空物体,需要一个脚本使其成为场景 UI 的控件,其下包含有几个简单的统计性的 UI 内容,分别为宝珠(Orb)的收集数量、角色死亡次数、关卡进行时间以及游戏结束的屏幕UI
为了控制他们在游戏进行时的显示,我们下面编写脚本 UI Manager 来控制这些逻辑:
1.首先,我们需要获得相关组件的引用:
(为了便于方法调用,我们同样将 UI Manager 设计为单例模式)
(另外,注意这里Text组件使用的是**“Text Mesh Pro UGUI”**,需要加入相应的命名空间)
2.我们编写修改 OrbText 的方法,并在 GameManager 中相应位置进行调用 (先前的 OrbNum 在这里被取代)
3.死亡次数的显示逻辑与上面相同
4.而对于时间的显示,我们在 GameManager 中新建一个浮点变量,在 Updata 中实时更新并传输到 UI Manager 中:
游戏关卡的切换
我们也可以进一步添加游戏内容——游戏关卡的切换
在 Props 文件夹中,可以找到预制 Door :
这是用于限制玩家进入其他关卡区域的门,其中包含两个子物体,一个是带有碰撞体的门本身的贴图素材,另一个则是开门后可以到达的Win Zone,玩家进入其触发器范围时可以触发脚本中的相应事件,比如加载下一个场景等。门本身带有相应的动画,可以编写脚本来控制门的开启,并在其后的 Win Zone 中设置代码控制场景的切换
控制大门的开启
我们将场景中的门注册到 GameManager 中:
并在收集宝珠中添加达成条件即开门的逻辑:
同时在 AudioManager 中加入开门音效的播放
(下面是挂载在门本体上的脚本 Door 的具体代码)
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class Door : MonoBehaviour
{
Animator anim;
int openID;
private void Start()
{
anim = GetComponent<Animator>();
openID = Animator.StringToHash("Open");
//register door
GameManager.RegisterDoor(this);
}
public void Open()
{
anim.SetTrigger(openID);
//play audio
AudioManager.PlayOpenDoorAudio();
}
}
控制到达 Win Zone 的事件
我们可以为 Win Zone编写代码来控制玩家完成收集到达门后区域的相关事件:
1.在 UI Manager 中添加代码使游戏结束后显示相应的文本(GameOver)
2.在 GameManager 中添加变量判断游戏的结束,并控制 UI 显示
3.在 Win Zone 中调用方法实现 UI 显示逻辑
此外,在游戏结束后,我们还应当阻断玩家控制,避免还可以随意走动的玩家可能带来的问题:
1.在 GameManager 中添加方法返回游戏状态(是否结束)
2.在 PlayerMovement 的两种 Updata 方法中阻断玩家的控制:
完成了上面的代码,当角色收集到场景中全部的 Orb 之后,大门开启,玩家进入到阴影中的 Win Zone, GameOver 字样就会显示在屏幕上,玩家操控随之阻断,游戏计时也同时停止(在 GameManager 的 Updata 方法中,判断结束即 return ):
补充:
脚本 Win Zone 中获取 Layer 时 player 首字母应大写,在此更正
18 游戏的最后处理
AudioMixer
音效补充
其中, levelStartClip 在方法 StratLevelAudio 中播放
winClip 则新建方法播放(在 GameManager 中调用):
使用 AudioMixer 进行音量控制
在 Extended -> Audio 文件夹下准备了设置好的声音混合器,我们需要通过代码将各个 AudioSource 输出到相应的 Group 中来进一步完善声音的控制
在代码中,我们先获取 AudioMixerGroup 的引用:
然后在 Awake 方法中,将获得引用的 AudioSource 输出到对应的 Group 中:
赋值如下:
有了这些设置,就可以进一步的建立 UI 对音量进行控制
Extended(待补充)
在 Extended 文件夹中包含有一些可以进一步拓展游戏的资源,诸如掉落的石块,弹出的机关尖刺以及旋转的斧头机关等。上面挂载了相应的组件以及完整的代码,我们可以将其中与角色交互部分的 Layer 修改为之前设置的 Traps ,置入场景即可发挥相应的效果(其中部分预制需要使用 Tilemap Brushes 进行绘制,相应的 Brushes 文件已经创建,因此在 Palette 中使用即可)
DeathPose
我们还可以在角色之前死亡的位置添置一个角色残影起到对下一次行动的警示作用,以丰富死亡机制。为了实现这一功能,我们可以仿照先前设置角色死亡烟雾特效的方法,在死亡位置添加这一内容
残影制作
残影本身可以利用起始制作时用以代替角色的 Robbie 贴图制作,调整颜色以及透明度即可达到大致的需求
为了使得残影在玩家死亡游戏重载后依然存在,我们需要添加代码使其保持到下一轮游戏:
并将残影保存为预制,在 PlayerHealth 中同烟雾特效一样应用即可(可以修改位置使残影效果多样化)
(这里可以将残影设置为单例模式,在生成新的残影时摧毁之前的单例)
BuildSetting
在游戏 Build 时,需要注意一些设置问题,尤其是输出到移动设备上时,Post Processing 组件对移动设备有所拖累,需要选择 Post Processing Volume 效果中的 Fast Mode (没有 Fast Mode 的效果很可能造成卡顿)