65.9K
CodeProject 正在变化。 阅读更多。
Home

将 Twiggery 用作 JavaME 游戏脚本语言

starIconstarIconstarIconstarIconstarIcon

5.00/5 (3投票s)

2010 年 8 月 27 日

LGPL3

5分钟阅读

viewsIcon

22342

downloadIcon

361

将 Twiggery 用作 JavaME 游戏脚本语言

引言

如今 移动开发似乎比以往任何时候都更受欢迎。我一直沉浸其中,并且自从我大学时代起就对此很感兴趣。我发现许多由脚本语言(如 Lua、Python、Ruby 等)驱动的 PC 应用程序展现出极大的灵活性和动态行为,但几乎没有什么能在移动平台上扮演类似脚本的角色,因为移动平台缺乏快速的 CPU、大量的 RAM 甚至足够的电池容量。这就是我的 Twiggery 脚本语言诞生的原因。

Twiggery 并非万能,而是领域特定的。您可以在此处找到更多关于 Twiggery 语法的信息并下载完整包。

本文将介绍如何编写一个带有 Twiggery 脚本接口的宿主应用程序,如何使用这些接口编写和编译 Twiggery 源代码为字节码,如何将字节码嵌入宿主中,最后是如何部署一个由 Twiggery 驱动的解决方案应用程序。我将以一个完整的井字棋游戏为例,包含规则检查和 AI 模拟。

背景

我假设您已经了解了一些 JavaME 编程的基础知识,但即使您不了解也没关系,因为我不会在本文中提及 JavaME 的惯用法。您将直接切入主题。

Twiggery 标准包中提供了基于 Java、C# 和 C++ 的 TVM(Twiggery 虚拟机)。令人难以置信的是,您只需继承一个 TVM 实现,就可以启动您的应用程序,不是吗?用几百行代码用 Objective-C、ActionScript、Python 等其他编程语言编写另一个实现也非常容易。

Using the Code

让我们分四步编写一个完整的井字棋游戏。当然,我们可以不使用任何脚本语言来构建一个游戏,那么我在这里絮絮叨叨是为了什么呢?答案是为了灵活性。我会在这方面轻松一些,如果您正在创建一个更复杂的游戏,具有“可选对话”、“任务”、“开关”等功能,您会发现使用 Twiggery 更加必要。

第一步。构建基本游戏框架

您应该知道脚本的运行速度比其原生的宿主编程语言慢。这意味着脚本语言更适合状态控制而不是密集计算。所以让我们用 Java 构建一个游戏框架,我们首先进行程序初始化、按键输入检查、渲染等。

游戏总是伴随着一个主循环。我会这样做

/**
* Main game loop
*/
public void run() {
    int timeStep = 50;
    while (true) {
        long start = System.currentTimeMillis();
        try {
            if (!canvas.render()) { // Looping until user exits
                destroyApp(true);
            }
        } catch (Exception ex) {
            ex.printStackTrace();
        }
        long end = System.currentTimeMillis();
        int duration = (int) (end - start);
        int d = timeStep - duration;
        if (d > 0) { // Delay?
            try {
                Thread.sleep(d);
            } catch (InterruptedException ex) {
                ex.printStackTrace();
            }
        }
    }
}

我会在 JavaME 中像这样启动那个循环作为线程

private TicTacToe canvas = null;
/**
* Entry point
*/
public void startApp() {
    canvas = new TicTacToe();
    Display.getDisplay(this).setCurrent(canvas);
    Thread thread = new Thread(this);
    thread.start();
}

会自动调用一个按键输入处理回调方法。我们会在其中处理光标移动和棋子放置,像这样

/**
* Process key events
* @param keyCode The key code got here
*/
protected void keyPressed(int keyCode) {
    int keyState = getGameAction(keyCode);
    int winner = logic.getWinner();
    if (Logic.EMPTY == winner) { // Nobody wins
        if (GameCanvas.FIRE == keyState) {
            if (logic.getCell(cursorX, cursorY) == Logic.EMPTY) {
                // Place a chessman of yours
                logic.setCell(cursorX, cursorY, logic.getHolder());
                logic.rule();
                logic.turn();
                // Not win?
                if (Logic.EMPTY == logic.getWinner()) {
                    // Place a chessman of AI automatically
                    logic.ai();
                    logic.rule();
                    logic.turn();
                }
            }
        } else {
            // Process arrow keys
            … // Make cursor movement
        }
    } else { // Somebody wins
        // Press fire key to continue
        if (GameCanvas.FIRE == keyState) {
            logic.reset();
        }
    }
}

请注意,调用 logic.ai()logic.rule() 会执行脚本,它们现在是空的,稍后会填入内容。

核心渲染方法会是这样的

/**
* Render all
* @return Keep on running if true, otherwise exit the game
*/
public boolean render() throws Exception {
    // Clear scene
    graphics.setColor(0xffffffff);
    graphics.fillRect(0, 0, SCREEN_WIDTH, SCREEN_HEIGHT);
    {
        // Render all
        …
        // Render cell grids
        …
        // Render chessmen
        …
        // Render cursor or information
        …
        // Render "Exit" text
        …
    }
    // Flush!
    flushGraphics(0, 0, SCREEN_WIDTH, SCREEN_HEIGHT);
    return running;
}

