侧栏导航

从零开发后端游戏引擎之二 脚本化



2019 -05-19 @ dz

后端游戏引擎的功能之一 脚本化

一个游戏引擎中,使用脚本(ex:JavaScript)来拓展业务以应对繁杂需求是一个必不可少的措施.

比如这么个场景: 玩家登录时判断该玩家是否是会员,广播消息全服发送欢迎语.

该场景下玩家登录(onUserLogin)和发送消息(sendMessage)就可以作为脚本化的点,

onUserLogin 作为钩子函数,可以吸附一段脚本,当该函数被触发时,交由脚本去实现具体的逻辑.

sendMessage 则可以被作为引擎的内置函数,能够在脚本中被调用.

实现该场景预期的脚本如下:

//filepath: './GateWay/onUserLogin.js'
var onUserLogin = function (user) {
    if (user&&user["propertyMap"]&&user["propertyMap"]["memberLevel"]) {
        var memberLevel = user["propertyMap"]["memberLevel"];
        if(memberLevel>MemberLevelEnum.VIP0){//判断是否是会员
            //发送世界广播消息
            sendMessage(MessageTypeEnum.WORLD,
                        String.format("欢迎尊贵的会员 %s 来到PK游戏世界 ",
                        user.name));
        }
    }else{
        console.warn("user or user.propertyMap is null, user:{}",user);
    }
};

脚本开发语言的选择

####

选择的是 JavaScript.因为可以达到这些要求:

  1. JavaScript(js)作为流行的脚本语言,几乎所有人都听说过它
  2. 很多前端游戏引擎都支持JavaScript,如Unity Cocos Egret Laya,这使得大多数游戏开发人员都会用JavaScript
  3. JDK1.6后官方内置的ScriptEngine仅仅支持JavaScript,可以被编译和解释执行,并且官方在不断优化它的性能
  4. 可以通过模块化的设计和运行时上下文(ScriptContext)来屏蔽JavaScript在构建复杂系统时不易查错的缺点.
  5. 可以为脚本引擎注入内置函数,内置全局变量(engine.put(console))
  6. 可以在原生代码中带参数动态调用脚本函数(engine.inkove("onUserLogin",user);)
  7. 可以在脚本上申明全局变量,全局函数,成员变量(global.js,define.js)
  8. 可以为脚本指定运行时的下上文(ScirptContext)

很多人接触JavaScript是从浏览器(Browser)开始的. 都知道浏览器的常用内置函数,如

setTimeOut,setInterval,Console.log

这些函数都是挂在浏览器的全局对象window上的,所以我们可以为游戏引擎中的脚本模块注入一个这样的window对象,实现脚本中能都使用常见的默认内置系统函数.

下面的java代码演示了具体的实现:

package gl.java.javascript;


import com.sun.istack.internal.Nullable;
import gl.java.game.Timer;
import jdk.nashorn.api.scripting.ScriptObjectMirror;
import lombok.extern.slf4j.Slf4j;

import javax.script.ScriptEngine;

@Slf4j
public class JavaScriptWindow {

    private final Timer timer;

    public JavaScriptWindow() {
        timer = new Timer();
    }


    public long setTimeout(@Nullable ScriptObjectMirror runnable, long millisecond) {
        long l = timer.setTimeout(() -> {
            try {
                runnable.call(runnable);
            } catch (Exception e) {
                log.error(e.getMessage(),e);
            }
        }, millisecond);
        log.debug("setTimeout.ID:{},timeout:{}ms", l, millisecond);
        return l;
    }


    public long setInterval(@Nullable ScriptObjectMirror runnable, long millisecond) {
        long l = timer.setInterval(() -> {
            try {
                runnable.call(runnable);
            } catch (Exception e) {
                log.error(e.getMessage(),e);
            }
        }, millisecond);
        log.debug("setInterval.ID:{} interval:{}ms", l, millisecond);
        return l;
    }

    public void clearInterval(@Nullable long id) {
        timer.clearInterval(id);
    }

    public void clearTimeout(@Nullable long id) {
        timer.clearTimeout(id);
    }

    public void attach(ScriptEngine engine) {
        try {
            //inject console.log && error
            engine.put("NativeLog", log);
            engine.put("String", new String());
            engine.eval("var console = NativeLog; String = Java.type('java.lang.String');\n" +
                    "console.info('[DEFINE LOG],{},{}',1var,2);");

            //inject window.setTimeout && window.clearInterval
            engine.put("window", this);
            engine.eval("var setTimeout= function (func,timeoutInMS) {\n" +
                    "    return window.setTimeout(func,timeoutInMS);\n" +
                    "};\n" +
                    "var setInterval= function (func,timeoutInMS) {\n" +
                    "    return window.setInterval(func,timeoutInMS);\n" +
                    "};\n" +
                    "var clearInterval = function (ID) {\n" +
                    "    window.clearInterval(ID);\n" +
                    "};\n" +
                    "var clearTimeout = function (ID) {\n" +
                    "    window.clearTimeout(ID);\n" +
                    "};");

        } catch (Exception e) {
            e.printStackTrace();
            log.error(e.toString());
        }
    }

    public void detach() {
        timer.clearAll();
    }
}

我们在脚本上可以和在浏览中一样去调用这些函数,

// filepath: ./scirptTest.js
var loop = 0;
var loop1 = setInterval(function () {
    loop++;
    console.info("[INFO]  loop.id:{}, {}.loop:{}" ,loop1, loop,this);
    for(var i in this){
        console.info("this[{}]:{}",i, typeof(this[i]));
    }
}.bind(this), 3000);

var loop2 = setInterval(function () {
    loop++;
    console.info("[INFO]  loop.id:{} ,{}.loop:{}" ,loop2, loop,typeof(this));
}, 1000);

setTimeout(function () {
    clearInterval(loop1);
    clearInterval(loop2);
    console.info("[INFO] clear loop1: {}",loop1);
    console.info("[INFO] clear loop2: {}",loop2);
}, 4000);

细心的同学可以发现,调用setInterval/setTimeout时还可以省去 window. 和浏览器开发JS的体验如出一辙,开发体验顺滑,不割裂,

其他脚本如Lua,ActionScirpt,因为不普及,不再考量范围,python的库虽然多,但语法不敢恭维.TypeScirpt虽好,但需要单独的编译器.

脚本的性能

简单测试JDK8内置的JavaScript引擎nashorn的性能, 同一段代码执行1000万次,其性能为Java代码的40分之一

其实是不需要太care这个数字的, 业务良好的设计远比优化脚本语言本身的性能带来的收益更多,

所以我们这里强调的是如何在设计和架构上优化系统的能力.

不限于更简短的流程,更具有拓展性的模块化设计.

下一章 <./后端游戏引擎的设计 - 脚本的热更新>

上一章 <./后端游戏引擎的设计 - 模块化设计>

项目地址` (https://gitee.com/geliang/PK)


最后更新于 19th May 2019
微信二维码
在微信上关注我