- improved the fps ticker

- latest Monster source


git-svn-id: https://openmw.svn.sourceforge.net/svnroot/openmw/trunk@81 ea6a568a-9f4f-0410-981a-c910a81bb256
pull/7/head
nkorslund 16 years ago
parent a0a95927c4
commit 879cc132d5

@ -224,16 +224,8 @@ void initializeInput()
// put another import in core.config. I should probably check the
// bug list and report it.
updateMouseSensitivity();
// Set up the FPS ticker
auto mo = (new MonsterClass("FPSTicker")).getSing();
frameCount = mo.getIntPtr("frameCount");
mo.setState("tick");
}
// Points directly to FPSTicker.frameCounter in Monster
int *frameCount;
extern(C) int ois_isPressed(int keysym);
// Check if a key is currently down
@ -247,8 +239,6 @@ bool isPressed(Keys key)
extern(C) int d_frameStarted(float time)
{
(*frameCount)++;
if(doExit) return 0;
// Run the Monster scheduler

@ -78,16 +78,32 @@ typedef extern(C) void function() c_callback;
struct Function
{
// These three variables (owner, lines and bcode) are common between
// Function and State. They MUST be placed and ordered equally in
// both structs because we're use some unsafe pointer trickery.
MonsterClass owner;
LineSpec[] lines; // Line specifications for byte code
union
{
ubyte[] bcode; // Final compiled code (normal functions)
dg_callback natFunc_dg; // Various types of native functions
fn_callback natFunc_fn;
c_callback natFunc_c;
IdleFunction idleFunc; // Idle function callback
}
Token name;
Type type; // Return type
FuncType ftype; // Function type
Token name;
Variable* params[]; // List of parameters
MonsterClass owner;
int index; // Unique function identifier within its class
int paramSize;
/*
int imprint; // Stack imprint of this function. Equals
// (type.getSize() - paramSize) (NOT USED YET)
// (type.getSize() - paramSize) (not implemented yet)
*/
// Is this function final? (can not be overridden in child classes)
bool isFinal;
@ -98,16 +114,6 @@ struct Function
// What function we override (if any)
Function *overrides;
union
{
ubyte[] bcode; // Final compiled code (normal functions)
dg_callback natFunc_dg; // Various types of native functions
fn_callback natFunc_fn;
c_callback natFunc_c;
IdleFunction idleFunc; // Idle function callback
}
LineSpec[] lines; // Line specifications for byte code
bool isNormal() { return ftype == FuncType.Normal; }
bool isNative()
{
@ -138,7 +144,7 @@ struct Function
// native code.
void call(MonsterObject *obj)
{
assert(obj !is null);
assert(obj !is null || isStatic);
// Make sure there's a thread to use
bool wasNew;
@ -187,9 +193,18 @@ struct Function
// kill it.
if(cthread.isUnused)
cthread.kill();
else
// Otherwise, store the stack
cthread.acquireStack();
cthread = null;
assert(fstack.isEmpty);
}
// I think we could also check fstack if it's empty instead of
// using wasNew. Leave these checks here to see if this assumtion
// is correct.
else assert(!fstack.isEmpty);
}
// Call without an object. TODO: Only allowed for functions compiled

@ -38,6 +38,13 @@ import std.stdio;
struct State
{
// These three variables (owner, lines and bcode) are common between
// Function and State. They MUST be placed and ordered equally in
// both structs because we're use some unsafe pointer trickery.
MonsterClass owner;
LineSpec[] lines; // Line specifications for byte code
ubyte[] bcode; // Final compiled code
Token name;
int index;
@ -45,16 +52,16 @@ struct State
HashTable!(char[], StateLabel*) labels;
StateLabel* labelList[];
// Cache the begin label since it has special meaning and is looked
// up often.
StateLabel* begin;
StateScope sc; // Scope for this state
MonsterClass owner; // Class where this state was defined
// State declaration - used to resolve forward references. Should
// not be kept around when compilation is finished.
StateDeclaration stateDec;
ubyte[] bcode;
LineSpec[] lines;
StateLabel* findLabel(char[] name)
{
StateLabel *lb;
@ -209,6 +216,13 @@ class StateDeclaration : Statement
assert(name == sl.name.str, "label name mismatch");
sl.index = cnt++;
st.labelList[sl.index] = sl;
// Cache the 'begin:' label
if(name == "begin")
{
assert(st.begin is null);
st.begin = sl;
}
}
}

@ -2,9 +2,11 @@ module monster.modules.all;
import monster.modules.io;
import monster.modules.timer;
import monster.modules.frames;
void initAllModules()
{
initIOModule();
initTimerModule();
initFramesModule();
}

@ -0,0 +1,88 @@
// Provides some simple numbers and functions regarding the rendering
// frames of the application. It's up to the user to some degree to
// provide this information, though. We rely on vm.frame to be called
// each frame.
module monster.modules.frames;
import monster.monster;
import monster.vm.mclass;
import monster.vm.idlefunction;
import monster.vm.thread;
const char[] moduleDef =
"module frames;
float time; // Time since last frame
float totalTime; // Time since rendering started
ulong counter; // Number of frames since program startup
// Sleep a given number of frames
idle sleep(int frameNum);
"; //"
// Keep local copies of these, since we don't want Monster code to
// overwrite them (we'll be able to explicitly forbid this later.)
ulong frames = 0;
float totTime = 0;
ulong *counter_ptr;
float *time_ptr;
float *totalTime_ptr;
// Add the given time and number of frames to the counters
void updateFrames(float time, int frmCount = 1)
{
// Add up to the totals
frames += frmCount;
totTime += time;
// Set the Monster variables
*counter_ptr = frames;
*time_ptr = time;
*totalTime_ptr = totTime;
// TODO: A similar priority queue like we're planning for timer
// would also be applicable here. However I'm guessing frameSleep()
// will be used a lot less than sleep() though, so this is really
// not high up on the priority list.
}
// Idle function that sleeps a given number of frames before
// returning.
class IdleFrameSleep : IdleFunction
{
override:
bool initiate(Thread* cn)
{
// Calculate the return frame
cn.idleData.l = frames + stack.popInt;
// Schedule us
return true;
}
bool hasFinished(Thread* cn)
{
// Are we at (or past) the correct frame?
return frames >= cn.idleData.l;
}
}
void initFramesModule()
{
static MonsterClass mc;
if(mc !is null) return;
mc = new MonsterClass(MC.String, moduleDef, "frames");
// Bind the idle
mc.bind("sleep", new IdleFrameSleep);
// Get pointers to the variables so we can write to them easily.
auto mo = mc.getSing();
counter_ptr = mo.getUlongPtr("counter");
time_ptr = mo.getFloatPtr("time");
totalTime_ptr = mo.getFloatPtr("totalTime");
}

@ -0,0 +1,25 @@
// This module provides an interface to the virtual threading API in
// Monster. Not done.
module monster.modules.threads;
/*
import monster.monster;
const char[] moduleDef =
"singleton thread;
native cancel();
native schedule();
idle pause();
"; //"
void initThreadModule()
{
static MonsterClass mc;
if(mc !is null)
return;
mc = new MonsterClass(MC.String, moduleDef, "thread");
}
*/

@ -56,6 +56,6 @@ static this()
// Initialize VM
scheduler.init();
initStack();
stack.init();
arrays.initialize();
}

@ -27,7 +27,6 @@ module monster.vm.codestream;
import std.string;
import std.stdio;
import monster.vm.error;
import monster.compiler.linespec;
// CodeStream is a simple utility structure for reading data
// sequentially. It holds a piece of byte compiled code, and keeps
@ -39,24 +38,11 @@ struct CodeStream
int len;
ubyte *pos;
// Position of the last instruction
ubyte *cmdPos;
// Used to convert position to the corresponding source code line,
// for error messages.
LineSpec[] lines;
// Size of debug output
const int preView = 50;
const int perLine = 16;
public:
void setData(ubyte[] data,
LineSpec[] lines)
void setData(ubyte[] data)
{
this.data = data;
this.lines = lines;
len = data.length;
pos = data.ptr;
}
@ -64,34 +50,11 @@ struct CodeStream
// Called when the end of the stream was unexpectedly encountered
void eos(char[] func)
{
char[] res = format("Premature end of input:\nCodeStream.%s() missing %s byte(s)\n",
char[] res = format("Premature end of input: %s() missing %s byte(s)",
func, -len);
res ~= debugString();
fail(res);
}
char[] debugString()
{
int start = data.length - preView;
if(start < 0) start = 0;
char[] res = format("\nLast %s bytes of byte code:\n", data.length-start);
foreach(int i, ubyte val; data[start..$])
{
if(i%perLine == 0)
res ~= format("\n 0x%-4x: ", i+start);
res ~= format("%-4x", val);
}
return res;
}
void debugPrint()
{
writefln(debugString());
}
// Jump to given position
void jump(int newPos)
{
@ -107,27 +70,12 @@ struct CodeStream
return pos-data.ptr;
}
// Get the current line
int getLine()
{
// call shared.linespec.findLine
return findLine(lines, cmdPos-data.ptr);
}
ubyte get()
{
if(len--) return *(pos++);
eos("get");
}
// Used for getting an instruction. It stores the offset which can
// be used to infer the line number later.
ubyte getCmd()
{
cmdPos = pos;
return get();
}
int getInt()
{
len -= 4;

@ -30,21 +30,15 @@ import monster.vm.stack;
import monster.vm.error;
import monster.compiler.states;
import monster.compiler.functions;
import monster.compiler.linespec;
// "friendly" parameter and stack handling.
enum SPType
{
Function, // A function (script or native)
Idle, // Idle function
State, // State code
NConst, // Native constructor
// The idle function callbacks are split because they handle the
// stack differently. We probably don't need to have one type for
// each though.
Idle_Initiate, // IdleFunction.initiate()
Idle_Reentry, // IdleFunction.reentry()
Idle_Abort, // IdleFunction.abort()
Idle_Check // IdleFunction.hasFinished()
}
// One entry in the function stack
@ -61,15 +55,46 @@ struct StackPoint
SPType ftype;
MonsterObject *obj; // "this"-pointer for the function
MonsterClass cls; // class owning the function
int afterStack; // Where the stack should be when this function
// returns
// Could have an afterStack to check that the function has the
// correct imprint (corresponding to an imprint-var in Function.)
int *frame; // Stack frame, stored when entering the function
// Get the class owning the function
MonsterClass getCls()
{
assert(isFunc || isState);
assert(func !is null);
return func.owner;
}
bool isStatic()
{ return isFunc() && func.isStatic; }
bool isFunc()
{ return (ftype == SPType.Function) || (ftype == SPType.Idle); }
bool isState()
{ return ftype == SPType.State; }
// Get the current source position (file name and line
// number). Mostly used for error messages.
Floc getFloc()
{
return (ftype == SPType.Function) && func.isStatic;
assert(isFunc || isState);
Floc fl;
fl.fname = getCls().name.loc.fname;
// Subtract one to make sure we get the last instruction executed,
// not the next.
int pos = code.getPos() - 1;
if(pos < 0) pos = 0;
fl.line = findLine(func.lines, pos);
return fl;
}
}
@ -123,11 +148,12 @@ struct FunctionStack
push(obj);
cur.ftype = SPType.Function;
cur.func = func;
cur.cls = func.owner;
assert(obj is null || func.owner.parentOf(obj.cls));
// Point the code stream to the byte code, if any.
if(func.isNormal)
cur.code.setData(func.bcode, func.lines);
cur.code.setData(func.bcode);
assert(!func.isIdle, "don't use fstack.push() on idle functions");
}
@ -143,10 +169,9 @@ struct FunctionStack
assert(obj !is null);
assert(st.owner.parentOf(obj.cls));
cur.cls = st.owner;
// Set up the byte code
cur.code.setData(st.bcode, st.lines);
cur.code.setData(st.bcode);
}
// Native constructor
@ -157,33 +182,16 @@ struct FunctionStack
cur.ftype = SPType.NConst;
}
private void pushIdleCommon(Function *fn, MonsterObject *obj, SPType tp)
void pushIdle(Function *fn, MonsterObject *obj)
{
// Not really needed - we will allow static idle functions later
// on.
assert(obj !is null);
push(obj);
cur.func = fn;
cur.cls = fn.owner;
cur.ftype = SPType.Idle;
assert(obj is null || fn.owner.parentOf(obj.cls));
assert(fn.isIdle, fn.name.str ~ "() is not an idle function");
cur.ftype = tp;
}
// These are used for the various idle callbacks. TODO: Probably
// overkill to have one for each, but leave it until you're sure.
void pushIdleInit(Function *fn, MonsterObject *obj)
{ pushIdleCommon(fn, obj, SPType.Idle_Initiate); }
void pushIdleReentry(Function *fn, MonsterObject *obj)
{ pushIdleCommon(fn, obj, SPType.Idle_Reentry); }
void pushIdleAbort(Function *fn, MonsterObject *obj)
{ pushIdleCommon(fn, obj, SPType.Idle_Abort); }
void pushIdleCheck(Function *fn, MonsterObject *obj)
{ pushIdleCommon(fn, obj, SPType.Idle_Check); }
// Pops one entry of the stack. Checks that the stack level has been
// returned to the correct position.
void pop()

@ -314,10 +314,6 @@ struct MonsterObject
// don't do anything.
else if(label is null) return;
// TODO: We can reorganize the entire function to deal with one
// sthread !is null test. Just do the label-checking first, and
// store the label offset
// Do we already have a thread?
if(sthread !is null)
{
@ -345,10 +341,9 @@ struct MonsterObject
"' is not part of class " ~ cls.getName());
if(label is null)
// findLabel will return null if the label is not found.
// TODO: The begin label should probably be cached within
// State.
label = st.findLabel("begin");
// Use the 'begin:' label, if any. It will be null there's
// no begin label.
label = st.begin;
if(label !is null)
{
@ -363,7 +358,7 @@ struct MonsterObject
}
// Don't leave an unused thread dangling - kill it instead.
if(sthread !is null && !sthread.isScheduled)
if(sthread !is null && sthread.isUnused)
{
sthread.kill();
sthread = null;

@ -33,16 +33,13 @@ import monster.compiler.scopes;
import monster.vm.mobject;
import monster.vm.mclass;
import monster.vm.arrays;
import monster.vm.fstack;
import monster.vm.error;
// Stack
// Stack. There's only one global instance, but threads will make
// copies when they need it.
CodeStack stack;
void initStack()
{
stack.init();
}
// A simple stack frame. All data are in chunks of 4 bytes
struct CodeStack
{
@ -71,8 +68,7 @@ struct CodeStack
frame = null;
}
// Get the current position index. Used mostly for debugging and
// error checking.
// Get the current position index.
int getPos()
{
return total-left;
@ -110,7 +106,11 @@ struct CodeStack
left = total;
pos = data.ptr;
assert(fleft == left);
if(fleft != 0)
writefln("left=%s total=%s fleft=%s", left, total, fleft);
assert(frame is null);
assert(fleft == 0);
assert(fstack.isEmpty);
}
void pushInt(int i)
@ -174,6 +174,7 @@ struct CodeStack
assert(len > 0);
int[] r = getInts(len-1, len);
pop(len);
assert(r.length == len);
return r;
}

