Introduction
Nowadays mobile development seems to be more popular than years ago. I've dived into it and have been paying interest since my college time. I found many applications on PC powered by scripting languages (such as Lua, Python, Ruby, etc.) show great flexibility and dynamic behavior, but almost nothing could take a similar role as those scripts on mobile platforms without fast CPU, bulk RAM or even enough battery capacity. So that is why my Twiggery scripting language is created.
Twiggery is not omnipotent, but domain specific. You can get more information about syntax of Twiggery and download the full package here.
This article will show how to write a host application with Twiggery scripting interface, how to code and compile Twiggery source into bytecode using those interfaces, how to embed bytecode in a host, and finally how to deploy a solution application powered by Twiggery. I'll make a full Tic-Tac-Toe game with rule checking and AI simulation as an example.
Background
I suppose you have known some fundamentals about JavaME programming before, but it doesn't matter if you haven't because I won't mention idioms of JavaME in this article. You'll come to the point.
Java, C# and C++ based TVM (Twiggery Virtual Machine) are ready-made in the standard Twiggery package. It’s incredible that you can power up your applications just after inheriting from a TVM implementation, isn't it? It’s also very easy to code another imitation in hundreds of lines with other programming languages such as Objective-C, ActionScript, Python, etc.
Using the Code
Let’s code a full Tic-Tac-Toe game in four steps. Of course, we can construct a game without any scripting language, then what I'm wordy here for, the answer is for flexibility. I'll get easy on that, you'd find it more necessary to use Twiggery if you were creating a more complex game with features such as “Optional dialog”, "Quest”, "Switch”, etc.
Step 1. Building a Basic Game Shell
You should know that script runs slower than its native host programming language. This means scripting languages are more suitable for states controlling than intensive computation. So let’s build up a game shell with Java, we would first do program initialization, key input checking, rendering, etc.
A game always comes with a main loop. I would do it like this:
public void run() {
int timeStep = 50;
while (true) {
long start = System.currentTimeMillis();
try {
if (!canvas.render()) {
destroyApp(true);
}
} catch (Exception ex) {
ex.printStackTrace();
}
long end = System.currentTimeMillis();
int duration = (int) (end - start);
int d = timeStep - duration;
if (d > 0) {
try {
Thread.sleep(d);
} catch (InterruptedException ex) {
ex.printStackTrace();
}
}
}
}
And I would start that looping as a thread in JavaME like this:
private TicTacToe canvas = null;
public void startApp() {
canvas = new TicTacToe();
Display.getDisplay(this).setCurrent(canvas);
Thread thread = new Thread(this);
thread.start();
}
A key input processing callback method is called automatically. We would deal with cursor movement and chessmen placement in it like this:
protected void keyPressed(int keyCode) {
int keyState = getGameAction(keyCode);
int winner = logic.getWinner();
if (Logic.EMPTY == winner) {
if (GameCanvas.FIRE == keyState) {
if (logic.getCell(cursorX, cursorY) == Logic.EMPTY) {
logic.setCell(cursorX, cursorY, logic.getHolder());
logic.rule();
logic.turn();
if (Logic.EMPTY == logic.getWinner()) {
logic.ai();
logic.rule();
logic.turn();
}
}
} else {
…
}
} else {
if (GameCanvas.FIRE == keyState) {
logic.reset();
}
}
}
Notice that calling logic.ai()
and logic.rule()
performs script execution; they are blank now but filled later.
The core rendering method would be like this:
public boolean render() throws Exception {
graphics.setColor(0xffffffff);
graphics.fillRect(0, 0, SCREEN_WIDTH, SCREEN_HEIGHT);
{
…
…
…
…
…
}
flushGraphics(0, 0, SCREEN_WIDTH, SCREEN_HEIGHT);
return running;
}
Here are some final game screens, let’s take a look at them first:
Step 2. Coding Script Programming Interfaces
After the game shell is done, we should make some script programming interfaces as well. This part is done with the host programming language and works as a glue which agglutinates the game shell and script together.
Twiggery allows running compiled bytecode on a virtual machine, it goes with some advantages:
- Only loading and just executing it, no compiling time cost
- Less memory occupation
- Script source protection
- Portability
The first two points are particularly important for mobile devices.
There would be two kinds of interfaces, one is accessing from host program to script, and the other one is the opposite way.
From Host to Script
A TVM provides initialization, loading and executing methods, and what we need to do is just put them together into a function like this if we want to call a compiled script:
public void rule() {
try {
String scriptFile = "/rule.tad";
loadAsm(scriptFile);
runAsm();
} catch (Exception ex) {
ex.printStackTrace();
}
}
public void ai() {
try {
String scriptFile = "/ai.tad";
loadAsm(scriptFile);
runAsm();
} catch (Exception ex) {
ex.printStackTrace();
}
}
You can call the rule and AI script execution methods like logic.rule()
, logic.ai()
as it appeared in step 1.
From Script to Host
We'd like to expose some programming interfaces to Twiggery so that we could control the game shell via script.
An interfaces list maybe like this:
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");
}
}
private void array_new() { … }
private void array_get() { … }
private void array_set() { … }
private void abs() { … }
private void _int() { … }
private void rnd() { … }
private void getCell() { … }
private void setCell() { … }
private void gameOver() { … }
Notice the “call” method is overridden from base class TVM. This method would be called to decide which interface should be executed when running a function call evaluation from script.
private void getCell() throws Exception {
int y = (int) popArgument();
int x = (int) popArgument();
int v = cells[x][y];
returnArgument((float) v);
}
Arguments are pushed from left to right, and that means they are popped from right to left. Like above, when we call the “getCell
” function from Twiggery such as: c = getCell(1, 2)
we are pushing 1
then 2
into a TVM stack, and we have to pop the second argument out then the first one (2
to y
, 1
to x
here). Finally, we could return some value to “c
” by calling “returnArgument
”.
Step 3. Coding & Compiling Game Scripts
In this part, we would write some Twiggery code to construct the rule checking and the AI script using those scripting interfaces. Actually step 2 and step 3 are cyclic iterative, sometimes we can't figure out how many scripting interfaces should be done before writing any script. When we need to use an interface which have not been created, we may go back to step 2 and make a new one.
Rule
In this tic-tac-toe game, I would check who wins the game in a Twiggery script like this:
function rule() {
WHITE = 1;
BLACK = -1;
EMPTY = 0;
TIE = 2;
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;
}
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;
}
}
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
And let’s make an AI:
function ai() {
WHITE = 1;
BLACK = -1;
EMPTY = 0;
TIE = 2;
array_new(8);
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);
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);
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);
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;
}
}
if(found) {
if(i >= 0 and i <= 2) {
for(j = 0 to 2) {
t = getCell(j, i);
if(t == 0) {
setCell(j, i, BLACK);
return;
}
}
} elseif(i >= 3 and i <= 5) {
k = i - 3;
for(j = 0 to 2) {
t = getCell(k, j);
if(t == 0) {
setCell(k, j, BLACK);
return;
}
}
} elseif(i == 6) {
for(j = 0 to 2) {
t = getCell(j, j);
if(t == 0) {
setCell(j, j, BLACK);
return;
}
}
} elseif(i == 7) {
for(j = 0 to 2) {
k = 2 - j;
t = getCell(k, j);
if(t == 0) {
setCell(k, j, BLACK);
return;
}
}
}
}
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);
}
Now we may compile these script source files into bytecode with CodeLeaf (supplied in the Twiggery development package) and put them in an accessible directory for our game shell.
Step 4. Making a Publishable Pack
You can deal with compiled Twiggery script similar to other types of data files.
When developing a JavaME application, I always put all resources (images, sound, data files) together with compiled *.class files in a single *.jar package, and I prefer doing the same with Twiggery bytecode even though I could put them in some other directories separated from the *.jar package. If you are not working on JavaME but some other platforms, I recommend you to do that in a corresponding regular way on those platforms.
Conclusion
It’s my pleasure to introduce to you such a mobile development aimed scripting language, I hope it would be helpful. I invite you to post your questions, suggestions and ideas below. I’ll do my best to make this better! Thank you for reading!
History
- Aug. 30, 2010: Fixed some spelling mistakes
- Aug. 27, 2010: First description