mirror of
https://github.com/4ian/GDevelop.git
synced 2025-10-15 10:19:04 +00:00
Compare commits
4 Commits
v5.4.218
...
culling-op
Author | SHA1 | Date | |
---|---|---|---|
![]() |
8a55078119 | ||
![]() |
4417d7e958 | ||
![]() |
79cd327c68 | ||
![]() |
c3580fd207 |
@@ -26,11 +26,12 @@ namespace gdjs {
|
||||
);
|
||||
};
|
||||
|
||||
class TextInputRuntimeObjectPixiRenderer {
|
||||
class TextInputRuntimeObjectPixiRenderer implements RendererObjectInterface {
|
||||
private _object: gdjs.TextInputRuntimeObject;
|
||||
private _input: HTMLInputElement | HTMLTextAreaElement | null = null;
|
||||
private _instanceContainer: gdjs.RuntimeInstanceContainer;
|
||||
private _runtimeGame: gdjs.RuntimeGame;
|
||||
private _isVisible = false;
|
||||
|
||||
constructor(
|
||||
runtimeObject: gdjs.TextInputRuntimeObject,
|
||||
@@ -113,14 +114,25 @@ namespace gdjs {
|
||||
this._destroyElement();
|
||||
}
|
||||
|
||||
//@ts-ignore
|
||||
set visible(isVisible: boolean) {
|
||||
this._isVisible = isVisible;
|
||||
if (!this._input) return;
|
||||
this._input.style.display = isVisible ? 'initial' : 'none';
|
||||
}
|
||||
|
||||
//@ts-ignore
|
||||
get visible(): boolean {
|
||||
return this._isVisible;
|
||||
}
|
||||
|
||||
updatePreRender() {
|
||||
if (!this._input) return;
|
||||
|
||||
// Hide the input entirely if the object is hidden.
|
||||
// Because this object is rendered as a DOM element (and not part of the PixiJS
|
||||
// scene graph), we have to do this manually.
|
||||
if (this._object.isHidden()) {
|
||||
this._input.style.display = 'none';
|
||||
if (!this._isVisible) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
@@ -102,7 +102,8 @@ namespace gdjs {
|
||||
}
|
||||
|
||||
getRendererObject() {
|
||||
return null;
|
||||
// The renderer is not a Pixi Object but it implements visible.
|
||||
return this._renderer;
|
||||
}
|
||||
|
||||
updateFromObjectData(
|
||||
|
@@ -670,6 +670,8 @@ void ExporterHelper::AddLibsInclude(bool pixiRenderers,
|
||||
InsertUnique(includesFiles, "ResourceCache.js");
|
||||
InsertUnique(includesFiles, "timemanager.js");
|
||||
InsertUnique(includesFiles, "polygon.js");
|
||||
InsertUnique(includesFiles, "ObjectSleepState.js");
|
||||
InsertUnique(includesFiles, "ObjectManager.js");
|
||||
InsertUnique(includesFiles, "runtimeobject.js");
|
||||
InsertUnique(includesFiles, "profiler.js");
|
||||
InsertUnique(includesFiles, "RuntimeInstanceContainer.js");
|
||||
|
122
GDJS/Runtime/ObjectManager.ts
Normal file
122
GDJS/Runtime/ObjectManager.ts
Normal file
@@ -0,0 +1,122 @@
|
||||
namespace gdjs {
|
||||
/**
|
||||
* Allow to do spacial searches on objects as fast as possible.
|
||||
*
|
||||
* Objects are put in an R-Tree only if they didn't move recently to avoid to
|
||||
* update the R-Tree too often.
|
||||
*/
|
||||
export class ObjectManager {
|
||||
private _allInstances: Array<RuntimeObject> = [];
|
||||
private _awakeInstances: Array<RuntimeObject> = [];
|
||||
private _rbush: RBush<RuntimeObject>;
|
||||
|
||||
constructor() {
|
||||
this._rbush = new RBush<RuntimeObject>();
|
||||
}
|
||||
|
||||
_destroy(): void {
|
||||
this._allInstances = [];
|
||||
this._awakeInstances = [];
|
||||
this._rbush.clear();
|
||||
}
|
||||
|
||||
search(
|
||||
searchArea: SearchArea,
|
||||
results: Array<RuntimeObject>
|
||||
): Array<RuntimeObject> {
|
||||
let instances = this._allInstances;
|
||||
if (instances.length >= 8) {
|
||||
this._rbush.search(searchArea, results);
|
||||
instances = this._awakeInstances;
|
||||
}
|
||||
for (const instance of instances) {
|
||||
// TODO Allow to use getAABB to optimize collision conditions
|
||||
const aabb = instance.getVisibilityAABB();
|
||||
if (
|
||||
!aabb ||
|
||||
(aabb.min[0] <= searchArea.maxX &&
|
||||
aabb.max[0] >= searchArea.minX &&
|
||||
aabb.min[1] <= searchArea.maxY &&
|
||||
aabb.max[1] >= searchArea.minY)
|
||||
) {
|
||||
results.push(instance);
|
||||
}
|
||||
}
|
||||
return results;
|
||||
}
|
||||
|
||||
private _onWakingUp(object: RuntimeObject): void {
|
||||
this._rbush.remove(object._rtreeAABB);
|
||||
this._awakeInstances.push(object);
|
||||
}
|
||||
|
||||
private _onFallenAsleep(object: RuntimeObject): void {
|
||||
// TODO Allow to use getAABB to optimize collision conditions
|
||||
const objectAABB = object.getVisibilityAABB();
|
||||
if (!objectAABB) {
|
||||
return;
|
||||
}
|
||||
this._rbush.remove(object._rtreeAABB);
|
||||
object._rtreeAABB.minX = objectAABB.min[0];
|
||||
object._rtreeAABB.minY = objectAABB.min[1];
|
||||
object._rtreeAABB.maxX = objectAABB.max[0];
|
||||
object._rtreeAABB.maxY = objectAABB.max[1];
|
||||
this._rbush.insert(object._rtreeAABB);
|
||||
}
|
||||
|
||||
updateAwakeObjects(): void {
|
||||
gdjs.ObjectSleepState.updateAwakeObjects(
|
||||
this._awakeInstances,
|
||||
(object) => object.getSpatialSearchSleepState(),
|
||||
(object) => this._onFallenAsleep(object),
|
||||
(object) => this._onWakingUp(object)
|
||||
);
|
||||
}
|
||||
|
||||
getAllInstances(): Array<RuntimeObject> {
|
||||
return this._allInstances;
|
||||
}
|
||||
|
||||
getAwakeInstances(): Array<RuntimeObject> {
|
||||
return this._awakeInstances;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add an object to the instances living in the container.
|
||||
* @param obj The object to be added.
|
||||
*/
|
||||
addObject(object: gdjs.RuntimeObject): void {
|
||||
this._allInstances.push(object);
|
||||
this._awakeInstances.push(object);
|
||||
}
|
||||
|
||||
/**
|
||||
* Must be called whenever an object must be removed from the container.
|
||||
* @param object The object to be removed.
|
||||
*/
|
||||
deleteObject(object: gdjs.RuntimeObject): boolean {
|
||||
const objId = object.id;
|
||||
let isObjectDeleted = false;
|
||||
for (let i = 0, len = this._allInstances.length; i < len; ++i) {
|
||||
if (this._allInstances[i].id == objId) {
|
||||
this._allInstances.splice(i, 1);
|
||||
isObjectDeleted = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
// TODO Maybe the state could be used but it would be more prone to errors.
|
||||
let isAwake = false;
|
||||
for (let i = 0, len = this._awakeInstances.length; i < len; ++i) {
|
||||
if (this._awakeInstances[i].id == objId) {
|
||||
this._awakeInstances.splice(i, 1);
|
||||
isAwake = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!isAwake) {
|
||||
this._rbush.remove(object._rtreeAABB);
|
||||
}
|
||||
return isObjectDeleted;
|
||||
}
|
||||
}
|
||||
}
|
112
GDJS/Runtime/ObjectSleepState.ts
Normal file
112
GDJS/Runtime/ObjectSleepState.ts
Normal file
@@ -0,0 +1,112 @@
|
||||
/*
|
||||
* GDevelop JS Platform
|
||||
* Copyright 2023-2023 Florian Rival (Florian.Rival@gmail.com). All rights reserved.
|
||||
* This project is released under the MIT License.
|
||||
*/
|
||||
namespace gdjs {
|
||||
export class ObjectSleepState {
|
||||
private static readonly framesBeforeSleep = 60;
|
||||
private _object: RuntimeObject;
|
||||
private _isNeedingToBeAwake: () => boolean;
|
||||
private _state: ObjectSleepState.State;
|
||||
private _lastActivityFrameIndex: integer;
|
||||
private _onWakingUpCallbacks: Array<(object: RuntimeObject) => void> = [];
|
||||
|
||||
constructor(
|
||||
object: RuntimeObject,
|
||||
isNeedingToBeAwake: () => boolean,
|
||||
initialSleepState: ObjectSleepState.State
|
||||
) {
|
||||
this._object = object;
|
||||
this._isNeedingToBeAwake = isNeedingToBeAwake;
|
||||
this._state = initialSleepState;
|
||||
this._lastActivityFrameIndex = this._object
|
||||
.getRuntimeScene()
|
||||
.getFrameIndex();
|
||||
}
|
||||
|
||||
canSleep(): boolean {
|
||||
return (
|
||||
this._state === gdjs.ObjectSleepState.State.CanSleepThisFrame ||
|
||||
this._object.getRuntimeScene().getFrameIndex() -
|
||||
this._lastActivityFrameIndex >=
|
||||
ObjectSleepState.framesBeforeSleep
|
||||
);
|
||||
}
|
||||
|
||||
isAwake(): boolean {
|
||||
return this._state !== gdjs.ObjectSleepState.State.ASleep;
|
||||
}
|
||||
|
||||
_forceToSleep(): void {
|
||||
if (!this.isAwake()) {
|
||||
return;
|
||||
}
|
||||
this._lastActivityFrameIndex = Number.NEGATIVE_INFINITY;
|
||||
}
|
||||
|
||||
wakeUp() {
|
||||
const object = this._object;
|
||||
this._lastActivityFrameIndex = object.getRuntimeScene().getFrameIndex();
|
||||
if (this.isAwake()) {
|
||||
return;
|
||||
}
|
||||
this._state = gdjs.ObjectSleepState.State.AWake;
|
||||
for (const onWakingUp of this._onWakingUpCallbacks) {
|
||||
onWakingUp(object);
|
||||
}
|
||||
}
|
||||
|
||||
registerOnWakingUp(onWakingUp: (object: RuntimeObject) => void) {
|
||||
this._onWakingUpCallbacks.push(onWakingUp);
|
||||
}
|
||||
|
||||
tryToSleep(): void {
|
||||
if (
|
||||
this._lastActivityFrameIndex !== Number.NEGATIVE_INFINITY &&
|
||||
this._isNeedingToBeAwake()
|
||||
) {
|
||||
this._lastActivityFrameIndex = this._object
|
||||
.getRuntimeScene()
|
||||
.getFrameIndex();
|
||||
}
|
||||
}
|
||||
|
||||
static updateAwakeObjects(
|
||||
awakeObjects: Array<RuntimeObject>,
|
||||
getSleepState: (object: RuntimeObject) => ObjectSleepState,
|
||||
onFallenAsleep: (object: RuntimeObject) => void,
|
||||
onWakingUp: (object: RuntimeObject) => void
|
||||
) {
|
||||
let writeIndex = 0;
|
||||
for (let readIndex = 0; readIndex < awakeObjects.length; readIndex++) {
|
||||
const object = awakeObjects[readIndex];
|
||||
const sleepState = getSleepState(object);
|
||||
sleepState.tryToSleep();
|
||||
if (sleepState.canSleep() || !sleepState.isAwake()) {
|
||||
if (sleepState.isAwake()) {
|
||||
// Avoid onWakingUp to be called if some managers didn't have time
|
||||
// to update their awake object list.
|
||||
sleepState._onWakingUpCallbacks.length = 0;
|
||||
}
|
||||
sleepState._state = gdjs.ObjectSleepState.State.ASleep;
|
||||
onFallenAsleep(object);
|
||||
sleepState._onWakingUpCallbacks.push(onWakingUp);
|
||||
} else {
|
||||
awakeObjects[writeIndex] = object;
|
||||
writeIndex++;
|
||||
}
|
||||
}
|
||||
awakeObjects.length = writeIndex;
|
||||
return awakeObjects;
|
||||
}
|
||||
}
|
||||
|
||||
export namespace ObjectSleepState {
|
||||
export enum State {
|
||||
ASleep,
|
||||
CanSleepThisFrame,
|
||||
AWake,
|
||||
}
|
||||
}
|
||||
}
|
@@ -34,7 +34,6 @@ namespace gdjs {
|
||||
|
||||
_layers: Hashtable<RuntimeLayer>;
|
||||
_orderedLayers: RuntimeLayer[]; // TODO: should this be a single structure with _layers, to enforce its usage?
|
||||
_layersCameraCoordinates: Record<string, [float, float, float, float]> = {};
|
||||
|
||||
// Options for the debug draw:
|
||||
_debugDrawEnabled: boolean = false;
|
||||
@@ -351,26 +350,6 @@ namespace gdjs {
|
||||
}
|
||||
}
|
||||
|
||||
_updateLayersCameraCoordinates(scale: float) {
|
||||
this._layersCameraCoordinates = this._layersCameraCoordinates || {};
|
||||
for (const name in this._layers.items) {
|
||||
if (this._layers.items.hasOwnProperty(name)) {
|
||||
const theLayer = this._layers.items[name];
|
||||
this._layersCameraCoordinates[name] = this._layersCameraCoordinates[
|
||||
name
|
||||
] || [0, 0, 0, 0];
|
||||
this._layersCameraCoordinates[name][0] =
|
||||
theLayer.getCameraX() - (theLayer.getCameraWidth() / 2) * scale;
|
||||
this._layersCameraCoordinates[name][1] =
|
||||
theLayer.getCameraY() - (theLayer.getCameraHeight() / 2) * scale;
|
||||
this._layersCameraCoordinates[name][2] =
|
||||
theLayer.getCameraX() + (theLayer.getCameraWidth() / 2) * scale;
|
||||
this._layersCameraCoordinates[name][3] =
|
||||
theLayer.getCameraY() + (theLayer.getCameraHeight() / 2) * scale;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Called to update effects of layers before rendering.
|
||||
*/
|
||||
@@ -625,6 +604,8 @@ namespace gdjs {
|
||||
return;
|
||||
}
|
||||
|
||||
onObjectChangedOfLayer(object: RuntimeObject, oldLayer: RuntimeLayer) {}
|
||||
|
||||
/**
|
||||
* Get the layer with the given name
|
||||
* @param name The name of the layer
|
||||
|
File diff suppressed because it is too large
Load Diff
@@ -147,6 +147,8 @@ namespace gdjs {
|
||||
return true;
|
||||
};
|
||||
|
||||
type RuntimeObjectCallback = (object: gdjs.RuntimeObject) => void;
|
||||
|
||||
/**
|
||||
* RuntimeObject represents an object being used on a RuntimeScene.
|
||||
*
|
||||
@@ -164,9 +166,12 @@ namespace gdjs {
|
||||
layer: string = '';
|
||||
protected _nameId: integer;
|
||||
protected _livingOnScene: boolean = true;
|
||||
protected _spatialSearchSleepState: ObjectSleepState;
|
||||
|
||||
readonly id: integer;
|
||||
private destroyCallbacks = new Set<() => void>();
|
||||
// HitboxChanges happen a lot, an Array is faster to iterate.
|
||||
private hitBoxChangedCallbacks: Array<RuntimeObjectCallback> = [];
|
||||
_runtimeScene: gdjs.RuntimeInstanceContainer;
|
||||
|
||||
/**
|
||||
@@ -181,12 +186,16 @@ namespace gdjs {
|
||||
* not "thread safe" or "re-entrant algorithm" safe.
|
||||
*/
|
||||
pick: boolean = false;
|
||||
pickingId: integer = 0;
|
||||
|
||||
//Hit boxes:
|
||||
protected _defaultHitBoxes: gdjs.Polygon[] = [];
|
||||
protected hitBoxes: gdjs.Polygon[];
|
||||
protected hitBoxesDirty: boolean = true;
|
||||
// TODO use a different AABB for collision mask and rendered image.
|
||||
protected aabb: AABB = { min: [0, 0], max: [0, 0] };
|
||||
_rtreeAABB: SearchedItem<RuntimeObject>;
|
||||
|
||||
protected _isIncludedInParentCollisionMask = true;
|
||||
|
||||
//Variables:
|
||||
@@ -229,10 +238,11 @@ namespace gdjs {
|
||||
instanceContainer: gdjs.RuntimeInstanceContainer,
|
||||
objectData: ObjectData & any
|
||||
) {
|
||||
const scene = instanceContainer.getScene();
|
||||
this.name = objectData.name || '';
|
||||
this.type = objectData.type || '';
|
||||
this._nameId = RuntimeObject.getNameIdentifier(this.name);
|
||||
this.id = instanceContainer.getScene().createNewUniqueId();
|
||||
this.id = scene.createNewUniqueId();
|
||||
this._runtimeScene = instanceContainer;
|
||||
this._defaultHitBoxes.push(gdjs.Polygon.createRectangle(0, 0));
|
||||
this.hitBoxes = this._defaultHitBoxes;
|
||||
@@ -241,8 +251,20 @@ namespace gdjs {
|
||||
);
|
||||
this._totalForce = new gdjs.Force(0, 0, 0);
|
||||
this._behaviorsTable = new Hashtable();
|
||||
this._rtreeAABB = {
|
||||
source: this,
|
||||
minX: 0,
|
||||
minY: 0,
|
||||
maxX: 0,
|
||||
maxY: 0,
|
||||
};
|
||||
this._spatialSearchSleepState = new gdjs.ObjectSleepState(
|
||||
this,
|
||||
() => !this.getVisibilityAABB(),
|
||||
gdjs.ObjectSleepState.State.CanSleepThisFrame
|
||||
);
|
||||
for (let i = 0; i < objectData.effects.length; ++i) {
|
||||
this._runtimeScene
|
||||
scene
|
||||
.getGame()
|
||||
.getEffectsManager()
|
||||
.initializeEffect(objectData.effects[i], this._rendererEffects, this);
|
||||
@@ -439,6 +461,14 @@ namespace gdjs {
|
||||
return false;
|
||||
}
|
||||
|
||||
getSpatialSearchSleepState(): ObjectSleepState {
|
||||
return this._spatialSearchSleepState;
|
||||
}
|
||||
|
||||
isAlive(): boolean {
|
||||
return this._livingOnScene;
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove an object from a scene.
|
||||
*
|
||||
@@ -486,6 +516,31 @@ namespace gdjs {
|
||||
|
||||
onDestroyed(): void {}
|
||||
|
||||
registerHitboxChangedCallback(callback: RuntimeObjectCallback) {
|
||||
if (this.hitBoxChangedCallbacks.includes(callback)) {
|
||||
return;
|
||||
}
|
||||
this.hitBoxChangedCallbacks.push(callback);
|
||||
}
|
||||
|
||||
/**
|
||||
* Send a signal that the object hitboxes are no longer up to date.
|
||||
*
|
||||
* The signal is propagated to parents so
|
||||
* {@link gdjs.RuntimeObject.hitBoxesDirty} should never be modified
|
||||
* directly.
|
||||
*/
|
||||
invalidateHitboxes(): void {
|
||||
// TODO EBO Check that no community extension set hitBoxesDirty to true
|
||||
// directly.
|
||||
this.hitBoxesDirty = true;
|
||||
this._spatialSearchSleepState.wakeUp();
|
||||
this._runtimeScene.onChildrenLocationChanged();
|
||||
for (const callback of this.hitBoxChangedCallbacks) {
|
||||
callback(this);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Called whenever the scene owning the object is paused.
|
||||
* This should *not* impact objects, but some may need to inform their renderer.
|
||||
@@ -570,20 +625,6 @@ namespace gdjs {
|
||||
this.invalidateHitboxes();
|
||||
}
|
||||
|
||||
/**
|
||||
* Send a signal that the object hitboxes are no longer up to date.
|
||||
*
|
||||
* The signal is propagated to parents so
|
||||
* {@link gdjs.RuntimeObject.hitBoxesDirty} should never be modified
|
||||
* directly.
|
||||
*/
|
||||
invalidateHitboxes(): void {
|
||||
// TODO EBO Check that no community extension set hitBoxesDirty to true
|
||||
// directly.
|
||||
this.hitBoxesDirty = true;
|
||||
this._runtimeScene.onChildrenLocationChanged();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the X position of the object.
|
||||
*
|
||||
@@ -758,6 +799,7 @@ namespace gdjs {
|
||||
oldLayer.getRenderer().remove3DRendererObject(rendererObject3D);
|
||||
newLayer.getRenderer().add3DRendererObject(rendererObject3D);
|
||||
}
|
||||
this._runtimeScene.onObjectChangedOfLayer(this, oldLayer);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@@ -6,6 +6,7 @@
|
||||
namespace gdjs {
|
||||
const logger = new gdjs.Logger('RuntimeScene');
|
||||
const setupWarningLogger = new gdjs.Logger('RuntimeScene (setup warnings)');
|
||||
type SearchArea = { minX: float; minY: float; maxX: float; maxY: float };
|
||||
|
||||
/**
|
||||
* A scene being played, containing instances of objects rendered on screen.
|
||||
@@ -45,6 +46,17 @@ namespace gdjs {
|
||||
_cachedGameResolutionWidth: integer;
|
||||
_cachedGameResolutionHeight: integer;
|
||||
|
||||
private _frameIndex: integer = 0;
|
||||
|
||||
_layersCameraCoordinates: Record<string, SearchArea> = {};
|
||||
private _layerObjectManagers = new Map<string, ObjectManager>();
|
||||
/**
|
||||
* Objects that were rendered for the last frame.
|
||||
*
|
||||
* They keep to be hide back without iterating every objects from the scene.
|
||||
*/
|
||||
private _objectsInsideCamera: Record<string, Array<RuntimeObject>> = {};
|
||||
|
||||
/**
|
||||
* @param runtimeGame The game associated to this scene.
|
||||
*/
|
||||
@@ -81,6 +93,43 @@ namespace gdjs {
|
||||
this._orderedLayers.push(layer);
|
||||
}
|
||||
|
||||
addObject(object: gdjs.RuntimeObject): void {
|
||||
super.addObject(object);
|
||||
this._addObjectToLayerObjectManager(object);
|
||||
}
|
||||
|
||||
onObjectChangedOfLayer(object: RuntimeObject, oldLayer: RuntimeLayer) {
|
||||
this._removeObjectFromLayerObjectManager(object, oldLayer.getName());
|
||||
this._addObjectToLayerObjectManager(object);
|
||||
}
|
||||
|
||||
private _addObjectToLayerObjectManager(object: gdjs.RuntimeObject): void {
|
||||
const layerName = object.getLayer();
|
||||
let objectManager = this._layerObjectManagers.get(layerName);
|
||||
if (!objectManager) {
|
||||
objectManager = new gdjs.ObjectManager();
|
||||
this._layerObjectManagers.set(layerName, objectManager);
|
||||
}
|
||||
objectManager.addObject(object);
|
||||
}
|
||||
|
||||
markObjectForDeletion(object: gdjs.RuntimeObject): void {
|
||||
super.markObjectForDeletion(object);
|
||||
const layerName = object.getLayer();
|
||||
this._removeObjectFromLayerObjectManager(object, layerName);
|
||||
}
|
||||
|
||||
private _removeObjectFromLayerObjectManager(
|
||||
object: gdjs.RuntimeObject,
|
||||
layerName: string
|
||||
): void {
|
||||
let objectManager = this._layerObjectManagers.get(layerName);
|
||||
if (!objectManager) {
|
||||
return;
|
||||
}
|
||||
objectManager.deleteObject(object);
|
||||
}
|
||||
|
||||
/**
|
||||
* Should be called when the canvas where the scene is rendered has been resized.
|
||||
* See gdjs.RuntimeGame.startGameLoop in particular.
|
||||
@@ -427,6 +476,7 @@ namespace gdjs {
|
||||
if (this._profiler) {
|
||||
this._profiler.endFrame();
|
||||
}
|
||||
this._frameIndex++;
|
||||
return !!this.getRequestedChange();
|
||||
}
|
||||
|
||||
@@ -437,6 +487,26 @@ namespace gdjs {
|
||||
this._renderer.render();
|
||||
}
|
||||
|
||||
_updateLayersCameraCoordinates(scale: float) {
|
||||
this._layersCameraCoordinates = this._layersCameraCoordinates || {};
|
||||
for (const name in this._layers.items) {
|
||||
if (this._layers.items.hasOwnProperty(name)) {
|
||||
const theLayer = this._layers.items[name];
|
||||
this._layersCameraCoordinates[name] = this._layersCameraCoordinates[
|
||||
name
|
||||
] || { minX: 0, minY: 0, maxX: 0, maxY: 0 };
|
||||
this._layersCameraCoordinates[name].minX =
|
||||
theLayer.getCameraX() - (theLayer.getCameraWidth() / 2) * scale;
|
||||
this._layersCameraCoordinates[name].minY =
|
||||
theLayer.getCameraY() - (theLayer.getCameraHeight() / 2) * scale;
|
||||
this._layersCameraCoordinates[name].maxX =
|
||||
theLayer.getCameraX() + (theLayer.getCameraWidth() / 2) * scale;
|
||||
this._layersCameraCoordinates[name].maxY =
|
||||
theLayer.getCameraY() + (theLayer.getCameraHeight() / 2) * scale;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Called to update visibility of the renderers of objects
|
||||
* rendered on the scene ("culling"), update effects (of visible objects)
|
||||
@@ -446,50 +516,68 @@ namespace gdjs {
|
||||
* object is too far from the camera of its layer ("culling").
|
||||
*/
|
||||
_updateObjectsPreRender() {
|
||||
// Check awake objects only once every 64 frames.
|
||||
if ((this._frameIndex & 63) === 0) {
|
||||
for (const objectManager of this._layerObjectManagers.values()) {
|
||||
objectManager.updateAwakeObjects();
|
||||
}
|
||||
}
|
||||
if (this._timeManager.isFirstFrame()) {
|
||||
super._updateObjectsPreRender();
|
||||
return;
|
||||
} else {
|
||||
// After first frame, optimise rendering by setting only objects
|
||||
// near camera as visible.
|
||||
// TODO: For compatibility, pass a scale of `2`,
|
||||
// meaning that size of cameras will be multiplied by 2 and so objects
|
||||
// will be hidden if they are outside of this *larger* camera area.
|
||||
// This is useful for:
|
||||
// - objects not properly reporting their visibility AABB,
|
||||
// (so we have a "safety margin") but these objects should be fixed
|
||||
// instead.
|
||||
// - objects having effects rendering outside of their visibility AABB.
|
||||
|
||||
// TODO (3D) culling - add support for 3D object culling?
|
||||
this._updateLayersCameraCoordinates(2);
|
||||
const allInstancesList = this.getAdhocListOfAllInstances();
|
||||
for (let i = 0, len = allInstancesList.length; i < len; ++i) {
|
||||
const object = allInstancesList[i];
|
||||
const rendererObject = object.getRendererObject();
|
||||
if (rendererObject) {
|
||||
if (object.isHidden()) {
|
||||
rendererObject.visible = false;
|
||||
} else {
|
||||
const cameraCoords = this._layersCameraCoordinates[
|
||||
object.getLayer()
|
||||
];
|
||||
if (!cameraCoords) {
|
||||
continue;
|
||||
}
|
||||
const aabb = object.getVisibilityAABB();
|
||||
rendererObject.visible =
|
||||
// If no AABB is returned, the object should always be visible
|
||||
!aabb ||
|
||||
// If an AABB is there, it must be at least partially inside
|
||||
// the camera bounds.
|
||||
!(
|
||||
aabb.min[0] > cameraCoords[2] ||
|
||||
aabb.min[1] > cameraCoords[3] ||
|
||||
aabb.max[0] < cameraCoords[0] ||
|
||||
aabb.max[1] < cameraCoords[1]
|
||||
);
|
||||
}
|
||||
rendererObject.visible = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
// After first frame, optimise rendering by setting only objects
|
||||
// near camera as visible.
|
||||
// TODO: For compatibility, pass a scale of `2`,
|
||||
// meaning that size of cameras will be multiplied by 2 and so objects
|
||||
// will be hidden if they are outside of this *larger* camera area.
|
||||
// This is useful for:
|
||||
// - objects not properly reporting their visibility AABB,
|
||||
// (so we have a "safety margin") but these objects should be fixed
|
||||
// instead.
|
||||
// - objects having effects rendering outside of their visibility AABB.
|
||||
|
||||
// TODO (3D) culling - add support for 3D object culling?
|
||||
this._updateLayersCameraCoordinates(2);
|
||||
|
||||
// Reset objects that were visible last frame.
|
||||
for (const layerName in this._objectsInsideCamera) {
|
||||
for (const object of this._objectsInsideCamera[layerName]) {
|
||||
const rendererObject = object.getRendererObject();
|
||||
if (rendererObject) {
|
||||
rendererObject.visible = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
for (const layerName in this._layers.items) {
|
||||
const cameraAABB = this._layersCameraCoordinates[layerName];
|
||||
let objectsInsideCamera = this._objectsInsideCamera[layerName];
|
||||
if (objectsInsideCamera === undefined) {
|
||||
objectsInsideCamera = [];
|
||||
this._objectsInsideCamera[layerName] = objectsInsideCamera;
|
||||
}
|
||||
if (!cameraAABB) {
|
||||
continue;
|
||||
}
|
||||
const layerObjectManager = this._layerObjectManagers.get(layerName);
|
||||
if (!layerObjectManager) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Find objects that are visible this frame.
|
||||
objectsInsideCamera.length = 0;
|
||||
layerObjectManager.search(cameraAABB, objectsInsideCamera);
|
||||
|
||||
for (const object of objectsInsideCamera) {
|
||||
const rendererObject = object.getRendererObject();
|
||||
if (rendererObject) {
|
||||
rendererObject.visible = !object.isHidden();
|
||||
|
||||
// Update effects, only for visible objects.
|
||||
if (rendererObject.visible) {
|
||||
@@ -734,6 +822,10 @@ namespace gdjs {
|
||||
sceneJustResumed(): boolean {
|
||||
return this._isJustResumed;
|
||||
}
|
||||
|
||||
getFrameIndex(): integer {
|
||||
return this._frameIndex;
|
||||
}
|
||||
}
|
||||
|
||||
//The flags to describe the change request by a scene:
|
||||
|
19
GDJS/Runtime/types/rbush.d.ts
vendored
Normal file
19
GDJS/Runtime/types/rbush.d.ts
vendored
Normal file
@@ -0,0 +1,19 @@
|
||||
type SearchArea = { minX: float; minY: float; maxX: float; maxY: float };
|
||||
type SearchedItem<T> = {
|
||||
source: T;
|
||||
minX: float;
|
||||
minY: float;
|
||||
maxX: float;
|
||||
maxY: float;
|
||||
};
|
||||
|
||||
declare class RBush<T> {
|
||||
constructor(maxEntries?: number);
|
||||
search(bbox: SearchArea, result?: Array<T>): Array<T>;
|
||||
insert(item: SearchedItem<T>): RBush<T>;
|
||||
clear(): RBush<T>;
|
||||
remove(
|
||||
item: SearchedItem<T>,
|
||||
equalsFn?: (item: SearchedItem<T>, otherItem: SearchedItem<T>) => boolean
|
||||
): RBush<T>;
|
||||
}
|
@@ -53,6 +53,8 @@ module.exports = function (config) {
|
||||
'./newIDE/app/resources/GDJS/Runtime/ResourceCache.js',
|
||||
'./newIDE/app/resources/GDJS/Runtime/timemanager.js',
|
||||
'./newIDE/app/resources/GDJS/Runtime/polygon.js',
|
||||
'./newIDE/app/resources/GDJS/Runtime/ObjectSleepState.js',
|
||||
'./newIDE/app/resources/GDJS/Runtime/ObjectManager.js',
|
||||
'./newIDE/app/resources/GDJS/Runtime/runtimeobject.js',
|
||||
'./newIDE/app/resources/GDJS/Runtime/RuntimeInstanceContainer.js',
|
||||
'./newIDE/app/resources/GDJS/Runtime/runtimescene.js',
|
||||
|
@@ -3,12 +3,15 @@
|
||||
*/
|
||||
gdjs.TestRuntimeScene = class TestRuntimeScene extends gdjs.RuntimeScene {
|
||||
/**
|
||||
* @param {gdjs.RuntimeGame} runtimeGame
|
||||
* @param {gdjs.RuntimeGame} runtimeGame
|
||||
*/
|
||||
constructor(runtimeGame) {
|
||||
constructor(runtimeGame, layerNames = ['']) {
|
||||
super(runtimeGame);
|
||||
|
||||
this.addLayer({ name: '', cameras: [], effects: [] });
|
||||
for (const layerName of layerNames) {
|
||||
this.addLayer({ name: layerName, cameras: [], effects: [] });
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
@@ -20,7 +20,32 @@
|
||||
/** @type {?float} */
|
||||
_customCenterY = null;
|
||||
|
||||
constructor(runtimeScene, objectData) {
|
||||
constructor(runtimeScene, objectData = {
|
||||
name: 'MySprite',
|
||||
type: '',
|
||||
behaviors: [],
|
||||
effects: [],
|
||||
animations: [
|
||||
{
|
||||
name: 'animation',
|
||||
directions: [
|
||||
{
|
||||
sprites: [
|
||||
{
|
||||
originPoint: { x: 0, y: 0 },
|
||||
centerPoint: { x: 0, y: 0 },
|
||||
points: [
|
||||
{ name: 'Center', x: 0, y: 0 },
|
||||
{ name: 'Origin', x: 0, y: 0 },
|
||||
],
|
||||
hasCustomCollisionMask: false,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
}) {
|
||||
// *ALWAYS* call the base gdjs.RuntimeObject constructor.
|
||||
super(runtimeScene, objectData);
|
||||
|
||||
@@ -50,10 +75,6 @@
|
||||
this.invalidateHitboxes();
|
||||
}
|
||||
|
||||
getRendererObject() {
|
||||
return null;
|
||||
}
|
||||
|
||||
getWidth() {
|
||||
return this._customWidth;
|
||||
}
|
||||
|
105
GDJS/tests/tests/runtimescene.culling.js
Normal file
105
GDJS/tests/tests/runtimescene.culling.js
Normal file
@@ -0,0 +1,105 @@
|
||||
// @ts-check
|
||||
describe('gdjs.RuntimeScene culling tests', () => {
|
||||
|
||||
it('should hide objects moving outside of the screen', () => {
|
||||
const game = gdjs.getPixiRuntimeGame();
|
||||
const scene = new gdjs.TestRuntimeScene(game, ['', 'MyLayer']);
|
||||
|
||||
scene.getLayer('MyLayer').setCameraX(8000);
|
||||
|
||||
const object = new gdjs.TestSpriteRuntimeObject(scene);
|
||||
object.setUnscaledWidthAndHeight(100, 100);
|
||||
object.setCustomWidthAndHeight(100, 100);
|
||||
scene.addObject(object);
|
||||
|
||||
scene.renderAndStep(1000 / 60);
|
||||
expect(object.getRendererObject().visible).to.be(true);
|
||||
|
||||
object.setY(8000);
|
||||
|
||||
scene.renderAndStep(1000 / 60);
|
||||
expect(object.getRendererObject().visible).to.be(false);
|
||||
});
|
||||
|
||||
it('should show objects moving back in the screen', () => {
|
||||
const game = gdjs.getPixiRuntimeGame();
|
||||
const scene = new gdjs.TestRuntimeScene(game, ['']);
|
||||
|
||||
const object = new gdjs.TestSpriteRuntimeObject(scene);
|
||||
object.setUnscaledWidthAndHeight(100, 100);
|
||||
object.setCustomWidthAndHeight(100, 100);
|
||||
scene.addObject(object);
|
||||
object.setY(8000);
|
||||
|
||||
scene.renderAndStep(1000 / 60);
|
||||
expect(object.getRendererObject().visible).to.be(false);
|
||||
|
||||
object.setY(200);
|
||||
|
||||
scene.renderAndStep(1000 / 60);
|
||||
expect(object.getRendererObject().visible).to.be(true);
|
||||
});
|
||||
|
||||
it('should handle objects changing of layer', () => {
|
||||
const game = gdjs.getPixiRuntimeGame();
|
||||
const scene = new gdjs.TestRuntimeScene(game, ['']);
|
||||
|
||||
const object = new gdjs.TestSpriteRuntimeObject(scene);
|
||||
object.setUnscaledWidthAndHeight(100, 100);
|
||||
object.setCustomWidthAndHeight(100, 100);
|
||||
scene.addObject(object);
|
||||
|
||||
scene.renderAndStep(1000 / 60);
|
||||
expect(object.getRendererObject().visible).to.be(true);
|
||||
|
||||
object.setLayer('MyLayer');
|
||||
|
||||
scene.renderAndStep(1000 / 60);
|
||||
expect(object.getRendererObject().visible).to.be(false);
|
||||
|
||||
object.setLayer('');
|
||||
|
||||
scene.renderAndStep(1000 / 60);
|
||||
expect(object.getRendererObject().visible).to.be(true);
|
||||
});
|
||||
|
||||
// This is important to avoid games with big levels to be CPU limited during
|
||||
// the 2 first seconds.
|
||||
it('should allow objects to sleep at the end of the 1st frame', () => {
|
||||
const game = gdjs.getPixiRuntimeGame();
|
||||
const scene = new gdjs.TestRuntimeScene(game, ['']);
|
||||
|
||||
const object = new gdjs.TestSpriteRuntimeObject(scene);
|
||||
object.setUnscaledWidthAndHeight(100, 100);
|
||||
object.setCustomWidthAndHeight(100, 100);
|
||||
scene.addObject(object);
|
||||
|
||||
scene.renderAndStep(1000 / 60);
|
||||
expect(object.getRendererObject().visible).to.be(true);
|
||||
expect(object.getSpatialSearchSleepState().isAwake()).to.be(false);
|
||||
|
||||
object.setY(200);
|
||||
expect(object.getSpatialSearchSleepState().isAwake()).to.be(true);
|
||||
});
|
||||
|
||||
it('should put objects to sleep when they don\'t move for 2 seconds', () => {
|
||||
const game = gdjs.getPixiRuntimeGame();
|
||||
const scene = new gdjs.TestRuntimeScene(game, ['']);
|
||||
|
||||
const object = new gdjs.TestSpriteRuntimeObject(scene);
|
||||
object.setUnscaledWidthAndHeight(100, 100);
|
||||
object.setCustomWidthAndHeight(100, 100);
|
||||
scene.addObject(object);
|
||||
|
||||
scene.renderAndStep(1000 / 60);
|
||||
object.setY(200);
|
||||
expect(object.getSpatialSearchSleepState().isAwake()).to.be(true);
|
||||
|
||||
// Objects can sleep every 64 frames if they haven't moved for 1 second.
|
||||
for (let index = 0; index < 60 + 64; index++) {
|
||||
scene.renderAndStep(1000 / 60);
|
||||
}
|
||||
expect(object.getSpatialSearchSleepState().isAwake()).to.be(false);
|
||||
expect(object.getRendererObject().visible).to.be(true);
|
||||
});
|
||||
});
|
Reference in New Issue
Block a user