@ -81,6 +81,21 @@ struct Thread
// The contents of idleObj's extra data for the idle's owner class.
SharedType extraData;
// Set to true whenever we are running from state code. If we are
// inside the state itself, this will be true and 'next' will be 1.
bool isActive;
// Set to true when a state change is in progress. Only used when
// state is changed from within a function in active code.
bool stateChange;
/*******************************************************
* *
* Private variables *
* *
*******************************************************/
private:
// Temporarily needed since we need a state and an object to push on
// the stack to return to state code. This'll change soon (we won't
// need to push anything to reenter, since the function stack will
@ -92,12 +107,45 @@ struct Thread
NodeList * list; // List owning this thread
int retPos; // Return position in byte code.
bool isActive; // Set to true whenever we are running from state
// code. If we are inside the state itself, this will
// be true and 'next' will be 1.
bool stateChange; // Set to true when a state change is in
// progress. Only used when state is changed from
// within a function in active code.
// Stored copy of the stack. Used when the thread is not running.
int[] sstack;
public:
/*******************************************************
* *
* Public functions *
* *
*******************************************************/
// Get a new thread. It starts in the 'unused' list.
static Thread* getNew(MonsterObject *obj = null)
{
auto cn = scheduler.unused.getNew();
cn.list = &scheduler.unused;
with(*cn)
{
theObj = obj;
// Initialize other variables
idle = null;
idleObj = null;
isActive = false;
stateChange = false;
retPos = -1;
sstack = null;
}
/*
if(obj !is null)
writefln("Got a new state thread");
else
writefln("Got a new non-state thread");
*/
return cn;
}
// Unschedule this node from the runlist or waitlist it belongs to,
// but don't kill it. Any idle function connected to this node is
@ -106,7 +154,7 @@ struct Thread
{
if(idle !is null)
{
fstack.pushIdleAbort(idle, idleObj);
fstack.pushIdle(idle, idleObj);
idle.idleFunc.abort(this);
fstack.pop();
idle = null;
@ -117,40 +165,46 @@ struct Thread
assert(!isScheduled);
}
static Thread* getNew(MonsterObject *obj = null)
{
auto cn = scheduler.unused.getNew();
cn.list = &scheduler.unused;
cn.initialize(obj);
return cn;
}
// Remove the thread comletely
void kill()
{
/*
if(theObj is null)
writefln("Killing non-state thread");
else
writefln("Killing state thread");
*/
cancel();
list.remove(this);
list = null;
if(sstack.length)
Buffers.free(sstack);
sstack = null;
/*
writefln("Thread lists:");
writefln(" run: ", scheduler.run.length);
writefln(" runNext: ", scheduler.runNext.length);
writefln(" wait: ", scheduler.wait.length);
writefln(" unused: ", scheduler.unused.length);
*/
}
bool isDead() { return list is null; }
// Schedule this thread to run next frame
void schedule(uint offs)
void schedule(int offs)
{
assert(!isScheduled,
"cannot schedule an already scheduled thread");
retPos = offs;
assert(offs >= 0);
moveTo(scheduler.runNext);
}
// Move this node to another list.
void moveTo(NodeList *to)
{
assert(list !is null);
list.moveTo(*to, this);
list = to;
}
// Are we currently scheduled?
bool isScheduled()
{
@ -180,24 +234,6 @@ struct Thread
( cast(NodeList.TList.Iterator)this ).getNext();
}
/*******************************************************
* *
* Public functions *
* *
*******************************************************/
void initialize(MonsterObject *obj)
{
theObj = obj;
// Initialize other variables
idle = null;
idleObj = null;
isActive = false;
stateChange = false;
retPos = -1;
}
// Reenter this thread to the point where it was previously stopped.
void reenter()
{
@ -210,7 +246,7 @@ struct Thread
assert(!isActive,
"reenter cannot be called when object is already active");
assert(fstack.isEmpty,
"state code can only run at the bottom of the function stack");
"can only reenter at the bottom of the function stack");
assert(isScheduled);
if(isIdle)
@ -219,7 +255,7 @@ struct Thread
assert(idleObj !is null || idle.isStatic);
// Tell the idle function that we we are reentering
fstack.pushIdleReentry(idle, idleObj);
fstack.pushIdle(idle, idleObj);
idle.idleFunc.reentry(this);
fstack.pop();
@ -227,9 +263,6 @@ struct Thread
idle = null;
}
// Remove the current node from the run list
moveTo(&scheduler.unused);
// Set the active flat to indicate that we are now actively
// running. (Might not be needed in the future)
isActive = true;
@ -238,6 +271,12 @@ struct Thread
assert(cthread is null);
cthread = this;
// Remove the current thread from the run list
moveTo(&scheduler.unused);
// Restore the stack
restoreStack();
// Set up the code stack for state code.
fstack.push(theObj.state, theObj);
@ -251,6 +290,8 @@ struct Thread
// Reset the thread
cthread = null;
fstack.pop();
// We are no longer active
isActive = false;
@ -258,8 +299,36 @@ struct Thread
format("Stack not returned to zero after state code, __STACK__=",
stack.getPos));
fstack.pop();
if(!isUnused)
// Store the stack
acquireStack();
else
// If the thread is not used for anything, might as well kill it
kill();
}
// Make a copy of the stack and store it for later. Reset the global
// stack.
void acquireStack()
{
assert(!isUnused(),
"unused threads should never need to aquire the stack");
assert(sstack.length == 0,
"Thread already has a stack");
assert(fstack.isEmpty);
// This can be optimized later
int len = stack.getPos();
if(len)
{
writefln("acquiring %s ints", len);
// Get a new buffer, and copy the stack
sstack = Buffers.getInt(len);
sstack[] = stack.popInts(len);
}
stack.reset();
}
private:
@ -269,21 +338,40 @@ struct Thread
* *
*******************************************************/
void fail(char[] msg)
void restoreStack()
{
int line = -1;
char[] file;
if(fstack.cur !is null)
assert(stack.getPos() == 0,
"cannot restore into a non-empty stack");
if(sstack.length)
{
line = fstack.cur.code.getLine();
file = fstack.cur.cls.name.loc.fname;
// Push the values back, and free the buffer
stack.pushInts(sstack);
Buffers.free(sstack);
assert(stack.getPos == sstack.length);
sstack = null;
}
}
// Move this node to another list.
void moveTo(NodeList *to)
{
assert(list !is null);
list.moveTo(*to, this);
list = to;
}
void fail(char[] msg)
{
Floc fl;
if(fstack.cur !is null)
fl = fstack.cur.getFloc();
.fail(msg, file, line);
.fail(msg, fl);
}
// Parse the BC.CallIdle instruction parameters and call schedule
// the given idle function.
// Parse the BC.CallIdle instruction parameters and schedule the
// given idle function.
void callIdle(MonsterObject *iObj)
{
assert(isActive && fstack.isStateCode,
@ -316,7 +404,7 @@ struct Thread
extraData = *idleObj.getExtra(idle.owner);
// Notify the idle function
fstack.pushIdleInit(idle, idleObj);
fstack.pushIdle(idle, idleObj);
if(idle.idleFunc.initiate(this))
moveTo(&scheduler.wait);
fstack.pop();
@ -365,13 +453,11 @@ struct Thread
// Get some values from the function stack
CodeStream *code = &fstack.cur.code;
MonsterObject *obj = fstack.cur.obj;
MonsterClass cls = fstack.cur.cls;
int clsInd = cls.getTreeIndex();
MonsterClass cls = fstack.cur.getCls();
// Only an object belonging to this thread can be passed to
// execute() on the function stack.
assert(obj is null || cls.parentOf(obj));
assert(obj is null || obj.cls.upcast(cls) == clsInd);
assert(obj !is null || fstack.cur.isStatic);
// Pops a pointer off the stack. Null pointers will throw an
@ -392,7 +478,7 @@ struct Thread
// Variable in this object
if(type == PT.DataOffs)
return obj.getDataInt(clsInd, index);
return obj.getDataInt(cls.treeIndex, index);
// This object, but another (parent) class
if(type == PT.DataOffsCls)
@ -449,7 +535,7 @@ struct Thread
//for(long i=0;i<limit;i++)
for(;;)
{
ubyte opCode = code.getCmd();
ubyte opCode = code.get();
//writefln("stack=", stack.getPos);
//writefln("exec(%s): %s", code.getLine, bcToString[opCode]);
@ -566,7 +652,7 @@ struct Thread
break;
case BC.PushClassVar:
stack.pushInt(*obj.getDataInt(clsInd, code.getInt()));
stack.pushInt(*obj.getDataInt(cls.treeIndex, code.getInt()));
break;
case BC.PushParentVar:
@ -1352,7 +1438,7 @@ struct Scheduler
// possible.
if(cn.isIdle)
{
fstack.pushIdleCheck(cn.idle, cn.idleObj);
fstack.pushIdle(cn.idle, cn.idleObj);
if(cn.idle.idleFunc.hasFinished(cn))
// Schedule the code to start running again this round. We
// move it from the wait list to the run list.

@ -36,6 +36,7 @@ import monster.compiler.assembler;
import monster.compiler.scopes;
import monster.modules.timer;
import monster.modules.frames;
import std.file;
import monster.util.string;
@ -65,6 +66,8 @@ struct VM
if(time != 0)
idleTime.add(time);
updateFrames(time);
scheduler.doFrame();
}

@ -3,8 +3,7 @@ singleton FPSTicker;
import io, timer;
// This is updated automatically by input/events.d
int frameCount;
ulong lastFrame;
float delay = 1.5;
@ -12,7 +11,7 @@ state tick
{
begin:
sleep(delay);
print("fps: ", frameCount / delay);
frameCount = 0;
print("fps:", (frames.counter-lastFrame) / delay);
lastFrame = frames.counter;
goto begin;
}

@ -54,7 +54,11 @@ void initMonsterScripts()
mc.bind("randInt",
{ stack.pushInt(rnd.randInt
(stack.popInt,stack.popInt));});
// Set up and run the fps ticker
auto mo = (new MonsterClass("FPSTicker")).getSing();
mo.setState("tick");
// Load and run the test script
mc = new MonsterClass("Test");
mc.createObject().call("test");

Loading…
Cancel
Save