← Back to Blog

May 6, 2026 · 10 min read

Porting ActionScript 3 Games to Phaser.js

Porting ActionScript 3 Games to Phaser.js

Adobe Flash Player reached end-of-life on December 31, 2020. Every browser blocked it by January 2021. Billions of SWF files went dark.

I have a handful of games I wrote in college for Full Sail's Flash ActionScript Techniques course. They've been sitting as .fla files on a drive for fifteen years. I'm working through porting them to Phaser.js one by one. Three are ports of the original AS3 games: a spider clicker called Creepy Spiders, an Asteroids-style space shooter called Solar Siege, and a carnival duck shooter called Shooting Gallery. The fourth — Arbalest — The Bridge — is something different: an original Phaser game built as a homage to Exidy's 1983 arcade classic Crossbow. The process turned out to be a good exercise in understanding what Flash actually was under the hood.

The Mental Model

ActionScript 3 and Phaser 3 share a lineage: both are built on the same game loop fundamentals, and Phaser was explicitly designed to fill the gap Flash left. The concepts map almost one-to-one, just with different names.

ActionScript 3Phaser 3
StageScene
Sprite / MovieClipPhaser.GameObjects.Sprite / Image
GraphicsPhaser.GameObjects.Graphics
ENTER_FRAME eventupdate() method
hitTestObject()physics.add.overlap()
stage.stageWidththis.scale.width
Sound classWeb Audio API (via this.sound)
Keyboard.isDown()cursors.left.isDown

The biggest shift isn't API surface. It's the move from a class hierarchy you extend to a scene-based system you compose.

Scenes Replace the Stage

In AS3, everything lived on a single Stage. You managed depth manually, added children with addChild(), and typically had one big ENTER_FRAME handler coordinating everything.

Phaser organizes code into Scene classes. Each scene has its own create() (setup), update() (loop), and lifecycle. For a simple arcade game this maps cleanly: one scene for the title screen, one for gameplay, one for game over, or just one scene for everything if the game is small.

class GameScene extends Phaser.Scene {
  constructor() {
    super({ key: 'Game' })
  }

  create() {
    // runs once when scene starts
  }

  update() {
    // runs every frame
  }
}

The AS3 equivalent was a Sprite or MovieClip subclass with addEventListener(Event.ENTER_FRAME, onUpdate). Same idea, different spelling.

Drawing: Graphics API

This one translates almost verbatim. AS3's Graphics class and Phaser's Graphics object use the same canvas-style API.

AS3:

var g:Graphics = sprite.graphics;
g.beginFill(0xff0000);
g.drawCircle(0, 0, 20);
g.endFill();

Phaser:

const g = this.add.graphics()
g.fillStyle(0xff0000, 1)
g.fillCircle(0, 0, 20)

One useful Phaser trick: you can render to a texture with g.generateTexture('key', width, height), then reuse that texture for sprites. That's how I drew all eight spider species. Each one is a programmatic texture generated once at scene start, then instantiated as lightweight Image objects.

Physics and Collision

AS3 had no built-in physics. You either rolled your own or used a library like Box2D or Nape. Collision detection was typically hitTestObject() (a bounding-box check) or manual distance math for circles.

Phaser ships with Arcade Physics, a fast AABB + circle system built for arcade games. Enabling it and setting up overlap detection takes a few lines:

// In create():
this.physics.add.overlap(
  this.enemyBullets,
  this.player,
  (obj1, obj2) => {
    // obj1 and obj2 are the two overlapping objects
    obj2.destroy()
    this.damagePlayer()
  }
)

One thing that bit me: the argument order in the overlap callback depends on which argument is a group and which is a single sprite. If you call overlap(group, sprite, cb), Phaser passes (sprite, groupMember) to the callback, not (groupMember, sprite) as you might expect. I lost an afternoon to this. The safe pattern is to always compare both arguments against the known object:

(obj1, obj2) => {
  const bullet = (obj1 as unknown) === this.player ? obj2 : obj1
  bullet.destroy()
}

Input

AS3 keyboard input was event-driven or polled via flash.ui.Keyboard:

if (Keyboard.isDown(Keyboard.UP)) {
  // thrust
}

Phaser uses a cursor key helper or you can create custom keys:

// In create():
const cursors = this.input.keyboard!.createCursorKeys()

// In update():
if (cursors.up.isDown) {
  // thrust
}

Same mental model, cleaner API.

Sound

This is where the port gets interesting. AS3's Sound class was simple: load an MP3, call play(). Phaser has a sound manager, but I skipped it entirely and wrote sounds with the Web Audio API directly. For a retro arcade game, synthesized sound is more appropriate than loaded audio files, and the Web Audio API gives you precise control over oscillators, envelopes, and filters.

A synthesized shoot sound:

private sndShoot() {
  const ctx = (this.sound as any).context as AudioContext
  const t = ctx.currentTime

  const osc = ctx.createOscillator()
  const gain = ctx.createGain()
  osc.type = 'square'
  osc.frequency.setValueAtTime(880, t)
  osc.frequency.exponentialRampToValueAtTime(110, t + 0.12)
  gain.gain.setValueAtTime(0.18, t)
  gain.gain.exponentialRampToValueAtTime(0.001, t + 0.12)
  osc.connect(gain)
  gain.connect(ctx.destination)
  osc.start(t)
  osc.stop(t + 0.12)
}

One important detail: browsers require a user gesture before any AudioContext can play. Phaser's sound manager handles this internally. You can hook into it:

if (this.sound.locked) {
  this.sound.once('unlocked', () => this.startMusic())
} else {
  this.startMusic()
}

Textures vs. Display Lists

In AS3, you had a display list: a tree of display objects you added and removed from the stage or from container sprites. Object identity and display hierarchy were the same thing.

Phaser separates texture management from scene objects. A texture is a GPU resource identified by a string key. Game objects reference textures by key. This means you can have many lightweight sprites all sharing the same underlying texture, which is how the particle systems in Phaser work efficiently.

For the spider game, this distinction helped: I generate one texture per spider species, then spawn dozens of Image instances reusing those textures. In AS3 I probably would have instantiated full MovieClip objects, each carrying their own display list overhead.

Arbalest — Building an Original Game in Phaser

The three AS3 ports were translation exercises. Arbalest was a design exercise: no source material to reference, just Exidy's 1983 Crossbow as a reference point, and Phaser as the toolbox.

Crossbow was a light-gun game. A party of adventurers crosses dangerous terrain and you protect them by shooting threats — falling boulders, enemy archers, snakes, and worse. The 1983 cabinet used a crossbow-shaped gun peripheral and tracked where players aimed on screen. For the browser version, the mouse does the same job.

No Assets — Pure Graphics API

Every visual in Arbalest is drawn with fillRect, fillCircle, and fillTriangle. No image files, no sprite sheets, no texture atlases. The castle silhouette, the pixel-art bridge, the gorilla in the tower window, the moat creatures rising out of the water — all procedural geometry.

// All eight spider species generated the same way
const g = this.add.graphics()
g.fillStyle(0xcc3333, 1)
g.fillRect(cx - 4, cy - 6, 8, 12)   // body
g.fillCircle(cx, cy - 9, 5)          // head
g.generateTexture('spider_red', 24, 24)
g.destroy()

For Arbalest, the same approach builds the whole world: the night sky gradient, stars placed by fillCircle, a full moon with crater detail, a multi-layered castle with arched tower windows and a flickering amber glow, pixel-art water with zigzag wave rows, and a fog layer drawn at depth 3 so it composites over the moat and under the bridge deck.

The depth system is critical. Phaser's scene graph uses explicit setDepth() values rather than AS3's z-order. Every layer in Arbalest has a fixed depth:

DepthLayer
0Sky, stars, moon, castle silhouette
1Secondary background towers
2Moat creatures
3Fog
4Bridge deck and rails
5Friends, goblins
8Bats
9Player arrows and enemy projectiles
20HUD

