phaser-jsx
Version:
Use JSX in Phaser.
312 lines (265 loc) • 8.51 kB
text/typescript
import Phaser from 'phaser';
import type { JSX } from 'react';
import { Fragment } from '../components';
import * as GameObjects from '../components/GameObjects';
import { events } from '../constants';
import { isValidElement } from '../element';
import type { GameObjectNode } from '../types';
import { setProps } from './props';
import { attachRef } from './ref';
/**
* Reconciles a new JSX element tree against the existing game object tree.
*
* @param element - The new JSX element to reconcile.
* @param oldNode - The existing game object node (or null if none).
* @param scene - The Phaser scene.
* @param parent - Optional parent container/layer.
* @returns The new game object node tree.
*/
export function reconcileTree(
element: JSX.Element | null,
oldNode: GameObjectNode | null,
scene: Phaser.Scene,
parent?: Phaser.GameObjects.Container | Phaser.GameObjects.Layer,
): GameObjectNode | null {
switch (true) {
case element === undefined:
case element === null:
if (oldNode) {
destroyNode(oldNode);
}
return null;
case Array.isArray(element):
return reconcileArray(element, oldNode?.children ?? null, scene, parent);
case element?.type === Fragment: {
const children = element.props?.children;
return reconcileArray(
children ? toArray(children) : [],
oldNode?.children ?? null,
scene,
parent,
);
}
case !isValidElement(element):
if (oldNode) {
destroyNode(oldNode);
}
return null;
// function component
case typeof element?.type === 'function' && !isGameObject(element.type):
return reconcileTree(element.type(element.props), oldNode, scene, parent);
case isGameObject(element?.type):
default:
return reconcileGameObject(element!, oldNode, scene, parent);
}
}
function reconcileArray(
elements: JSX.Element[],
oldChildren: (GameObjectNode | null)[] | null,
scene: Phaser.Scene,
parent?: Phaser.GameObjects.Container | Phaser.GameObjects.Layer,
): GameObjectNode {
const node: GameObjectNode = {
gameObject: null as unknown as Phaser.GameObjects.GameObject,
props: {},
children: [],
};
const oldLength = oldChildren?.length ?? 0;
for (let i = 0; i < elements.length; i++) {
const oldChild = oldChildren?.[i] ?? null;
const newChild = reconcileTree(elements[i], oldChild, scene, parent);
node.children.push(newChild);
if (oldChild && !newChild) {
destroyNode(oldChild);
}
}
// Destroy any extra old children beyond new length
for (let i = elements.length; i < oldLength; i++) {
const oldChild = oldChildren![i];
if (oldChild) {
destroyNode(oldChild);
}
}
return node;
}
function reconcileGameObject(
element: JSX.Element,
oldNode: GameObjectNode | null,
scene: Phaser.Scene,
parent?: Phaser.GameObjects.Container | Phaser.GameObjects.Layer,
): GameObjectNode | null {
const { children, ref, ...props } = element.props;
let gameObject: Phaser.GameObjects.GameObject | null;
if (oldNode) {
// Reuse existing game object - just patch changed props
gameObject = oldNode.gameObject;
patchProps(gameObject, oldNode.props, props, scene);
attachRef(gameObject, ref);
} else {
// Create new game object
const newGameObject = createGameObject(element, scene);
// Add to scene
if (typeof parent?.add === 'function') {
parent.add(newGameObject);
} else {
scene.add.existing(newGameObject);
}
setProps(newGameObject, props, scene);
attachRef(newGameObject, ref);
gameObject = newGameObject;
}
// Reconcile children for Container/Layer
const node: GameObjectNode = {
gameObject,
props,
children: [],
};
if (
element.type === Phaser.GameObjects.Container ||
element.type === Phaser.GameObjects.Layer
) {
const childArray = children ? toArray(children) : [];
const oldChildren = oldNode?.children ?? null;
const oldLength = oldChildren?.length ?? 0;
for (let i = 0; i < childArray.length; i++) {
const oldChild = oldChildren?.[i] ?? null;
const newChild = reconcileTree(
childArray[i],
oldChild,
scene,
gameObject as Phaser.GameObjects.Container | Phaser.GameObjects.Layer,
);
node.children.push(newChild);
if (oldChild && !newChild) {
destroyNode(oldChild);
}
}
// Destroy extra old children beyond new length
for (let i = childArray.length; i < oldLength; i++) {
const oldChild = oldChildren![i];
if (oldChild) {
destroyNode(oldChild);
}
}
}
return node;
}
function createGameObject(
element: JSX.Element,
scene: Phaser.Scene,
): Phaser.GameObjects.GameObject {
const { props, color, frame, points, shader, style, texture } = element.props;
switch (true) {
case element.type === Phaser.GameObjects.BitmapText:
case element.type === Phaser.GameObjects.DynamicBitmapText:
return new element.type(scene, props?.x, props?.y, props?.font);
case element.type === Phaser.GameObjects.Bob:
return new element.type(scene, props?.x, props?.y, frame, props?.visible);
case element.type === Phaser.GameObjects.Container:
case element.type === Phaser.GameObjects.Layer:
return new element.type(scene);
case element.type === Phaser.GameObjects.GameObject:
return new element.type(scene, props?.type);
case element.type === Phaser.GameObjects.Image:
case element.type === Phaser.GameObjects.Sprite:
case element.type === Phaser.GameObjects.NineSlice:
return new element.type(scene, props?.x, props?.y, texture, frame);
case element.type === Phaser.GameObjects.Light:
return new element.type(
scene,
props?.x,
props?.y,
props?.radius,
color?.r,
color?.g,
color?.b,
props?.intensity,
);
case element.type === Phaser.GameObjects.PathFollower:
return new element.type(
scene,
props?.path,
props?.x,
props?.y,
texture,
frame,
);
case element.type === Phaser.GameObjects.Plane:
return new element.type(
scene,
props?.x,
props?.y,
texture,
frame,
props?.width,
props?.height,
props?.isTiled,
);
case element.type === Phaser.GameObjects.PointLight:
return new element.type(scene, props?.x, props?.y, color);
case element.type === Phaser.GameObjects.Rectangle:
case element.type === Phaser.GameObjects.Zone:
return new element.type(scene, props?.x, props?.y);
case element.type === Phaser.GameObjects.Rope:
return new element.type(
scene,
props?.x,
props?.y,
texture,
frame,
points,
);
case element.type === Phaser.GameObjects.Shader:
return new element.type(scene, shader);
case element.type === Phaser.GameObjects.Text:
return new element.type(scene, props?.x, props?.y, props?.text, style);
case element.type === Phaser.GameObjects.TileSprite:
return new element.type(
scene,
props?.x,
props?.y,
props?.width,
props?.height,
texture,
frame,
);
case element.type === Phaser.GameObjects.Video:
return new element.type(scene, props?.x, props?.y, props?.cacheKey);
default:
return new element.type(scene);
}
}
function patchProps(
gameObject: Phaser.GameObjects.GameObject,
oldProps: Record<string, unknown>,
newProps: Record<string, unknown>,
scene: Phaser.Scene,
): void {
// Remove old event listeners
for (const key in oldProps) {
if (events[key] && typeof oldProps[key] === 'function') {
const eventName = key.slice(2).toLowerCase();
gameObject.off(eventName);
}
}
setProps(gameObject, newProps, scene);
}
function destroyNode(node: GameObjectNode): void {
if (node.gameObject?.active) {
node.gameObject.destroy();
}
for (const child of node.children) {
if (child) {
destroyNode(child);
}
}
}
const gameObjects = Object.keys(GameObjects).map(
(key) => GameObjects[key as keyof typeof GameObjects],
);
function isGameObject(type: unknown): boolean {
return gameObjects.some((gameObject) => gameObject === type);
}
function toArray<Type>(item: Type | Type[]) {
return Array.isArray(item) ? item : [item];
}