在游戏中使用脚本语言

这只是一个有趣的探索,demo 使用 java 编写。模拟了一个龙与地下城类 RPG 游戏中,在不同的房间内移动的简单游戏场景。

阅读本文前,首先下载使用 Netbeans 6.5 建立的完整项目代码:下载。然后,我会用 UML 图的方式来说明如何在游戏中使用脚本,其中可能还会简介一下游戏中实体对象的建立和管理(不知道值得不值得另外写一篇文章来介绍了)。

I have a dream…

现在要构建一个极为无聊的小世界。说它小,是因为我只打算让它有三个房间,三个房间之间两两互通的门。仅此而已。首先上图:三个房间

我们假设左边上面的房间叫 room-1,右边上面的房间叫 room-2,下面的房间叫 room-3。玩家在这三个房间中穿行,当然,不可能是穿墙。人,一定是要走门的。如图。就这么简单的逻辑而已,不用脚本语言也能轻松完成。不过如果希望多来一点拓展性呢?比如,room-3 不允许等级在5级以下玩家进入;room-2 当十级以上玩家进入后就会自动瞬间移动到 room-1;门锁住以后就不能通过,更夸张一点,门锁住以后如果不把锁打坏就不能通过……可能性太多了。不用脚本的情况下,如果要将这么多都实现,是一件非常繁琐的事情。

好吧,让我们来看看那些游戏公司是怎么解决这个问题的。哦,需要说明的是,这里的解决方案仅仅是一个 demo,只用来解释原理。真正的环境中,还要更复杂一点,不过也就是复杂一点点而已。

类图游戏中实体类如图设计,为了简化期间,没有实体管理器,所以也没有集成自统一的父类。

Ninny 类也就是玩家,有保存当前所在房间的成员变量。Room 类也就是房间,保存有在当前房间的玩家列表。Door 是描述门的类,保存了这个门连接的两个房间的列表。

为了简单期间,demo 中使用了 java 内置的 javascript 作为脚本语言,详情看这里

本来还想多写一点的,急着出门。算了,反正有代码,大家看看先。有空回头写。重点在 js 目录下的那几个脚本 enterDoor.js,exitDoor.js,enterRoom.js,exitRoom.js。

好,继续!

为了用起来方便,稍稍封装了一下Scripting的代码。大家直接看代码,不说话:

import java.io.FileNotFoundException;
import java.io.FileReader;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.script.ScriptEngine;
import javax.script.ScriptEngineManager;
import javax.script.ScriptException;

public class Script {

    private final static ScriptEngineManager factory = new ScriptEngineManager();
    private final static ScriptEngine engine = factory.getEngineByName("JavaScript");

    public void put(String key, Object value) {
        engine.put(key, value);
    }

    public Object get(String key) {
        return engine.get(key);
    }

    public Object eval(String fileName) throws ScriptException {
        try {
            return engine.eval(new FileReader(System.getProperty("user.dir", ".") + "/js/" + fileName + ".js"));
        } catch (FileNotFoundException ex) {
            Logger.getLogger(Script.class.getName()).log(Level.SEVERE, null, ex);
        }
        return null;
    }
}

从类图上可以看到,Door 和 Room 都实现了 Enterable 接口,两个方法:一个入 enter,一个出 exit。显然,当进入一个 Room 的时候,必须从另外一个房间出来。为了保证这个一致性,所以限定玩家不能穿墙只能走门:

        Room r1 = new Room("room-1");
        Room r2 = new Room("room-2");
        Door d12 = new Door();
        d12.add(r1);
        d12.add(r2);
        Ninny player = new Ninny("ninny", r1);
        d12.enter(player);

在不用脚本的情况下怎么写这个 Door 的 enter 方法呢?首先判断一下 ninny 是不是跟 Door 在同一个房间;获得 ninny 的当前房间后 exit;获得门另一端的房间 enter。就是这么简单。不过考虑到前面说说的那种种可能性,为了拓展让我们来看一看用脚本是如何处理的:

// Door 的 enter 方法
    public boolean enter(Ninny ninny) {
        Room currentRoom = ninny.getOwner();
        // 玩家在当前门所在的房间
        if(!member.containsKey(currentRoom.getName())) {
            return false;
        }
        // 门另一侧的房间
        Room nextRoom = getAnotherRoom(currentRoom);
        
        try {
            Script script = new Script();
            script.put("room1", currentRoom);
            script.put("room2", nextRoom);
            script.put("door", this);
            script.put("player", ninny);
            script.eval("enterDoor");
            System.out.println("enterDoor: " + script.get("message"));
            return Boolean.valueOf(script.get("result").toString());
        } catch (ScriptException ex) {
            Logger.getLogger(Door.class.getName()).log(Level.SEVERE, null, ex);
            return false;
        }        
    }

这里实际上 enter 什么也没有做,只是传递了一些数据到 Script,然后执行了 enterDoor 这个脚本:

importClass(Packages.foobar.Room);
importClass(Packages.foobar.Ninny);
importClass(Packages.foobar.Door);

room1.exit(player);
room2.enter(player);

var message = player.getName() + " enter from the room " + room1.getName() +  " to the romm " + room2.getName() + "!";
var result = true;

脚本也很简单,只是让玩家推出当前房间,进入下一个房间。如果房间对进入的玩家有等级要求,则只需:

importClass(Packages.foobar.Room);
importClass(Packages.foobar.Ninny);
importClass(Packages.foobar.Door);

var result = false;
var message = '';
if(room2.needLevel >= player.level ) {
    room1.exit(player);
    room2.enter(player);

    message = player.getName() + " enter from the room " + room1.getName() +  " to the romm " + room2.getName() + "!";
    result = true;
} else {
    message = player.getName() + " can't enter the romm " + room2.getName() + "! Level" + room2.needLevel +" needed!";
}

在我的例子代码中并没有这部分代码,实际上 Room、Door、Ninny 这几个类都应该从一个父类 Entity 中继承。这个 Entity 有一个 Map 的成员变量。通过 setAttr(String, Object) 和 getAttr(String) 两个方法就可以分别设置和获取每个 Entity 的属性。这样就可以在脚本中自由的使用各种属性来实现各种功能。
通过 Enterable(可进入),Pickable(可捡起),Attackable(可攻击)等接口,调用对应的脚本来完成真正的游戏逻辑。
其实那神秘的游戏脚本化就是这么简单。本来还想画几个序列图说明一下脚本的调用,实在有些困了。
准备洗洗睡觉。尚未补充完整的内容,全当大家进阶学习吧。

Join the Conversation

4 Comments

  1. 我觉得,若是java的话,大可不必这么做,有强悍的JRUBY,JPTYON,JPHP之类什么的,名字别在意,瞎扯的..

    直接公用java写好的类库,更强大方便,这不是也挺好的吗?

  2. 路神,兄弟!Jruby、Jytion、或者 Jphp 之类的都需要三方库,Javascript 的 Scripting 是 java 环境自带的,所以 javascript 是比其他更简单的解决方案。
    而且我这里有 Script 类,你大可封装其他的 Scripting 脚本语言来嵌入。Just a demo.

Leave a comment

Your email address will not be published. Required fields are marked *