当前位置: 首页 > 工具软件 > LuoYing RPG > 使用案例 >

3D游戏中NPC的跳跃技能实现、原理解析、视频演示及代码演示 - LuoYing RPG

邓季
2023-12-01

        LuoYing RPG的技能系统已经开发了很长一段时间,期间对于技能系统的重构也进行了很多次,其中对于走路、跑步、空闲、攻击、射击、魔法等技能都已经有了一个实现,但由于在“落樱之剑”中没有没有对“跳跃”技能的要求,所以“跳跃”技能的实现一直放在优先级非常靠后的情况,最近几天想想觉得如果不实现确实有一些遗憾,所以这几天把跳跃技能实现了,同时也已经集成到了编辑器中,后面技能系统应该会放下一段时间。

先来看一下跳跃技能在落樱RPG中的的实现原理,这个跳跃技能依赖物理系统,整个技能过程由三个阶段组成:

  • 起跳阶段,这个阶段会执行起跳动画,即角色起跳时的预备动作或跃向空中时的动作。
  • 滞空阶段:这个阶段会执行角色在空中坠落时的动画,一般这个阶段的时间是不确定的,动画循环模式一般是单向(loop)或是周期(cycle)循环。
  • 落地阶段:这个阶段会执行角色落地时的缓冲动作,然后技能结束。

那么要在游戏中让NPC实现跳跃功能,要怎么做呢?

一般来说,首先需要用3D软件制作一个角色模型,并实现模型跳跃时的三个阶段的动画,如上所示(当然,如果不制作模型或者动画也是可以的,可以随便找一个模型代替,只不过最终实现起来不是太好看而已,没有模型动画的话,你只能看到一个僵硬的物理跳跃现象)

然后在游戏程序中是这样实现的, 把整个技能过程分成几个阶段:

  1. 第一阶段就是播放起跳动画了,在起跳动画结束或者快结束的时候给角色一个起跳的物理作用力。
  2. 由于这个起跳的物理作用力,角色会飞向作用力的方向(一般为空中),同时角色结束第一阶段的起跳动画,转而执行“滞空动画”,即在空中的坠落时的摆动动画,然而这个时间是不确定的,因为角色跳多高、从多高落下是不确定的,所以这个过程的动画可以一直循环,并持续判断角色是否接触到了地面(这个判断可以使用射线检测)。
  3. 当角色接触到地面时接着结束第二阶段的动画,转而播放“落地动画",落地动画播放结束后就结束整个技能过程。

简单的来说,整个跳跃过程的实现就是这样的,这里用”简单的来说“是因为实际上整个技能系统中的各种技能是可能互相影响的,比如角色在跳跃到空中的时候可能会被攻击、被其它技能打断等,但这里不想把问题搞得太复杂。

先来看一下这个跳跃技能在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;
    }
}

转载于:https://my.oschina.net/huliqing/blog/884707

 类似资料: