Files
GDevelop/Extensions/TileMap/AbstractTileMapRuntimeObject.ts
T

572 lines
17 KiB
TypeScript

/// <reference path="helper/TileMapHelper.d.ts" />
namespace gdjs {
/**
* Displays a Tilemap object (LDtk and Tiled).
* @category Objects > Tile Map
*/
export abstract class AbstractTileMapRuntimeObject
extends gdjs.RuntimeObject
implements gdjs.TileMap
{
/**
* A reusable Point to avoid allocations.
*/
private static readonly workingPoint: FloatPoint = [0, 0];
private _sceneToTileMapTransformation: gdjs.AffineTransformation =
new gdjs.AffineTransformation();
private _tileMapToSceneTransformation: gdjs.AffineTransformation =
new gdjs.AffineTransformation();
private _transformationIsUpToDate: boolean = false;
constructor(
instanceContainer: gdjs.RuntimeInstanceContainer,
objectData: ObjectData,
instanceData?: InstanceData
) {
super(instanceContainer, objectData, instanceData);
}
abstract getTileMap(): TileMapHelper.EditableTileMap | null;
/**
* Return a tile map that is safe to modify.
*/
abstract getTileMapForEdition(): TileMapHelper.EditableTileMap | null;
abstract getCollisionTileMap(): gdjs.TileMap.TransformedCollisionTileMap | null;
/**
* Return the layer that is used to edit and read tiles.
*/
abstract getEditedLayerIndex(): integer;
abstract getCollisionMaskTag(): string;
abstract getScaleX(): float;
abstract getScaleY(): float;
abstract invalidateTileMap(): void;
abstract getTileSetColumnCount(): integer;
abstract getTileSetRowCount(): integer;
/**
* This method is expensive and should not be called.
* Prefer using {@link getHitBoxesAround} rather than getHitBoxes.
*/
override getHitBoxes(): gdjs.Polygon[] {
return super.getHitBoxes();
}
override updateHitBoxes(): void {
this.updateTransformation();
const collisionTileMap = this.getCollisionTileMap();
if (!collisionTileMap) return;
this.hitBoxes = Array.from(
collisionTileMap.getAllHitboxes(this.getCollisionMaskTag())
);
this.hitBoxesDirty = false;
this.updateAABB();
}
// This implementation doesn't use updateHitBoxes.
// It's important for good performances.
override getAABB(): AABB {
// It's fine to compute it every time because tile maps are rarely rotated.
// It avoids calling updateHitBoxes to rely on hitBoxesDirty to know when
// to update.
this.updateAABB();
return this.aabb;
}
// This implementation doesn't use updateHitBoxes.
// It's important for good performances.
override updateAABB(): void {
if (this.getAngle() === 0) {
// Fast computation of AABB for non rotated object
this.aabb.min[0] = this.x;
this.aabb.min[1] = this.y;
this.aabb.max[0] = this.aabb.min[0] + this.getWidth();
this.aabb.max[1] = this.aabb.min[1] + this.getHeight();
} else {
const collisionTileMap = this.getCollisionTileMap();
if (!collisionTileMap) return;
const affineTransformation = collisionTileMap.getTransformation();
const left = 0;
const right = collisionTileMap.getWidth();
const top = 0;
const bottom = collisionTileMap.getHeight();
const workingPoint = this.aabb.min;
workingPoint[0] = left;
workingPoint[1] = top;
affineTransformation.transform(workingPoint, workingPoint);
const topLeftX = workingPoint[0];
const topLeftY = workingPoint[1];
workingPoint[0] = right;
workingPoint[1] = top;
affineTransformation.transform(workingPoint, workingPoint);
const topRightX = workingPoint[0];
const topRightY = workingPoint[1];
workingPoint[0] = right;
workingPoint[1] = bottom;
affineTransformation.transform(workingPoint, workingPoint);
const bottomRightX = workingPoint[0];
const bottomRightY = workingPoint[1];
workingPoint[0] = left;
workingPoint[1] = bottom;
affineTransformation.transform(workingPoint, workingPoint);
const bottomLeftX = workingPoint[0];
const bottomLeftY = workingPoint[1];
this.aabb.min[0] = Math.min(
topLeftX,
topRightX,
bottomRightX,
bottomLeftX
);
this.aabb.max[0] = Math.max(
topLeftX,
topRightX,
bottomRightX,
bottomLeftX
);
this.aabb.min[1] = Math.min(
topLeftY,
topRightY,
bottomRightY,
bottomLeftY
);
this.aabb.max[1] = Math.max(
topLeftY,
topRightY,
bottomRightY,
bottomLeftY
);
}
}
getHitBoxesAround(
left: float,
top: float,
right: float,
bottom: float
): Iterable<gdjs.Polygon> {
// This implementation doesn't call updateHitBoxes.
// It's important for good performances because there is no need to
// update the whole collision mask where only a few hitboxes must be
// checked.
this.updateTransformation();
const collisionTileMap = this.getCollisionTileMap();
if (!collisionTileMap) {
return [];
}
return collisionTileMap.getHitboxesAround(
this.getCollisionMaskTag(),
left,
top,
right,
bottom
);
}
override isSpatiallyIndexed(): boolean {
return true;
}
invalidateTransformation(): void {
this._transformationIsUpToDate = false;
}
updateTransformation() {
if (this._transformationIsUpToDate) {
return;
}
this._tileMapToSceneTransformation.setToIdentity();
this._tileMapToSceneTransformation.translate(this.getX(), this.getY());
this._tileMapToSceneTransformation.rotateAround(
(this.getAngle() * Math.PI) / 180,
this.getCenterX(),
this.getCenterY()
);
this._tileMapToSceneTransformation.scale(
Math.abs(this.getScaleX()),
Math.abs(this.getScaleY())
);
const collisionTileMap = this.getCollisionTileMap();
if (collisionTileMap) {
const collisionTileMapTransformation =
collisionTileMap.getTransformation();
collisionTileMapTransformation.copyFrom(
this._tileMapToSceneTransformation
);
collisionTileMap.setTransformation(collisionTileMapTransformation);
}
this._sceneToTileMapTransformation.copyFrom(
this._tileMapToSceneTransformation
);
this._sceneToTileMapTransformation.invert();
this._transformationIsUpToDate = true;
}
/**
* The returned array is alway the same.
*/
getGridCoordinatesFromSceneCoordinates(
x: float,
y: float
): [integer, integer] {
this.updateTransformation();
const result = AbstractTileMapRuntimeObject.workingPoint;
const tileMap = this.getTileMap();
if (!tileMap) {
result[0] = 0;
result[1] = 0;
return result;
}
this._sceneToTileMapTransformation.transform([x, y], result);
result[0] = Math.floor(result[0] / tileMap.getTileWidth());
result[1] = Math.floor(result[1] / tileMap.getTileHeight());
return result;
}
getGridRowCount(): integer {
const tileMap = this.getTileMap();
return tileMap ? tileMap.getDimensionY() : 0;
}
getGridColumnCount(): integer {
const tileMap = this.getTileMap();
return tileMap ? tileMap.getDimensionX() : 0;
}
/**
* @param x The layer column.
* @param y The layer row.
* @param layerIndex The layer index.
* @returns The tile's id.
*/
private getTileId(x: integer, y: integer, layerIndex: integer): integer {
const tileMap = this.getTileMap();
if (!tileMap) return -1;
return tileMap.getTileId(x, y, layerIndex);
}
/**
* @param x The layer column.
* @param y The layer row.
* @param layerIndex The layer index.
*/
private isTileFlippedOnX(
x: integer,
y: integer,
layerIndex: integer
): boolean {
const tileMap = this.getTileMap();
if (!tileMap) return false;
return tileMap.isTileFlippedOnX(x, y, layerIndex);
}
/**
* @param x The layer column.
* @param y The layer row.
* @param layerIndex The layer index.
*/
private isTileFlippedOnY(
x: integer,
y: integer,
layerIndex: integer
): boolean {
const tileMap = this.getTileMap();
if (!tileMap) return false;
return tileMap.isTileFlippedOnY(x, y, layerIndex);
}
getSceneXCoordinateOfTileCenter(
columnIndex: integer,
rowIndex: integer
): float {
const tileMap = this.getTileMap();
if (!tileMap) {
return 0;
}
const sceneCoordinates: FloatPoint =
AbstractTileMapRuntimeObject.workingPoint;
this._tileMapToSceneTransformation.transform(
[
(columnIndex + 0.5) * tileMap.getTileWidth(),
(rowIndex + 0.5) * tileMap.getTileHeight(),
],
sceneCoordinates
);
return sceneCoordinates[0];
}
getSceneYCoordinateOfTileCenter(
columnIndex: integer,
rowIndex: integer
): float {
const tileMap = this.getTileMap();
if (!tileMap) {
return 0;
}
const sceneCoordinates: FloatPoint =
AbstractTileMapRuntimeObject.workingPoint;
this._tileMapToSceneTransformation.transform(
[
(columnIndex + 0.5) * tileMap.getTileWidth(),
(rowIndex + 0.5) * tileMap.getTileHeight(),
],
sceneCoordinates
);
return sceneCoordinates[1];
}
getColumnIndexAtPosition(x: float, y: float): integer {
return this.getGridCoordinatesFromSceneCoordinates(x, y)[0];
}
getRowIndexAtPosition(x: float, y: float): integer {
return this.getGridCoordinatesFromSceneCoordinates(x, y)[1];
}
getTileAtPosition(x: float, y: float): integer {
const [columnIndex, rowIndex] =
this.getGridCoordinatesFromSceneCoordinates(x, y);
return this.getTileAtGridCoordinates(columnIndex, rowIndex);
}
getTileAtGridCoordinates(columnIndex: integer, rowIndex: integer): integer {
return this.getTileId(columnIndex, rowIndex, this.getEditedLayerIndex());
}
setTileAtPosition(tileId: number, x: float, y: float) {
const [columnIndex, rowIndex] =
this.getGridCoordinatesFromSceneCoordinates(x, y);
this.setTileAtGridCoordinates(tileId, columnIndex, rowIndex);
}
setTileAtGridCoordinates(
tileId: number,
columnIndex: integer,
rowIndex: integer
) {
const tileMap = this.getTileMapForEdition();
if (!tileMap) {
return;
}
const layer = tileMap.getTileLayer(this.getEditedLayerIndex());
if (!layer) {
return;
}
const oldTileId = layer.getTileId(columnIndex, rowIndex);
if (tileId === oldTileId) {
return;
}
layer.setTile(columnIndex, rowIndex, tileId);
const collisionTileMap = this.getCollisionTileMap();
if (collisionTileMap) {
const oldTileDefinition =
oldTileId !== undefined && tileMap.getTileDefinition(oldTileId);
const newTileDefinition = tileMap.getTileDefinition(tileId);
const hadFullHitBox =
!!oldTileDefinition &&
oldTileDefinition.hasFullHitBox(this.getCollisionMaskTag());
const haveFullHitBox =
!!newTileDefinition &&
newTileDefinition.hasFullHitBox(this.getCollisionMaskTag());
if (hadFullHitBox !== haveFullHitBox) {
collisionTileMap.invalidateTile(
this.getEditedLayerIndex(),
columnIndex,
rowIndex
);
}
}
this.invalidateTileMap();
}
flipTileOnYAtPosition(x: float, y: float, flip: boolean) {
const [columnIndex, rowIndex] =
this.getGridCoordinatesFromSceneCoordinates(x, y);
this.flipTileOnYAtGridCoordinates(columnIndex, rowIndex, flip);
}
flipTileOnXAtPosition(x: float, y: float, flip: boolean) {
const [columnIndex, rowIndex] =
this.getGridCoordinatesFromSceneCoordinates(x, y);
this.flipTileOnXAtGridCoordinates(columnIndex, rowIndex, flip);
}
flipTileOnYAtGridCoordinates(
columnIndex: integer,
rowIndex: integer,
flip: boolean
) {
this.flipTileOnY(columnIndex, rowIndex, this.getEditedLayerIndex(), flip);
this.invalidateTileMap();
// No need to invalidate hit boxes since at the moment, collision mask
// cannot be configured on each tile.
}
flipTileOnXAtGridCoordinates(
columnIndex: integer,
rowIndex: integer,
flip: boolean
) {
this.flipTileOnX(columnIndex, rowIndex, this.getEditedLayerIndex(), flip);
this.invalidateTileMap();
// No need to invalidate hit boxes since at the moment, collision mask
// cannot be configured on each tile.
}
/**
* @param x The layer column.
* @param y The layer row.
* @param layerIndex The layer index.
* @param flip true if the tile should be flipped.
*/
private flipTileOnY(
x: integer,
y: integer,
layerIndex: integer,
flip: boolean
) {
const tileMap = this.getTileMapForEdition();
if (!tileMap) return;
tileMap.flipTileOnY(x, y, layerIndex, flip);
}
/**
* @param x The layer column.
* @param y The layer row.
* @param layerIndex The layer index.
* @param flip true if the tile should be flipped.
*/
private flipTileOnX(
x: integer,
y: integer,
layerIndex: integer,
flip: boolean
) {
const tileMap = this.getTileMapForEdition();
if (!tileMap) return;
tileMap.flipTileOnX(x, y, layerIndex, flip);
}
isTileFlippedOnXAtPosition(x: float, y: float) {
const [columnIndex, rowIndex] =
this.getGridCoordinatesFromSceneCoordinates(x, y);
return this.isTileFlippedOnX(
columnIndex,
rowIndex,
this.getEditedLayerIndex()
);
}
isTileFlippedOnXAtGridCoordinates(columnIndex: integer, rowIndex: integer) {
return this.isTileFlippedOnX(
columnIndex,
rowIndex,
this.getEditedLayerIndex()
);
}
isTileFlippedOnYAtPosition(x: float, y: float) {
const [columnIndex, rowIndex] =
this.getGridCoordinatesFromSceneCoordinates(x, y);
return this.isTileFlippedOnY(
columnIndex,
rowIndex,
this.getEditedLayerIndex()
);
}
isTileFlippedOnYAtGridCoordinates(columnIndex: integer, rowIndex: integer) {
return this.isTileFlippedOnY(
columnIndex,
rowIndex,
this.getEditedLayerIndex()
);
}
removeTileAtPosition(x: float, y: float) {
const [columnIndex, rowIndex] =
this.getGridCoordinatesFromSceneCoordinates(x, y);
this.removeTileAtGridCoordinates(columnIndex, rowIndex);
}
removeTileAtGridCoordinates(columnIndex: integer, rowIndex: integer) {
const tileMap = this.getTileMapForEdition();
if (!tileMap) {
return;
}
const layer = tileMap.getTileLayer(this.getEditedLayerIndex());
if (!layer) {
return;
}
const oldTileId = layer.getTileId(columnIndex, rowIndex);
if (oldTileId === undefined) {
return;
}
layer.removeTile(columnIndex, rowIndex);
const collisionTileMap = this.getCollisionTileMap();
if (collisionTileMap) {
const oldTileDefinition =
oldTileId !== undefined && tileMap.getTileDefinition(oldTileId);
const hadFullHitBox =
!!oldTileDefinition &&
oldTileDefinition.hasFullHitBox(this.getCollisionMaskTag());
if (hadFullHitBox) {
collisionTileMap.invalidateTile(
this.getEditedLayerIndex(),
columnIndex,
rowIndex
);
}
}
this.invalidateTileMap();
}
setGridRowCount(targetRowCount: integer) {
const tileMap = this.getTileMapForEdition();
if (targetRowCount <= 0) return;
if (!tileMap) return;
tileMap.setDimensionY(targetRowCount);
const collisionTileMap = this.getCollisionTileMap();
if (collisionTileMap) {
collisionTileMap.updateDimensions();
}
this.invalidateTileMap();
this.invalidateHitboxes();
}
setGridColumnCount(targetColumnCount: integer) {
const tileMap = this.getTileMapForEdition();
if (targetColumnCount <= 0) return;
if (!tileMap) return;
tileMap.setDimensionX(targetColumnCount);
const collisionTileMap = this.getCollisionTileMap();
if (collisionTileMap) {
collisionTileMap.updateDimensions();
}
this.invalidateTileMap();
this.invalidateHitboxes();
}
}
}