Unity程序猿勇闯茶杯之魂(二)

阅读 116
收藏 3
2018-01-18
原文链接:zhuanlan.zhihu.com

好久不见!茶杯头系列竟然更新啦,是不是很良心(别告诉我你是看了题图进来的~)!

前段时间惊闻ios上出现了假的茶杯头手游(已被苹果商店移除),就很好奇的去找了找视频看看效果如何…… 好吧,更加坚定了我填坑的信心(你们还能看到这一期更新真的要感谢这个视频啊喂)……


还是我自己搞吧……

那么废话不多说,让我们书接上回:

在上一期中,主要介绍了主角的一些基本操作。在本期中,将先补充一些主角的其他操作(填坑)并制作一个Boss并能与玩家产生交互(再挖一个大坑)。

角色操作补充

上一期涉及的基本操作包括移动,跳跃,下蹲,冲刺和普通射击。本期将补充EX射击(也就是开大)和Parry(消除)两个操作。

1.EX射击

EX射击和普通射击的原理类似,但需要注意的是,EX射击的子弹的射击方向为八方向,所以代码中要考虑到各种方向键输入的情况;而且在地面上和在空中释放EX射击的动画是有一点差别的(前几帧动画稍有不同),在不同状态下要出发进入不同的动画;此外,还有一个细节需要留意,EX射击会产生很微弱的反冲力从而得到更好的视觉效果。

private void _Explosion()
{
    _isExplosion = true;

    _rigidbody.velocity = new Vector2(0, 0);
    _rigidbody.gravityScale = 0;

    //根据是否在地面触发对应动画
    if (_isGrounded)
    {
        _explosionDustBornEffectPos = _explosionDustBornEffectGroundPoint;
        _animator.SetTrigger("GroundExplosion");
    }
    else
    {
        _explosionDustBornEffectPos = _explosionDustBornEffectAirPoint;
        _animator.SetTrigger("AirExplosion");
    }

    //根据输入的方向键改变射击方向
    if(Input.GetKeyDown(KeyCode.W) || Input.GetKey(KeyCode.W))
    {
        if (Input.GetKeyDown(KeyCode.A) || Input.GetKey(KeyCode.A) || Input.GetKeyDown(KeyCode.D) || Input.GetKey(KeyCode.D))
        {
            _explosionDustPos = _explosionDustBornPointRightUp;
            _shootDic = new Vector2(_faceDic, 1);
            _animator.SetFloat("ExplosionState", 0.5f);
        }
        else
        {
            _explosionDustPos = _explosionDustBornPointUp;
            _shootDic = new Vector2(0, 1);
            _animator.SetFloat("ExplosionState", 0.25f);
        }
    }
    else if (Input.GetKeyDown(KeyCode.S) || Input.GetKey(KeyCode.S))
    {
        if (Input.GetKeyDown(KeyCode.A) || Input.GetKey(KeyCode.A) || Input.GetKeyDown(KeyCode.D) || Input.GetKey(KeyCode.D))
        {
            _explosionDustPos = _explosionDustBornPointRightDown;
            _shootDic = new Vector2(_faceDic, -1);
            _animator.SetFloat("ExplosionState", 0.75f);
        }
        else
        {
            _explosionDustPos = _explosionDustBornPointDown;
            _shootDic = new Vector2(0, -1);
            _animator.SetFloat("ExplosionState", 1f);
        }
    }
    else
    {
        _explosionDustPos = _explosionDustBornPointRight;
        _shootDic = new Vector2(_faceDic, 0);
        _animator.SetFloat("ExplosionState", 0);
    }
}

再将对应的灰尘特效和子弹运动的脚本挂载到事先做好的预制体上,得到的效果如下(可以无限EX射击的感觉真的是爽):

2.Parry消除

Parry可以说是茶杯头中一个很重要的操作,不仅影响最终的评分,关键时刻还能通过Parry躲过敌人攻击。那么在实现这项功能时要注意:一是Parry仅能在跳跃状态下出发,所以要在跳跃状态中才能相应Parry的输入指令;另一点是在Parry成功时,会有一个很短暂的玩家停在空中的效果,这里需要在Parry成功的同时将Animator的速度变为0,在暂停时间过后恢复为1,同时在暂停时要将玩家的速度和重力影响都改为0,就可以基本表现出暂停的效果啦。

