绯雪
6 min read Hero Image 游戏引擎 / 入门学习

02 Player 创建(勇者传说-2022Unity入门)

通过M_studio的勇者传说学习Unity入门基础

M_Studio的勇者传说-2022Unity入门 主要是知识点整理笔记

02 Player 创建

2.1 设置人物与基本组件

2.1.1 人物组件的添加

给人物添加Rigidbody 2D(重力)和CapsuleCollider 2D(碰撞体积)组件. 点击CapsuleCollider 2D组件下的”Edit Collider2D”编辑.按住Shift可以以中心为标准来调整碰撞箱的范围.

调整Offset以调整人物碰撞箱锚点的位置.

在Rigidbody下的Constrains里冻结z轴的旋转. Collision Detection设为continuous,以保证以后写代码方便进行探测.

2.1.2 平台组件的添加

给Platform添加Composite Collider 2D组件 (若Platform没有Rigidbody2D组件,unity会为你自动添加),使每一个瓦片的碰撞箱结合成一体,并将Rigidboay2D的Body Type设为Static来防止平台掉落。

2.2 创建及配置新输入系统

2.2.1 升级Input System

Edit/Project Settings/Player/Other Settings 修改如图所示两个选项

然后去PackageManager搜索input,下载如下package.

为Player添加Player Input组件并创建Input Actions.创建完毕删掉Player Input组件。 在PlayerInputControl中勾选如图所示方框,然后点击Apply.就会生成同名C#脚本.

2.2.2 PlayerController脚本

using JetBrains.Annotations;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.InputSystem;

public class PlayerController : MonoBehaviour
{
    // Start is called before the first frame update
    public PlayerInputControl inputControl;
    public Vector2 inputDirection;
    private void Awake()
    {
        inputControl = new PlayerInputControl();
    }
    private void OnEnable()
    {
        inputControl.Enable();
    }
    private void OnDisable()
    {
        inputControl.Disable();
    }
    private void Update()
    {
        inputDirection = inputControl.GamePlay.Move.ReadValue<Vector2>();
    }
}

大概思路是创建一个 PlayerInputControl 的实例 inputControl,然后读取其中Gameplay.Move中的移动相关的向量.

下面的这个ReadValue<>方法可以单独拿出来记一记.

private void Update()
    {
        inputDirection = inputControl.GamePlay.Move.ReadValue<Vector2>();
    }

2.3 实现人物行为

2.3.1 人物移动与翻转

使用速度来驱动物体,创建变量speed并输入值

在FixedUpdate() (持续性的按键情况,而如jump则是当作触发性的事件,减少性能开销) 中执行函数Move(),修改刚体速度,获取刚体组件

 private void FixedUpdate()
 {
     Move();    
 }
public void Move()
{
     rb.velocity = new Vector2(speed * Time.deltaTime * inputDirection.x , rb.velocity.y);
     transform.localScale = new Vector3(inputDirection.x < 0 ? -1 : 1, 1, 1);
 }

在Unity编辑器中使用变量,以及如何通过修改变量来控制游戏角色的移动。获得自身组件的速度,修改速度。

在Unity中如何实现人物的左右移动和反转,包括使用transform组件和sprite renderer组件(只改变图层,不涉及刚体和碰撞体积)两种方法。

transform.localScale

使用localScale的方法需要注意人物图片的锚点位置和localScale的数值。

2.3.2 人物跳跃

实现人物跳跃,编辑按键,添加跳跃方法

施加力,纵轴跳跃,编写跳跃代码,注意按键状态和执行时机

public float JumpForce;

   private void Awake()
   {
       inputControl = new PlayerInputControl();
       rb = GetComponent<Rigidbody2D>();
       inputControl.GamePlay.Jump.started += jump;
   }
   
    private void jump(InputAction.CallbackContext context)
 {
     // Debug.Log("Jump");
     rb.AddForce(transform.up * JumpForce, ForceMode2D.Impulse);
 }

将跳跃函数放在Awake()中执行,即给Jump.started注册一个jump事件函数,并且使用force mode的瞬时力方法实现跳跃

修改gravity scale为4,测试舒适的手感

2.4 环境物理检测

实现人物跳跃的连续动作,以及如何检测人物周围的物理环境,包括地面和墙面等。同时,还介绍了如何写物理检查代码。

创建一个通用脚本,检测周围的物理环境

使用2D物理检测方法实现物理检测,通过使用overlap circle方法,圆形检测范围判断碰撞箱之间是否接触,赋值给变量。

public void check()
{
   isGround = Physics2D.OverlapCircle((Vector2)transform.position + transform.localScale*bottomOffset, checkRadius, groundLayer);
}

其中设置选取合适的layer

选择人物脚底中心点作为坐标点,并设置检测范围,通过创建变量来访问组件,限制跳跃次数。

