-
-
Save cwkx/6032771b4e8f5c74e848 to your computer and use it in GitHub Desktop.
HaxeFlixel hscript component system with custom pre-processor in Script.hx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#(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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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