该项目回顾旨在说明3D游戏项目与2D游戏项目之间的不同,3D游戏具有2D游戏不具备的特性和需要注意的事项。
1.1主角移动PlayerMovement
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class PlayerMovement : MonoBehaviour
{
Vector3 m_Movement;
float horizontal;
float vertical;
Rigidbody m_rigidbody;
Animator m_animator;
//初始化旋转,四元数用来表示旋转
Quaternion m_rotation = Quaternion.identity;
//旋转速度
public float turnSpeed = 20.0f;
//声明音源组件
AudioSource m_AudioSource;
// Start is called before the first frame update
void Start()
{
m_rigidbody = GetComponent<Rigidbody>();
m_animator = GetComponent<Animator>();
m_AudioSource = GetComponent<AudioSource>();
}
// Update is called once per frame
void Update()
{
horizontal = Input.GetAxis("Horizontal");
vertical = Input.GetAxis("Vertical");
}
private void FixedUpdate()
{
//用户输入组装为三维矢量
m_Movement.Set(horizontal, 0.0f, vertical);
m_Movement.Normalize();
//判断是否有横向移动
bool hasHorizontal = !Mathf.Approximately(horizontal, 0.0f);
bool hasVertical = !Mathf.Approximately(vertical, 0.0f);
bool isWalking = hasHorizontal || hasVertical;
m_animator.SetBool("IsWalking", isWalking);
//三维矢量表示转向后的角色
Vector3 desiredForward = Vector3.RotateTowards(transform.forward, m_Movement, turnSpeed * Time.deltaTime, 0f);
m_rotation = Quaternion.LookRotation(desiredForward);
//如果走动,播放音效
if (isWalking)
{
//保证不是每帧都重新播放!!
if (!m_AudioSource.isPlaying)
{
m_AudioSource.Play();
}
}
else
{
m_AudioSource.Stop();
}
}
private void OnAnimatorMove()
{
//使用用户输入的三维矢量为移动方向,动画每0.02秒运动的距离为距离
m_rigidbody.MovePosition(m_rigidbody.position + m_Movement * m_animator.deltaPosition.magnitude);
m_rigidbody.MoveRotation(m_rotation);
}
}
因为本次tutorial的内容是鬼屋探险,所以人物并未有太多可操作性,基本上就是基础的移动。需要注意的是,3D游戏中由于不能像2D游戏那样,通过角色动画表现方向,而是3D物体本身进行旋转表示方向,所以引入Quaternion四元数用来表示旋转。具体代码说明都在注释里。其中OnAnimatorMove()代表的是,每当动画产生了根运动时触发。由于根运动有自己的频率,人物移动速度也得和根运动速度相同,故采用delta的设置,使得0.02秒内的运动一致。
1.2幽灵移动WayPointPatrol
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.AI;
public class WayPointPatrol : MonoBehaviour
{
NavMeshAgent navMeshAgent;
public Transform[] waypoints;
int m_CurrentWaypointIndex;
// Start is called before the first frame update
void Start()
{
navMeshAgent = GetComponent<NavMeshAgent>();
navMeshAgent.SetDestination(waypoints[0].position);
}
//每次刷新,都要试着去获取下一个路径点
//如果满足要求,指定下一个路径点
//通过算法,让路径点位循环往复
// Update is called once per frame
void Update()
{
if (navMeshAgent.remainingDistance < navMeshAgent.stoppingDistance)
{
//获取下一个路径点在数组中的索引数
m_CurrentWaypointIndex = (m_CurrentWaypointIndex + 1) % waypoints.Length;
navMeshAgent.SetDestination(waypoints[m_CurrentWaypointIndex].position);
//Debug.Log($"{gameObject.name} is going to {waypoints[m_CurrentWaypointIndex].position}!");
}
}
}
在幽灵项目中,由于幽灵不会因被角色碰撞而改变自己的位置,所以在Rigidbody当中勾选Is Kinematic,如此一来就只会按照自己的运动学路径移动。
幽灵在本项目中用到了导航系统Nav Mesh。在Window-AI-Navigation中找到,选中地图Level组件,并且在Navigation的Bake烘培中更改参数,最后Bake后得到地图。
Attention:每次更改level的transform后,都要重新bake一遍地图,才能得到新的地图导航。
其中NavMeshAgent就是在地图中的控制组件,可以控制选中目的地。这里使用的是Transform组,用来遍历一个幽灵需要前往的不同地址,并且循环。其中stoppingDistance指的是在这个距离后就算到达,一般不设为0.0f,本项目设置为0.2f。
1.3静态敌人与动态敌人的捕捉视野Observer
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class Observer : MonoBehaviour
{
public Transform player;
bool m_IsPlayerInRange;
public GameEnding gameEnding;
// Start is called before the first frame update
void Start()
{
}
// Update is called once per frame
void Update()
{
if (m_IsPlayerInRange)
{
//设置投射射线用到的方向矢量
Vector3 direction = player.position - transform.position + Vector3.up;
Ray ray = new Ray(transform.position, direction);
RaycastHit raycastHit;
//若击中物体,则进入第一层if。第二个参数out代表输出参数,可以将射线射到的物体输出到变量raycastHit
if (Physics.Raycast(ray,out raycastHit))
{
if (raycastHit.collider.transform == player)
{
gameEnding.CaughtPlayer();
}
}
}
}
private void OnTriggerEnter(Collider other)
{
if (other.transform == player)
{
m_IsPlayerInRange = true;
}
}
private void OnTriggerExit(Collider other)
{
if (other.transform == player)
{
m_IsPlayerInRange = false;
}
}
}
本方法在Update()中过于复杂,本可将游戏结束放在触发器事件中,进行离开设定是为了将m_IsPlayerInRange值改回false。但是可以将初始值放进Start()中,这样就不必进行繁琐的判断了。而且对于视线的设置更直观了,毕竟就是碰撞体本身。
1.4游戏结束
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.SceneManagement;
public class GameEnding : MonoBehaviour
{
//声明开关变量,存储用户是否在触发器中
bool m_IsPlayerAtExit;
public GameObject player;
//淡入淡出的时间,要有计时器
public float fadeDuration = 1.0f;
//计时器
float fadeDurationTimer;
//正常(完全不透明状态)显示结束UI的时间
public float displayerImageDuration = 1.0f;
//声明CanvasGroup,用来获取UI中的实例,来更改UI中图像的透明度
public CanvasGroup exitBackgroundImageCanvasGroup;
//新增一个表示游戏失败的结束界面UI
public CanvasGroup caughtBackgroundImageCanvasGroup;
bool m_IsPlayerCaught;
//公开声明需要的音频文件,用以挂接
public AudioSource exitAudio;
public AudioSource caughtAudio;
//因为两种声音只能播放一次,所以设定bool作为开关
//默认false,播放过则true
bool m_HasAudioPlayed;
// Update is called once per frame
void Update()
{
if (m_IsPlayerAtExit)
{
EndLevel(exitBackgroundImageCanvasGroup,false, exitAudio);
}
else if (m_IsPlayerCaught)
{
EndLevel(caughtBackgroundImageCanvasGroup, true, caughtAudio);
}
}
private void OnTriggerEnter(Collider other)
{
if (other.gameObject == player)
{
m_IsPlayerAtExit = true;
}
}
public void CaughtPlayer()
{
m_IsPlayerCaught = true;
}
//参数1:结束UI对应的游戏对象;参数2:是否重新开始
private void EndLevel(CanvasGroup imageCanvasGroup, bool doRestart, AudioSource audioSource)
{
if (!m_HasAudioPlayed)
{
audioSource.Play();
m_HasAudioPlayed = true;
}
fadeDurationTimer += Time.deltaTime;
//alpha值控制透明度
imageCanvasGroup.alpha = fadeDurationTimer / fadeDuration;
//当计时器时长大于展示总时长,则退出游戏,故计时器随deltaTime增大
if (fadeDurationTimer > fadeDuration + displayerImageDuration)
{
if (doRestart)
{
SceneManager.LoadScene(SceneManager.GetActiveScene().buildIndex);
}
else
{
//退出当前应用程序
//Application.Quit();
UnityEditor.EditorApplication.isPlaying = false;
}
}
}
}
游戏结束的组件中包含了两种结束方式,一种是AtExit,即通关逃脱,另一种是Caught,即被怪物抓住。分别挂接两个canvas group,一个是通关,一个是结束,都是canvas group下挂子对象image,且canvas group初始alpha值为0(1为不透明)。
该脚本有两种挂载形式,一种是直接挂在一个碰撞盒上,作为终点触发器结束游戏。另一种是挂在怪物的Observer上,作为玩家被Caught的结束方式。
二者都会叫出对应的UI,并且进行展示倒计时以及音效播放,展示通过更改Canvas group的alpha值渐增渐减决定。根据需求使得游戏获胜或者重新加载当前场景。
2.1相机抗锯齿
使用最简单的Main Camera配合Cinemachine。但注意在Main Camera上挂接Post-process Layer,选择设置的layer层。
抗锯齿Anti-aliasing,这里是用的是FXAA来节省性能。
延迟雾Deferred Fog也开启。
2.2光照
为了营造室内的鬼屋气氛,使用的是directional light 。具体配置在Windows>Rendering>Lighting中配置。将新的Light Setting存入文件New Lighting Setting中。
2.3后期处理
详细可见:Post Processing 后期处理_multi scale volumetric obscurance_童萌依然的博客-CSDN博客
创建新的空游戏对象,挂接Post-process Volume。并且设置新的layer,将Global Post的layer更改。
2.3.1 Color Grading色阶
Tonmapping mode - ACES为电影质感。
Post-exposure(EV) 曝光,大小为画面整体亮度。
Saturation饱和度。
Contrast对比度。
Lift, Gamma, Gain分别为暗部、灰部、亮部的调整。这里将暗部和灰部的色彩往蓝色冷色调整,就是为了营造鬼屋阴冷的效果。亮部往暖色调是为了将画面整体观感调回来,亮部也往往是灯光光源。
2.3.2 Bloom泛光效果
Intersity强度,泛光效果在屏幕上的作用区域。
Treshold阈值,超过一定光亮度的光源才使用Bloom。
泛光效果是为了营造柔和光,基本用于高光、灯光,使得光线柔和。
2.3.3 Ambient Occlusion环境光遮挡
画面上不同地区由于Z-buffer有不同的深度,为了体现不同的深度,一个墙体在其边缘处一般有阴影,用来表现画面中物体的遮挡关系。
Intensity强度
Thickness Modifier 修改阴影的厚度
2.3.4 Vignette渐晕
渐晕为屏幕周围的晕影,这里由于是鬼屋项目,要体现阴暗的环境。
Intensity强度,这里体现晕影的总距离
Smoothness柔和度,这里体现的是淡出距离
2.3.5 Lens Distortion镜头失真
营造镜头透视,这里使用的是轻量的鱼眼透视。
Intensity镜头畸变强度
Scale镜头畸变大小
3.1音频(2D、3D)挂接
音频分为3d和2d。环境音、ui音、人物自己发出的声音(走路)都为2d音,不用立体音。因此将Audio Listener挂接到主角身上。