private void jump(InputAction.CallbackContext context)
{
    // Debug.Log("Jump");
    if(physicsCheck.isGround)
        rb.AddForce(transform.up * JumpForce, ForceMode2D.Impulse);
}

限制人物的跳跃,通过改变脚底的位移差值来实现检测范围的绘制,并使用内置方法在窗口中绘制出球形检测范围,以便直接改动范围。

private void OnDrawGizmosSelected()
{
    Gizmos.DrawWireSphere((Vector2)transform.position + transform.localScale*bottomOffset, checkRadius);
}

创建物理材质和避免人物粘在墙上。

2.5 创建人物动画

2.5.1 人物基本动画

制作人物的动画,也就是制作帧动画。创建新的文件夹保存所有的动画和动画控制器,给它起名叫做animations 创建animator控制器,并打开界面

将人物动画并添加到animator中,并且调整采样率和播放速度 在这里可以显示采样率,并且改为帧显示。

创建人物跑动动画并设置播放状态

在animator中可以连接各个animation,并且设置不同的条件进行状态切换 而使用exit time实现动画切换,不需要任何条件 在parameters创建条件,以速度条件来切换跑步动画, 创建新的C#脚本 PlayerAnimation,获得animator组件的使用权,控制人物动画切换

区分animator和animation的概念,要在脚本中获取animator的组件权限,要创建变量,animator的变量命名为animal缩写。

private Animator anim;
private Rigidbody2D rb;
private void Awake()
{
    anim = GetComponent<Animator>(); 
    rb = GetComponent<Rigidbody2D>();
}

设置float变量值,同时创建一个函数来执行所有的动画切换,并输入set float括号中的变量名和具体值。在翻转时存在负值,使用绝对值函数来赋值

 private Animator anim;
 private Rigidbody2D rb;
 private void Awake()
 {
     anim = GetComponent<Animator>(); 
     rb = GetComponent<Rigidbody2D>();
 }
 private void Update()
 {
     SetAnimation();
 }
 public void SetAnimation()
 {
     anim.SetFloat("velocityX", Mathf.Abs(rb.velocity.x));  
 }

完成绑定和相互的操作

可以创建走路和跑步的动画,通过条件切换实现

2.5.2 跳跃动画

使用Blend tree混合来创建多个阶段的动画

在动画制作中使用float值和列表来添加动画片段和控制其转换速度,创建一个新的float值,名为velocityY。

跳跃的状态转换通过 velocityY 切换 使用代码填入控制动画的切换条件变量

public class PlayerAnimation : MonoBehaviour
{
    private Animator anim;
    private Rigidbody2D rb;
    private PhysicsCheck physicsCheck;
    private void Awake()
    {
        anim = GetComponent<Animator>(); 
        rb = GetComponent<Rigidbody2D>();
        physicsCheck = GetComponent<PhysicsCheck>();
    }
    private void Update()
    {
        SetAnimation();
    }
    public void SetAnimation()
    {
        anim.SetFloat("velocityX", Mathf.Abs(rb.velocity.x));
        anim.SetFloat("velocityY", rb.velocity.y);
        anim.SetBool("isGround",physicsCheck.isGround);
    }
}

并且设置animator中的转换过程,其中land可以设置为有速度就强制停止,避免细节落地后的蹲起的时候在跑步。

2.6 人物属性与伤害计算

添加敌人,即野猪的不同状态 调整野猪的Sorting Layer和Layer,使其与玩家在同一层中,但是优先级小于player

Sorting Layer

Layer

设置触发器,实现野猪和玩家互相穿越和穿过过程中发生事情的具体逻辑,即通过触发器检测实现攻击和伤害判定。 \

给野猪设置两个不同的碰撞体,一个是矩形,一个是胶囊形,调整胶囊大小和方向。

同时添加碰撞剔除层,将角色之间的碰撞取消,避免堵塞。

在脚本中,使用on trigger state2D方法检测碰撞触发器

 private void OnTriggerStay2D(Collider2D collision)
 {
     collision.GetComponent<Character>()?.TakeDamage(this);
 }

游戏性中需要计算血量增减、受伤和无敌,创建character代码,而在attack代码中使用on trigger state访问被攻击者,调用take damage函数计算伤害值

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

public class Character : MonoBehaviour
{
    [Header("基本属性")]
    public float maxHealth;
    public float currentHealth;

    [Header("受伤无敌")]
    public float invulnerableDuration;
    private float invulnerableCounter;
    public bool invulnerable;

    private void Start()
    {
        currentHealth = maxHealth;
    }
    private void Update()
    {
        if(invulnerable) { 
            invulnerableCounter -= Time.deltaTime;
            if(invulnerableCounter < 0 )
            {
                invulnerable = false;
            }
        }
    }
    public void TakeDamage(Attack attacker)
    {
        if (invulnerable)
            return;
        if(currentHealth - attacker.damage > 0)
        {
            currentHealth -= attacker.damage;
            TriggerInvulnerable();
        }
        else
        {
            currentHealth = 0;
        }
    }
    private void TriggerInvulnerable()
    {
        if(!invulnerable)
        {
            invulnerable = true;
            invulnerableCounter = invulnerableDuration;
        }
    }
}

