LuoYing RPG的技能系统已经开发了很长一段时间,期间对于技能系统的重构也进行了很多次,其中对于走路、跑步、空闲、攻击、射击、魔法等技能都已经有了一个实现,但由于在“落樱之剑”中没有没有对“跳跃”技能的要求,所以“跳跃”技能的实现一直放在优先级非常靠后的情况,最近几天想想觉得如果不实现确实有一些遗憾,所以这几天把跳跃技能实现了,同时也已经集成到了编辑器中,后面技能系统应该会放下一段时间。
先来看一下跳跃技能在落樱RPG中的的实现原理,这个跳跃技能依赖物理系统,整个技能过程由三个阶段组成:
- 起跳阶段,这个阶段会执行起跳动画,即角色起跳时的预备动作或跃向空中时的动作。
- 滞空阶段:这个阶段会执行角色在空中坠落时的动画,一般这个阶段的时间是不确定的,动画循环模式一般是单向(loop)或是周期(cycle)循环。
- 落地阶段:这个阶段会执行角色落地时的缓冲动作,然后技能结束。
那么要在游戏中让NPC实现跳跃功能,要怎么做呢?
一般来说,首先需要用3D软件制作一个角色模型,并实现模型跳跃时的三个阶段的动画,如上所示(当然,如果不制作模型或者动画也是可以的,可以随便找一个模型代替,只不过最终实现起来不是太好看而已,没有模型动画的话,你只能看到一个僵硬的物理跳跃现象)
然后在游戏程序中是这样实现的, 把整个技能过程分成几个阶段:
- 第一阶段就是播放起跳动画了,在起跳动画结束或者快结束的时候给角色一个起跳的物理作用力。
- 由于这个起跳的物理作用力,角色会飞向作用力的方向(一般为空中),同时角色结束第一阶段的起跳动画,转而执行“滞空动画”,即在空中的坠落时的摆动动画,然而这个时间是不确定的,因为角色跳多高、从多高落下是不确定的,所以这个过程的动画可以一直循环,并持续判断角色是否接触到了地面(这个判断可以使用射线检测)。
- 当角色接触到地面时接着结束第二阶段的动画,转而播放“落地动画",落地动画播放结束后就结束整个技能过程。
简单的来说,整个跳跃过程的实现就是这样的,这里用”简单的来说“是因为实际上整个技能系统中的各种技能是可能互相影响的,比如角色在跳跃到空中的时候可能会被攻击、被其它技能打断等,但这里不想把问题搞得太复杂。
先来看一下这个跳跃技能在LuoYing RPG中的xml配置:
<skillBase id="skillJumpTagBase" types="jump" prior="30" overlapTypes="skin,walk,run" interruptTypes="idle,wait" />
<skillJump id="skillJumpBase" extends="skillJumpTagBase" useTime="1" />
<skillJump id="skillSinbadJumpTest" extends="skillJumpBase" animStart="JumpStart" animInAir="JumpLoop" animEnd="JumpEnd" jumpForce="0,400,0" useTimeInStart="0.5" useTimeInAir="2" useTimeInEnd="0.5" forceApplyTime="0.3" />
在LuoYing RPG中所有的游戏元素几乎都是可配置的, 比如这个跳跃技能,三阶段的跳跃动画、时间限制、跳跃力、物理作用力时间点、技能优先级等都是可以配置的。
XML配置起来虽然比较自由,但稍微有一些麻烦,也不够可视化,那么来看一下在编辑器中的样子,后面再看代码是怎么实现的:
跳跃技能的代码实现原理(如果要查看完整的代码,请参考我的开源项目:LuoYing RPG)
package name.huliqing.luoying.object.skill;
import com.jme3.animation.LoopMode;
import com.jme3.bullet.control.BetterCharacterControl;
import com.jme3.math.Vector3f;
import com.jme3.util.TempVars;
import java.util.logging.Level;
import java.util.logging.Logger;
import name.huliqing.luoying.data.SkillData;
import name.huliqing.luoying.message.StateCode;
import name.huliqing.luoying.object.attribute.NumberAttribute;
import name.huliqing.luoying.object.module.ChannelModule;
/**
* 跳跃技能
* @author huliqing
*/
public class JumpSkill extends AbstractSkill {
private static final Logger LOG = Logger.getLogger(JumpSkill.class.getName());
private ChannelModule channelModule;
// 角色的起跳、滞空、落地动画
private String animStart;
private String animInAir;
private String animEnd;
// 滞空动画的循环模式
private LoopMode animInAirLoop = LoopMode.Cycle;
// 角色的起跳动作、空中动作、落地动作的时间,
private float useTimeInStart = 0.5f;
private float useTimeInAir = 1f;
private float useTimeInEnd = 0.3f;
// 跳跃作用力的开始时间
private float forceApplyTime = 0.3f;
// 跳跃的作用力这个力量强度和角色的质量大小有关系,当角色的质量越大,就需要更强的跳跃力
private Vector3f jumpForce = new Vector3f(0, 100, 0);
// 如果角色是在向前走动的时候进行跳跃,那么跳跃的时候会有一个向前的冲力,这个参数用于控制这个冲力的大小,
// 例如,如果不想让角色跳得太远,则要减少这个值,否则增加这个值。
private float walkForceIntensity = 0.3f;
// 跳跃的强度(绑定实体属性)
private String bindJumpIntensityAttribute;
// 一个用于超时结束当前动作的限制,避免当角色卡在空中时无法退出当前技能的BUG
private float timeout = 15;
// ---- 不需要开放的参数
// 标记相应的各动作的动画是否已经执行过
private boolean animStartPlayed;
private boolean animInAirPlayed;
private boolean animEndPlayed;
private boolean forceApplied;
// 计算技能在空中的停留时间
private float timeUsedInAir;
// 计算技能在落下地面后的落地动画时间
private float timeUsedInEnd;
// ---- inner
private NumberAttribute jumpIntensityAttribute;
// 角色控制器
private BetterCharacterControl bcc;
private Vector3f lastWalkDirection = new Vector3f();
private float lastPhysicsDamping;
@Override
public void setData(SkillData data) {
super.setData(data);
animStart = data.getAsString("animStart");
animInAir = data.getAsString("animInAir");
animEnd = data.getAsString("animEnd");
animInAirLoop = getLoopMode(data.getAsString("animInAirLoop"));
useTimeInStart = data.getAsFloat("useTimeInStart", useTimeInStart);
useTimeInAir = data.getAsFloat("useTimeInAir", useTimeInAir);
useTimeInEnd = data.getAsFloat("useTimeInEnd", useTimeInEnd);
forceApplyTime = data.getAsFloat("forceApplyTime", forceApplyTime);
jumpForce = data.getAsVector3f("jumpForce", jumpForce);
walkForceIntensity = data.getAsFloat("walkForceIntensity", walkForceIntensity);
bindJumpIntensityAttribute = data.getAsString("bindJumpIntensityAttribute");
timeout = data.getAsFloat("timeout", timeout);
animStartPlayed = data.getAsBoolean("animStartPlayed", animStartPlayed);
animInAirPlayed = data.getAsBoolean("animInAirPlayed", animInAirPlayed);
animEndPlayed = data.getAsBoolean("animEndPlayed", animEndPlayed);
forceApplied = data.getAsBoolean("forceApplied", forceApplied);
timeUsedInAir = data.getAsFloat("timeUsedInAir", timeUsedInAir);
timeUsedInEnd = data.getAsFloat("timeUsedInEnd", timeUsedInEnd);
// 内部参数
lastWalkDirection = data.getAsVector3f("_lastWalkDirection", lastWalkDirection);
lastPhysicsDamping = data.getAsFloat("_lastPhysicsDamping", lastPhysicsDamping);
}
@Override
public void updateDatas() {
super.updateDatas();
data.setAttribute("animStartPlayed", animStartPlayed);
data.setAttribute("animInAirPlayed", animInAirPlayed);
data.setAttribute("animEndPlayed", animEndPlayed);
data.setAttribute("forceApplied", forceApplied);
data.setAttribute("timeUsedInAir", timeUsedInAir);
data.setAttribute("timeUsedInEnd", timeUsedInEnd);
// 内部参数
data.setAttribute("_lastWalkDirection", lastWalkDirection);
data.setAttribute("_lastPhysicsDamping", lastPhysicsDamping);
// 不会改变的数据不需要更新回去
}
@Override
public void initialize() {
super.initialize();
channelModule = actor.getModule(ChannelModule.class);
jumpIntensityAttribute = actor.getAttribute(bindJumpIntensityAttribute, NumberAttribute.class);
// JumpStart动画
if (!animStartPlayed) {
animStartPlayed = true;
if (animStart != null) {
channelModule.playAnim(animStart, null, LoopMode.DontLoop, useTimeInStart , 0);
}
}
bcc = actor.getSpatial().getControl(BetterCharacterControl.class);
if (bcc != null) {
// 获取物理控制器,并记住跳跃之前角色的移动方向,跳跃之前必须先将角色的移动清0。
// 因为这个WalkDirection是持续的作用力过程,会造成跳跃空中时,如果遇到障碍物有可能会导致角色紧贴着物体不会落下
// 或者遇到障碍物后这个力仍然一直推着角色向前滑动的bug.
lastWalkDirection.set(bcc.getWalkDirection());
bcc.setWalkDirection(new Vector3f());
// 记住这个阻尼值,当角色在跳跃时这个值必须清0,否则会导致很难跳起来。在角色跳到空中后,这个设置可以还原。
lastPhysicsDamping = bcc.getPhysicsDamping();
} else {
LOG.log(Level.WARNING, "Jump failure! BetterCharacterControl not found from Entity, entityId={0}, uniqueId={1}"
, new Object[] {actor.getData().getId(), actor.getData().getUniqueId()});
}
}
@Override
public void cleanup() {
animStartPlayed = false;
animInAirPlayed = false;
animEndPlayed = false;
forceApplied = false;
timeUsedInAir = 0;
timeUsedInEnd = 0;
if (bcc != null) {
// 不应该恢复动量,这不是这个技能应该做的,有可能造成BUG,由walkSkill去处理就行(如果WalkSkill同时在执行)。
// 那么当JumpSkill执行完之后,WalkSkill应该自己去恢复这个移动量.
// bcc.setWalkDirection(lastWalkDirection)
// 这个应该恢复,因为技能有可能在中途被打断,所在必须确保技能结束的时候恢复这个参数
bcc.setPhysicsDamping(lastPhysicsDamping);
}
super.cleanup();
}
@Override
public int checkState() {
bcc = actor.getSpatial().getControl(BetterCharacterControl.class);
if (bcc == null || !bcc.isOnGround()) {
return StateCode.SKILL_USE_FAILURE;
}
return super.checkState();
}
@Override
protected void doSkillUpdate(float tpf) {
if (bcc == null) {
return;
}
// 跳跃的作用力是由jumpDir所设置方向上的力加上角色移动方向上的力合成的。
if (!forceApplied && time >= forceApplyTime) {
forceApplied = true;
TempVars tv = TempVars.get();
Vector3f finalJumpForce = tv.vect1.set(jumpForce);
actor.getSpatial().getWorldRotation().mult(finalJumpForce, finalJumpForce);
Vector3f walkDirectionForce = tv.vect2.set(lastWalkDirection).multLocal(jumpForce.length())
.multLocal(walkForceIntensity); // walkForceIntensity调整向前的冲力
if (jumpIntensityAttribute != null) {
finalJumpForce.multLocal(jumpIntensityAttribute.floatValue());
walkDirectionForce.multLocal(jumpIntensityAttribute.floatValue());
}
finalJumpForce.addLocal(walkDirectionForce);
bcc.setPhysicsDamping(0);
bcc.setJumpForce(finalJumpForce);
bcc.jump();
tv.release();
}
// 执行空中落下时的动画
if (!animInAirPlayed && time >= useTimeInStart) {
animInAirPlayed = true;
if (animInAir != null) {
channelModule.playAnim(animInAir, null, animInAirLoop, useTimeInAir, 0);
}
}
// 稍微延迟一下后再判断是否isOnGround(),因为bcc.jump()后角色不会立即离开地面.
if (!animEndPlayed && timeUsedInAir > 0.05f) {
if (bcc.isOnGround() || time >= timeout) {
animEndPlayed = true;
if (animEnd != null) {
channelModule.playAnim(animEnd, null, LoopMode.DontLoop, useTimeInEnd, 0);
}
// 当角色跳跃到空中之后重新设置回阻尼。
bcc.setPhysicsDamping(lastPhysicsDamping);
// LOG.log(Level.INFO, "animEndPlayed, bcc.isOnGround={0}", new Object[]{bcc.isOnGround()});
}
}
if (animInAirPlayed) {
timeUsedInAir += tpf;
}
if (animEndPlayed) {
timeUsedInEnd += tpf;
if (timeUsedInEnd >= useTimeInEnd) {
bcc.setWalkDirection(lastWalkDirection); // 还原walkDirection
}
}
}
@Override
public boolean isEnd() {
// remove20170418,因为跳跃技能有一个滞空的情况,这个情况会导致技能在空中的时间是不确定的。
// 因而技能的整个执行时间也是不确定的。
// super.isEnd();
if (bcc == null)
return true;
if (time > timeout)
return true;
// 当最后一阶段(落地动作)时间执行完毕时,视为跳跃技能技能结束
if (timeUsedInEnd >= useTimeInEnd) {
return true;
}
return false;
}
@Override
public void restoreAnimation() {
// 只恢复空中动画
if (animInAirPlayed && !animEndPlayed) {
if (animInAir != null) {
channelModule.restoreAnimation(animInAir, null, animInAirLoop, useTimeInAir, 0);
}
}
}
// 获取loopMode设置,如果找不到匹配,则默认使用loop模式
private LoopMode getLoopMode(String name) {
for (LoopMode lm : LoopMode.values()) {
if (lm.name().equals(name)) {
return lm;
}
}
return LoopMode.Loop;
}
}