动作游戏制作

开个新坑,最近瓶颈期,我觉得需要多敲代码,让自己更熟悉unity.

教程原链接:

https://www.udemy.com/course/project-little-adventurer-learn-to-create-a-3d-action-game/

b站视频链接:

【UNITY教程 | 学习使用高质量的游戏资产创建 3D 动作游戏(更新完毕)】https://www.bilibili.com/video/BV1B14y1E7SW?p=6&vd_source=b702db14148b48ad87406aaf674e1744

  • 付费教程且看且珍惜,是生肉,但是勉强硬啃.(编辑,发现b站有自动生成英文和翻译功能,机翻可以啃的更舒服了)
  • 下面内容是我边查资料,边尝试理解作者的操作意图所作,如有不对欢迎指出.

导入资源包

首先,项目设置里,设置渲染为线性渲染.以下内容为转载

线性渲染

Linear Rendering 和 Gamma Rendering的区别

Linear Rendering就是在shader中所有计算会在线性空间下进行,Gamma Rendering就是在shader中不进行转换到线性空间下,直接计算。然就是计算方程式不同,也就意味例如光照表面会有不同的响应曲线和图片效果,表现不相同。

1.Light Falloff

光照表现一般受光源的距离和法线两个因素影响(在同等光强下)。首先当我们用Linear Rendering时,执行Gamma矫正将会使光照范围变大。第二种会使边缘模糊,分不清界限。这更准确的表现了表面光照强度下降。

image-20230216184910891

2.表面响应强度

随着光强的增加,非线性方式计算的表面会更亮一些。这导致了光照在表面很多地方曝光过度,而且给场景模型一个褪色(变白色了)的感觉。当你用线性渲染时,表面颜色仍然随着光照强度线性增加的,这样就使表面材质和颜色更接近现实.

image-20230216184947469

3.混合

混合是在帧缓冲区发生的,当使用Gamma Rendering,这表示颜色之间混合是在非线性空间下计算的。然而这是不正确的。

image-20230216184958794

上图是在Linear Space中混合结果,颜色之间过度不是很明显。

image-20230216185011404

上图是Gamma Space中混合结果,颜色交界处出现了明显的其它颜色,颜色更亮,出现褪色的现象。

4.Mipmaps

计算纹理Mipmap是种线性计算,需要对某个方形区域内像素取平均值,如果纹理存储在非线性空间,那么计算时也是在非线性空间里计算,这样就会得到错误的结果。正确的做法是先转换到线性空间在计算mipmap。

5.Lightmapping

切换linear 和gamma方式,需要重新烘焙相关的Lightmapping。

关于渲染我了解的很少,具体资料可以参考:

————————————————
版权声明:本文为CSDN博主「追风者t」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/k46023/article/details/52489363/


回到项目,设置渲染为线性渲染:

image-20221212180600417

image-20221212181110296

导入包

刚导入时是一片粉色

image-20221214153814297

Package Manager中选择注册表

image-20221214153957120

安装Universal RP

image-20221214154046812

安装完成后打开项目设置,

image-20221214160257935

再安装一个这个

image-20221214161906033

这个

image-20221214162152389

添加包

com.unity.progrids

image-20221214163904210

设置游戏场景

创建一个新空白场景,找到场景预制体拖入场景,并且重置位置属性

image-20221214164716099

新建相机

设置tag为MainCamera

位置可以参考视频里的位置

image-20221214171137639

后处理要打开,虽然我打开之后画面非常模糊,接着往下做看看吧.

image-20221214171548570

新建和配置角色

新建空物体,位置重置为0,0,0

image-20221214171704852

拖入对象

image-20221214172657153

移除动画器

image-20221214172847757

添加灯光

image-20221214185813809

需要设置角色Mesh的layer层级为player

image-20221214185841910

设置完就可以看到灯光

image-20221214185902512

设置虚拟摄像机

image-20221214190728271

注意图里圈出来的地方改一下

image-20221214191340918

添加角色控制器组件

注意数值变化

image-20221214191654870

玩家行走

功能实现写在代码注释中

新建俩脚本添加到player上

image-20221214192130468

