文档章节

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

h
 huliqing
发布于 2017/04/21 23:12
字数 2289
阅读 80
收藏 0

        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;
    }
}

© 著作权归作者所有

共有 人打赏支持
h

huliqing

粉丝 12
博文 5
码字总数 13706
作品 1
广州
程序员
huliqing/LuoYing

##落樱RPG## 落樱(LuoYing)是一个用于简化3D角色扮演游戏开发的游戏框架,该项目基于Java及JME3(JMonkeyEngine)进行开发,兼容JME,创建的游戏可以蹖多个平台发布(WindowLinuxMacAndroidIOS)...

huliqing
2017/05/02
0
0
Java 3D RPG游戏开发框架--LuoYing RPG

落樱RPG(LuoYing)是一个用于简化3D角色扮演游戏开发的游戏框架,该项目基于Java及JME3(JMonkeyEngine)进行开发,兼容JME,创建的游戏可以蹖多个平台发布(WindowLinuxMacAndroidIOS)等。 这个...

huliqing
2017/04/16
392
0
Silverlight 2.5D RPG游戏技巧与特效处理:(三)动态光影

通常来说,只要谈到影子及影子制作,首先想到的不外乎3D。游戏中的影子设计大致可分为硬实现和软实现两种,比如像“游戏影子制作技术”这篇文章所谈到3D游戏影子制作方案Projective Shadow、...

晨曦之光
2012/03/09
0
0
绯末/Chronicle_of_End

##说在前头 这个游戏是我闲暇之余开的一个大坑,用的工具很简单,素材也都是现成的,自然比不上那些搞设计的真玩家。我只是小打小闹。 请不要在意那些中二的技能名和武器道具名,有些不合适o...

绯末
2017/03/21
0
0
粉丝用开源游戏引擎重制超级马里奥 64

《超级马里奥64》的忠实粉丝Aryok Piñera正利用开源游戏引擎Blender Game Engine重制游戏。Blender Game Engine是开源3D绘图软件Blender的一部分,《超级马里奥64》是Nintendo64平台上最畅销...

oschina
2014/03/18
5K
16

没有更多内容

加载失败,请刷新页面

加载更多

python进制转换

#进制转换print(bin(10)) #十进制转换成二进制print(oct(10)) #十进制转换成八进制print(hex(10)) #十进制转换成十六进制print(int('1010',2)) #二进制转十进制print(int(...

fadsaa
17分钟前
3
0
syntax error near unexpected token

最近不断重复在虚拟机CentOS测试安装gitlab,因为gitlab有一个脚本需要饭强才能下载,于是我先在windows下载好再上传到虚拟机,可是执行脚本的时候提示“syntax error near unexpected toke...

W_Lu
27分钟前
2
0
Redis基础、高级特性与性能调优

本文将从Redis的基本特性入手,通过讲述Redis的数据结构和主要命令对Redis的基本能力进行直观介绍。之后概览Redis提供的高级能力,并在部署、维护、性能调优等多个方面进行更深入的介绍和指导...

Java干货分享
28分钟前
2
0
Redis使用lua脚本实现increase + expire 的原子操作

lua脚本: public Integer incrEX(String key, long defaultExpire){ String script = "local current = redis.call('incr',KEYS[1]);" + " local t = redis.call......

大海201506
29分钟前
1
0
Dubbo 源码分析(一)一环境搭建

环境搭建的步骤有哪些 依赖外部的环境 使用的开发工具 源码的拉取 结构大致介绍 1 依赖的外部环境 安装JDK 安装Git 安装maven 这边我们就不介绍怎么安装这些外部环境了,大家自行从安装这些外...

小刀爱编程
30分钟前
2
0

没有更多内容

加载失败,请刷新页面

加载更多

返回顶部
顶部