private void _Parry()
{
    _isParry = true;
    _isAlreadyParry = true;

    if (OnParry != null)
    {
        OnParry(true);
    }

    _animator.SetTrigger("Parry");
}

public void EnterPause(Vector2 parryHitPos) //进入暂停状态
{
    _isInParryState = true;
    _rigidbody.velocity = new Vector2(0, 0);
    _rigidbody.gravityScale = 0; //暂停无速度无重力

    _curAnimatorSpeed = _animator.speed;
    _animator.speed = 0f;
    _isOnPause = true;

    if(OnEnableParryEffect != null)
    {
        OnEnableParryEffect(_parryEffectPos.position, parryHitPos);
    }
}

那么将相应的效果预制体之类的准备好,来试验一下吧:

玩家的操作就介绍到这里了,玩家的其他状态例如被击中和死亡会放到后面再说,接下来就要开始制作Boss了。

Boss的制作

茶杯头中的Boss众多,各有特色,这里仅选取菜园关卡中的土豆Boss做个示范(其实是没来得及做那么多(lll¬ω¬))。

1.场景搭建

既然是菜园关的Boss,那么就先把场景搭建好:

挺还原的!

好了,又到了扣细节的时间,没错,这个场景中也是有很多细节的!树上的轮胎是在不断播放动画的,而且天空中的云朵是在运动的,这里需要对云朵资源进行回收与复用来避免频繁的实例化,那么就需要用到在上一期提及的对象池,对象池的基本原理就是将要多次使用的物体存放于一个设定好的池内(在代码中通常是列表或字典),需要时从池内取出对应物体,在使用完毕后放回池内来实现循环利用。

public void GetCloudInstance(int num, GameObject go)
{
    var startPos = _GetStartPos(num);
    var instanceToPool = _instancePool.GetInstance(go).GetComponent<Cloud>();

    Vector3 newStartPos = new Vector3(startPos.x, startPos.y, go.transform.position.z);
    instanceToPool.ResetPos(newStartPos, instanceToPool.CurrentType, this);
}

public void ReturnInstance(GameObject go)
{
    _instancePool.ReturnInstance(go);
}

该段代码中的_instancePool即为设定好的对象池类,来实现出池和入池。这个类的具体代码可以参考已上传至GitHub上的SimpleGameObjectPool.cs脚本。

那么,设定好云朵的速度,产生位置和消失位置后,整个场景的效果就是下面这样了:

2.Boss状态分析

场景搭建完毕,接下来让Boss动起来。这个土豆Boss大体分为四种状态:跳出地面,闲置状态,攻击状态和被击败状态。跳出地面及被击败只需要运用Animator就足够,这个Boss的攻击模式也很简单:每轮攻击会发射四颗子弹(前三颗为普通子弹,最后一颗为可Parry子弹),攻击的动画速度会随着轮数的增加而变快,并以三轮为一个循环,每轮攻击之间都有固定的时间间隔。那么在代码中可以使用一个储存了三个速度值的数组来控制每一轮攻击时攻击动画的播放速度,每当一轮攻击的四个子弹发射完毕后,马上切换至下一个速度,并在设定的时间间隔后进入下一轮攻击。

//update中与攻击相关的部分代码
if (_isTimerWorking) //每一轮攻击结束后开始计时,超过设定的时间间隔就进入下一轮攻击
{
    _ChangeLocalPos(-0.2f);
    _animator.speed = 1;
    _timer += Time.deltaTime;
}

if(_timer < _intervalTime)
{
     return;
}

if(!_isInAttackState)
{
     _isTimerWorking = false
     _animator.speed = _curSpeed;
     _animator.SetTrigger("Attack");
     _isInAttackState = true;
}
//end update

//每一轮攻击结束后切换至下一个动画播放速度
private void _OnNextSpeed(int stage)
{
    if (stage != 2)
    {
        stage = stage + 1;
        _curAttackStage = (AttackStage)stage;
    }
    else
    {
        stage = 0;
        _curAttackStage = AttackStage.StageOne;
    }

    _curSpeed = _fireSpeed[stage];
}

