Files
GDevelop/Extensions/TileMap/tilemapruntimeobject-pixi-renderer.ts
D8H f76a834284 Optimize simple tile maps with huge dimensions (#8202)
- Huge tile maps are faster to create and modify at runtime
2026-02-10 11:08:02 +01:00

238 lines
7.6 KiB
TypeScript

/// <reference path="helper/TileMapHelper.d.ts" />
/// <reference path="pixi-tilemap/dist/pixi-tilemap.d.ts" />
namespace gdjs {
/**
* The PIXI.js renderer for the Tile map runtime object.
*
* @class TileMapRuntimeObjectPixiRenderer
* @category Renderers > Tile Map
*/
export class TileMapRuntimeObjectPixiRenderer {
private _object:
| gdjs.TileMapRuntimeObject
| gdjs.SimpleTileMapRuntimeObject;
private _pixiObject: PIXI.tilemap.CompositeTilemap;
private _lastCullingLeftBound = 0;
private _lastCullingRightBound = 0;
private _lastCullingTopBound = 0;
private _lastCullingBottomBound = 0;
/**
* @param runtimeObject The object to render
* @param instanceContainer The gdjs.RuntimeScene in which the object is
*/
constructor(
runtimeObject:
| gdjs.TileMapRuntimeObject
| gdjs.SimpleTileMapRuntimeObject,
instanceContainer: gdjs.RuntimeInstanceContainer
) {
this._object = runtimeObject;
// This setting allows tile maps with more than 16K tiles.
PIXI.tilemap.settings.use32bitIndex = true;
// Load (or reset)
this._pixiObject = new PIXI.tilemap.CompositeTilemap();
this._pixiObject.tileAnim = [0, 0];
instanceContainer
.getLayer('')
.getRenderer()
.addRendererObject(this._pixiObject, runtimeObject.getZOrder());
this.updateAngle();
this.updateOpacity();
this.updatePosition();
}
getRendererObject() {
return this._pixiObject;
}
incrementAnimationFrameX(instanceContainer: gdjs.RuntimeInstanceContainer) {
this._pixiObject.tileAnim[0] += 1;
}
updatePosition(): void {
this._pixiObject.pivot.x = this._object.getTileMapWidth() / 2;
this._pixiObject.pivot.y = this._object.getTileMapHeight() / 2;
this._pixiObject.position.x = this._object.x + this.getWidth() / 2;
this._pixiObject.position.y = this._object.y + this.getHeight() / 2;
}
updateAngle(): void {
this._pixiObject.rotation = gdjs.toRad(this._object.angle);
}
updateOpacity(): void {
const newAlpha = this._object._opacity / 255;
if (this._pixiObject.alpha === newAlpha) {
return;
}
this._pixiObject.alpha = newAlpha;
const tileMap = this._object.getTileMap();
if (!tileMap) return;
for (const layer of tileMap.getLayers()) {
const isLayerHidden =
(this._object._displayMode === 'index' &&
this._object._layerIndex !== layer.id) ||
(this._object._displayMode === 'visible' && !layer.isVisible());
// Only set alpha on editable layers that are not hidden,
// as others are not rendered.
if (isLayerHidden) continue;
if (layer instanceof TileMapHelper.EditableTileMapLayer) {
layer.setAlpha(this._pixiObject.alpha);
}
}
// Changing the alpha requires a full re-render of the tile map.
this._object.updateTileMap(true);
}
setWidth(width: float): void {
this._pixiObject.scale.x = width / this._object.getTileMapWidth();
this._pixiObject.position.x = this._object.x + width / 2;
}
setHeight(height: float): void {
this._pixiObject.scale.y = height / this._object.getTileMapHeight();
this._pixiObject.position.y = this._object.y + height / 2;
}
setScaleX(scaleX: float): void {
this._pixiObject.scale.x = scaleX;
const width = scaleX * this._object.getTileMapWidth();
this._pixiObject.position.x = this._object.x + width / 2;
}
setScaleY(scaleY: float): void {
this._pixiObject.scale.y = scaleY;
const height = scaleY * this._object.getTileMapHeight();
this._pixiObject.position.y = this._object.y + height / 2;
}
getWidth(): float {
return this._object.getTileMapWidth() * this._pixiObject.scale.x;
}
getHeight(): float {
return this._object.getTileMapHeight() * this._pixiObject.scale.y;
}
getScaleX(): float {
return this._pixiObject.scale.x;
}
getScaleY(): float {
return this._pixiObject.scale.y;
}
refreshPixiTileMap(
textureCache: TileMapHelper.TileTextureCache,
forceRefresh: boolean
) {
const object = this._object;
const tileMap = object.getTileMap();
if (!tileMap) return;
const dimX = tileMap.getDimensionX();
const dimY = tileMap.getDimensionY();
let leftBound = 0;
let rightBound = dimX;
let topBound = 0;
let bottomBound = dimY;
const instanceContainer = this._object.getInstanceContainer();
const scene = instanceContainer.getScene();
const layerName = this._object.getLayer();
const layer = this._object.getInstanceContainer().getLayer(layerName);
if (
// Don't cull small maps or chunks.
dimX + dimY > 100 &&
// TODO Handle culling for TileMapRuntimeObject.
isSimpleTileMap(object) &&
instanceContainer === scene &&
(!gdjs.scene3d ||
(gdjs.scene3d.camera.getCameraRotationX(scene, layerName, 0) === 0 &&
gdjs.scene3d.camera.getCameraRotationY(scene, layerName, 0) === 0))
) {
const cameraX = layer.getCameraX();
const cameraY = layer.getCameraY();
let cameraHalfWidth = layer.getCameraWidth() / 2;
let cameraHalfHeight = layer.getCameraHeight() / 2;
if (layer.getCameraRotation() !== 0) {
const hypot = cameraHalfWidth + cameraHalfHeight;
cameraHalfWidth = hypot;
cameraHalfHeight = hypot;
}
const [cameraLeftTile, cameraTopTile] =
object.getGridCoordinatesFromSceneCoordinates(
cameraX - cameraHalfWidth,
cameraY - cameraHalfHeight
);
const [cameraRightTile, cameraBottomTile] =
object.getGridCoordinatesFromSceneCoordinates(
cameraX + cameraHalfWidth,
cameraY + cameraHalfHeight
);
leftBound = Math.min(cameraLeftTile, cameraRightTile);
rightBound = Math.max(cameraLeftTile, cameraRightTile) + 1;
topBound = Math.min(cameraTopTile, cameraBottomTile);
bottomBound = Math.max(cameraTopTile, cameraBottomTile) + 1;
}
if (
forceRefresh ||
this._lastCullingLeftBound !== leftBound ||
this._lastCullingRightBound !== rightBound ||
this._lastCullingTopBound !== topBound ||
this._lastCullingBottomBound !== bottomBound
) {
this._lastCullingLeftBound = leftBound;
this._lastCullingRightBound = rightBound;
this._lastCullingTopBound = topBound;
this._lastCullingBottomBound = bottomBound;
TileMapHelper.PixiTileMapHelper.updatePixiTileMap(
this._pixiObject,
tileMap,
textureCache,
// @ts-ignore
this._object._displayMode,
this._object._layerIndex,
leftBound,
rightBound,
topBound,
bottomBound
);
}
}
destroy(): void {
// Keep textures because they are shared by all tile maps.
this._pixiObject.destroy(false);
}
}
function isSimpleTileMap(
object: gdjs.SimpleTileMapRuntimeObject | gdjs.TileMapRuntimeObject
): object is gdjs.SimpleTileMapRuntimeObject {
//@ts-ignore We are checking if the methods are present.
return object.getGridCoordinatesFromSceneCoordinates;
}
/**
* @category Renderers > Tile Map
*/
export const TileMapRuntimeObjectRenderer =
gdjs.TileMapRuntimeObjectPixiRenderer;
/**
* @category Renderers > Tile Map
*/
export type TileMapRuntimeObjectRenderer =
gdjs.TileMapRuntimeObjectPixiRenderer;
}