系列文章: 用Unity重现《空洞骑士》的苦痛之路(1):动作篇 用Unity重现《空洞骑士》的苦痛之路(2)——人物控制篇 用Unity重现《空洞骑士》的苦痛之路(3)——地图篇 用Unity重现《空洞骑士》的苦痛之路(4)——特效篇 本篇难度:★★★☆☆ 大家好。受苦受难的虫子们啊,我又来继续了。 小姐姐这厢有礼了 紧接上一期的内容,这期主要讲解人物控制的代码。 代码逻辑块相对复杂,并且代码较多,本期将会挑选重点进行讲解,代码不会完全贴出(太长了),所以大部分方法都不是完整的,细节方面还请下载文章末尾的工程,打开查看。 本期相对于上篇难度巨幅提高,如果说上篇是normal那这篇就是very hard。食用时请注意别噎着。 移动核心逻辑(难) 为了让游戏中玩家的移动能够完全被开发者掌握,本工程并没有使用Unity的物理引擎来进行移动操作,而是手动模拟相应的物理特性并进行移动。 在2D游戏中,玩家的移动总是能够分解成2个标的目的上的位移。我们移动的逻辑实现也是这样。 首先获取到这一帧的移动速度,然后计算出2个标的目的上的位移,再计算出下一个位置是否能够进行移动。如果无法移动,就需要对下一帧该标的目的的移动逻辑进行修正后再实施移动。原理如下图: 移动原理 其中关于位置修正,我们还得手动打Box射线来检测是否碰撞,并按照结果进行相应的操作。原理如下: 位移修正原理 请注意:由于我们是先移动X轴,在移动Y轴,并没有进行线性的移动,会在特定情况下舍弃部分位移(即位置出现偏差)。但是由于我们的移动是每一帧进行的,此处的误差实际上可以忽略,强迫症患者可以考虑在这个方法基础上进行修改。 代码实现需要获取到当前帧的速度,并且按照速度计算出这帧玩家给个标的目的的位移,用于下面的计算。然后按照移动的标的目的,分开处理。 先处理摆布标的目的的位移,然后使用box射线检测,判断下一帧是否会碰到碰撞体或者陷阱,如果是墙壁(碰撞体),还需要通过碰撞点的位置进行修正,达到满意的移动效果。同时按照需求,更新对应的动画状态。代码如下: - public Vector3 moveSpeed; //每一帧的移动速度
- public Vector2 boxSize; //玩家碰撞盒的大小
- public void CheckNextMove()
- {
- Vector3 moveDistance = moveSpeed * Time.deltaTime;//当前帧的移动位移
- if (moveSpeed.x!= 0)//当摆布速度有值时
- {
- RaycastHit2D lRHit2D = Physics2D.BoxCast(transform.position, boxSize, 0, Vector2.right * moveSpeed.x, 5.0f, playerLayerMask);
- if (lRHit2D.collider != null)//如果当前标的目的上有碰撞体
- {
- float tempXVaule = (float)Math.Round(lRHit2D.point.x, 1); //取X轴标的目的的数值,并保留1位小数精度。防止由于精度产生鬼畜行为
- Vector3 colliderPoint = new Vector3(tempXVaule, transform.position.y); //重新构建射线的碰撞点
- float tempDistance = Vector3.Distance(colliderPoint, transform.position); //计算玩家与碰撞点的位置
- if (tempDistance > (boxSize.x * 0.5f + distance)) //如果距离大于 碰撞盒子的高度的一半+最小地面距离
- {
- transform.position += new Vector3(moveDistance.x, 0, 0); //说明此时还能进行正常移动,不需要进行修正
- }
- else//如果距离小于 按照标的目的进行位移修正
- {
- float tempX = 0;//新的X轴的位置
- if (moveSpeed.x> 0)
- {
- tempX = tempXVaule - boxSize.x * 0.5f - distance + 0.05f; //多加上0.05f的修正距离,防止出现由于精度问题产生的鬼畜行为
- }
- else
- {
- tempX = tempXVaule + boxSize.x * 0.5f + distance - 0.05f;
- }
- transform.position = new Vector3(tempX, transform.position.y, 0);//修改玩家的位置
- }
- }
- else
- {
- transform.position += new Vector3(moveDistance.x, 0, 0);
- }
- }
- }
复制代码 同理,上下也是一样的。只不外需要考虑受到重力的情况。在不进行上下移动的时候,加上判断是否在地面的功能,用来决定是否添加重力。这可以通过朝地面打射线的方式进行判断。 代码如下: - public bool CheckIsGround()
- {
- RaycastHit2D hit2D = Physics2D.BoxCast(transform.position, boxSize, 0, Vector2.down,5f, playerLayerMask);
- if (hit2D.collider != null)
- {
- float tempDistance = Vector3.Distance(transform.position, hit2D.point);
- if (tempDistance > (boxSize.y * 0.5f + distance))//如果距离大于 碰撞盒子的高度的一半+最小地面距离
- {
- return false;
- }
- else
- {
- return true;
- }
- }
- else
- {
- return false;
- }
- }
复制代码 摆布移动 完成了上面主要移动核心逻辑、并在Update函数调用后,接下来的内容就要稍微简单一些。 由于实际的移动是由别的函数来进行实现,那么摆布移动函数就只需要按照玩家的按键输入,来更新玩家的速度即可。如下: - public void LRMove()
- {
- float h = Input.GetAxis("Horizontal");
- moveSpeed.x = h * speed;//更新摆布轴上的速度
- //接下来更新各种动画状态
- }
复制代码 跳跃 在《空洞骑士》中,跳跃是按照按键的蓄力时长,来控制跳跃高度。如果我们需要实现这个功能,就需要在KeyDown事件中触发跳跃,key事件中进行蓄力,KeyUp事件中停止跳跃蓄力,跳跃状态取消,并更新动画。逻辑图如下: 跳跃功能逻辑图 按照上面的逻辑图实现的跳跃代码如下: - public float jumpTime; //跳跃的最大蓄力时间
- float timeJump; //跳跃当前的蓄力时间
- public void Jump()
- {
- if (Input.GetKeyDown(KeyCode.Space))
- {
- jumpState = true; //进入跳跃状态
- moveSpeed.y += jumpPower;//初始添加向上的力
- timeJump = 0;//蓄力时间清零
- }
- else if (Input.GetKey(KeyCode.Space) && jumpCount<=2 && jumpState)
- {
- timeJump += Time.deltaTime;//蓄力时间增加
- if (timeJump < jumpTime)
- {
- moveSpeed.y += jumpPower;//蓄力
- }
- }
- else if (Input.GetKeyUp(KeyCode.Space))
- {
- jumpState = false;//退出跳跃状态
- timeJump = 0;//蓄力时间清零
- }
- }
复制代码 二段跳实现的方法原理也是一样,只是需要在KeyDown事件中,区分当前是二段跳还是一段跳,并别离进行蓄力操作即可。 暗影冲刺 在冲刺状态下,玩家不受到重力,且此时不会接受按键输入。于是我们需要声明2个变量别离用来控制是否获取按键输入,以及应用重力。并在冲刺开始的时候关闭对应的开关,给玩家一个固定的移动速度,在冲刺结束后重新打开对应的开关,速度回滚。逻辑图如下: 暗影冲刺逻辑图 实现的代码如下: - public bool gravityEnable; //重力开关
- public bool inputEnable; //接受输入开关 true 游戏接受按键输入 false不接受按键输入
- public void SprintFunc()
- {
- if (Input.GetKeyDown(KeyCode.J) && isCanSprint)
- {
- StartCoroutine(SprintMove(sprintTime));//冲刺协程
- }
- }
- IEnumerator SprintMove(float time)
- {
- inputEnable = false;
- gravityEnable = false;//关闭按键输入,以及不在应用重力
- moveSpeed.y = 0;//Y轴速度清零
- isCanSprint = false;
- if (nowDir == PlayDir.Left)
- {
- moveSpeed.x = 15*-1;
- }
- else
- {
- moveSpeed.x = 15;
- }//按照标的目的施加速度
- yield return new WaitForSeconds(time);//延迟time秒后执行
- inputEnable = true;
- gravityEnable = true;
- isCanSprint = true;//状态回滚
- }
复制代码 超级冲刺原理同上,但是需要考虑到需要蓄力,并且该状态结束是由按键与移动进行控制的。所以我们在实现超级冲刺的时候,需要用两种方式来结束。思路图如下: 超级冲刺逻辑图 代码就不贴出了,欢迎参考工程食用。 爬墙切换 原版游戏里,在空中时碰到特定墙壁会进入到爬墙的状态。 条件是下一帧需要碰到墙壁,且距离地面有必然的高度才行。代码如下: - public void EnterClimpFunc(Vector3 rayPoint) //移动检测到下一帧碰到墙壁时调用
- {
- //设定碰到墙 且 从碰撞点往下 玩家碰撞盒子高度内 没有碰撞体 就可进入碰撞状态。
- RaycastHit2D hit2D = Physics2D.BoxCast(rayPoint, boxSize, 0, Vector2.down, boxSize.y, playerLayerMask);
- if (hit2D.collider != null)
- {
- Debug.Log("无法进入爬墙状态 "+ hit2D.collider.name);
- }
- else
- {
- playAnimator.SetTrigger("IsClimb");//动画切换
- isClimb = true;
- isCanSprint = true; //爬墙状态,冲刺重置
- }
- }
复制代码 有3种方式可以退出爬墙状态: 1.玩家本身受重力下落,超出墙壁的范围。 2.玩家按下跳跃键退出。 3.玩家按下反标的目的移动键退出。 这里只提一下跳跃退出实现的原理。比力简单,就不贴出代码了。在按下跳跃键后朝着墙壁的反标的目的施加一个朝上的力,就可使玩家离开碰撞体,退出爬墙状态。图示如下: 跳跃退出实现原理图 攻击交互 在《空洞骑士》中,攻击碰到特定的物体时,会有后座力的效果,并且会刷新玩家身上特定的技能状态。实现逻辑就是在攻击的时候,进行射线检测,并按照碰撞体的标签,以及攻击的标的目的,来决定状态的刷新,以及是否施加后坐力的效果。逻辑图大致如下: 攻击判定逻辑图 在代码中的实现: - public void AttackFunc()
- {
- if (Input.GetKeyDown(KeyCode.K))//按下攻击键
- {
- CheckAckInteractive((int)nowDir);
- }
- }
- public void CheckAckInteractive(int dir) //参数为攻击的标的目的
- {
- float distance = 1.8f; //射线的检测长度
- RaycastHit2D hit2D = new RaycastHit2D();
- Vector2 raySize = new Vector2(boxSize.x + 0.5f, boxSize.y); //扩大检测X轴范围
- switch (dir)
- {
- case 1:
- hit2D = Physics2D.BoxCast(transform.position, raySize, 0, Vector2.left, distance, playerLayerMask);
- break;
- case 2:
- hit2D = Physics2D.BoxCast(transform.position, raySize, 0, Vector2.right, distance, playerLayerMask);
- break;
- case 3:
- hit2D = Physics2D.BoxCast(transform.position, raySize, 0, Vector2.up, distance, playerLayerMask);
- break;
- case 4:
- hit2D = Physics2D.BoxCast(transform.position, raySize, 0, Vector2.down, distance, playerLayerMask);
- break;
- }
- if (hit2D.collider!=null)
- {
- if (hit2D.collider.gameObject.CompareTag("Trap")) //如果是陷阱就有后坐力
- {
- StartCoroutine(InteractiveMove(dir, 10)); //开启协程 施加后座力效果,并刷新状态
- }
- }
- }
复制代码 结语 完成了本期文章的内容后,我们的主角在动作方面已经完全满足了这个项目的要求。 接下来需要完善的就是发挥本身的创意,搭建快乐地图啦。并且在本期文章中,考虑到有的童鞋可能动画切换这块的想法跟我不一致,我有意删减了动画切换相关的代码。若是需要参考的话,欢迎点击文章末尾的下载链接下载后进行查看。(PS:我使用的是2017.4.17f1版本) 工程下载链接 链接:https://pan.baidu.com/s/1wJTQmSup2EOOdGBYVmYvVw提取码:etvn 相关链接,很(mai)重(mai)要(mai) 空洞骑士购买链接:https://store.steampowered.com/app/367520/Hollow_Knight/ 有线下学习游戏开发打算的童鞋,欢迎拜候http://levelpp.com/。 线上课程的传送门则如下:简明易懂的C#入门指南-网易云课堂?study.163.com 另有专业开发交(gao)流(ji)群等待大家强势插入:869551769 作者:繁华如梦 专栏地址:https://zhuanlan.zhihu.com/p/58882234
|