玩家输入类PlayerInput.cs

public class PlayerInput : MonoBehaviour
{
[SerializeField] float HorizontalInput;
[SerializeField] float VerticalInput;
private void Update()
{
HorizontalInput = Input.GetAxisRaw("Horizontal");
VerticalInput = Input.GetAxisRaw("Vertical");
}
private void OnEnable()
{
HorizontalInput = 0;
VerticalInput = 0;
}
}

角色控制类character

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

public class Character : MonoBehaviour
{
/// <summary>
/// 胶囊碰撞器
/// </summary>
private CharacterController cc;
[SerializeField] float turnSpeed = 0.5f;//转向速度
[SerializeField] float moveSpeed = 5f;//移速
private Vector3 movementVelocity;//运动速度
private PlayerInput playerInput;//玩家输入
private float verticalVelocity;//垂直速度
public float G = -9.8f;//重力

private void Awake()
{
cc = GetComponent<CharacterController>();
playerInput = GetComponent<PlayerInput>();
}
/// <summary>
/// 计算玩家运动
/// </summary>
private void CalculatePlayerMovement()
{
movementVelocity.Set(playerInput.HorizontalInput, 0f, playerInput.VerticalInput);
movementVelocity.Normalize();
movementVelocity = Quaternion.Euler(0, -45f, 0) * movementVelocity;//操作对准相机
//返回围绕x轴旋转x度和围绕y轴旋转y度的旋转,围绕z轴旋转z度;按照该顺序应用。
movementVelocity *= moveSpeed * Time.deltaTime;

//不为0时
if (movementVelocity != Vector3.zero)
{
transform.rotation = Quaternion.Lerp(transform.rotation, Quaternion.LookRotation(movementVelocity), Time.deltaTime * turnSpeed);//平滑转向运动方
//transform.rotation = Quaternion.LookRotation(movementVelocity);
}

}
private void FixedUpdate()
{
CalculatePlayerMovement();
//判断是否接地
if (cc.isGrounded == false)
{
verticalVelocity = G;//下落速度为重力加速度
}
else
{
verticalVelocity = G * 0.5f;//落地后继续施加重力(防止下楼梯时悬空)
}
movementVelocity += verticalVelocity * Vector3.up * Time.deltaTime;//赋予垂直方向的速度
cc.Move(movementVelocity);//控制器位移
}
}

动画控制器

创建动画控制器

新建AnimatorController,双击打开.

image-20221216154024185

添加idle状态

新建状态,命名为idle,附加上idle资源,顺便检查下有没有开启循环播放动画.

image-20221216155823086

image-20221216155920146

给角色添加武器

打开角色骨骼,找到左手,将剑资源拖入

image-20221216160510427

设置旋转和位置

image-20221216160626368

右手同上操作,右手剑的位置信息如下:

image-20221216160901397

运动动画

新建float参数speed,新建状态Run,附加上动画,通过make Transition建立状态过渡连线,两端都建一条箭头.

image-20221216175120672

image-20221216180018321

选中Idle->Run的箭头,接下来看图,关闭Has Exit Time

image-20221216175439241

image-20221216175917110

接着选中Run->Idle箭头,关闭Has Exit Time,设置状态过渡用时和过渡时机.

image-20221216182517576

回到character.cs脚本中,添加一个动画控制器变量,CalculatePlayerMovement方法中,为Speed赋值.

image-20221217165641230

image-20221217165718101

下落动画

动画控制器中新建一个bool变量airBorne

image-20221217171320279

确保动画是循环播放

image-20221217171512579

新建状态Fall,由Any State指向Fall

image-20221217172212304

选中该箭头,注意has Exit Time不选中,因为我们要立即执行

image-20221217172310387

拉到最下,连接AirBorne参数为true时进行下落状态

image-20221217172317278

同理Fall->Idle状态设置如下:

image-20221217172536563

image-20221217172527159

接下来回到character脚本,CalculatePlayerMovement方法中添加一条代码控制AirBorne参数.

image-20221217174546466

回到编辑器运行测试,我发现下落和落地后的动作过于僵硬,所以把上面的两个状态之间的过渡都调成了0.1;