在游戏中要实现碰撞后的单次伤害以及受伤无敌的判断算法,其中包括计时器的使用和状态的改变。

首先计算当前应该有的血量,先输出伤害值

private void Start()
    {
        currentHealth = maxHealth;
    }

在游戏开始时,将当前血量设为max血量,减去受到的伤害

			 currentHealth -= attacker.damage;
            TriggerInvulnerable();

创建无敌时间和计时器变量,触发无敌状态,判断是否需要减少血量,通过在受到伤害后触发无敌,并在update中计时来缩减无敌时间。

默认是false,一旦受到一次伤害就会触发无敌,无敌时间可以通过update计时器来缩减。而计时器计时结束后,将受伤无敌状态改回为false,再次接受伤害。最后判断当前血量是否足以触发受伤无敌,避免出现负数。

    private void Update()
    {
        if(invulnerable) { 
            invulnerableCounter -= Time.deltaTime;
            if(invulnerableCounter < 0 )
            {
                invulnerable = false;
            }
        }
    }
    public void TakeDamage(Attack attacker)
    {
        if (invulnerable)
            return;
        if(currentHealth - attacker.damage > 0)
        {
            currentHealth -= attacker.damage;
            TriggerInvulnerable();
        }
        else
        {
            currentHealth = 0;
        }
    }
    private void TriggerInvulnerable()
    {
        if(!invulnerable)
        {
            invulnerable = true;
            invulnerableCounter = invulnerableDuration;
        }
    }

2.7 角色动画逻辑实现与判定实装

2.7.1 受伤和死亡的逻辑和动画

1.受伤2种动画表现 在animation中创建受伤与死亡的动画效果 由于在任何动作情况下,角色都可能受伤,因此在Layer创建新的单独层可以覆盖到上一层的各个动作;

为给角色添加闪烁的受伤效果,在animation可以添加新的 property(性质);修改如rgb值,并且在时间轴挑选时间点改动,过渡段就能创造出闪烁的效果 之后在animator中,重新在hurt layer创建并且标好条件

2.UnityEvent 使用方法 创建trigger触发器的功能,在 PlayerAnimation.cs 中,加入代码

public void PlayHurt()
{
    anim.SetTrigger("hurt");//触发hurt后执行
}

在character.cs中,使用unityevent的方法

 public UnityEvent<Transform> OnTakeDamage;

这样就能实现将各种想要实现的功能集成起来,只需要传入所需的参数 在代码中,执行unityevent需要使用固定的invoke方法,?. 能避免空值时候的错误,是一种固定写法。

3.受伤和死亡的函数 为了实现受伤就会反弹的效果,需要在PlayerController.cs中实现函数功能

public void GetHurt(Transform attacker)
{
    isHurt = true;  
    rb.velocity = Vector2.zero;
    Vector2 dir = new Vector2((transform.position.x - attacker.position.x), 0).normalized;
    rb.AddForce(dir*hurtForce, ForceMode2D.Impulse);

}

同时在animator中也可以创建用于控制人物状态的代码

同样,人物死亡也有事件方法,即 public UnityEvent onDie ; 并且触发 onDie?.Invoke(); 在PlayerController.cs中,死亡即执行

public void PlayerDead()
{
    isDead = true;
    inputControl.GamePlay.Disable();
}

2.7.2 三段攻击动画

创建三段的攻击动画,为attack1、2、3;并且在animation中单独创建attack layer,权重也设置为1

创建布尔值 isattack,和用于触发多段攻击的触发器attack;设置对应的条件 在inputsystem中也加入攻击相关的输入,同时在playercontroller.cs和playeranimation中添加对应的代码

animator中,攻击退出后,将isattack也设置回去,也在动画状态上添加脚本

2.7.3 攻击效果实装

在攻击动画中,出现剑影的动画片段实装攻击效果,单独在角色的下面创建子集gameobject,添加上碰撞体还有触发器

添加多边形碰撞体 ,并且勾选触发器

为防止攻击时的判定范围覆盖人物,伤到角色,在layers override中将人物本身进行排除; 同时,修改野猪也就是敌人身上碰撞体的优先级

使用光滑材质会导致人物移动距离位移,需要修改材质,避免这种情况,重新创建不一样的物理材质并且在代码中加变量来控制材质

[Header("物理材质")]
public PhysicsMaterial2D normal;
public PhysicsMaterial2D wall;

private void Update()
{
    inputDirection = inputControl.GamePlay.Move.ReadValue<Vector2>();
    CheckState();
}

public void CheckState()
{
    coll.sharedMaterial = physicsCheck.isGround ? normal : wall;
}