Custom Collision — No Arcade Physics

Phaser's Arcade Physics is designed for physics bodies attached to sprites. Arbalest doesn't use sprites — it uses Graphics objects. Arcade Physics can't track them.

Instead, every entity carries its own position and a hit radius. Collision is a single distance function checked against all live entity pairs every frame:

private dist(ax: number, ay: number, bx: number, by: number): number {
  const dx = ax - bx, dy = ay - by
  return Math.sqrt(dx * dx + dy * dy)
}

// In checkCollisions():
if (this.dist(arrow.x, arrow.y, gorilla.x, gorilla.y) < HIT_R_GORILLA) {
  this.killGorilla(gorilla)
}

This is exactly what AS3 developers did before hitTestObject became common. The brute-force O(n²) check is fine for arcade scale — Arbalest has at most thirty entities in flight simultaneously.

Web Audio Synthesis

Every sound is synthesized with the Web Audio API. There are no audio files. The gorilla death sound uses a filtered noise burst with a sharp attack and long decay. The boulder hit uses a low triangle wave with pitch fall. The fanfare on stage clear plays a four-note ascending sequence timed with ctx.currentTime offsets.

private sndGorilla() {
  const ctx = (this.sound as any).context as AudioContext
  const t = ctx.currentTime
  const osc = ctx.createOscillator()
  const gain = ctx.createGain()
  osc.type = 'sawtooth'
  osc.frequency.setValueAtTime(180, t)
  osc.frequency.exponentialRampToValueAtTime(40, t + 0.5)
  gain.gain.setValueAtTime(0.25, t)
  gain.gain.exponentialRampToValueAtTime(0.001, t + 0.5)
  osc.connect(gain)
  gain.connect(ctx.destination)
  osc.start(t); osc.stop(t + 0.5)
}

The ambient background sound layers three oscillators: a low rumble, a mid drone, and a high shimmer, all at low gain. The result reads as ominous castle atmosphere rather than recognizable music.

Moat Creature Lifecycle

The moat has four creatures: a watcher, a tentacle, a serpent, and a pair of deep eyes. Each has a randomized surface/hide cycle — they rise from the water, linger, then submerge, and repeat on a random delay.

Each creature's position is tracked in a MoatCreatureState object. When a creature is shot, it plays a sink tween, sets alive = false to stop the cycle, then respawns after a delay by calling a stored revive callback.

The key insight: Phaser tweens animate gfx.y directly on the Graphics object, but the creature's world-space Y for collision needs to account for that offset:

// World-space Y = base cy + current tween offset
const worldY = mc.cy + mc.gfx.y
if (this.dist(arrow.x, arrow.y, mc.cx, worldY) < mc.hr) {
  // hit
}

Miss that offset and shots visually hit but register as misses — a subtle but confidence-breaking bug.

The Trade-offs

Phaser runs in any browser tab without a plugin, uses WebGL by default, and has active development and a real TypeScript type library. AS3 had software rendering and tooling frozen in 2020.

What doesn't transfer: Flash Professional's visual timeline. For frame-by-frame animation, cutscenes, or anything tied to a timeline it was genuinely good. There's no Phaser equivalent. Asset management is also manual in Phaser; Flash had a built-in library panel. And Ctrl+Enter in the Flash IDE compiled and launched instantly. Tighter than anything I've used since, including Vite HMR.

For arcade games with programmatic graphics the trade-off is easy. For anything animation-heavy it's a real loss.

The Takeaway

The port was fast because the conceptual distance between AS3 and Phaser is small. Same game loop, same drawing model, same input pattern. The hard parts were the details: collision callback argument order, AudioContext unlock timing, the split between scene lifecycle and game object lifecycle.

The games work again. They run in the browser, on mobile, without any setup. The three ports proved the AS3-to-Phaser translation is mechanical. Arbalest proved you can build a full arcade game in Phaser with zero assets — just the Graphics API, the Web Audio API, and a clear picture of what you're making. I'll keep adding to the /games section as I work through the backlog. Source for all four games is in the portfolio repository.