I'm getting there with the engine part of my game and I'm planning on trying this whole scripting-thingy I've been hearing so much about. It seems a fairly critical factor in whether you succeed or not is exposing the right level of functionality in the engine, but I haven't found any examples.
I'd really appreciate a short example or any tips at all to get a feel for the kind of functionality I should expose, or rather use since AngelScript doesn't seem to need exposing as such but can bind directly which is a really neat feature
I thought a little about it I think I'd want the scripts to be on the level of
Script vs engine side I thought I'd use something along the lines of:
each tick run
for c in creatures:
c->behaviour->run(state);
where behaviour is a script and state is what's happening, just do something, respond to an attack or something else.
In the previous version of my game I created units in c++, but if I already got a scripting engine running why not use it for this too
addType("Tree", Movement::Stationary, stats = { hp = 17, attack = 0 });
addGraphicsForType("Tree", "tree.png");
I thought about having them in one call, but with this split it's much easier to later upgrade the graphics if I want to (currently running simple ascii-graphics to speed up development) so it'd be more like "addGraphicsForType("Tree", \u47);".
Thoughts on:
- Level of scripts?
- Running of scripts?
- Entity creation scripts?
There's obviously a number of ways to do this, and I'm not a long time user of AngelScript, but some thoughts based on how I've approached this:
1. Regarding running of the scripts, I'm a big fan of coroutines, as they allow you to express logic that occurs over a period of time cleanly.
i.e.
void PursueTarget(Creature enemy)
{
while(CanDetect(enemy) && !IsInRange(enemy))
{
MoveTowards(enemy.GetPosition());
yield();
}
if (CanDetect(enemy))
{
Attack(enemy);
}
}
To this end I run many events originating from the engine side as coroutines, which can be stopped for a frame with a yield command. I also don't give objects an update function that is called every frame, and instead let them start a coroutine or if they need this. In future I'll probably expand this so that couroutines can be stopped for a period of time, or while a long running engine function is being processed (i.e. pathfinding on a separate thread, or even the MoveTo(X) function).
2. The way I've done game objects in general is I have a C++ backing object that is wrapped by a base script controller object, which all other script objects can then inherit. The C++ backing object holds the world location/rendering information and is the bit that generally interacts with the engine subsystems and receives events from them - which are then promoted up to their script controller. For instance, the backing object is actually placed in the world (a 2D grid) and can then a engine exposes a function to query all objects in a grid square.
This could actually be broken down into a component model, where a script object has a number of engine-implemented components - Display components that are rendered, collision components that exist in the world and block or detect other collision components, sound components, etc.
I'm not a big fan of co-routines. They are excellent for controlled sequences where you know exactly the order that things happen, but I think they will complicate things when it comes to a game where the future is not known. Instead I prefer an event based model, where the scripts implement event handlers.
In my own game engine I have several types of scripts controlling different things:
- The game script is responsible for keeping track of player stats between levels and also progressing the game, e.g. decide which is the order of the levels that will be played. During the game play, this script doesn't do anything, but other scripts can query and update properties, such as number of lives, gold coins, etc.
- The level script controls level specific logic. This can be used to create animated sequences, or control the camera movement. Each level can have a different level script with its own unique behaviour.
- The GUI script is responsible for rendering and animating the GUI. The game script is also able to define different GUI scripts for different levels.
- The AI controller scripts. Each entity type has it's own AI controller script.
The actual movement of entities is done by the physics engine (Box2D in my case), so the AI controller just sets the desired velocity to move itself. Dumb objects, such as boxes, don't need a script, and are entirely controlled by the physics engine.
Here's a list of the event handlers that the AI controller scripts can implement:
- onThink. Called each frame as long as the object is awake.
- onCollide. Called when an object collides with something. If the collision is between two objects the event handler will receive a handle to the other object so it can interact with it.
- onTouch. Called when another object 'touches' another. This is the principal way that two objects interact.
- onTimer. An entity can set timers to schedule future actions, when timer fires the onTimer event handler is called.
I plan on writing a sample for the AngelScript SDK that implements a simplified version of the scripting model in my game engine. I'm not sure if I'll be able to include it for the release 2.21.1, but at least for 2.21.2 I should be able to conclude it.
I'm not a big fan of co-routines. They are excellent for controlled sequences where you know exactly the order that things happen, but I think they will complicate things when it comes to a game where the future is not known. Instead I prefer an event based model, where the scripts implement event handlers.
In my own game engine I have several types of scripts controlling different things:
- The game script is responsible for keeping track of player stats between levels and also progressing the game, e.g. decide which is the order of the levels that will be played. During the game play, this script doesn't do anything, but other scripts can query and update properties, such as number of lives, gold coins, etc.
- The level script controls level specific logic. This can be used to create animated sequences, or control the camera movement. Each level can have a different level script with its own unique behaviour.
- The GUI script is responsible for rendering and animating the GUI. The game script is also able to define different GUI scripts for different levels.
- The AI controller scripts. Each entity type has it's own AI controller script.
The actual movement of entities is done by the physics engine (Box2D in my case), so the AI controller just sets the desired velocity to move itself. Dumb objects, such as boxes, don't need a script, and are entirely controlled by the physics engine.
Here's a list of the event handlers that the AI controller scripts can implement:
- onThink. Called each frame as long as the object is awake.
- onCollide. Called when an object collides with something. If the collision is between two objects the event handler will receive a handle to the other object so it can interact with it.
- onTouch. Called when another object 'touches' another. This is the principal way that two objects interact.
- onTimer. An entity can set timers to schedule future actions, when timer fires the onTimer event handler is called.
I plan on writing a sample for the AngelScript SDK that implements a simplified version of the scripting model in my game engine. I'm not sure if I'll be able to include it for the release 2.21.1, but at least for 2.21.2 I should be able to conclude it.
I'll try to introduce something along those thought-lines. I like the division of game, level etc etc.
What's the difference between collide and touch in your case?
I've got Angelscript up and running, commendation on making it easy, only took around an hour or so from download to "woo, script running" output on my screen Now I just have to thread it and some other things.
Regarding addons, I added the .h/.cpp-files to the angelscript solution and rebuild the libraries, is there a "proper" way to introduce addons through some addon system or can I keep adding stuff my way?
The onCollide event handler is called as response to physical collisions, and includes information on the collision, i.e. point of impact, normal, and impulse applied.
The onTouch event handler is called upon a manual interaction, and doesn't necessarily need a physical contact between the objects in the world. Perhaps the event handler could have been better named as onInteract, or onMessage. For example, if I wrote a squad based AI, I could use the onTouch event to allow the squad members to communicate with each other.
Ah, I have one more event handler, called onProximity. This is also called as response from the physical simulation, but rather than a collision it is just a notification that an object entered or exited the area of interest. This is useful for invisible triggers or pickups.
As for the add-ons, I prefer simply including the ones that are appropriate to my application in the actual application code, rather than building them into the angelscript library, but that's just my preference, there is nothing wrong with including them in the library and linking them from there.
Here's what my player script currently looks like:
void onThink()
{
// TODO: To jump down from a platform, the player should
// set the collision mask to not collide with platforms,
// and then set a timer to turn on the collisions again
// as the movement has taken him down under the platform
vector2 vel = entity.velocity;
float dy = 0;
bool walking = false;
if( input.GetActionState(actionLeft) )
{
walking = true;
if( direction >= 0 )
direction--;
}
if( input.GetActionState(actionRight) )
{
walking = true;
if( direction <= 0 )
direction++;
}
if( input.GetActionState(actionJump) )
{
// Am I on the ground? If there is ground 1 cm below me, I consider it yes
float f = game.RayCast(entity, 1, entity.position, entity.position - vector2(0, 1.01), null);
if( f < 1 )
{
dy = jumpSpeed;
}
}
if( input.GetActionState(actionUse) && !isAttacking )
{
// Did we hit something with the sword?
const CEntity @obj;
if( direction > 0 )
{
game.RayCast(entity, 0, entity.position, entity.position + vector2(2, 0), obj);
entity.SetAnimation("attack_r");
}
else if( direction < 0 )
{
game.RayCast(entity, 0, entity.position, entity.position + vector2(-2, 0), obj);
entity.SetAnimation("attack_l");
}
if( obj !is null )
{
// Touch it. This will tell the other object we're hitting it
entity.Touch(obj);
}
// Set timer to let us attack again
isAttacking = true;
vel.x = 0;
entity.SetTimer(attackTime, canAttackAgain);
}
void onCollide(const CEntity @object, const vector2 &in position, const vector2 &in normal, float appliedImpulse)
{
if( object !is null )
{
if( object.type == "zombie" )
{
if( normal.y > 0.5 )
{
// Make the player jump/bounce off the zombie's head
entity.velocity = vector2(entity.velocity.x, jumpSpeed);
}
else
{
if( !isInvincible )
TakeLife();
}
}
}
}
void TakeLife()
{
// Take a life
uint lives;
game.GetProperty('lives', lives);
game.SetProperty('lives', --lives);
if( lives == 0 )
game.EndLevel(endLevelFail);
// For a while the player should not take more damage
isInvincible = true;
entity.SetTransparency(0.5);
entity.SetTimer(invincibleTime, makeVulnerable);
}
void onTimer(int id)
{
switch( id )
{
case makeVulnerable:
isInvincible = false;
entity.SetTransparency(0);
break;
case canAttackAgain:
isAttacking = false;
break;
}
}
CEntity @entity;
bool isAttacking; // is the player currently attacking?
bool isInvincible; // is the player currently invincible?
int direction; // in which direction is the player looking?
}
My game engine is still very much a work in progress, but almost all the game play mechanics are in place.
Wow, I really like that approach of setting up your controller scripts, currently i can only have a function that's the same name as the entity be ran every loop, It works but its quite limiting and I cant create any more instance level variables, they all have to be in the base class, which creates a giant, unmanageable monster, I look forward to you you implemented this in the next SDK!
Ah, thanks for the clarification, I think I've got what I need to get started now. I'll try something that's fairly similar to yours and see how it goes.
context->Execute returns when the script is done or it yield():s right? The reason you've got timeout-limits in the samples.
I thought about only using a onMessage but onProximity and onTimer was fairly nifty ideas too. Support for timers will be a little work but is probably very nice to have once it's done. Think I might have an old scheduler somewhere...
I'm not a big fan of co-routines. They are excellent for controlled sequences where you know exactly the order that things happen, but I think they will complicate things when it comes to a game where the future is not known. Instead I prefer an event based model, where the scripts implement event handlers.
I'm having a bit of a rethink of how I want coroutines to work in my integration now, based on your comments. I don't feel there is necessarily any conflict between coroutines and an event based model, although for that conflict to be removed coroutines need to be "named" and stoppable. I supposed that is reaching towards treating coroutines as states that can be entered and left, or possibly stacked. For example a placeable bomb script may look like:
class Bomb : BaseObject
{
void Start()
{
StartCoroutine("DelayedExplode");
}
void DelayedExplode()
{
for (int i = 0; i < 10; ++i)
{
Flash();
WaitForSeconds(1);
}
Explode();
}
Admittedly for something more complex like an AI involved in tactical behaviour (like presently engaging a player, as opposed to patrolling or pursing long-term activities) this may not work as well, as you want to be constantly reassessing the situation. There is probably room for both things to be mixed together in the same class though, for different purposes.
Anyhow I'm looking at things in light of my previous experiences with UnrealScript and its State construct (with simple coroutine support) and Unity, and perhaps I need to review those previous learnings.
I've been learning about coroutines in Lua today but I haven't quite figured out the concept.
Let's have a look at your example, Immortius. Wouldn't your game halt for ten seconds as soon as you called StartCoroutine("DelayedExplode")? Wouldn't there have to be yield() commands scattered around to give control back to the game after every command or a few commands?
Wunderwerk Engine is an OpenGL-based, shader-driven, cross-platform game engine. It is targeted at aspiring game designers who have been kept from realizing their ideas due to lacking programming skills.
I should explain that coroutines are not actually a core Angelscript feature. Angelscript merely provides the ability to suspend script contexts and resume them later to the C++ code. yield() is just an arbitrary function that is registered for scripts to suspends a context until the next frame/cycle as part as the ContextManager add on. You can develop a coroutine system that provides other yield-like functions that suspend script contexts until future events occur - in the example given I intend WaitForSeconds() to be a yield style function that suspends the context until an amount of time passes, rather than just a single frame. You could have a whole suite of yield functions that suspend until different events occur - as long as you are willing to implement them in the C++/native code.
I guess the other point is that the "DelayedExplosion" coroutine would not be activated immediately, but later in the same frame - the StartCoroutine function would queue it up, but the current context must end/suspend before it will start. That's the way I've implemented it anyhow, and I believe the context manager implementation is similar.
Edit: I think I've just about convinced myself to cut back on coroutine use at this point. A timer system addresses a lot the need without the overhead of keeping contexts in memory. Rather than the timer-with-id style used in WitchLord's example I might mess with something where you register a function name (or pointer) to be called after the time has passed.