image-20221217180135673

image-20221217180208520

脚步视觉特效

新建一个空对象命名为VFX,然后资产文件中找到脚步特效prefab,拖入其中

image-20221221151449669

image-20221221151636802

调整位置

image-20221221151910296

现在奔跑烟雾特效一直显示在角色脚下,我们要做的是将特效改为奔跑时触发.

首先取消自动播放,其次在相应状态动画中创建脚本进行控制.

阻止特效动画自动播放

Initial Event name:初始事件名称

image-20230131182224126

特效管理脚本:PlayerVFXManager

using UnityEngine;
using UnityEngine.VFX;

public class PlayerVFXManager : MonoBehaviour
{
[SerializeField] VisualEffect footStep;//特效
public void Update_FootStep(bool state){
if (state)
{
footStep.Play();
}else{
footStep.Stop();
}
}
}

拖入特效

image-20230131183143053

打开Player动画控制器,选中Run状态,在下面新建脚本Player_Run

image-20230131184243697

image-20230131184305643

状态脚本Player_Run.cs

进入状态时启动特效管理器中的Update_FootStep(true),播放特效

退出状态时关闭特效Update_FootStep(false);

using UnityEngine;

public class Player_Run : StateMachineBehaviour
{
// OnStateEnter is called when a transition starts and the state machine starts to evaluate this state
// 当转换开始并且状态机开始评估此状态时,调用OnStateEnter
override public void OnStateEnter(Animator animator, AnimatorStateInfo stateInfo, int layerIndex)
{
if (animator.GetComponent<PlayerVFXManager>() != null)
{
animator.GetComponent<PlayerVFXManager>().Update_FootStep(true);
}
}
// OnStateExit is called when a transition ends and the state machine finishes evaluating this state
//当转换结束且状态机完成评估此状态时,调用OnStateExit
override public void OnStateExit(Animator animator, AnimatorStateInfo stateInfo, int layerIndex)
{
if (animator.GetComponent<PlayerVFXManager>() != null)
{
animator.GetComponent<PlayerVFXManager>().Update_FootStep(false);
}
}

敌人对象

创建敌人对象

在目前项目还不复杂的时候创建敌人对象,可以使用同样的动画控制器角色脚本.

新建对象命名为Enemy_01,拖入npc模型

image-20230131192532587

新建一个重载动画控制器Animator Override Controller

image-20230131193711239

将player拖入

image-20230131194158530

为敌人添加新的动画来重写原主角的动画.

image-20230131194354128

敌人对象添加animator

image-20230131194602342

敌人ai

依次打开Window->AI->Navigation,菜单栏选Bake,更改参数如下

image-20230131195809551

slope斜坡设置为0度,确保ai不会上坡

image-20230131195856946

Step Height设为0.1只覆盖平面,效果图如下

image-20230131200147518

给敌人ai添加碰撞体,并更改参数

添加Nav Mesh Agent(Agent:代理人)

更改Stopping Distance为2 ai距离人类2时停下进行攻击

image-20230131200431481

敌人脚本

修改Character.cs

增加[SerializeField] bool IsPlayer = true;//是否玩家

Awake()中添加敌人定义

private void Awake()
{
cc = GetComponent<CharacterController>();
animator = GetComponent<Animator>();

//如果不是玩家
if (!IsPlayer)
{
navMeshAgent = GetComponent<UnityEngine.AI.NavMeshAgent>();
targetPlayer = GameObject.FindWithTag("Player").transform;
navMeshAgent.speed = moveSpeed;
}
else
{
playerInput = GetComponent<PlayerInput>();
}
}

添加敌人行动方法CalculateEnemyMovement(),跟随玩家且播放动画

/// <summary>
/// 敌人行动
/// </summary>
private void CalculateEnemyMovement()
{
//玩家和ai距离 如果大于ai停止距离
if (Vector3.Distance(targetPlayer.position, transform.position) >= navMeshAgent.stoppingDistance)
{
//SetDestination:设置或更新目标,从而触发新路径的计算
navMeshAgent.SetDestination(targetPlayer.position);
animator.SetFloat("Speed", 0.2f);
}
else
{
navMeshAgent.SetDestination(transform.position);//呆在原地
animator.SetFloat("Speed", 0f);
}
}

FixedUpdate()中新增判断,来区分玩家和ai

private void FixedUpdate()
{
if (IsPlayer)
{
CalculatePlayerMovement();
//判断是否接地
if (cc.isGrounded == false)
{
verticalVelocity = G;//下落速度为重力加速度
}
else
{
verticalVelocity = G * 0.5f;//落地后继续施加重力(防止下楼梯时悬空)
}
movementVelocity += verticalVelocity * Vector3.up * Time.deltaTime;//赋予垂直方向的速度
cc.Move(movementVelocity);//控制器位移
}
else
{
CalculateEnemyMovement();
}
}

敌人VFX

新建空对象->改名->拖入预制体

image-20230131213338834

删除Initial Event Name防止自动播放特效动画

image-20230131213501796

AI踩踏时触发特效

添加事件

找到npc移动动画

image-20230222162717811

在脚落地的那一帧分别添加事件

image-20230222163714856

创建敌人特效管理脚本EnemyVFXManager

添加命名空间using UnityEngine.VFX;

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

public class EnemyVFXManager : MonoBehaviour
{
[SerializeField] VisualEffect footStep;//特效
public void BurstFootStep()
{//与事件同名方法
footStep.SendEvent("OnPlay");//启动动画
}
}

回到unity,给enemy附加脚本和特效

image-20230222165154214

效果

image-20230222165245336

修改玩家输入脚本

新增部分:添加变量mouseButtonDown代表鼠标左键,判断是否按下?若未按下且游戏正常运行,则将它设置为当前鼠标左键状态

using UnityEngine;
public class PlayerInput : MonoBehaviour
{
public float horizontalInput;//水平输入
public float verticalInput;//垂直输入
public bool mouseButtonDown;//鼠标左键状态
private void Update()
{
if (!mouseButtonDown && Time.timeScale != 0)
{//鼠标松开,且游戏未暂停时
mouseButtonDown = Input.GetMouseButtonDown(0);//0:左键
}
horizontalInput = Input.GetAxisRaw("Horizontal");
verticalInput = Input.GetAxisRaw("Vertical");
}
private void OnEnable()
{
mouseButtonDown = false;
horizontalInput = 0;
verticalInput = 0;
}
}

状态机

创建状态枚举

打开Character.cs,声明一个类:CharacterState,并且创建该类变量

#region State
//声明一个状态类
public enum CharacterState
{
normal,
Attacking
}
//创建变量
public CharacterState CurrentState;

#endregion

状态转换

同一脚本中,创建一个方法SwitchStateTo进行状态更新

    /// <summary>
/// 状态切换
/// </summary>
/// <param name="newState"></param>
private void SwitchStateTo(CharacterState newState)
{
//Exiting State
switch (CurrentState)
{
case CharacterState.normal:
break;
case CharacterState.Attacking:
break;
}
//Entering State
switch (newState)
{
case CharacterState.normal:
break;
case CharacterState.Attacking:
break;
}
CurrentState = newState;//状态更新
}
}

FixedUpdate()方法中添加代码,判断何时切换状态

switch (CurrentState)
{
case CharacterState.normal:
if (IsPlayer)
{
CalculatePlayerMovement();
}
else
{
CalculateEnemyMovement();
}
break;
case CharacterState.Attacking:
break;
}

在计算玩家运动的CalculatePlayerMovement()方法中添加下面一段判断:玩家在地上且鼠标左键按下时,状态切换成攻击

if (playerInput.mouseButtonDown && cc.isGrounded)
{
SwitchStateTo(CharacterState.Attacking);
}

修改SwitchStateTo方法,在开头清理缓存,在最后log查看状态

private void SwitchStateTo(CharacterState newState)
{
//Clear Cache
playerInput.mouseButtonDown = false;
//Exiting State
switch (CurrentState)
{
case CharacterState.normal:
break;
case CharacterState.Attacking:
break;
}
//Entering State
switch (newState)
{
case CharacterState.normal:
break;
case CharacterState.Attacking:
break;
}
CurrentState = newState;//状态更新
Debug.Log("当前状态:" + CurrentState);
}