这里有一些最终的游戏屏幕截图,让我们先看看它们

第二步。编写脚本编程接口

游戏框架完成后,我们还需要制作一些脚本编程接口。这部分是用宿主编程语言完成的,并作为将游戏框架和脚本粘合在一起的胶水。

Twiggery 允许在虚拟机上运行编译后的字节码,这有一些优势

  1. 只需加载和执行,没有编译时间成本
  2. 内存占用更少
  3. 脚本源代码保护
  4. 可移植性

前两点对于移动设备尤其重要。

将有两种接口,一种是从宿主程序访问脚本,另一种是反向访问。

从宿主到脚本

TVM 提供了初始化、加载和执行方法,如果我们想调用一个编译后的脚本,我们所要做的就是将它们组合成一个函数,像这样

/**
* Call a rule script to check current composition
*/
public void rule() {
    try {
        String scriptFile = "/rule.tad"; // Compiled bytecode file
        loadAsm(scriptFile);
        runAsm();
    } catch (Exception ex) {
        ex.printStackTrace();
    }
}
/**
* Call a AI script to make a automatic move
*/
public void ai() {
    try {
        String scriptFile = "/ai.tad"; // Compiled bytecode file
        loadAsm(scriptFile);
        runAsm();
    } catch (Exception ex) {
        ex.printStackTrace();
    }
}

您可以像第一步中出现的那样,调用规则和 AI 脚本执行方法,如 logic.rule()logic.ai()

从脚本到宿主

我们想向 Twiggery 暴露一些编程接口,以便我们可以通过脚本控制游戏框架。

接口列表可能是这样的

/**
* Make a function call
* Overridden from TVM
* @param funName Remote function name
* @param argCount Arguments count
*/
protected void call(String funName, int argCount) throws Exception {
    if (funName.equals("abs")) {
        abs();
    } else if (funName.equals("int")) {
        _int();
    } else if (funName.equals("rnd")) {
        rnd();
    } else if (funName.equals("array_new")) {
        array_new();
    } else if (funName.equals("array_get")) {
        array_get();
    } else if (funName.equals("array_set")) {
        array_set();
    } else if (funName.equals("getCell")) {
        getCell();
    } else if (funName.equals("setCell")) {
        setCell();
    } else if (funName.equals("gameOver")) {
        gameOver();
    } else {
        throw new Exception("Unknown function call");
    }
}
/**
* Initialize a new array
*/
private void array_new() { … }
/**
* Get a value from a array unit
*/
private void array_get() { … }
/**
* Set a value to a array unit
*/
private void array_set() { … }
/**
* Get the absolute value of a number
*/
private void abs() { … }
/**
* Floor
*/
private void _int() { … }
/**
* Get a random number
*/
private void rnd() { … }
/**
* Get the state of a cell
*/
private void getCell() { … }
/**
* Set the state of a cell
*/
private void setCell() { … }
/**
* Set game over
*/
private void gameOver() { … }

请注意,“call”方法是从基类 TVM 覆盖的。当运行来自脚本的函数调用评估时,将调用此方法来决定应该执行哪个接口。

/**
* Get the state of a cell
* Args:
* 1) X index of a cell
* 2) Y index of a cell
* Return:
* A constant among WHITE, BLACK, EMPTY
*/
private void getCell() throws Exception {
    int y = (int) popArgument();
    int x = (int) popArgument();
    int v = cells[x][y];
    returnArgument((float) v);
}

参数是从左到右压栈的,这意味着它们是从右到左弹出的。如上所述,当我们从 Twiggery 调用“getCell”函数时,例如:c = getCell(1, 2),我们将 1 然后 2 推入 TVM 堆栈,我们必须先弹出第二个参数,然后是第一个参数(在这里是 2y1x)。最后,我们可以通过调用“returnArgument”将某个值返回给“c”。

第三步。编写和编译游戏脚本

在这一部分,我们将编写一些 Twiggery 代码,利用这些脚本接口来构建规则检查和 AI 脚本。实际上,第二步和第三步是循环迭代的,有时在编写任何脚本之前,我们都无法确定需要多少脚本接口。当我们发现需要使用一个尚未创建的接口时,我们可能会回到第二步并创建一个新的。

规则

在这个井字棋游戏中,我将用 Twiggery 脚本来检查谁赢得了游戏,像这样

function rule() { ' Twiggery code
    ' Consts
    WHITE = 1;
    BLACK = -1;
    EMPTY = 0;
    TIE = 2;
    ' Check diagonal lines
    diagonal_0 = getCell(0, 0) + getCell(1, 1) + getCell(2, 2);
    diagonal_1 = getCell(2, 0) + getCell(1, 1) + getCell(0, 2);
    abs_diagonal_0 = abs(diagonal_0);
    abs_diagonal_1 = abs(diagonal_1);
    c = 0;
    if(abs_diagonal_0 == 3) {
        c = diagonal_0;
    } elseif(abs_diagonal_1 == 3) {
        c = diagonal_1;
    }
    if(c > 0) {
        gameOver(WHITE);
        return;
    } elseif(c < 0) {
        gameOver(BLACK);
        return;
    }
    ' Check columns and rows
    for(j = 0 to 2) {
        l = 0;
        r = 0;
        for(i = 0 to 2) {
            l = l + getCell(i, j);
            r = r + getCell(j, i);
        }
        al = abs(l);
        ar = abs(r);
        c = 0;
        if(al == 3) {
            c = l;
        } elseif(ar == 3) {
            c = r;
        }
        if(c > 0) {
            gameOver(WHITE);
            return;
        } elseif(c < 0) {
            gameOver(BLACK);
            return;
        }
    }
    ' Tie
    c = 0;
    for(i = 0 to 2) {
        for(j = 0 to 2) {
            t = getCell(i, j);
            if(t ~= 0) {
                c = c + 1;
            }
        }
    }
    if(c == 9) {
        gameOver(TIE);
    }
}

AI

让我们做一个 AI

function ai() { ' Twiggery code
    ' Consts
    WHITE = 1;
    BLACK = -1;
    EMPTY = 0;
    TIE = 2;
    ' Initialize a array
    array_new(8);
    ' Rows counting
    i = getCell(0, 0) + getCell(1, 0) + getCell(2, 0);
    array_set(0, i);
    i = getCell(0, 1) + getCell(1, 1) + getCell(2, 1);
    array_set(1, i);
    i = getCell(0, 2) + getCell(1, 2) + getCell(2, 2);
    array_set(2, i);
    ' Columns counting
    i = getCell(0, 0) + getCell(0, 1) + getCell(0, 2);
    array_set(3, i);
    i = getCell(1, 0) + getCell(1, 1) + getCell(1, 2);
    array_set(4, i);
    i = getCell(2, 0) + getCell(2, 1) + getCell(2, 2);
    array_set(5, i);
    ' Diagonal lines counting
    i = getCell(0, 0) + getCell(1, 1) + getCell(2, 2);
    array_set(6, i);
    i = getCell(2, 0) + getCell(1, 1) + getCell(0, 2);
    array_set(7, i);
    ' Search
    found = false;
    for(c = 2 to 1 step -1) {
        for(i = 0 to 7) {
            t = array_get(i);
            t = abs(t);
            if(t == c) {
                found = true;
                break;
            }
        }
        if(found) {
            break;
        }
    }
    ' Found a target cell
    if(found) {
        if(i >= 0 and i <= 2) { ' Rows
            for(j = 0 to 2) {
                t = getCell(j, i);
                if(t == 0) {
                    setCell(j, i, BLACK);
                    return;
                }
            }
        } elseif(i >= 3 and i <= 5) { ' Columns
            k = i - 3;
            for(j = 0 to 2) {
                t = getCell(k, j);
                if(t == 0) {
                    setCell(k, j, BLACK);
                    return;
                }
            }
        } elseif(i == 6) { ' Diagonal \
            for(j = 0 to 2) {
                t = getCell(j, j);
                if(t == 0) {
                    setCell(j, j, BLACK);
                    return;
                }
            }
        } elseif(i == 7) { ' Diagonal /
            for(j = 0 to 2) {
                k = 2 - j;
                t = getCell(k, j);
                if(t == 0) {
                    setCell(k, j, BLACK);
                    return;
                }
            }
        }
    }
    ' Did not find any cell, choose one randomly
    c = 0;
    for(i = 0 to 2) {
        for(j = 0 to 2) {
            t = getCell(i, j);
            if(t == 0) {
                k = i + j * 3;
                array_set(c, k);
                c = c + 1;
            }
        }
    }
    r = rnd(0, c);
    k = array_get(r);
    i = k % 3;
    j = k / 3;
    j = int(j);
    setCell(i, j, BLACK);
}

现在我们可以使用 CodeLeaf(在 Twiggery 开发包中提供)将这些脚本源文件编译成字节码,并将它们放在游戏框架可以访问的目录中。

第四步。制作可发布包

您可以像处理其他类型的数据文件一样处理编译后的 Twiggery 脚本。

在开发 JavaME 应用程序时,我总是将所有资源(图像、声音、数据文件)与编译后的 *.class 文件一起打包到单个 *.jar 包中,并且我倾向于对 Twiggery 字节码也这样做,尽管我可以将它们放在与 *.jar 包分开的其他目录中。如果您不是在开发 JavaME 而是其他平台,我建议您在这些平台上按照相应常规方式进行。

结论

我很高兴向您介绍这样一个面向移动开发的脚本语言,希望它能有所帮助。我邀请您在下方提出您的问题、建议和想法。我将尽力使其变得更好!感谢您的阅读!

历史

  • 2010 年 8 月 30 日:修复了一些拼写错误
  • 2010 年 8 月 27 日:首次发布
© . All rights reserved.