Boss发射子弹和玩家类似,在动画某一特定帧调用帧事件,并运用事件机制将发射子弹的消息通知给相应的管理脚本,并在管理脚本中获取对应的类型子弹实例。

private void _ShootBullet()
{
    _bulletCount += 1;

    //已发射子弹数小于4时发射普通子弹,等于4时发射可Parry子弹
    if (_bulletCount < 4)
    {
        if(OnShoot != null)
        {
            OnShoot(0);
        }
    }
    else
    {
        if (OnShoot != null)
        {
            OnShoot(1);
        }
    }
}

那么,Boss的初步效果就是这样的了:

3.玩家与Boss的交互

到了最激动人心的时刻了,玩家和Boss终于可以开始互殴了!为了有更好的表现效果,我们需要在玩家和Boss发射的子弹上添加脚本来判断是否击中了对方,这里玩家和Boss子弹都运用了OnTriggerEnter的物理触发模式,若检测到接触到的物体是要造成伤害的对象,则会使其出发受到伤害的函数。无论是玩家还是Boss,受到伤害后均要有视觉上的反馈:

  • 对于玩家来说,接触到Boss的子弹会触发被击中动画,进入被击中状态,进入被击中状态时不会响应玩家的任何输入;并且在被击中动画播放完毕后进入一段时间的无敌状态,在无敌状态下,玩家会有规律的闪烁效果,这里只需要按照一定时间间隔改变Sprite Renderer的透明度即可,并且在此期间不会触发被攻击判定。
private void _EnterInvincibleState() //进入无敌状态
{
    _invincibleTimer += Time.deltaTime; //记录无敌时间
    if(_invincibleTimer >= HIT_INVINCIBLE_TIME) //超过无敌时间,退出无敌状态,停止闪烁
    {
        _isInvincible = false;
        _isFadeAway = false;
        _spriteRender.color = new Color(1, 1, 1, _maxAlpha);
        _invincibleTimer = 0;
    }
}

private void _FadeAway() //无敌时闪烁
{
    _fadeAwayTimer += Time.deltaTime;
    if(!_isInFade && _fadeAwayTimer >= _normalTime)
    {
        //通过改变透明度实现闪烁的效果
        _spriteRender.color = new Color(1, 1, 1, _fadeAlpha);
        _isInFade = true;
        _fadeAwayTimer = 0f;
    }
    else if(_isInFade && _fadeAwayTimer >= _fadeAwayTime)
    {
        _spriteRender.color = new Color(1, 1, 1, _maxAlpha);
        _isInFade = false;
        _fadeAwayTimer = 0f;
    }
}
  • 对于Boss来说,若接触到玩家发射的子弹也会有类似的闪烁效果,但Boss的闪烁原理与玩家不同,是通过Sprite Mask并控制遮罩层的显隐来实现的。

最后我们再来处理一下玩家和Boss的被击败/死亡状态。这里要涉及到数据的交互,由于现在仅需要实现效果,所以还没有用到专门的类去处理数据(正式项目中不仅需要专门的数据类,同时也需要用专门的手段来存储和读取关卡,伤害,Boss血量等各类数据信息)。这里用最简单的在玩家类中设置生命数,每被击中一次生命数减一,为0时触发死亡动画,在玩家死亡时要注意使按键输入无效,以免出现bug;Boss则是在Boss类中直接设定血量,被子弹击中时扣除相应的子弹伤害,血量为0时播放被击败动画。实现效果可以参考以下两个短视频:

首先是玩家被击中三次后死亡的效果

再来看看Boss被击败后的效果

嗯,至少比那个ios上的假茶杯头好多了(根本不是一个平台的啊喂~)。那么至此,茶杯头系列算是暂时告一段落了,至于还会不会有更新,我只想说:有梦是好事,可惜不现实,还是梦里有缘再见吧~

目前涉及到的所有的代码已经传至我的GitHub(Yukimine33/CupheadCodeByMyself),欢迎查阅。

哦,对了,茶杯头刚刚获得了steam评选出的"超乎想象"和"最佳原声音轨"两项奖哦,这么良心的作品还不快去买爆(逃~)!!!

照例,有想踏上游戏开发道路的同学,欢迎到www.levelpp.com/强势围观。

评论