Scripting
Game logic lives in scripts: TypeScript classes you add to entities like any other component. Studio checks them as you type and save, and the running game picks up a saved change straight away.
Your first script
Create one with Assets → Create → Script, or select an entity and use Component → New Script…, which also adds it to the entity. A script looks like this:
ts
import { Script, script } from "mmpx";
@script('Spinner')
export class Spinner extends Script
{
public OnUpdate(dt : number) : void
{
this.entity.transform.rotationZ += dt;
}
}@script('Spinner')registers the class under a name. Scenes save that name, so keep it stable once the script is in use.this.entityis the entity the script is on;this.entity.transformis its position, rotation and scale.OnUpdate(dt)runs every fixed tick, withdtin seconds.
Add it to an entity by dragging the file onto the Inspector, or with Add Component.
Fields in the Inspector
Mark fields with @property and they show in the Inspector, are saved with the scene, and can differ per entity:
ts
import { Entity, Script, TextComponent, property, script } from "mmpx";
@script('Counter')
export class Counter extends Script
{
@property({ min : 1, max : 99, tooltip : 'How many taps to win' }) goal = 5;
@property({ unit : 'px/s' }) speed = 300;
@property(TextComponent) label : TextComponent | null = null;
@property(Entity) endCard : Entity | null = null;
private m_count = 0;
public OnStart() : void
{
if(this.label) this.label.text = `0 / ${this.goal}`;
}
}- Numbers, strings and booleans take their type from the default value.
min,max,unit,tooltipandgroupshape how they show. @property(Entity)takes an entity from the scene;@property(TextComponent)(or any component class) takes that component on some entity. Drag either from the Hierarchy onto the field.- Fields without
@property(likem_count) are the script's own state: not saved, not shown.
Lifecycle
A script runs while it's enabled and its entity is active. Its hooks run in this order, as in Unity:
| Hook | When |
|---|---|
OnAttach() | Once, the first time the entity is active and fully loaded — fields and the entity's other components are there. |
OnEnable() | Each time it starts running (enabled, or its entity becomes active). |
OnStart() | Once, before its first OnUpdate. |
OnUpdate(dt) | Every fixed tick. |
OnLateUpdate(dt) | Every fixed tick, after everything's OnUpdate. |
OnFrameUpdate(dt) | Once per rendered frame — for visual-only work. |
OnDisable() | Each time it stops running. |
OnDestroy() | When the entity or the script is removed. |
Scripts run only in Play and in builds, never while you edit.
Finding things
ts
import { Script, SpriteComponent, script } from "mmpx";
@script('Finder')
export class Finder extends Script
{
public OnStart() : void
{
// A component on this entity — undefined when it has none.
const sprite = this.entity.TryGetComponent(SpriteComponent);
if(sprite) sprite.tint = 0xFFCC00;
// Children by name or path, and the whole scene by name or tag.
const label = this.entity.FindChild('Label');
const coin = this.scene.Find('Coin');
const enemies = this.scene.FindAllByTag('enemy');
label?.SetActive(false);
coin?.SetActive(true);
for(const enemy of enemies) enemy.Destroy();
}
}Prefer a @property field to a name lookup when the script always needs the same entity — it survives renames.
Taps and clicks
An entity with a Button (or Interaction) component receives pointer events. Override the hook on a script on that entity:
ts
import { PointerEntityEvent, Script, script } from "mmpx";
@script('TapMe')
export class TapMe extends Script
{
public OnClick(_e : PointerEntityEvent) : void
{
this.entity.transform.scaleX *= 1.1;
this.entity.transform.scaleY *= 1.1;
}
}OnClick fires for a Button; OnPointerDown, OnPointerUp, OnPointerMove and OnPointerTap fire for any interactive entity. Events bubble, so a parent's scripts hear its children's taps.
To listen to another entity — a game manager watching a button — subscribe to its events:
ts
import { Entity, EntityEvent, Script, property, script } from "mmpx";
@script('Manager')
export class Manager extends Script
{
@property(Entity) playButton : Entity | null = null;
public OnStart() : void
{
this.playButton?.on(EntityEvent.ButtonClick, this.onPlay, this);
}
private onPlay() : void
{
this.playButton?.SetActive(false);
}
}Movement and timing
Tweens animate any numeric fields over time:
ts
import { Easing, Script, Tween, script } from "mmpx";
@script('PopIn')
export class PopIn extends Script
{
public OnStart() : void
{
const t = this.entity.transform;
t.scaleX = 0;
t.scaleY = 0;
new Tween(t).To({ scaleX : 1, scaleY : 1 }, 0.4).WithEasing(Easing.OutBack).OnComplete(() => this.ready()).Start();
}
private ready() : void
{
// Again in two seconds, then every half second, three times.
this.ScheduleOnce(() => this.pulse(), 2);
this.Schedule(() => this.pulse(), 0.5, 3);
}
private pulse() : void
{
new Tween(this.entity.transform).To({ rotationZ : this.entity.transform.rotationZ + 0.2 }, 0.1).Start();
}
}ScheduleOnce, Schedule and ScheduleNextFrame call back on the script and are cancelled automatically when it's destroyed. Tweens and schedules wait while their entity is inactive.
The Game API
Game is a script's way to everything outside its entity:
| Member | What it does |
|---|---|
Game.input | Keys and pointer: IsKeyDown('ArrowLeft'), IsPointerDown(), pointerWorld, OnPointerDown(cb). |
Game.time | delta, time, realTime, and scale for slow motion. |
Game.audio | Play(clipId), Stop(handle), volume, muted. |
Game.scenes | Load(name), LoadAdditive(name), Instantiate(prefabId, parent). |
Game.assets | Load and list assets at runtime. |
Game.screen | Canvas size, isPortrait, ToWorld(x, y), OnResize(cb). |
Game.ads | The ad network: OpenStore(), GameComplete(), Finished(). See Playable ads. |
Game.build | Which build this is: profile, network, variant, params, isPlayable. |
Game.physics2D | Raycasts and overlap queries, gravity. |
Subscriptions made through Game end with the run, so there's nothing to clean up.
ts
import { Game, Script, script } from "mmpx";
@script('Mover')
export class Mover extends Script
{
public OnUpdate(dt : number) : void
{
const t = this.entity.transform;
if(Game.input.IsKeyDown('ArrowLeft')) t.x -= 400 * dt;
if(Game.input.IsKeyDown('ArrowRight')) t.x += 400 * dt;
const pointer = Game.input.IsPointerDown() ? Game.input.pointerWorld : null;
if(pointer) t.x = pointer.x;
}
}Talking between scripts
For loose coupling, scripts can send named events anywhere in the game:
ts
import { Script, script } from "mmpx";
@script('ScoreBoard')
export class ScoreBoard extends Script
{
private m_score = 0;
public OnStart() : void
{
this.OnGlobal<number>('score', points => { this.m_score += points; });
}
}
@script('Gem')
export class Gem extends Script
{
public OnClick() : void
{
this.EmitGlobal('score', 10);
this.entity.Destroy();
}
}Listeners added with OnGlobal are removed when the script is destroyed.
Next
- Component reference — every component a script can read and change.
- Scripting API — every export of
mmpx.