RecursiveFrog 发表于 2022-4-25 14:45

我们来用Unity做个2D像素boss战(2)角色篇

(本文作者 @酒九大魔王)
好久不见米娜桑,我,酒九大魔王又来了。
原本上一次的Boss制作教程更新完了以后就准备更新制作玩家部分的文章,结果因为种种原因才拖延到今天发出来(并不是忘了)。



苍白无力的辩解

所以废话不多说,本文的主题就是关于Boss战项目中的玩家的制作。Boss部分的内容传送门如下:
好了,咱们开始。



包含歉意


[*]准备工作
找到大家自己喜欢的玩家素材。当然这里建议大家选择素材时要注意图片的大小、所包含的动画数量、是否已经被等比列划分好了等等。这里了我选择了小红帽的素材(本人自称)。



截取的部分素材

然后和Boss制作的流程一样,我们导入进Unity之后,调整到合适的单位像的大小(Pixels Per Unit),进行切割(Sprite Editor)。这一次我们的素材依然是整整齐齐排列好的,但每个小人的大小不统一,所以了这里我们还是选择Grid By Cell Count模式来进行切割。
把素材都切出来后,选择一个图片作为在游戏中的人物并在这个物体身上挂载好需要使用的各个组件,还要记得将素材中的动画都制作出来。制作动画的流程就是在Project窗口右键新建出Animation,命名好之后将一个一个的切好的精灵图拖进去,就完成一个动画片段啦。再将这些动画片段拖入玩家的Animator中去。之后我们就可以在代码中控制动画播放了。别忘把碰撞体调整到一个合适的位置大小。
这里的流程比较繁琐,但是大部分2D游戏都是一样的。大家可以参考一些的视频学习:





玩家角色需要的组件



Animation窗口

现在准备工作基本上已经完成,下面就是编写玩家的脚本内容了。玩家身上只会挂载两个脚本,分别是PlayerCharacter和PlayerController。
现在让我们来编写其中的内容吧。

[*]PlayerCharacter脚本
这个脚本负责玩家的所有属性和行为,所以在编写之前,我们要想清楚需要做什么。
在我的这个实现里,玩家的属性包括了:血量,速度,伤害数值,滑铲CD(闪避CD)等等。在后面会直接通过代码块贴出来方便大家理解。
玩家的行为是重点,在这个项目中玩家拥有以下的行为:移动,跳跃,轻攻击,重攻击,滑铲,受伤,无敌时间。咱们一个一个来实现他们。
    Rigidbody2D playerRig;//玩家刚体
    Animator playerAni;//玩家动画控制器
    Transform playerTra;//玩家的Transform组件
    CapsuleCollider2D playerCol;//玩家的碰撞体(用于滑铲时改变大小位置)
    LayerMask ground;//地面的层级
    GameObject JumpBox;//实现空气跳的辅助
    SpriteRenderer playerRenderer;//玩家的图片显示

    public AudioSource music;//音频播放器
    public List<AudioClip> list;//玩家身上需要播放的音频

    public float MaxHp;//最大血量
    public float Hp;//当前血量

    public int AttackMode;//当前的攻击模式
    public float LightAttackDamge;//轻攻击伤害 15
    public float HeavyAttackDamge;//斧子攻击伤害 20

    public float Speed;//左右移动速度
    public float SlidSpeed;//闪避速度
    public float JumpSpeed;//跳跃速度
    public float AttackSpeed;//轻攻击补偿速度
    public float HeavySpeed;//重攻击补偿速度
    public float BeHitSpeed;//击退速度
    public float HeavyBeHitSpeed;//重攻击击退速度

    public float FallMultiplier;//下落加速度
    public float LowJumpMultiplier;//跳跃加速度

    public Vector3 NowDir;//记录开始时的按键方向

    public bool IsGround;//判断能否跳跃
    public bool IsJump;//判断是否在空中
    public bool IsSliding;//是否再闪避中
    public bool IsAttack;//是否在攻击中
    public bool canInput;//攻击中不能输入
    public bool canSliding;//闪避间隔
    public bool IsHit;//是否处于被攻击状态
    public bool IsDefend;//是否处于无敌状态
    public bool IsDisplay;//闪烁中的开关
    public bool IsMove;//奔跑的音频控制

    public int JumpMax;//最大跳跃数
    public int JumpCount;//当前跳跃数
    public int ComboCount;//当前攻击动作

    public float SlidTime;//冲刺时间
    public float SlidCountTime;//冲刺计时器
    public float SlidCd;//冲刺Cd
    public float StartCombo;//开始连击时间
    public float ComboTime;//连击时间
    public float DefendTime;//无敌时间
    public float FlashingTime;//闪烁间隔
    public float FlashTep;//闪烁计时器
    public float BackTime;//后撤步时间

    public Vector2 InitialSize;//初始碰撞体大小
    public Vector2 InitialOffset;//初始碰撞体位置
以上就是接下来会使用到的一些数据,我在代码中也会进行说明。
移动:
public void Move(float h)//移动
    {
      if (!canInput || IsHit)
      {
            return;
      }
      playerRig.velocity = new Vector2(h * Speed, playerRig.velocity.y);
      if (h > 0)
      {
            playerTra.localScale = new Vector2(-NowDir.x, NowDir.y);
            if (!IsJump && !IsSliding && !IsAttack)
            {
                playerAni.Play("Run");
                music.clip = list;
                if (!music.isPlaying)
                {
                  music.Play();
                }
            }
      }
      if (h < 0)
      {
            playerTra.localScale = new Vector2(NowDir.x, NowDir.y);
            if (!IsJump && !IsSliding && !IsAttack)
            {
                playerAni.Play("Run");
                music.clip = list;
                if (!music.isPlaying)
                {
                  music.Play();
                }
            }
      }
      if (h == 0)
      {
            if (!IsJump && !IsSliding && !IsAttack)
            {
                playerAni.Play("Idle");
                music.Pause();
            }
      }
    }
本次利用的是Rigidbody2D的移动,直接更改刚体的速度来控制左右的移动。
在这个代码中我们要考虑到,主角移动时需要朝向前进方向,因此我们在游戏一开始就记录下玩家的初始朝向NowDir(这里是一个Vector3值,记录玩家的Transform.localScale)根据这个来决定我们移动时的朝向。当然在移动时咱们就播放奔跑动画(Run)和对应的音频,而没有输入时就播放待机动画(Idle)且暂停音频播放。
跳跃:
    public void Jump() //跳跃
    {
      if (!canInput )
      {
            return;
      }
      if (IsGround)
      {
            IsJump = true;
            playerAni.Play("Jump");
            JumpCount = 0;
            playerRig.velocity = new Vector2(playerRig.velocity.x, JumpSpeed);
            JumpCount++;
            IsGround = false;
      }
      else if (JumpCount > 0 && JumpCount < JumpMax)
      {
            if (!IsHit)
            {
                playerRig.velocity = new Vector2(playerRig.velocity.x, JumpSpeed);
                JumpCount++;
                playerAni.Play("Jump");
            }
      }
    }
    public void CheckGround()//地板检测
    {
      IsGround = Physics2D.OverlapCircle(JumpBox.transform.position, 0.1f, ground);
      IsJump = !IsGround;
    }
    public void PlayFallAni() //播放下落动画
    {
      if (!IsGround && canInput && !IsSliding && !IsHit)
      {
            if (playerRig.velocity.y < 0)
            {
                playerRig.velocity += Vector2.up * Physics2D.gravity.y * (FallMultiplier - 1) * Time.deltaTime;
                playerAni.Play("Fall");
            }
            else if (playerRig.velocity.y > 0 && Input.GetAxis("Jump")!=1)
            {
                playerRig.velocity += Vector2.up * Physics2D.gravity.y * (LowJumpMultiplier - 1) * Time.deltaTime;
            }
      }
    }
