Skip to content

Instantly share code, notes, and snippets.

@cwkx
Last active March 31, 2019 17:16
Show Gist options
  • Save cwkx/6032771b4e8f5c74e848 to your computer and use it in GitHub Desktop.
Save cwkx/6032771b4e8f5c74e848 to your computer and use it in GitHub Desktop.
HaxeFlixel hscript component system with custom pre-processor in Script.hx
Scripting:
(1) Here is an example script (all one file):
#(init) input, sprite -- An "init" event (runs once) that initializes the "input" and "sprite" packages
Sprite.AddTrail(-1); -- This function is called from the sprite package (without sprite, it will crash)
#(init) controller -- Another "init" event that initializers the "controller" package (so that it can be used in update)
#(update) input, controller -- An "update" event that uses the "input" and "controller" packages
if (Input.Left())
Controller.Move(-100.0);
#(destroy) input, sprite, controller -- Make sure to destroy all packages that have been initialized
-- Add destroy code that runs when the instance goes out of the ROI
(2) Only the packages that are defined in the #meta line get bound and processed in the relevant event sections.
Comments are not supported in the #meta line, but they're supported in the code sections.
Do not use the character sequence: #( anywhere in your codes, as it is used internally to split the sections.
The event order doesn't matter, although its only recommended to have one (init), (update), and (destroy) function per file.
Files do not need to contain all event types.
Multiple scripts can be added to one instance in Tiled, allowing for modular complex behaviours.
Functions should start with an uppercase letter, whereas data should start with a lowercase letter.
package packages;
import entities.Instance;
import packages.Package.ExamplePackage;
import systems.Script;
/** This is an example package for exposing new APIs to instances in the scripts. Remember to add it to Script.hx @author cwkx**/
@:final class ExamplePackage
{
/** Constructor (not called from scripts) **/
public function new(instance:Instance) { _instance = instance; }
/** Destructor (not called from scripts) **/
public function destroy() { _instance = null; }
/** Scripting functions **/
public static function add(instance:Instance) { if (instance.body.userData.examplePackage == null) instance.body.userData.examplePackage = new ExamplePackage(instance); }
public static function set(instance:Instance) { Script.interp.variables.set("ExamplePackage", cast(instance.body.userData.examplePackage, ExamplePackage)); }
public static function del(instance:Instance) { if (instance.body.userData.examplePackage != null) { var examplePackage:ExamplePackage = instance.body.userData.examplePackage; examplePackage.destroy(); instance.body.userData.examplePackage = null; } }
// Internal instance to which this sprite is bound
private var _instance:Instance;
}
package packages;
import entities.Instance;
import flixel.addons.effects.FlxTrail;
import flixel.FlxBasic;
import flixel.group.FlxTypedGroup;
import flixel.tweens.FlxTween;
import systems.Script;
/** This class exposes script functions to manipulate the instance sprite **/
@:final class Sprite
{
// Optional trail
public var trail:FlxTrail;
/** Plays an existing animation (e.g. 'run') **/
public inline function Animate(anim:String, force:Bool = false, frame:Int = 0):Void { _instance.animation.play(anim, force, frame); }
/** Gets the animation name **/
public inline function AnimationName():String { if (_instance.animation.curAnim != null) return _instance.animation.curAnim.name; else return null; }
/** This is useful for continuing forced animations **/
public inline function AnimationFinished():Bool { return _instance.animation.finished; }
public inline function AnimationFrame():Int { return _instance.animation.frameIndex; }
/** Adds a trail to the sprite **/
public function AddTrail(duration:Float = -1.0, ?image:Dynamic, length:Int = 10, delay:Int = 3, alpha:Float = 0.4, diff:Float = 0.05):Void
{
trail = new FlxTrail(_instance, image, length, delay, alpha, diff);
var layer:FlxTypedGroup<FlxBasic> = _instance.body.userData.layer;
layer.add(trail);
if (duration > 0.0)
FlxTween.num(1.0, 0.0, duration, null, function(x:Float) {
if (x == 0) {
trail.destroy();
trail = null;
}
});
}
/** Constructor (not called from scripts) **/
public function new(instance:Instance) { _instance = instance; }
/** Destructor (not called from scripts) **/
public function destroy()
{
// Cleanup data
_instance = null;
// Cleanup members
if (trail != null)
{
trail.destroy();
trail = null;
}
}
/** Scripting functions **/
public static function add(instance:Instance) { if (instance.body.userData.sprite == null) instance.body.userData.sprite = new Sprite(instance); }
public static function set(instance:Instance) { Script.interp.variables.set("Sprite", cast(instance.body.userData.sprite, Sprite)); }
public static function del(instance:Instance) { if (instance.body.userData.sprite != null) { var sprite:Sprite = instance.body.userData.sprite; sprite.destroy(); instance.body.userData.sprite = null; } }
// Internal instance to which this sprite is bound
private var _instance:Instance;
}
package entities;
import flixel.addons.nape.FlxNapeState;
import flixel.FlxBasic;
import flixel.group.FlxTypedGroup;
import nape.phys.Body;
import systems.Script;
import systems.Scene;
import flixel.FlxObject;
/** Spawned instance of a base class @author cwkx **/
class Instance extends Base
{
// Internal
var _parentSpawner:Body;
var _layer:FlxTypedGroup<FlxBasic>;
// Scripting events
public var initPrograms:Array<Program>;
public var updatePrograms:Array<Program>;
public var destroyPrograms:Array<Program>;
/** Clones the base class, grafting in the properties of the parent spawner (the layer, position, etc) **/
public function instantiate(copyFrom:Base, parentSpawner:Body):Base
{
// Returns a new exact copy of this one, added to the space
_parentSpawner = parentSpawner;
_layer = parentSpawner.userData.layer;
// Create a clone of the parent sprite
loadGraphicFromSprite(copyFrom);
pixelPerfectRender = Scene.pixelPerfect;
origin.set(0, 0);
// Create a clone of the parent body
body = copyFrom.body.copy();
body.position = parentSpawner.position;
body.space = FlxNapeState.space;
body.userData.aminstance = this;
body.userData.layer = parentSpawner.userData.layer;
body.userData.filter = parentSpawner.userData.filter;
// Set the position
setPosition(parentSpawner.position.x, parentSpawner.position.y);
if (animation.getByName("idle") != null)
animation.play("idle");
// Adds it to the layer, so it gets updated
_layer.add(this);
// Loop through setting materials and filters of new body shapes
body.setShapeFilters(parentSpawner.userData.filter);
if (parentSpawner.userData.material != null)
body.setShapeMaterials(parentSpawner.userData.material);
return this;
}
/** Class called after instantiation to preprocess the scripts**/
public function preprocess():Void
{
// Parse scripts
if (body.userData.scripts != null)
Script.preprocess(this);
// Run the init scripts
if (initPrograms != null)
{
for (program in initPrograms)
{
for (pkg in program.api)
{
pkg.add(this);
pkg.set(this);
}
Script.interp.execute(program.expr);
}
}
}
override public function update()
{
// Run the update scripts
if (updatePrograms != null)
{
for (program in updatePrograms)
{
for (pkg in program.api)
pkg.set(this);
Script.interp.execute(program.expr);
}
}
// Update the sprite after the physics
super.update();
}
override public function destroy()
{
super.destroy();
// Run the destroy scripts
if (destroyPrograms != null)
{
for (program in destroyPrograms)
{
for (pkg in program.api)
{
pkg.set(this);
pkg.del(this);
}
Script.interp.execute(program.expr);
}
}
initPrograms = null;
updatePrograms = null;
destroyPrograms = null;
_parentSpawner.userData.instance = null;
_parentSpawner = null;
if (_layer != null)
_layer.remove(this);
if (body != null)
FlxNapeState.space.bodies.remove(body);
_layer = null;
body = null;
parts = null;
}
}
#(init) input, camera, sprite, controller
// Setup sensors and sample the lower region of the shape to find locations for the feet rays
Controller.SetupFeet(16);
Camera.Follow();
Controller.maxJumps = 2;
#(update) input, controller, sprite
// Pass the jump state to the controller
Controller.jumpPressed = Input.JumpPressed() && !Controller.isKilling;
Controller.jumpJustPressed = Input.JumpJustPressed() && !Controller.isKilling;
// Perform the updates
Controller.StickToGround();
Controller.UpdateFeet();
Controller.UpdateTouch();
// Handling killing
if (Controller.isKilling)
{
Controller.isClimbing = false;
if (Sprite.AnimationFinished())
Controller.isKilling = false;
Controller.Stop();
return;
}
// Input killing
if (Input.KillJustPressed())
{
//Controller.Stop();
Controller.isKilling = true;
if (Input.RunPressed() && (Input.LeftPressed() || Input.RightPressed()))
Sprite.Animate("hitright");
else
if (Input.UpPressed())
Sprite.Animate("hitup");
else
if (Input.DownPressed())
Sprite.Animate("hitdown");
else
Sprite.Animate("hit");
return;
}
// Handle climbing
Controller.Climb((Input.UpPressed() && Controller.fallenClimbTime > 0.4),
(Input.ActionJustPressed() || Input.JumpJustPressed()),
(Input.RightPressed() - Input.LeftPressed()) * 50,
(Input.DownPressed() - Input.UpPressed()) * 50);
// On climbing shape
if (Controller.isClimbing)
{
Sprite.Animate("climb");
return;
}
// Handle jumping
Controller.Jump();
// Handle jumping animation
if (Controller.isJumping)
{
// Still perform motion in mid-air
if (Input.LeftPressed() != Input.RightPressed())
{
Controller.targetVelocity = (Input.LeftPressed() ? -1.0 : 1.0) * (Input.RunPressed() ? 140 : 70);
Controller.Move();
}
else
Controller.Stop();
// Mid-air jump fun
if (Controller.numJumps > 1 && Input.JumpJustPressed() && Controller.isFlipping)
{
Sprite.Animate("flip", true);
return;
}
if (Sprite.AnimationName() == "flip")
{
Controller.isFlipping = false;
if (!Sprite.AnimationFinished())
return;
}
// Don't animate jumps when on the ground
if (Controller.onGround)
{
// Don't stumble when jumping into the ground, but still land
if (Controller.apexTime > Controller.stumbleTime)
{
Controller.isJumping = false;
Controller.isLanding = true;
Controller.landingTime = 0.0;
}
return;
}
// Perform the jump animations if we're not jumping into the ground for too long
if (Controller.currentVelocityY < -.55 * Controller.jumpVelocity)
Sprite.Animate("jump", true, 0);
else
if (Controller.currentVelocityY < -.2 * Controller.jumpVelocity)
Sprite.Animate("jump", true, 1);
else
if (Controller.currentVelocityY < 0.2 * Controller.jumpVelocity)
Sprite.Animate("jump", true, 2);
else
if (Controller.currentVelocityY < 0.55 * Controller.jumpVelocity)
Sprite.Animate("jump", true, 3);
else
Sprite.Animate("jump", true, 4);
return;
}
// Handle landing animation (multiply by 2 as theirs currently 3 landing frames [0,1,2])
if (Controller.isLanding)
{
// Don't stumble when landing on the apex of a one-way platform
if (Controller.apexTime > Controller.stumbleTime)
Sprite.Animate("land", true, (Controller.landingTime / Controller.landingMaxTime) * 2);
return;
}
if (Input.LeftPressed() != Input.RightPressed())
{
// Move controller
Controller.targetVelocity = (Input.LeftPressed() ? -1.0 : 1.0) * (Input.RunPressed() ? 140 : 70);
Controller.Move();
// Slide if running and there's a sign change in velocity
if (Input.RunPressed() && Controller.VelocitySignChanged())
Controller.isSliding = true;
else
Sprite.Animate(Input.RunPressed() ? "run" : "walk");
}
else
{
// Stop controller
Controller.Stop();
if (Controller.VelocityAbove(100))
Controller.isSliding = true;
// Animate idle (force start the animation at frame 16, which is where his head is facing right)
else
if (Sprite.AnimationName() != "idle")
Sprite.Animate("idle", true, 16);
}
if (Controller.isSliding)
{
// Animate sliding
Sprite.Animate("slide");
}
#(destroy) input, camera, sprite, controller
package systems;
import entities.Instance;
import haxe.ds.StringMap;
import hscript.Expr;
import hscript.Interp;
import hscript.Parser;
import openfl.Assets;
import packages.Body;
import packages.Camera;
import packages.Controller;
import packages.Dialogue;
import packages.Input;
import packages.Sprite;
import StringTools;
/** This class preprocesses scripts and organizes data for the interpretor @author cwkx**/
@:final class Script
{
// Interpreter
public static var interp:Interp = new Interp();
public static var database:StringMap<Dynamic> = new StringMap<Dynamic>();
/** Preprocesses script files, reading the sections and splitting the packages into corresponding classes/events, so that set(..) can be called for only the desired data **/
public static function preprocess(instance:Instance)
{
var scriptNames:String = instance.body.userData.scripts;
var splitNames:Array<String> = scriptNames.split(",");
for (rawFilename in splitNames)
{
var rawScriptWhole:String = Assets.getText("scripts/" + StringTools.trim(rawFilename) + ".hx");
if (rawScriptWhole != null)
{
// Split script into sections (we add a new line character at the eof, and split it by #( which is what the meta data starts with)
var rawScriptSplits:Array<String> = (rawScriptWhole + "\n").split("#(");
for (rawScript in rawScriptSplits)
{
if (rawScript == "") continue;
// Skips the type characters, e.g. "#init" or "#update"
var skipTypeChars:Int = 0;
var program:Program = new Program();
// Create the program
if (StringTools.startsWith(rawScript, "update)"))
{
if (instance.updatePrograms == null)
instance.updatePrograms = new Array<Program>();
skipTypeChars = 7;
instance.updatePrograms.push(program);
}
else
if (StringTools.startsWith(rawScript, "init)"))
{
if (instance.initPrograms == null)
instance.initPrograms = new Array<Program>();
skipTypeChars = 5;
instance.initPrograms.push(program);
}
else
if (StringTools.startsWith(rawScript, "destroy)"))
{
if (instance.destroyPrograms == null)
instance.destroyPrograms = new Array<Program>();
skipTypeChars = 8;
instance.destroyPrograms.push(program);
}
else
{
trace("Error reading script type in #meta line of: " + StringTools.trim(rawFilename) + ".hx");
return;
}
// Sort out the API
var endOfLine:Int = rawScript.indexOf("\n");
var packages:Array<String> = rawScript.substring(skipTypeChars, endOfLine).split(",");
for (pkg in packages)
{
var pkg:String = StringTools.trim(pkg);
// Add the packages - this needs to be updated whenever new packages are designed
switch (pkg)
{
case "input" : program.api.push(Input);
case "sprite" : program.api.push(Sprite);
case "controller" : program.api.push(Controller);
case "dialogue" : program.api.push(Dialogue);
case "camera" : program.api.push(Camera);
case "body" : program.api.push(Body);
}
}
// Remove the #meta line and parse the script
rawScript = rawScript.substr(endOfLine);
// Parse the remaining script
if (rawScript != null)
{
var parser = new hscript.Parser();
program.expr = parser.parseString(rawScript);
}
}
}
else
trace("Error reading script file: " + StringTools.trim(rawFilename) + ".hx");
}
}
}
/** A script program consists of an API and an expression **/
@:final class Program
{
public var api:Array<Dynamic>;
public var expr:Expr;
public function new() { api = new Array<Dynamic>(); }
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment