“角色碰撞
前提是这样的:我们要做一个物理部分简单、健壮、响应迅速、准确和可预测的2D平台游戏。我们不想在开发中采用一个复杂的2D物理引擎,主要有以下原因:
·不好预测的碰撞响应·难以构建准确、健壮的人物移动·非常复杂比较难使用·需要比简单物理引擎消耗更多的处理能力
当然,使用现成的物理引擎也有很多好处,比如可以很容易设置复杂的相互作用,但是这并不是我们游戏所需要的。
一个定制的物理引擎可以让游戏有独特的感觉,所以它真的非常重要!即使仅仅在游戏中做一些简单的基本设置,物体的移动以及和其他物体相互作用的方式都会受到你自己规则的影响,而不是被别人的规则所支配。让我们开始吧!角色的包围盒让我们从定义物理引擎将要使用的形状开始。可以用来表示物理物体的一个最基本的形状是轴对齐矩形包围盒(AABB)。AABB基本是一个不旋转的矩形。在很多平台游戏里,AABB已经足够来近似游戏的每一个物体。它们非常有效,因为AABB的重叠计算非常简单而且需要非常少的数据,要描述一个AABB,只要中心点和大小就足够了。
闲话少说,让我们创建一个AABB结构。
public struct AABB{}
正如前面提到的,我们全部需要的数据就是2个向量:第一个向量是AABB的中心,第二个变量是这个AABB大小的一半。为什么是大小的一半?因为大部分的计算需要的都是大小的一半,所以为了不在每一次使用的时候都去算一下,我们直接记录大小的一半而不是大小。
public struct AABB{ public Vector2 center; public Vector2 halfSize;}让我们添加一个构造函数,这样就可以用特定的参数来创建一个结构。
public AABB(Vector2 center, Vector2 halfSize){ this.center = center; this.halfSize = halfSize;}
有了AABB,我们就可以创建碰撞检测的功能了。首先,让我们做一个简单的检测来判断两个AABB是否互相碰撞。这很容易实现-我们只需要判断每个轴上的中心点之间的距离是否小于大小的一半之和。
public bool Overlaps(AABB other){ if ( Mathf.Abs(center.x - other.center.x) > halfSize.x + other.halfSize.x ) return false; if ( Mathf.Abs(center.y - other.center.y) > halfSize.y + other.halfSize.y ) return false; return true;}下面这张图说明了x轴上的检测方法,y轴的检测方法也是一样的。正如你可以看到的,如果大小的一半之和比中心点之间的距离要小,那么不可能发生重叠。请注意在上面的代码里,如果在第一个坐标轴就判断物体不可能发生重叠,我们就可以退出碰撞检测了。如果AABB在2D空间中发生了碰撞,那么重叠一定会同时出现在两个坐标轴上。移动的物体我们首先创建一个类来描述受到物理影响的物体。稍后,我们将使用这个类作为一个实际运行对象的基础。我们称这个基础类为MovingObject。
public class MovingObject{}让我们给这个类填充数据。我们需要非常多的信息来描述这个物体:
· 位置以及前一帧的位置· 速度以及前一帧的速度· 大小· AABB 和它的一个偏移量(这样可以用来与一个精灵进行对齐)·这个物体是否在地面上以及在上一帧它是否在地面上·物体是否靠在左边的墙上以及上一帧是否靠在左边的墙上·物体是否靠在右边的墙上以及上一帧是否靠在右边的墙上·物体是否在天花板上以及上一帧是否在天花板上位置、速度以及大小都是2D向量。现在让我们加入AABB和偏移量。偏移量是必须的,因为有了它我们才能让AABB与物体精灵的位置相匹配。
public AABB mAABB;public Vector2 mAABBOffset; 最后,我们来声明变量,包括物体的位置状态、物体是否在地面上、是否靠墙以及是否在天花板上。这些变量都非常重要,因为有了它们我们才能知道各种信息,比如角色是否能跳,再比如角色需要在被墙弹回来之后播放一个声音。这些是基本,现在我们来创建一个函数来更新这个物体。我们并没有设置好所有的东西,但是已经足够我们开始创建基本的角色控制了。
public void UpdatePhysics(){ }我们要做的第一件事就是把上一帧的数据保存到合适的变量中。现在让我们用当前的速度来更新位置。
1mPosition += mSpeed*Time.deltaTime;然后我们做一个处理,如果物体的垂直方向上的坐标比0小,我们就把这个物体重新定位到地面上。这是临时的方案以便我们可以设置角色控制。稍后,我们将使用瓦片地图与角色进行碰撞。这样处理以后,我们还需要更新AABB中心的位置,以便与物体的新位置一致。
1mAABB.center = mPosition + mAABBOffset;对于这个演示项目,我将使用Unity进行开发,这样在需要更新物体的位置的时候会去更新物体的位移组件,现在让我们开始做这个事情。对大小也要执行相同的操作。
mTransform.position = new Vector3(Mathf.Round(mPosition.x), Mathf.Round(mPosition.y),-1.0f);mTransform.localScale = new Vector3(mScale.x, mScale.y, 1.0f);正如你看到的,渲染的位置被取整了。这可以确保渲染出来的角色的坐标总是与像素对齐的。角色控制数据
现在我们已经基本完成了我们的基础类MovingObject,我们可以开始进行角色移动了。这是游戏中非常重要的一部分,并且可以独立完成而无需与游戏系统发生太多的耦合,当我们需要测试我们的角色与地图的碰撞时候,角色移动需要提前准备好。
首先,我们来创建一个角色类Character并让它继承MovingObject类。
public class Character : MovingObject{}我们需要在这里面处理一些事情。首先是输入部分,让我们建立一个枚举来覆盖所有玩家的控制状态。我们在另外一个文件创建这个枚举并命名为KeyInput。
public enum KeyInput{ GoLeft = 0, GoRight, GoDown, Jump, Count} 正如你看到的,我们的角色能向左、右、下方移动,还能跳。向下移动只能在单向平台上起作用,发生在我们需要角色能够下降的时候。
现在让我们在Character类里面声明2个数组,一个数组记录着当前帧的输入,另外一个数组记录之前帧的输入。根据游戏的不同,这些设置能起到的作用也会有差别。通常的做法是并不把按键的状态保存到数组里,而是由引擎或者框架的一个特定函数进行动态的检测。但是,有一个与实际输入非严格绑定的数组是有帮助的,比如我们想模拟按键输入的话就很方便。
protected bool[] mInputs;protected bool[] mPrevInputs;这些数组内部的顺序与KeyInput枚举里面的顺序是一致的。为了让使用这些数组变得更加容易,我们来创建一些辅助函数来帮助判断是否是某一个特定按键。这里没有什么特别的,我们只是想能够判断一个键是否被按下、被释放或者一个键是开还是关。
现在让我们创建另外一个枚举,用来保存角色所有可能的状态。
public enum CharacterState{ Stand, Walk, Jump, GrabLedge,};正如你看到的,我们的角色现在可以站着不动、行走、跳跃或者抓着平台。既然已经有了这些状态,我们需要添加一些表示诸如跳跃速度、行走速度、当前状态的变量。
public CharacterState mCurrentState = CharacterState.Stand;public float mJumpSpeed;public float mWalkSpeed;当然这里还需要更多的数据比如角色精灵,但是哪些数据是需要的,非常依赖于你将使用哪种引擎。因为我使用Unity,我需要一个对Animator的引用来确保精灵会播放对应状态的动画。
更新循环
好的,现在我们可以开始实现更新循环了。我们需要做的东西严格依赖于角色当前的状态。
静止站立状态
我们应该从角色不移动的时候开始填充角色的逻辑。首先,速度应该设置为零。
case CharacterState.Stand: mSpeed = Vector2.zero; break;我们还要根据状态来展示合适的精灵。
case CharacterState.Stand: mSpeed = Vector2.zero; mAnimator.Play("Stand"); break;现在,如果角色不是在地面上,那么它就没法静止站立,我们需要把状态变为跳跃状态。如果向左或者向右的按键被按下,我们需要把角色的状态从静止站立状态切换到行走状态。如果跳跃键被按下,我们要把垂直速度的值设置成跳跃速度的值,并且把角色的状态从静止站立状态切换到跳跃状态。静止站立状态就需要开发这么多内容了,至少目前是这样。
行走状态
现在让我们创建在地面行走的逻辑,并且一旦行走会立即播放行走的动画。
case CharacterState.Walk: mAnimator.Play("Walk"); break;在这里的逻辑里,如果我们没有按左键或者右键,或者两个都按了,角色将从行走状态返回到静止站立状态。
if (KeyState(KeyInput.GoRight) == KeyState(KeyInput.GoLeft)){ mCurrentState = CharacterState.Stand; mSpeed = Vector2.zero; break;}如果向右键被按下,我们需要设置水平速度的值为mWalkSpeed,并且确保精灵被正确放缩,因为如果我们需要对精灵进行水平翻转的话,水平方向的放缩系数需要调整。
如果前面没有障碍物的话,角色应该向前移动。如果mPushesRightWall为真,那么角色向右移动的话应该设置水平速度的值为0。我们对角色往左边的移动也应该用同样的方法处理。
正如我们在静止站立状态中做过的那样,我们需要监听跳跃键,如果它被按下,那么要设置垂直速度的值。
if (KeyState(KeyInput.Jump)){ mSpeed.y = mJumpSpeed; mAudioSource.PlayOneShot(mJumpSfx, 1.0f); mCurrentState = CharacterState.Jump; break;}如果角色不是在地面上,那么需要把它的状态变为跳跃状态,但是垂直速度还是0,所以它就是简单的往下落。关于行走的部分就做完了,现在让我们开始写跳跃状态的代码。
跳跃状态
让我们从给精灵设置一个合适的动画开始完成跳跃状态。
1mAnimator.Play("Jump");在跳跃状态中,我们需要在计算角色速度的时候考虑重力,这样当角色落向地面的时候速度会越来越快。
1mSpeed.y += Constants.cGravity * Time.deltaTime;但是最好给速度的值加一个上限,这样角色就不会落的太快。
1mSpeed.y = Mathf.Max(mSpeed.y, Constants.cMaxFallingSpeed);在许多游戏里面,如果角色在空中,那么可操作性就会降低。但是我们会提供一些非常简单和精确的操作让玩家对空中的角色也有充分灵活的控制。所以当我们对空中的角色按下向左或者向右的键时,角色除了会继续按照之前的规则进行跳跃以外,还会像在地面上一样进行左右移动。我们可以简单的复制行走状态的移动逻辑。最后,我们还要在持续跳着跳跃键的时候让角色跳的更高一点。要做到这一点,我们其实是在跳跃键没有持续按着的时候,让角色跳的矮一点。(这样就显得在持续按着跳跃键的时候,角色跳的高了一些。)
if (!KeyState(KeyInput.Jump) && mSpeed.y > 0.0f) mSpeed.y = Mathf.Min(mSpeed.y, Constants.cMinJumpSpeed);
正如你看到的,如果现在跳跃键没有被按下同时垂直速度为正,那么我们会限制垂直速度的范围,保证不超过cMinJumpSpeed的值(也就是200个像素每秒)。这意味着如果我们只是点了一下跳跃键,跳跃的速度不是mJumpSpeed(默认是410个像素每秒),而是会降低到200个像素每秒,因此跳跃的高度会降低。
因为我们现在没有任何关卡的几何信息,所以我们现在跳过抓住平台状态的实现。
更新之前的输入
一旦这一帧的逻辑处理全部完成,我们就可以更新用来保存之前帧输入状态的变量。让我们为这个目的创建一个新的函数。所有我们要做的就是把mInputs数组里面保存的按键状态转移到mPrevInputs数组里面。
public void UpdatePrevInputs(){ var count = (byte)KeyInput.Count; for (byte i = 0; i < count; ++i) mPrevInputs[i] = mInputs[i];}在CharacterUpdate函数的最后部分,我们还需要做一些事情。首先是对物理部分进行更新。
1UpdatePhysics();
物理部分更新之后,我们可以看下是否需要播放声音。当角色碰到任何表面的时候,我们都想播放一个声音进行提示,但是目前只有角色和地面碰撞的时候才会发出声音,因为与瓦片地图的碰撞现在尚未实现。
我们还要判断下角色是否刚刚落到了地面上。根据目前的设置很容易实现这个功能,我们只需要判断下角色目前在地面上,而前一帧不在地面上就可以。
if (mOnGround && !mWasOnGround) mAudioSource.PlayOneShot(mHitWallSfx, 0.5f);最后,让我们来更新之前的输入。
1UpdatePrevInputs();总而言之,这就是CharacterUpdate函数应该的样子,取决于你使用的引擎或者框架,可能会有一些微小的差别。
角色初始化
现在让我们来写角色的初始化函数。这个函数需要输入数组作为传入参数。稍后会由manager类来提供这个参数。除此之外,我们还需要做以下事情:
·给角色大小这个成员变量赋值·给角色的跳跃速度赋值·给角色的行走速度赋值·设置角色的初始位置·设置角色的AABB包围体
public void CharacterInit(bool[] inputs, bool[] prevInputs){}我们将使用一些预定义的常量。
public const float cWalkSpeed = 160.0f;public const float cJumpSpeed = 410.0f;public const float cMinJumpSpeed = 200.0f;public const float cHalfSizeY = 20.0f;public const float cHalfSizeX = 6.0f;在这个demo中,我们可以把角色的初始位置设置成编辑器中角色所在的位置。
public void CharacterInit(bool[] inputs, bool[] prevInputs){ mPosition = transform.position;}对于AABB,我们需要设置它的偏移量和大小的一半。Demo中精灵的偏移量需要和AABB大小的一半一致。
public void CharacterInit(bool[] inputs, bool[] prevInputs){ mPosition = transform.position; mAABB.halfSize = new Vector2(Constants.cHalfSizeX, Constants.cHalfSizeY); mAABBOffset.y = mAABB.halfSize.y;}现在可以对其他成员变量进行赋值了。
public void CharacterInit(bool[] inputs, bool[] prevInputs){ mPosition = transform.position; mAABB.halfSize = new Vector2(Constants.cHalfSizeX, Constants.cHalfSizeY); mAABBOffset.y = mAABB.halfSize.y; mInputs = inputs; mPrevInputs = prevInputs; mJumpSpeed = Constants.cJumpSpeed; mWalkSpeed = Constants.cWalkSpeed; mScale = Vector2.one;}我们需要在游戏管理器中调用这个函数。游戏的管理器有很多种设置的方式,主要取决于你使用的工具,但是处理问题的思路是一样的。在管理器的初始化中,我们需要创建输入数组,创建一个玩家并且初始化它。
public class Game{ public Character mPlayer; bool[] mInputs; bool[] mPrevInputs; void Start () { inputs = new bool[(int)KeyInput.Count]; prevInputs = new bool[(int)KeyInput.Count]; player.CharacterInit(inputs, prevInputs); }}此外,在管理器的update调用中,我们需要更新玩家状态以及玩家的输入。
void Update(){ inputs[(int)KeyInput.GoRight] = Input.GetKey(goRightKey); inputs[(int)KeyInput.GoLeft] = Input.GetKey(goLeftKey); inputs[(int)KeyInput.GoDown] = Input.GetKey(goDownKey); inputs[(int)KeyInput.Jump] = Input.GetKey(goJumpKey);} void FixedUpdate(){ player.CharacterUpdate();}请注意我们是在fixed update调用中更新玩家的物理。这是为了确保玩家的跳跃总是能到相同的高度,而不受我们游戏帧率的影响。
测试角色控制器
现在我们可以测试角色的移动来看下感觉如何。如果我们不喜欢这个感觉,我们可以持续的来修改参数或者改变按键对速度影响的方式来进行调整。“总结本文中实现的角色控制似乎非常轻量,看上去不像那些基于动量的运动那么酷,但是采用什么样子的角色控制取决于你的游戏类型,适合最重要。幸运的是,改变角色的移动方式非常的简单,只需要修改速度对行走和跳跃状态的影响就行了。
这是本系列的第一部分。我们在这个部分只是实现了一个简单的角色移动框架,更重要的内容会在第二部分出现,我们将描述角色是如何与瓦片地图进行交互的。
前提是这样的:我们要做一个物理部分简单、健壮、响应迅速、准确和可预测的2D平台游戏。我们不想在开发中采用一个复杂的2D物理引擎,主要有以下原因:
·不好预测的碰撞响应·难以构建准确、健壮的人物移动·非常复杂比较难使用·需要比简单物理引擎消耗更多的处理能力
当然,使用现成的物理引擎也有很多好处,比如可以很容易设置复杂的相互作用,但是这并不是我们游戏所需要的。
一个定制的物理引擎可以让游戏有独特的感觉,所以它真的非常重要!即使仅仅在游戏中做一些简单的基本设置,物体的移动以及和其他物体相互作用的方式都会受到你自己规则的影响,而不是被别人的规则所支配。让我们开始吧!角色的包围盒让我们从定义物理引擎将要使用的形状开始。可以用来表示物理物体的一个最基本的形状是轴对齐矩形包围盒(AABB)。AABB基本是一个不旋转的矩形。在很多平台游戏里,AABB已经足够来近似游戏的每一个物体。它们非常有效,因为AABB的重叠计算非常简单而且需要非常少的数据,要描述一个AABB,只要中心点和大小就足够了。
闲话少说,让我们创建一个AABB结构。
public struct AABB{}
正如前面提到的,我们全部需要的数据就是2个向量:第一个向量是AABB的中心,第二个变量是这个AABB大小的一半。为什么是大小的一半?因为大部分的计算需要的都是大小的一半,所以为了不在每一次使用的时候都去算一下,我们直接记录大小的一半而不是大小。
public struct AABB{ public Vector2 center; public Vector2 halfSize;}让我们添加一个构造函数,这样就可以用特定的参数来创建一个结构。
public AABB(Vector2 center, Vector2 halfSize){ this.center = center; this.halfSize = halfSize;}
有了AABB,我们就可以创建碰撞检测的功能了。首先,让我们做一个简单的检测来判断两个AABB是否互相碰撞。这很容易实现-我们只需要判断每个轴上的中心点之间的距离是否小于大小的一半之和。
public bool Overlaps(AABB other){ if ( Mathf.Abs(center.x - other.center.x) > halfSize.x + other.halfSize.x ) return false; if ( Mathf.Abs(center.y - other.center.y) > halfSize.y + other.halfSize.y ) return false; return true;}下面这张图说明了x轴上的检测方法,y轴的检测方法也是一样的。正如你可以看到的,如果大小的一半之和比中心点之间的距离要小,那么不可能发生重叠。请注意在上面的代码里,如果在第一个坐标轴就判断物体不可能发生重叠,我们就可以退出碰撞检测了。如果AABB在2D空间中发生了碰撞,那么重叠一定会同时出现在两个坐标轴上。移动的物体我们首先创建一个类来描述受到物理影响的物体。稍后,我们将使用这个类作为一个实际运行对象的基础。我们称这个基础类为MovingObject。
public class MovingObject{}让我们给这个类填充数据。我们需要非常多的信息来描述这个物体:
· 位置以及前一帧的位置· 速度以及前一帧的速度· 大小· AABB 和它的一个偏移量(这样可以用来与一个精灵进行对齐)·这个物体是否在地面上以及在上一帧它是否在地面上·物体是否靠在左边的墙上以及上一帧是否靠在左边的墙上·物体是否靠在右边的墙上以及上一帧是否靠在右边的墙上·物体是否在天花板上以及上一帧是否在天花板上位置、速度以及大小都是2D向量。现在让我们加入AABB和偏移量。偏移量是必须的,因为有了它我们才能让AABB与物体精灵的位置相匹配。
public AABB mAABB;public Vector2 mAABBOffset; 最后,我们来声明变量,包括物体的位置状态、物体是否在地面上、是否靠墙以及是否在天花板上。这些变量都非常重要,因为有了它们我们才能知道各种信息,比如角色是否能跳,再比如角色需要在被墙弹回来之后播放一个声音。这些是基本,现在我们来创建一个函数来更新这个物体。我们并没有设置好所有的东西,但是已经足够我们开始创建基本的角色控制了。
public void UpdatePhysics(){ }我们要做的第一件事就是把上一帧的数据保存到合适的变量中。现在让我们用当前的速度来更新位置。
1mPosition += mSpeed*Time.deltaTime;然后我们做一个处理,如果物体的垂直方向上的坐标比0小,我们就把这个物体重新定位到地面上。这是临时的方案以便我们可以设置角色控制。稍后,我们将使用瓦片地图与角色进行碰撞。这样处理以后,我们还需要更新AABB中心的位置,以便与物体的新位置一致。
1mAABB.center = mPosition + mAABBOffset;对于这个演示项目,我将使用Unity进行开发,这样在需要更新物体的位置的时候会去更新物体的位移组件,现在让我们开始做这个事情。对大小也要执行相同的操作。
mTransform.position = new Vector3(Mathf.Round(mPosition.x), Mathf.Round(mPosition.y),-1.0f);mTransform.localScale = new Vector3(mScale.x, mScale.y, 1.0f);正如你看到的,渲染的位置被取整了。这可以确保渲染出来的角色的坐标总是与像素对齐的。角色控制数据
现在我们已经基本完成了我们的基础类MovingObject,我们可以开始进行角色移动了。这是游戏中非常重要的一部分,并且可以独立完成而无需与游戏系统发生太多的耦合,当我们需要测试我们的角色与地图的碰撞时候,角色移动需要提前准备好。
首先,我们来创建一个角色类Character并让它继承MovingObject类。
public class Character : MovingObject{}我们需要在这里面处理一些事情。首先是输入部分,让我们建立一个枚举来覆盖所有玩家的控制状态。我们在另外一个文件创建这个枚举并命名为KeyInput。
public enum KeyInput{ GoLeft = 0, GoRight, GoDown, Jump, Count} 正如你看到的,我们的角色能向左、右、下方移动,还能跳。向下移动只能在单向平台上起作用,发生在我们需要角色能够下降的时候。
现在让我们在Character类里面声明2个数组,一个数组记录着当前帧的输入,另外一个数组记录之前帧的输入。根据游戏的不同,这些设置能起到的作用也会有差别。通常的做法是并不把按键的状态保存到数组里,而是由引擎或者框架的一个特定函数进行动态的检测。但是,有一个与实际输入非严格绑定的数组是有帮助的,比如我们想模拟按键输入的话就很方便。
protected bool[] mInputs;protected bool[] mPrevInputs;这些数组内部的顺序与KeyInput枚举里面的顺序是一致的。为了让使用这些数组变得更加容易,我们来创建一些辅助函数来帮助判断是否是某一个特定按键。这里没有什么特别的,我们只是想能够判断一个键是否被按下、被释放或者一个键是开还是关。
现在让我们创建另外一个枚举,用来保存角色所有可能的状态。
public enum CharacterState{ Stand, Walk, Jump, GrabLedge,};正如你看到的,我们的角色现在可以站着不动、行走、跳跃或者抓着平台。既然已经有了这些状态,我们需要添加一些表示诸如跳跃速度、行走速度、当前状态的变量。
public CharacterState mCurrentState = CharacterState.Stand;public float mJumpSpeed;public float mWalkSpeed;当然这里还需要更多的数据比如角色精灵,但是哪些数据是需要的,非常依赖于你将使用哪种引擎。因为我使用Unity,我需要一个对Animator的引用来确保精灵会播放对应状态的动画。
更新循环
好的,现在我们可以开始实现更新循环了。我们需要做的东西严格依赖于角色当前的状态。
静止站立状态
我们应该从角色不移动的时候开始填充角色的逻辑。首先,速度应该设置为零。
case CharacterState.Stand: mSpeed = Vector2.zero; break;我们还要根据状态来展示合适的精灵。
case CharacterState.Stand: mSpeed = Vector2.zero; mAnimator.Play("Stand"); break;现在,如果角色不是在地面上,那么它就没法静止站立,我们需要把状态变为跳跃状态。如果向左或者向右的按键被按下,我们需要把角色的状态从静止站立状态切换到行走状态。如果跳跃键被按下,我们要把垂直速度的值设置成跳跃速度的值,并且把角色的状态从静止站立状态切换到跳跃状态。静止站立状态就需要开发这么多内容了,至少目前是这样。
行走状态
现在让我们创建在地面行走的逻辑,并且一旦行走会立即播放行走的动画。
case CharacterState.Walk: mAnimator.Play("Walk"); break;在这里的逻辑里,如果我们没有按左键或者右键,或者两个都按了,角色将从行走状态返回到静止站立状态。
if (KeyState(KeyInput.GoRight) == KeyState(KeyInput.GoLeft)){ mCurrentState = CharacterState.Stand; mSpeed = Vector2.zero; break;}如果向右键被按下,我们需要设置水平速度的值为mWalkSpeed,并且确保精灵被正确放缩,因为如果我们需要对精灵进行水平翻转的话,水平方向的放缩系数需要调整。
如果前面没有障碍物的话,角色应该向前移动。如果mPushesRightWall为真,那么角色向右移动的话应该设置水平速度的值为0。我们对角色往左边的移动也应该用同样的方法处理。
正如我们在静止站立状态中做过的那样,我们需要监听跳跃键,如果它被按下,那么要设置垂直速度的值。
if (KeyState(KeyInput.Jump)){ mSpeed.y = mJumpSpeed; mAudioSource.PlayOneShot(mJumpSfx, 1.0f); mCurrentState = CharacterState.Jump; break;}如果角色不是在地面上,那么需要把它的状态变为跳跃状态,但是垂直速度还是0,所以它就是简单的往下落。关于行走的部分就做完了,现在让我们开始写跳跃状态的代码。
跳跃状态
让我们从给精灵设置一个合适的动画开始完成跳跃状态。
1mAnimator.Play("Jump");在跳跃状态中,我们需要在计算角色速度的时候考虑重力,这样当角色落向地面的时候速度会越来越快。
1mSpeed.y += Constants.cGravity * Time.deltaTime;但是最好给速度的值加一个上限,这样角色就不会落的太快。
1mSpeed.y = Mathf.Max(mSpeed.y, Constants.cMaxFallingSpeed);在许多游戏里面,如果角色在空中,那么可操作性就会降低。但是我们会提供一些非常简单和精确的操作让玩家对空中的角色也有充分灵活的控制。所以当我们对空中的角色按下向左或者向右的键时,角色除了会继续按照之前的规则进行跳跃以外,还会像在地面上一样进行左右移动。我们可以简单的复制行走状态的移动逻辑。最后,我们还要在持续跳着跳跃键的时候让角色跳的更高一点。要做到这一点,我们其实是在跳跃键没有持续按着的时候,让角色跳的矮一点。(这样就显得在持续按着跳跃键的时候,角色跳的高了一些。)
if (!KeyState(KeyInput.Jump) && mSpeed.y > 0.0f) mSpeed.y = Mathf.Min(mSpeed.y, Constants.cMinJumpSpeed);
正如你看到的,如果现在跳跃键没有被按下同时垂直速度为正,那么我们会限制垂直速度的范围,保证不超过cMinJumpSpeed的值(也就是200个像素每秒)。这意味着如果我们只是点了一下跳跃键,跳跃的速度不是mJumpSpeed(默认是410个像素每秒),而是会降低到200个像素每秒,因此跳跃的高度会降低。
因为我们现在没有任何关卡的几何信息,所以我们现在跳过抓住平台状态的实现。
更新之前的输入
一旦这一帧的逻辑处理全部完成,我们就可以更新用来保存之前帧输入状态的变量。让我们为这个目的创建一个新的函数。所有我们要做的就是把mInputs数组里面保存的按键状态转移到mPrevInputs数组里面。
public void UpdatePrevInputs(){ var count = (byte)KeyInput.Count; for (byte i = 0; i < count; ++i) mPrevInputs[i] = mInputs[i];}在CharacterUpdate函数的最后部分,我们还需要做一些事情。首先是对物理部分进行更新。
1UpdatePhysics();
物理部分更新之后,我们可以看下是否需要播放声音。当角色碰到任何表面的时候,我们都想播放一个声音进行提示,但是目前只有角色和地面碰撞的时候才会发出声音,因为与瓦片地图的碰撞现在尚未实现。
我们还要判断下角色是否刚刚落到了地面上。根据目前的设置很容易实现这个功能,我们只需要判断下角色目前在地面上,而前一帧不在地面上就可以。
if (mOnGround && !mWasOnGround) mAudioSource.PlayOneShot(mHitWallSfx, 0.5f);最后,让我们来更新之前的输入。
1UpdatePrevInputs();总而言之,这就是CharacterUpdate函数应该的样子,取决于你使用的引擎或者框架,可能会有一些微小的差别。
角色初始化
现在让我们来写角色的初始化函数。这个函数需要输入数组作为传入参数。稍后会由manager类来提供这个参数。除此之外,我们还需要做以下事情:
·给角色大小这个成员变量赋值·给角色的跳跃速度赋值·给角色的行走速度赋值·设置角色的初始位置·设置角色的AABB包围体
public void CharacterInit(bool[] inputs, bool[] prevInputs){}我们将使用一些预定义的常量。
public const float cWalkSpeed = 160.0f;public const float cJumpSpeed = 410.0f;public const float cMinJumpSpeed = 200.0f;public const float cHalfSizeY = 20.0f;public const float cHalfSizeX = 6.0f;在这个demo中,我们可以把角色的初始位置设置成编辑器中角色所在的位置。
public void CharacterInit(bool[] inputs, bool[] prevInputs){ mPosition = transform.position;}对于AABB,我们需要设置它的偏移量和大小的一半。Demo中精灵的偏移量需要和AABB大小的一半一致。
public void CharacterInit(bool[] inputs, bool[] prevInputs){ mPosition = transform.position; mAABB.halfSize = new Vector2(Constants.cHalfSizeX, Constants.cHalfSizeY); mAABBOffset.y = mAABB.halfSize.y;}现在可以对其他成员变量进行赋值了。
public void CharacterInit(bool[] inputs, bool[] prevInputs){ mPosition = transform.position; mAABB.halfSize = new Vector2(Constants.cHalfSizeX, Constants.cHalfSizeY); mAABBOffset.y = mAABB.halfSize.y; mInputs = inputs; mPrevInputs = prevInputs; mJumpSpeed = Constants.cJumpSpeed; mWalkSpeed = Constants.cWalkSpeed; mScale = Vector2.one;}我们需要在游戏管理器中调用这个函数。游戏的管理器有很多种设置的方式,主要取决于你使用的工具,但是处理问题的思路是一样的。在管理器的初始化中,我们需要创建输入数组,创建一个玩家并且初始化它。
public class Game{ public Character mPlayer; bool[] mInputs; bool[] mPrevInputs; void Start () { inputs = new bool[(int)KeyInput.Count]; prevInputs = new bool[(int)KeyInput.Count]; player.CharacterInit(inputs, prevInputs); }}此外,在管理器的update调用中,我们需要更新玩家状态以及玩家的输入。
void Update(){ inputs[(int)KeyInput.GoRight] = Input.GetKey(goRightKey); inputs[(int)KeyInput.GoLeft] = Input.GetKey(goLeftKey); inputs[(int)KeyInput.GoDown] = Input.GetKey(goDownKey); inputs[(int)KeyInput.Jump] = Input.GetKey(goJumpKey);} void FixedUpdate(){ player.CharacterUpdate();}请注意我们是在fixed update调用中更新玩家的物理。这是为了确保玩家的跳跃总是能到相同的高度,而不受我们游戏帧率的影响。
测试角色控制器
现在我们可以测试角色的移动来看下感觉如何。如果我们不喜欢这个感觉,我们可以持续的来修改参数或者改变按键对速度影响的方式来进行调整。“总结本文中实现的角色控制似乎非常轻量,看上去不像那些基于动量的运动那么酷,但是采用什么样子的角色控制取决于你的游戏类型,适合最重要。幸运的是,改变角色的移动方式非常的简单,只需要修改速度对行走和跳跃状态的影响就行了。
这是本系列的第一部分。我们在这个部分只是实现了一个简单的角色移动框架,更重要的内容会在第二部分出现,我们将描述角色是如何与瓦片地图进行交互的。