跳跃这一行为,是通过一个过程来实现的:首先检测玩家是否满足跳跃条件,再进行跳跃,然后进行下落操作。因此,这里分别写了三个方法来负责。
首先是跳跃的基本操作内容,就是播放跳跃动画(Jump)和给刚体一个向上的速度,还有就是通过JumpConut的跳跃计数器来限制玩家进行多次跳跃。
然后是检测地面,这里我是这样做的:在玩家脚下位置放一个空物体(JumpBox)作为玩家的子物体,以它为中心来检测指定的地面层级(ground),之所以这么做是为了实现空气跳(就是当看上去觉得角色接触到地面但实际上还未真正碰到地面时,也可以进行跳跃),改善手感。
最后就是下落的部分,其中的主要操作是播放下落的动画(Fall)并且要在玩家起跳之后给一个向下的加速度。不然的话玩家的跳跃落下就会像一个气球一样,手感很差。而且还有一个好处就是:这样实现之后,轻击跳跃键和长按跳跃键的跳跃高度也不同。
轻攻击和重攻击:
    public void Attack()//攻击
    {
      if (!IsAttack && !IsSliding && !IsHit)
      {
            IsAttack = true;
            canInput = false;
            AttackMode = 1;
            playerRig.velocity = Vector2.zero;
            ComboCount++;
            if (ComboCount > 3)
            {
                ComboCount = 1;
            }
            StartCombo = ComboTime;
            if (ComboCount == 1)
            {
                playerAni.Play("Attack1");
            }
            if (ComboCount == 2)
            {
                playerAni.Play("Attack2");
            }
            if (ComboCount == 3)
            {
                playerAni.Play("Attack3");
            }
            playerRig.velocity = new Vector2(-playerTra.localScale.x * AttackSpeed, playerRig.velocity.y);
      }
    public void InAttack()//连击计时
    {
      if (StartCombo != 0)
      {
            StartCombo -= Time.deltaTime;
            if (StartCombo <= 0)
            {
                StartCombo = 0;
                ComboCount = 0;
            }
      }
    }
    private void OnTriggerEnter2D(Collider2D collision)
    {
      if (collision.gameObject.CompareTag("Boss"))
      {
            if (AttackMode == 1)
            {
                collision.GetComponent<FireCentipede>().BeHit(LightAttackDamge);
            }
            else if (AttackMode == 2)
            {
                collision.GetComponent<FireCentipede>().BeHit(HeavyAttackDamge);
            }
      }
    }
这里轻重攻击脚本中的内容其实是差不多的,这里的攻击效果是模仿了《死亡细胞》的攻击时不可移动但是存在向前方小幅度前进的速度补正。为了实现连击了,两个脚本共用一个连击计数器(ComboCount)可以实现轻重攻击之间的切换。当然连击衔接是有时间限制的,所以需要一个方法来倒计时,在规定时间中进行了输入才可以接上,一旦超过就从头开始。攻击到敌人的判定方式还是选择的利用OnTriggerEnter2D来实现敌人检测,但是上方的代码中并没有打开HitBox的代码,那么是在哪儿打开的呢?答案是在通过录制的功能录制在了攻击动画里。步骤如下:
在玩家物体下新建一个带有BoxCollider2D组件的物体(HitBox),把碰撞体组件中的IsTrigger勾选上,然后关闭掉默认物体的显示。
接下来点击玩家,并打开Animation窗口,在攻击动画中点击左上角的录制按钮(那个红色的位置)就开始录制了。录制过程中在合适的时候打开HitBox,并且根据动画攻击的范围大小调整HitBox的大小,最后关闭HitBox。完成录制之后关闭录制,这样做完之后当我们播放这个动画时,HitBox就会自动打开关闭,并且自动调整大小(精准攻击到敌人)。



Animation界面



效果展示

滑铲:
    public void StartSliding() //滑铲时的操作
    {
      if (!IsSliding && canInput && IsGround && !IsHit && SlidCountTime <= 0)
      {
            IsSliding = true;
            playerCol.offset = new Vector2(playerCol.offset.x, -0.7f);//更改玩家碰撞体大小(通过狭小空间)
            playerCol.size = new Vector2(playerCol.size.x, playerCol.size.x);//更改玩家碰撞体大小(通过狭小空间)
            StartCoroutine(SlidMove(SlidTime));
            playerAni.Play("Sliding");
            gameObject.layer = LayerMask.NameToLayer("Flashing");
      }
    }
    IEnumerator SlidMove(float time) //滑铲(通过协程实现)
    {
      canInput = false;
      music.Pause();
      if (NowDir.x == playerTra.localScale.x)
      {
            playerRig.velocity = new Vector2(-SlidSpeed, 0);
      }
      else if(NowDir.x == -playerTra.localScale.x)
      {
            playerRig.velocity = new Vector2(SlidSpeed, 0);
      }
      yield return new WaitForSeconds(time);
      playerCol.offset = InitialOffset;//回到初始玩家碰撞体大小
      playerCol.size = InitialSize;;//回到初始玩家碰撞体大小
      canInput = true;
      IsSliding = false;
      gameObject.layer = LayerMask.NameToLayer("Player");
      SlidCountTime=SlidCd;
    }
这里的滑铲可以理解为闪避,也就是朝一个方向快速移动,并且移动的时候是可以穿过敌人的(与敌人不会发生碰撞)。当然既然是滑铲,在空中肯定是不能施放的。基本思路就是使用滑铲时将玩家的层级放入一个与敌人没有碰撞的层级中去,开启一个协程在一定时间内向前方移动(更改刚体速度),时间结束后再将玩家放回原来层级,进入滑铲CD。



各个层级之间的碰撞情况

受伤和无敌时间:
    public void BeHit(Vector2 Dir,float damge)//被敌人攻击(轻微位移)
    {
      music.clip = list;
      music.Play();
      IsHit = true;
      playerRig.velocity = Dir * BeHitSpeed;
      playerAni.SetTrigger("Hit");
      Hp -= damge;
    }
    public void Defend() //无敌状态
    {
      if (IsDefend)
      {
            DefendTime -= Time.deltaTime;
            if (DefendTime > 0)
            {
                gameObject.layer = LayerMask.NameToLayer("Flashing");
                FlashTep += Time.deltaTime;
                if (FlashTep >= FlashingTime)
                {
                  if (IsDisplay)
                  {
                        playerRenderer.enabled = false;
                        IsDisplay = false;
                        FlashTep = 0;
                  }
                  else
                  {
                        playerRenderer.enabled = true;
                        IsDisplay = true;
                        FlashTep = 0;
                  }
                }
            }
            else if (DefendTime <= 0)
            {
                gameObject.layer = LayerMask.NameToLayer("Player");
                IsDefend = false;
                IsDisplay = true;
                playerRenderer.enabled = true;
                DefendTime = 3f;
            }
      }
    }
受伤就是被攻击到之后减少血量,并被击退(给一个朝后方的速度)。无敌状态其实也是将玩家放入一个与敌人不会产生碰撞的层级中去,但为了视觉上体现出玩家处于无敌时间,可以使用一个计时器,将玩家的图片显示关闭再打开制作出闪烁效果。

[*]PlayerCharacter脚本
public class PlayerController : MonoBehaviour
{
    PlayerCharacter player;
    void Awake()
    {
      player = GetComponent<PlayerCharacter>();
    }
    void Update()
    {
      float h = Input.GetAxis("Horizontal");
      player.Move(h);
      player.InAttack();
      if (Input.GetKeyDown(KeyCode.Space))
      {
            player.Jump();
      }
      if (Input.GetKeyDown(KeyCode.Z))
      {
            player.StartSliding();
      }
      if (Input.GetKeyDown(KeyCode.X))
      {
            player.Attack();
      }
      if (Input.GetKeyDown(KeyCode.S))
      {
            player.StartBackStep();
      }
      if (Input.GetKeyDown(KeyCode.C))
      {
            player.HeavyAttack();
      }
    }
}
控制脚本中就只需要进行按键输入读取就好啦。
工程链接:
https://pan.baidu.com/share/init?surl=czDhXjecYDR1hzbEwtbpYg
提取码: im9u
<hr/>皮皮关与网易联合开发了完备的游戏开发线上课程,第一期班本周五即会开班。想要进军游戏开发领域的童鞋,可戳下面链接了解课程详情:
如果有任何问题,还可以通过以下入口与网易小伙伴取得联系,有什么问题直接正面怼他(战术后退
同时,欢迎加入游戏开发群欢乐搅基:1082025059
页: [1]
查看完整版本: 我们来用Unity做个2D像素boss战(2)角色篇