There was a bit of buzz in the indie game community recently around the idea of editing object properties at runtime as a great way to tune gameplay quickly. I haven’t seen any attempts at a reusable system to handle it, so I thought I’d give it a go. AScalpel is a basic editor shell which uses metadata to dynamically create editors for custom classes. There’s still some issues to iron out with it, but I don’t really have enough free time to do it. It’s up on Git now.

Normally I’m a diehard Flex fan, but I know not everyone shares my views so I’ve used the Bit-101 Minimal Components for this. It could easily be retrofitted to use Flex as well (I will probably do this later). For this initial release, it is purely an Actionscript 3.0 library. I’ve set up a Git repository to share it with everyone. An explanation of how to use it, below the cut. You’ll need the Bit-101 components in order to use it at the moment. Currently hosted on Google Code, here

AScalpel Class

The main AScalpel class is a singleton which holds onto the root of the editor display. You have full control over where and how this is created, hidden, and shown. The AScalpel class is used to create and destroy LiveEditor instances, as well as for registering custom editors and setting global options. In the simplest case all that needs to be done is the following. Details on that first call are later in the post.

StandardBit101Editors.install();
addChild(AScalpel.instance.display);

There are two ways to create editor windows for objects. You can add objects to an internal list using AScalpel.instance.addObject and then select objects from the dropdown that appears to create editors for them. Alternatively, you can call AScalpel.instance.open directly and just create editor windows without clogging the screen with the main editor window.

[Editable]

The [Editable] tag is the core of the system. Any public variable or getter/setter pair tagged as [Editable] will have a matching field created in the editor. If the member is a basic Actionscript type, the bare [Editable] tag will likely be enough. AScalpel has default fields defined for all of the basic data types. There are additional parameters that are available to handle other situations or to enhance the editor that is created.

explicitCommit=true

By default, all editors will apply their updated values to the target object as you type. In many cases this may be undesirable due to unexpected or invalid values being set. If explicitCommit is set to "true", a Commit button will be added next to the field and the updated value will only be passed into the target object when the button is clicked.

[Editable(explicitCommit="true")]
public function get x():Number
{
	return _x;
}
public function set x(in_value:Number):void
{
	_x = in_value;
}
private var _x:Number;

customEditor="com.fully.qualified.ClassName"

You can specify a custom class to be used for the field for this particular member. A common case for this would be using a colour picker control for colour properties. AScalpel will try to figure out how to use any editor class, but is is always better to explicitly register it. That's covered near the end of this post.

[Editable(editorClass="com.bit101.components.ColorChooser")]
public function get color():uint
{
	return _color;
}
public function set color(in_value:uint):void
{
	_color = in_value;
	redraw();
}
private var _color:uint = 0x000000;

Any Property Supported by the Editor

If a parameter of the editor field is not recognized by the ObjectEditor, it will check if a property of that name exists on the field editor itself and apply the value if so. For example:

[Editable(editorClass="com.bit101.components.NumericStepper", minimum="1", maximum="30", step="0.1")]
public function get radius():Number
{
	return _radius;
}
public function set radius(in_value:Number):void
{
	_radius = in_value;
	redraw();
}

Configuration

Assigning default editors

For types which are used in multiple places in the game, it's much more convenient to define a default editor for that type than to specify it in every [Editable] tag. This is done using AScalpel.instance.setDefaultEditorClassForType method of the ASCalpel singleton.

Configuring custom editors

All custom editors should ideally be configured, even if they would work with the default values; It's a convenient way to be sure that the custom editor is actually visible to the compiler. When registering an editor, you pass four things to AScalpel through AScalpel.instance.registerEditorClass

  1. A reference to the editor class (not an instance of the class or the name of the class)
  2. The name of the member variable which is used to set and retrieve the current value of the editor
  3. The event which the component dispatches when it is initialized (or null if there is no such event)
  4. The event which the component dispatches when the value changes

The first version of AScalpel has integration with the Bit-101 minimal components built in, through the StandardBit101Editors class, shown below. This provides a relatively simple example of how to configure AScalpel.

package com.andrewtraviss.ascalpel
{
	import com.bit101.components.CheckBox;
	import com.bit101.components.ColorChooser;
	import com.bit101.components.Component;
	import com.bit101.components.InputText;
	import com.bit101.components.Knob;
	import com.bit101.components.NumericStepper;
	import com.bit101.components.Slider;
	import com.bit101.components.TextArea;

	import flash.events.Event;
	import flash.events.MouseEvent;

	public class StandardBit101Editors
	{
		public static function install():void
		{
			AScalpel.instance.registerEditorClass(com.bit101.components.InputText, "text", Component.DRAW, Event.CHANGE);
			AScalpel.instance.registerEditorClass(com.bit101.components.NumericStepper, "value", Component.DRAW, Event.CHANGE);
			AScalpel.instance.registerEditorClass(com.bit101.components.ColorChooser, "value", Component.DRAW, Event.CHANGE);
			AScalpel.instance.registerEditorClass(com.bit101.components.CheckBox, "selected", Component.DRAW, MouseEvent.CLICK);
			AScalpel.instance.registerEditorClass(com.bit101.components.Knob, "value", Component.DRAW, Event.CHANGE);
			AScalpel.instance.registerEditorClass(com.bit101.components.Slider, "value", Component.DRAW, Event.CHANGE);
			AScalpel.instance.registerEditorClass(com.bit101.components.TextArea, "text", Component.DRAW, Event.CHANGE);

			AScalpel.instance.setDefaultEditorClassForType("com.bit101.components.InputText", String);
			AScalpel.instance.setDefaultEditorClassForType("com.bit101.components.InputText", Number, {restrict:"0-9.\\-"});
			AScalpel.instance.setDefaultEditorClassForType("com.bit101.components.InputText", int, {restrict:"0-9\\-"});
			AScalpel.instance.setDefaultEditorClassForType("com.bit101.components.InputText", uint, {restrict:"0-9"});
			AScalpel.instance.setDefaultEditorClassForType("com.bit101.components.CheckBox", Boolean);
		}
	}
}