测试:运行后点击左键正常触发log内容,但是视频中行动中切换到attacking状态会无法控制角色行走,我做出来的内容依然可以控制 不知道什么原因.

第二天编辑:昨天之所以攻击状态还能移动,是FixedUpdate()函数中,写状态相关代码之前,没有注释之前调用的玩家移动函数

image-20230302163459557

玩家攻击动画

创建攻击动画

动画器中创建trigger变量,命名为Attack

image-20230301173123072

新建Sub-State Machine 子类状态机命名为Attack

image-20230301173319442

image-20230301173656957

右键添加箭头,这俩箭头配置如下图

image-20230301174113816

image-20230301174058696

Attack状态转idle是灰色箭头,后面修改.

双击点进Attack子状态机,添加combo01状态

image-20230301174415713

箭头参数设置:意思是在攻击动画播完就去下一个动画,并且无过渡部分,动画不与下一个混合

image-20230301174550325

给combo01状态添加动画:

image-20230301174715868

做完当前内容,在游戏中进行攻击,攻击动画播放完成后无法返回idle,要解决这个问题最简单的方式是发送动画事件,在组合动画的最后一帧添加事件

找到攻击动画,并在下方events中快结束的地方添加关键帧,命名为AttackAnimationEnds

image-20230301175800741

脚本部分

来到SwitchStateTo()函数,新增部分:

image-20230301180313241

接着创建动画事件同名函数AttackAnimationEnds

public void AttackAnimationEnds()
{
//切换到普通状态
SwitchStateTo(CharacterState.normal);
}

刀光粒子

找到资源,拖入角色下的visual下的vfx对象

image-20230301184715205

打开PlayerVFXManager.cs玩家特效管理器脚本,新增刀光粒子相关代码

image-20230301185913999

玩家攻击前滑行(瞬移)代码

定义攻击开始时间,攻击时长,攻击速度等变量

#region Player slides
private float attackStartTime;//攻击开始时刻
public float attackSlideDuration = 0.4f;//攻击时长
public float attackSlideSpeed = 0.006f;//攻击速度

#endregion

SwitchStateTo状态更新函数的新状态(newState)分支中添加代码,

判定为玩家后保存当前时刻.

image-20230302143804605

FixedUpdate()中更新代码

image-20230302164125164

测试:点击左键角色会往前瞬移一段然后发出攻击.

敌人攻击

设置敌人攻击动画

找到enemy01,添加攻击动画

image-20230302171703249

通过点击动画可跳转到资源

image-20230302171747225

找到敌人攻击动画,在末尾添加关键帧,命名为AttackAnimationEnds

image-20230302172049162

脚本部分

敌人到达主角位置时攻击SwitchStateTo(CharacterState.Attacking);//切换到攻击状态,具体代码如下:

/// <summary>
/// 敌人行动
/// </summary>
private void CalculateEnemyMovement()
{
//玩家和ai距离 如果大于ai停止距离
if (Vector3.Distance(playerTarget.position, transform.position) >= navMeshAgent.stoppingDistance)
{
//SetDestination:设置或更新目标,从而触发新路径的计算
navMeshAgent.SetDestination(playerTarget.position);
animator.SetFloat("Speed", 0.2f);
}
else
{
navMeshAgent.SetDestination(transform.position);//呆在原地
animator.SetFloat("Speed", 0f);
SwitchStateTo(CharacterState.Attacking);//切换到攻击状态
}
}

测试发现有报错,原因是SwitchStateTo()开头有个玩家输入操作代码,加一个判断语句即可

image-20230302172955896

再次测试出现的问题是敌人攻击前不会转向玩家

来到SwitchStateTo(),找到newState部分.新加代码:

if (!IsPlayer)
{
//攻击前转向玩家
Quaternion newRotation = Quaternion.LookRotation(targetPlayer.position - transform.position);
transform.rotation = newRotation;
}

整体代码如下:

image-20230302173542054

设置敌人攻击特效

找到特效拖入

image-20230302180003155

调整位置

image-20230302180056259

在特效脚本中添加敌人部分