Compare commits

..

4 Commits

Author SHA1 Message Date
Davy Hélard
f8f692fff4 Fix duplicated active objects after recycling. 2023-11-28 20:51:27 +01:00
Davy Hélard
d051aa4302 Avoid to iterate on inactive objects 2023-11-28 14:29:38 +01:00
Florian Rival
d34f1a654f Improve navigation for extensions in the documentation (#5950) 2023-11-24 11:43:47 +01:00
D8H
5abc74b66b Remove only (#5946)
Don't show in changelog
2023-11-23 19:05:08 +01:00
33 changed files with 407 additions and 927 deletions

View File

@@ -103,11 +103,6 @@ void EventsCodeGenerationContext::EmptyObjectsListNeeded(
depthOfLastUse[objectName] = GetContextDepth();
}
void EventsCodeGenerationContext::AddUsedObjectsMapNames(
const gd::String& objectMapName) {
usedObjectsMapNames.insert(objectMapName);
}
std::set<gd::String> EventsCodeGenerationContext::GetAllObjectsToBeDeclared()
const {
std::set<gd::String> allObjectListsToBeDeclared(

View File

@@ -175,12 +175,6 @@ class GD_CORE_API EventsCodeGenerationContext {
return emptyObjectsListsToBeDeclared;
};
const std::set<gd::String>& GetUsedObjectsMapNames() const {
return usedObjectsMapNames;
};
void AddUsedObjectsMapNames(const gd::String& objectMapName);
/**
* Return the objects lists which are already declared and can be used in the
* current context without declaration.
@@ -303,8 +297,6 @@ class GD_CORE_API EventsCodeGenerationContext {
///< necessary objects can be
///< backed up.
std::set<gd::String> usedObjectsMapNames;
std::map<gd::String, unsigned int>
depthOfLastUse; ///< The context depth when an object was last used.
gd::String

View File

@@ -408,25 +408,6 @@ gd::String EventsCodeGenerator::GenerateConditionCode(
arguments, instrInfos, returnBoolean, condition.IsInverted(), context);
}
// Flag the ObjectsLists as modified.
gd::ParameterMetadataTools::IterateOverParameters(
condition.GetParameters(), instrInfos.parameters,
[this, &context,
&conditionCode](const gd::ParameterMetadata &parameterMetadata,
const gd::Expression &parameterValue,
const gd::String &lastObjectName) {
// objectListOrEmptyWithoutPicking are only used by SceneInstancesCount
// and PickedInstancesCount conditions. They are not pass for one
// condition to another.
if (parameterMetadata.GetType() == "objectList" ||
parameterMetadata.GetType() == "objectListOrEmptyIfJustDeclared") {
// TODO FIXME What about groups using the object?
conditionCode +=
GetObjectMapName(parameterValue.GetPlainString(), context) +
".isPicked = true;\n";
}
});
return conditionCode;
}

View File

@@ -585,12 +585,6 @@ class GD_CORE_API EventsCodeGenerator {
return "fakeObjectListOf_" + objectName;
}
// TODO Documentation
virtual gd::String GetObjectMapName(const gd::String &objectName,
gd::EventsCodeGenerationContext &context) {
return "fakeObjectListOf_" + objectName;
}
virtual gd::String GeneratePropertyGetter(
const gd::PropertiesContainer& propertiesContainer,
const gd::NamedPropertyDescriptor& property,

View File

@@ -1475,11 +1475,10 @@ void GD_CORE_API BuiltinExtensionsImplementer::ImplementsBaseObjectExtension(
_("Position"),
"res/conditions/distance24.png",
"res/conditions/distance.png")
.AddParameter("objectListOrEmptyIfJustDeclared", _("Object"))
.AddParameter("objectListOrEmptyIfJustDeclared", _("Object 2"))
.AddParameter("objectList", _("Object"))
.AddParameter("objectList", _("Object 2"))
.AddParameter("expression", _("Distance"))
.AddCodeOnlyParameter("conditionInverted", "")
.AddCodeOnlyParameter("objectsContext", "")
.MarkAsSimple();
extension
@@ -1586,10 +1585,10 @@ void GD_CORE_API BuiltinExtensionsImplementer::ImplementsBaseObjectExtension(
_("Collision"),
"res/conditions/collision24.png",
"res/conditions/collision.png")
.AddParameter("objectListOrEmptyIfJustDeclared", _("Object"))
.AddParameter("objectListOrEmptyIfJustDeclared", _("Object"))
.AddParameter("objectList", _("Object"))
.AddParameter("objectList", _("Object"))
.AddCodeOnlyParameter("conditionInverted", "")
.AddCodeOnlyParameter("objectsContext", "")
.AddCodeOnlyParameter("currentScene", "")
.AddParameter("yesorno",
_("Ignore objects that are touching each other on their "
"edges, but are not overlapping (default: no)"),

View File

@@ -113,6 +113,10 @@ namespace gdjs {
);
}
if (this.isNeedingLifecycleFunctions()) {
this.getLifecycleSleepState().wakeUp();
}
// *ALWAYS* call `this.onCreated()` at the very end of your object constructor.
this.onCreated();
}
@@ -194,6 +198,10 @@ namespace gdjs {
}
}
isNeedingLifecycleFunctions(): boolean {
return super.isNeedingLifecycleFunctions() || this._animations.length > 0;
}
update(instanceContainer: gdjs.RuntimeInstanceContainer): void {
const elapsedTime = this.getElapsedTime() / 1000;
this._renderer.updateAnimation(elapsedTime * this._animationSpeedScale);

View File

@@ -1,9 +1,9 @@
namespace gdjs {
export namespace physics2 {
export const objectsCollide = function (
objectsLists1: ObjectsLists,
objectsLists1: Hashtable<Array<gdjs.RuntimeObject>>,
behaviorName: string,
objectsLists2: ObjectsLists,
objectsLists2: Hashtable<Array<gdjs.RuntimeObject>>,
inverted: boolean
) {
return gdjs.evtTools.object.twoListsTest(
@@ -16,9 +16,9 @@ namespace gdjs {
};
export const haveObjectsStartedColliding = function (
objectsLists1: ObjectsLists,
objectsLists1: Hashtable<Array<gdjs.RuntimeObject>>,
behaviorName: string,
objectsLists2: ObjectsLists,
objectsLists2: Hashtable<Array<gdjs.RuntimeObject>>,
inverted: boolean
) {
return gdjs.evtTools.object.twoListsTest(
@@ -31,9 +31,9 @@ namespace gdjs {
};
export const haveObjectsStoppedColliding = function (
objectsLists1: ObjectsLists,
objectsLists1: Hashtable<Array<gdjs.RuntimeObject>>,
behaviorName: string,
objectsLists2: ObjectsLists,
objectsLists2: Hashtable<Array<gdjs.RuntimeObject>>,
inverted: boolean
) {
return gdjs.evtTools.object.twoListsTest(
@@ -45,11 +45,7 @@ namespace gdjs {
);
};
export const setTimeScale = function (
objectsLists: ObjectsLists,
behavior: gdjs.Physics2RuntimeBehavior,
timeScale: float
) {
export const setTimeScale = function (objectsLists, behavior, timeScale) {
const lists = gdjs.staticArray(gdjs.physics2.setTimeScale);
objectsLists.values(lists);
for (let i = 0, len = lists.length; i < len; i++) {

View File

@@ -39,7 +39,7 @@ namespace gdjs {
/**
* Add a platform to the list of existing platforms.
*/
addPlatform(platformBehavior: gdjs.PlatformRuntimeBehavior) {
private addPlatform(platformBehavior: gdjs.PlatformRuntimeBehavior) {
if (platformBehavior.currentRBushAABB)
platformBehavior.currentRBushAABB.updateAABBFromOwner();
else
@@ -53,7 +53,7 @@ namespace gdjs {
* Remove a platform from the list of existing platforms. Be sure that the platform was
* added before.
*/
removePlatform(platformBehavior: gdjs.PlatformRuntimeBehavior) {
private removePlatform(platformBehavior: gdjs.PlatformRuntimeBehavior) {
if (!platformBehavior.currentRBushAABB) {
return;
}
@@ -64,11 +64,21 @@ namespace gdjs {
this.movedPlatforms.push(platformBehavior);
}
onDestroy(platformBehavior: gdjs.PlatformRuntimeBehavior): void {
if (!platformBehavior.activated()) {
return;
}
if (platformBehavior.isAABBInvalidated()) {
const index = this.movedPlatforms.indexOf(platformBehavior);
this.movedPlatforms.splice(index, 1);
}
this.removePlatform(platformBehavior);
}
doStepPreEvents() {
for (const platformBehavior of this.movedPlatforms) {
this.removePlatform(platformBehavior);
// TODO What if the object is recycled before it can be removed from the tree?
if (platformBehavior.activated() && platformBehavior.owner.isAlive()) {
if (platformBehavior.activated()) {
this.addPlatform(platformBehavior);
}
platformBehavior.onHitboxUpdatedInTree();
@@ -185,8 +195,8 @@ namespace gdjs {
return true;
}
onDestroy() {
this.onHitboxChanged();
onDestroy(): void {
this._manager.onDestroy(this);
}
usesLifecycleFunction(): boolean {
@@ -216,7 +226,7 @@ namespace gdjs {
}
onHitboxChanged() {
if (this._isAABBInvalidated) {
if (this._isAABBInvalidated || !this.owner.isAlive()) {
return;
}
this._isAABBInvalidated = true;
@@ -227,6 +237,10 @@ namespace gdjs {
this._isAABBInvalidated = false;
}
isAABBInvalidated() {
return !this._isAABBInvalidated;
}
changePlatformType(platformType: string) {
if (platformType === 'Ladder') {
this._platformType = PlatformRuntimeBehavior.LADDER;

View File

@@ -2,9 +2,9 @@ namespace gdjs {
export namespace evtTools {
export namespace platform {
export const isOnPlatform = function (
objectsLists1: ObjectsLists,
objectsLists1: Hashtable<Array<gdjs.RuntimeObject>>,
behaviorName: string,
objectsLists2: ObjectsLists,
objectsLists2: Hashtable<Array<gdjs.RuntimeObject>>,
inverted: boolean
) {
return gdjs.evtTools.object.twoListsTest(

View File

@@ -26,12 +26,11 @@ namespace gdjs {
);
};
class TextInputRuntimeObjectPixiRenderer implements RendererObjectInterface {
class TextInputRuntimeObjectPixiRenderer {
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,
@@ -114,25 +113,14 @@ 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._isVisible) {
if (this._object.isHidden()) {
this._input.style.display = 'none';
return;
}

View File

@@ -102,8 +102,7 @@ namespace gdjs {
}
getRendererObject() {
// The renderer is not a Pixi Object but it implements visible.
return this._renderer;
return null;
}
updateFromObjectData(

View File

@@ -39,6 +39,10 @@ namespace gdjs {
);
this._updateTileMap();
if (this.isNeedingLifecycleFunctions()) {
this.getLifecycleSleepState().wakeUp();
}
// *ALWAYS* call `this.onCreated()` at the very end of your object constructor.
this.onCreated();
}
@@ -47,6 +51,11 @@ namespace gdjs {
return this._renderer.getRendererObject();
}
isNeedingLifecycleFunctions(): boolean {
// TODO Tile maps without animated tiles should return false.
return true;
}
update(instanceContainer: gdjs.RuntimeInstanceContainer): void {
if (this._animationSpeedScale <= 0 || this._animationFps === 0) {
return;

View File

@@ -767,14 +767,6 @@ gd::String EventsCodeGenerator::GenerateObjectCondition(
}
if (conditionInverted) predicate = GenerateNegatedPredicate(predicate);
// TODO FIXME It doesn't work because usedObjectsMapNames maybe be filled after.
// TODO FIXME What about groups using the object?
// Flag the picking list as modified.
auto objectsMapName = GetObjectMapName(objectName, context);
if (context.GetUsedObjectsMapNames().find(objectsMapName) != context.GetUsedObjectsMapNames().end()) {
conditionCode += objectsMapName + ".isPicked = true;\n";
}
// Generate whole condition code
conditionCode +=
"for (var i = 0, k = 0, l = " + GetObjectListName(objectName, context) +
@@ -831,14 +823,6 @@ gd::String EventsCodeGenerator::GenerateBehaviorCondition(
<< "\" requested for object \'" << objectName
<< "\" (condition: " << instrInfos.GetFullName() << ")." << endl;
} else {
// TODO FIXME It doesn't work because usedObjectsMapNames maybe be filled after.
// TODO FIXME What about groups using the object?
// Flag the picking list as modified.
auto objectsMapName = GetObjectMapName(objectName, context);
if (context.GetUsedObjectsMapNames().find(objectsMapName) != context.GetUsedObjectsMapNames().end()) {
conditionCode += objectsMapName + ".isPicked = true;\n";
}
conditionCode +=
"for (var i = 0, k = 0, l = " + GetObjectListName(objectName, context) +
".length;i<l;++i) {\n";
@@ -1057,7 +1041,7 @@ gd::String EventsCodeGenerator::GenerateObjectsDeclarationCode(
if (!context.ObjectAlreadyDeclaredByParents(object)) {
objectListDeclaration += "gdjs.copyArray(" +
GenerateAllInstancesGetterCode(object, context) +
", " + GetObjectListName(object, context) + ");\n";
", " + GetObjectListName(object, context) + ");";
} else
objectListDeclaration = declareObjectListFromParent(object, context);
@@ -1084,10 +1068,6 @@ gd::String EventsCodeGenerator::GenerateObjectsDeclarationCode(
declarationsCode += objectListDeclaration + "\n";
}
for (auto objectsMapName : context.GetUsedObjectsMapNames()) {
declarationsCode += objectsMapName + ".isPicked = false;\n";
}
return declarationsCode;
}
@@ -1203,26 +1183,6 @@ gd::String EventsCodeGenerator::GenerateParameterCodes(
return argOutput;
}
gd::String EventsCodeGenerator::GetObjectMapName(
const gd::String& objectName,
gd::EventsCodeGenerationContext& context) {
std::vector<gd::String> realObjects =
GetObjectsContainersList().ExpandObjectName(objectName,
context.GetCurrentObject());
// The map name must be unique for each set of objects lists.
// We generate it from the objects lists names.
gd::String objectsMapName = GetCodeNamespaceAccessor() + "mapOf";
// Map each declared object to its list.
for (auto &objectName : realObjects) {
objectsMapName += ManObjListName(GetObjectListName(objectName, context));
}
return objectsMapName;
}
gd::String EventsCodeGenerator::GenerateObject(
const gd::String& objectName,
const gd::String& type,
@@ -1274,7 +1234,6 @@ gd::String EventsCodeGenerator::GenerateObject(
for (auto& objectName : realObjects) context.ObjectsListNeeded(objectName);
gd::String objectsMapName = declareMapOfObjects(realObjects, context);
context.AddUsedObjectsMapNames(objectsMapName);
output = objectsMapName;
} else if (type == "objectListOrEmptyIfJustDeclared") {
std::vector<gd::String> realObjects =
@@ -1284,7 +1243,6 @@ gd::String EventsCodeGenerator::GenerateObject(
context.ObjectsListNeededOrEmptyIfJustDeclared(objectName);
gd::String objectsMapName = declareMapOfObjects(realObjects, context);
context.AddUsedObjectsMapNames(objectsMapName);
output = objectsMapName;
} else if (type == "objectListOrEmptyWithoutPicking") {
std::vector<gd::String> realObjects =
@@ -1306,7 +1264,6 @@ gd::String EventsCodeGenerator::GenerateObject(
gd::String objectsMapName = declareMapOfObjects(
objectToBeDeclaredNames, context, objectNotYetDeclaredNames);
context.AddUsedObjectsMapNames(objectsMapName);
output = objectsMapName;
} else if (type == "objectPtr") {
std::vector<gd::String> realObjects =

View File

@@ -335,9 +335,6 @@ class EventsCodeGenerator : public gd::EventsCodeGenerator {
const gd::String& type,
gd::EventsCodeGenerationContext& context) override;
virtual gd::String GetObjectMapName(const gd::String &objectName,
gd::EventsCodeGenerationContext &context) override;
virtual gd::String GenerateNegatedPredicate(const gd::String& predicate) const override {
return "!(" + predicate + ")";
};

View File

@@ -205,13 +205,13 @@ BaseObjectExtension::BaseObjectExtension() {
"gdjs.evtTools.object.getPickedInstancesCount");
GetAllConditions()["CollisionNP"].SetFunctionName(
"gdjs.evtTools.object.position.hitBoxesCollisionCheck");
"gdjs.evtTools.object.hitBoxesCollisionTest");
GetAllConditions()["Raycast"].SetFunctionName(
"gdjs.evtTools.object.raycastObject");
GetAllConditions()["RaycastToPosition"].SetFunctionName(
"gdjs.evtTools.object.raycastObjectToPosition");
GetAllConditions()["Distance"].SetFunctionName(
"gdjs.evtTools.object.position.distanceCheck");
"gdjs.evtTools.object.distanceTest");
GetAllConditions()["SeDirige"].SetFunctionName(
"gdjs.evtTools.object.movesTowardTest");
GetAllConditions()["EstTourne"].SetFunctionName(

View File

@@ -659,7 +659,6 @@ void ExporterHelper::AddLibsInclude(bool pixiRenderers,
// First, do not forget common includes (they must be included before events
// generated code files).
InsertUnique(includesFiles, "libs/jshashtable.js");
InsertUnique(includesFiles, "ObjectsLists.js");
InsertUnique(includesFiles, "logger.js");
InsertUnique(includesFiles, "gd.js");
InsertUnique(includesFiles, "libs/rbush.js");
@@ -672,7 +671,6 @@ void ExporterHelper::AddLibsInclude(bool pixiRenderers,
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");
@@ -700,7 +698,6 @@ void ExporterHelper::AddLibsInclude(bool pixiRenderers,
InsertUnique(includesFiles, "events-tools/runtimescenetools.js");
InsertUnique(includesFiles, "events-tools/inputtools.js");
InsertUnique(includesFiles, "events-tools/objecttools.js");
InsertUnique(includesFiles, "events-tools/ObjectPositionTools.js");
InsertUnique(includesFiles, "events-tools/cameratools.js");
InsertUnique(includesFiles, "events-tools/soundtools.js");
InsertUnique(includesFiles, "events-tools/storagetools.js");

View File

@@ -1,118 +0,0 @@
namespace gdjs {
// TODO Do something like BehaviorRBushAABB
// TODO Allow to use getVisibilityAABB or getAABB
/**
* 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) {
const aabb = instance.getAABB();
if (
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 {
const objectAABB = object.getAABB();
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;
}
}
}

View File

@@ -10,7 +10,7 @@ namespace gdjs {
private _isNeedingToBeAwake: () => boolean;
private _state: ObjectSleepState.State;
private _lastActivityFrameIndex: integer;
private _onWakingUpCallbacks: Array<(object: RuntimeObject) => void> = [];
_onWakingUpCallbacks: Array<(object: RuntimeObject) => void> = [];
constructor(
object: RuntimeObject,
@@ -25,6 +25,21 @@ namespace gdjs {
.getFrameIndex();
}
_reinitialize(
initialSleepState: ObjectSleepState.State
) : void {
this._onWakingUpCallbacks.length = 0;
this._state = initialSleepState;
this._lastActivityFrameIndex = this._object
.getRuntimeScene()
.getFrameIndex();
}
_destroy(): void {
this._state = gdjs.ObjectSleepState.State.Destroyed;
this._onWakingUpCallbacks.length = 0;
}
canSleep(): boolean {
return (
this._state === gdjs.ObjectSleepState.State.CanSleepThisFrame ||
@@ -35,14 +50,8 @@ namespace gdjs {
}
isAwake(): boolean {
return this._state !== gdjs.ObjectSleepState.State.ASleep;
}
_forceToSleep(): void {
if (!this.isAwake()) {
return;
}
this._lastActivityFrameIndex = Number.NEGATIVE_INFINITY;
return this._state === gdjs.ObjectSleepState.State.AWake ||
this._state === gdjs.ObjectSleepState.State.CanSleepThisFrame;
}
wakeUp() {
@@ -62,10 +71,7 @@ namespace gdjs {
}
tryToSleep(): void {
if (
this._lastActivityFrameIndex !== Number.NEGATIVE_INFINITY &&
this._isNeedingToBeAwake()
) {
if (this._isNeedingToBeAwake()) {
this._lastActivityFrameIndex = this._object
.getRuntimeScene()
.getFrameIndex();
@@ -91,7 +97,7 @@ namespace gdjs {
}
sleepState._state = gdjs.ObjectSleepState.State.ASleep;
onFallenAsleep(object);
sleepState._onWakingUpCallbacks.push(onWakingUp);
sleepState.registerOnWakingUp(onWakingUp);
} else {
awakeObjects[writeIndex] = object;
writeIndex++;
@@ -107,6 +113,7 @@ namespace gdjs {
ASleep,
CanSleepThisFrame,
AWake,
Destroyed,
}
}
}

View File

@@ -1,37 +0,0 @@
/*
* GDevelop JS Platform
* Copyright 2013-2023 Florian Rival (Florian.Rival@gmail.com). All rights reserved.
* This project is released under the MIT License.
*/
/**
* Picked objects lists.
*/
class ObjectsLists extends Hashtable<Array<gdjs.RuntimeObject>> {
/** Is true as soon as an instruction has pick some or every instances */
isPicked: boolean = false;
}
namespace gdjs {
export namespace evtTools {
export namespace objectsLists {
/**
* Construct a ObjectsLists from a JS object.
*
* @param items The content of the Hashtable.
* @returns The new picked objects lists.
*/
export const newFrom = (
items: {
[key: string]: Array<gdjs.RuntimeObject>;
},
isPicked: boolean
): ObjectsLists => {
const hashtable = new ObjectsLists();
hashtable.items = items;
hashtable.isPicked = isPicked;
return hashtable;
};
}
}
}

View File

@@ -13,7 +13,7 @@ namespace gdjs {
_initialBehaviorSharedData: Hashtable<BehaviorSharedData | null>;
/** Contains the instances living on the container */
_instances: Hashtable<ObjectManager>;
_instances: Hashtable<RuntimeObject[]>;
_activeInstances: Array<RuntimeObject> = [];
@@ -36,6 +36,7 @@ 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;
@@ -43,8 +44,6 @@ namespace gdjs {
_debugDrawShowPointsNames: boolean = false;
_debugDrawShowCustomPoints: boolean = false;
_nextPickingId = 1;
constructor() {
this._initialBehaviorSharedData = new Hashtable();
this._instances = new Hashtable();
@@ -181,7 +180,7 @@ namespace gdjs {
*/
registerObject(objectData: ObjectData) {
this._objects.put(objectData.name, objectData);
this._instances.put(objectData.name, new gdjs.ObjectManager());
this._instances.put(objectData.name, []);
// Cache the constructor
const Ctor = gdjs.getObjectConstructor(objectData.type);
@@ -215,7 +214,7 @@ namespace gdjs {
* @param objectName The name of the object to unregister.
*/
unregisterObject(objectName: string) {
const instances = this._instances.get(objectName).getAllInstances();
const instances = this._instances.get(objectName);
if (instances) {
// This is sub-optimal: markObjectForDeletion will search the instance to
// remove in instances, so cost is O(n^2), n being the number of instances.
@@ -354,6 +353,26 @@ 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.
*/
@@ -429,7 +448,7 @@ namespace gdjs {
let currentListSize = 0;
for (const name in this._instances.items) {
if (this._instances.items.hasOwnProperty(name)) {
const list = this._instances.items[name].getAllInstances();
const list = this._instances.items[name];
const oldSize = currentListSize;
currentListSize += list.length;
for (let j = 0, lenj = list.length; j < lenj; ++j) {
@@ -450,14 +469,6 @@ namespace gdjs {
* @returns the instances of a given object in the container.
*/
getInstancesOf(objectName: string): gdjs.RuntimeObject[] {
return this._instances.items[objectName].getAllInstances();
}
/**
* @param objectName The name of the object
* @returns the manager of a given object in the container.
*/
getObjectManager(objectName: string): gdjs.ObjectManager {
return this._instances.items[objectName];
}
@@ -490,13 +501,6 @@ namespace gdjs {
* Update the objects before launching the events.
*/
_updateObjectsPreEvents() {
// Check awake objects only once every 64 frames.
if ((this.getScene().getFrameIndex() & 63) === 0) {
for (const name in this._instances.items) {
const objectManager = this._instances.items[name];
objectManager.updateAwakeObjects();
}
}
// It is *mandatory* to create and iterate on a external list of all objects, as the behaviors
// may delete the objects.
const allInstancesList = this.getActiveInstances();
@@ -543,12 +547,12 @@ namespace gdjs {
* @param obj The object to be added.
*/
addObject(obj: gdjs.RuntimeObject) {
let objectManager = this._instances.get(obj.name);
if (!objectManager) {
objectManager = new gdjs.ObjectManager();
this._instances.put(obj.name, objectManager);
let instances = this._instances.get(obj.name);
if (!instances) {
instances = [];
this._instances.put(obj.name, instances);
}
objectManager.addObject(obj);
instances.push(obj);
this._allInstancesList.push(obj);
if (obj.getLifecycleSleepState().isAwake()) {
this._activeInstances.push(obj);
@@ -571,9 +575,9 @@ namespace gdjs {
name +
'"! Adding it.'
);
this._instances.put(name, new gdjs.ObjectManager());
this._instances.put(name, []);
}
return this._instances.get(name).getAllInstances();
return this._instances.get(name);
}
/**
@@ -620,11 +624,15 @@ namespace gdjs {
}
// Delete from the living instances.
const objectManager = this._instances.get(obj.getName());
if (objectManager) {
const hasDeleted = objectManager.deleteObject(obj);
if (hasDeleted) {
this._allInstancesListIsUpToDate = false;
if (this._instances.containsKey(obj.getName())) {
const objId = obj.id;
const allInstances = this._instances.get(obj.getName());
for (let i = 0, len = allInstances.length; i < len; ++i) {
if (allInstances[i].id == objId) {
allInstances.splice(i, 1);
this._allInstancesListIsUpToDate = false;
break;
}
}
}
@@ -638,8 +646,6 @@ namespace gdjs {
return;
}
onObjectChangedOfLayer(object: RuntimeObject, oldLayer: RuntimeLayer) {}
/**
* Get the layer with the given name
* @param name The name of the layer
@@ -722,7 +728,7 @@ namespace gdjs {
getInstancesCountOnScene(objectName: string): integer {
const instances = this._instances.get(objectName);
if (instances) {
return instances.getAllInstances().length;
return instances.length;
}
return 0;
@@ -732,19 +738,22 @@ namespace gdjs {
* Update the objects positions according to their forces
*/
updateObjectsForces(): void {
for (const name in this._instances.items) {
if (this._instances.items.hasOwnProperty(name)) {
const list = this._instances.items[name].getAwakeInstances();
for (let j = 0, listLen = list.length; j < listLen; ++j) {
const obj = list[j];
if (!obj.hasNoForces()) {
const averageForce = obj.getAverageForce();
const elapsedTimeInSeconds = obj.getElapsedTime() / 1000;
obj.setX(obj.getX() + averageForce.getX() * elapsedTimeInSeconds);
obj.setY(obj.getY() + averageForce.getY() * elapsedTimeInSeconds);
obj.updateForces(elapsedTimeInSeconds);
}
}
for (
let i = 0, listLen = this._activeInstances.length;
i < listLen;
++i
) {
const object = this._activeInstances[i];
if (!object.hasNoForces()) {
const averageForce = object.getAverageForce();
const elapsedTimeInSeconds = object.getElapsedTime() / 1000;
object.setX(
object.getX() + averageForce.getX() * elapsedTimeInSeconds
);
object.setY(
object.getY() + averageForce.getY() * elapsedTimeInSeconds
);
object.updateForces(elapsedTimeInSeconds);
}
}
}
@@ -765,14 +774,5 @@ namespace gdjs {
this._allInstancesList = [];
this._instancesRemoved = [];
}
getNewPickingId(): integer {
const newPickingId = this._nextPickingId;
if (this._nextPickingId === Number.MAX_SAFE_INTEGER) {
this._nextPickingId = 0;
}
this._nextPickingId++;
return newPickingId;
}
}
}

View File

@@ -1,227 +0,0 @@
namespace gdjs {
export namespace evtTools {
export namespace object {
export namespace position {
type SearchArea = {
minX: float;
minY: float;
maxX: float;
maxY: float;
};
const searchArea = { minX: 0, minY: 0, maxX: 0, maxY: 0 };
const nearbyObjects: Array<RuntimeObject> = [];
export const twoListsSpatialCheck = (
instanceContainer: RuntimeInstanceContainer | EventsFunctionContext,
predicate: (
object1: gdjs.RuntimeObject,
object2: gdjs.RuntimeObject,
extraArg: any
) => boolean,
getSearchArea: (
object: gdjs.RuntimeObject,
searchArea: SearchArea,
extraArg: any
) => SearchArea,
objectsLists1: ObjectsLists,
objectsLists2: ObjectsLists,
inverted: boolean,
predicateExtraArg?: any,
areaExtraArg?: any
): boolean => {
const isPicked1 = objectsLists1.isPicked;
const isPicked2 = objectsLists2.isPicked;
if (isPicked1 && isPicked2) {
// Both are already filtered fallback on the naïve check
return gdjs.evtTools.object.twoListsTest(
predicate,
objectsLists1,
objectsLists2,
inverted,
predicateExtraArg
);
}
if (
inverted ||
!(instanceContainer instanceof gdjs.RuntimeInstanceContainer)
) {
// Fast spatial conditions are using objectListOrEmptyIfJustDeclared
// where naive spatial implementation is expecting objectList.
if (!isPicked1) {
fillPickedLists(instanceContainer, objectsLists1);
}
if (!isPicked2) {
fillPickedLists(instanceContainer, objectsLists2);
}
// Both are already filtered fallback on the naïve check
return gdjs.evtTools.object.twoListsTest(
predicate,
objectsLists1,
objectsLists2,
inverted,
predicateExtraArg
);
}
let isAnyObjectPicked = false;
let iteratedLists = isPicked1 ? objectsLists1 : objectsLists2;
let treeLists = isPicked1 ? objectsLists2 : objectsLists1;
let objectsMaxCount1 = 0;
if (!isPicked1) {
for (const objectName in objectsLists1.items) {
const objects = instanceContainer.getObjects(objectName);
objectsMaxCount1 = Math.max(
objectsMaxCount1,
objects ? objects.length : 0
);
}
}
let objectsMaxCount2 = 0;
if (!isPicked2) {
for (const objectName in objectsLists2.items) {
const objects = instanceContainer.getObjects(objectName);
objectsMaxCount2 = Math.max(
objectsMaxCount2,
objects ? objects.length : 0
);
}
}
if (!isPicked1 && !isPicked2) {
if (objectsMaxCount1 < objectsMaxCount2) {
iteratedLists = objectsLists1;
treeLists = objectsLists2;
}
}
if (!iteratedLists.isPicked) {
fillPickedLists(instanceContainer, iteratedLists);
}
const pickingId = instanceContainer.getNewPickingId();
for (const iteratedObjectName in iteratedLists.items) {
const iteratedObjects = iteratedLists.items[iteratedObjectName];
let isAnyIteratedObjectPicked = false;
for (const objectName in treeLists.items) {
const treeObjects = treeLists.items[objectName];
const objectManager = instanceContainer.getObjectManager(
objectName
);
for (const object of iteratedObjects) {
nearbyObjects.length = 0;
objectManager.search(
getSearchArea(object, searchArea, areaExtraArg),
nearbyObjects
);
for (const nearbyObject of nearbyObjects) {
if (predicate(object, nearbyObject, predicateExtraArg)) {
isAnyObjectPicked = true;
isAnyIteratedObjectPicked = true;
object.pickingId = pickingId;
if (nearbyObject.pickingId !== pickingId) {
treeObjects.push(nearbyObject);
nearbyObject.pickingId = pickingId;
}
}
}
}
}
if (isAnyIteratedObjectPicked) {
gdjs.evtTools.object.filterPickedObjectsListWithId(
iteratedObjects,
pickingId
);
} else if (iteratedLists.isPicked) {
iteratedObjects.length = 0;
}
}
return isAnyObjectPicked;
};
const fillPickedLists = (
instanceContainer: RuntimeInstanceContainer | EventsFunctionContext,
objectsLists: ObjectsLists
) => {
for (const objectName in objectsLists.items) {
const pickedObjects = objectsLists.items[objectName];
const allObjects = instanceContainer.getObjects(objectName);
pickedObjects.push.apply(pickedObjects, allObjects);
}
};
const getSearchAreaForDistanceCheck = (
object: gdjs.RuntimeObject,
searchArea: SearchArea,
distance: float
): SearchArea => {
const centerX = object.getX();
const centerY = object.getY();
searchArea.minX = centerX - distance;
searchArea.maxX = centerX + distance;
searchArea.minY = centerY - distance;
searchArea.maxY = centerY + distance;
return searchArea;
};
export const distanceCheck = (
objectsLists1: ObjectsLists,
objectsLists2: ObjectsLists,
distance: float,
inverted: boolean,
instanceContainer:
| gdjs.RuntimeInstanceContainer
| EventsFunctionContext
): boolean => {
return twoListsSpatialCheck(
instanceContainer,
gdjs.evtTools.object._distanceBetweenObjects,
getSearchAreaForDistanceCheck,
objectsLists1,
objectsLists2,
inverted,
distance * distance,
distance
);
};
const getSearchAreaForCollisionCheck = (
object: gdjs.RuntimeObject,
searchArea: SearchArea
): SearchArea => {
const centerX = object.getCenterXInScene();
const centerY = object.getCenterYInScene();
const boundingRadius = Math.sqrt(object.getSqBoundingRadius());
searchArea.minX = centerX - boundingRadius;
searchArea.maxX = centerX + boundingRadius;
searchArea.minY = centerY - boundingRadius;
searchArea.maxY = centerY + boundingRadius;
return searchArea;
};
export const hitBoxesCollisionCheck = (
objectsLists1: ObjectsLists,
objectsLists2: ObjectsLists,
inverted: boolean,
instanceContainer:
| gdjs.RuntimeInstanceContainer
| EventsFunctionContext,
ignoreTouchingEdges: boolean
): boolean => {
return twoListsSpatialCheck(
instanceContainer,
gdjs.RuntimeObject.collisionTest,
getSearchAreaForCollisionCheck,
objectsLists1,
objectsLists2,
inverted,
ignoreTouchingEdges
);
};
}
}
}
}

View File

@@ -388,7 +388,7 @@ namespace gdjs {
};
export const cursorOnObject = function (
objectsLists: ObjectsLists,
objectsLists: Hashtable<gdjs.RuntimeObject[]>,
instanceContainer: gdjs.RuntimeInstanceContainer,
accurate: boolean,
inverted: boolean

View File

@@ -218,25 +218,6 @@ namespace gdjs {
arr.length = finalSize;
};
/**
* Filter in-place the specified array to remove objects for which
* `pick` property is set to false.
*/
export const filterPickedObjectsListWithId = function (
objects: gdjs.RuntimeObject[],
pickingId: integer
) {
let finalSize = 0;
for (let k = 0, lenk = objects.length; k < lenk; ++k) {
const obj = objects[k];
if (obj.pickingId === pickingId) {
objects[finalSize] = obj;
finalSize++;
}
}
objects.length = finalSize;
};
export const hitBoxesCollisionTest = function (
objectsLists1: ObjectsLists,
objectsLists2: ObjectsLists,

View File

@@ -16,6 +16,21 @@ namespace gdjs {
visible: boolean;
};
/**
* Return the squared bounding radius of an object given its width/height and its center of rotation
* (relative to the top-left of the object). The radius is relative to the center of rotation.
*/
const computeSqBoundingRadius = (
width: float,
height: float,
centerX: float,
centerY: float
) => {
const radiusX = Math.max(centerX, width - centerX);
const radiusY = Math.max(centerY, height - centerY);
return Math.pow(radiusX, 2) + Math.pow(radiusY, 2);
};
/**
* Arrays and data structure that are (re)used by
* {@link RuntimeObject.separateFromObjects} to avoid any allocation.
@@ -152,7 +167,6 @@ namespace gdjs {
protected _nameId: integer;
protected _livingOnScene: boolean = true;
protected _lifecycleSleepState: ObjectSleepState;
protected _spatialSearchSleepState: ObjectSleepState;
readonly id: integer;
private destroyCallbacks = new Set<() => void>();
@@ -180,7 +194,6 @@ namespace gdjs {
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;
@@ -238,23 +251,12 @@ namespace gdjs {
);
this._totalForce = new gdjs.Force(0, 0, 0);
this._behaviorsTable = new Hashtable();
this._timers = new Hashtable();
this._lifecycleSleepState = new gdjs.ObjectSleepState(
this,
() => this.isNeedingLifecycleFunctions(),
gdjs.ObjectSleepState.State.ASleep
);
this._rtreeAABB = {
source: this,
minX: 0,
minY: 0,
maxX: 0,
maxY: 0,
};
this._spatialSearchSleepState = new gdjs.ObjectSleepState(
this,
() => false,
gdjs.ObjectSleepState.State.CanSleepThisFrame
);
for (let i = 0; i < objectData.effects.length; ++i) {
scene
.getGame()
@@ -274,7 +276,6 @@ namespace gdjs {
}
this._behaviorsTable.put(autoData.name, behavior);
}
this._timers = new Hashtable();
}
//Common members functions related to the object and its runtimeScene :
@@ -338,6 +339,7 @@ namespace gdjs {
this.aabb.max[1] = 0;
this._variables = new gdjs.VariablesContainer(objectData.variables);
this.clearForces();
this._lifecycleSleepState._reinitialize(gdjs.ObjectSleepState.State.ASleep);
// Reinitialize behaviors.
this._behaviorsTable.clear();
@@ -360,6 +362,7 @@ namespace gdjs {
}
behaviorsCount++;
if (behavior.usesLifecycleFunction()) {
this._lifecycleSleepState.wakeUp();
if (
behaviorsUsingLifecycleFunctionCount < this._activeBehaviors.length
) {
@@ -479,10 +482,6 @@ namespace gdjs {
return this._lifecycleSleepState;
}
getSpatialSearchSleepState(): ObjectSleepState {
return this._spatialSearchSleepState;
}
isAlive(): boolean {
return this._livingOnScene;
}
@@ -497,7 +496,7 @@ namespace gdjs {
if (this._livingOnScene) {
instanceContainer.markObjectForDeletion(this);
this._livingOnScene = false;
this._lifecycleSleepState._forceToSleep();
this._lifecycleSleepState._destroy();
}
}
@@ -553,7 +552,6 @@ namespace gdjs {
// 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);
@@ -818,7 +816,6 @@ namespace gdjs {
oldLayer.getRenderer().remove3DRendererObject(rendererObject3D);
newLayer.getRenderer().add3DRendererObject(rendererObject3D);
}
this._runtimeScene.onObjectChangedOfLayer(this, oldLayer);
}
/**
@@ -1621,18 +1618,6 @@ namespace gdjs {
return this.hitBoxes;
}
/**
* Return the squared bounding radius of an object given its width/height and its center of rotation
* (relative to the top-left of the object). The radius is relative to the center of rotation.
*/
getSqBoundingRadius() {
const centerX = this.getCenterX();
const centerY = this.getCenterY();
const radiusX = Math.max(centerX, this.getWidth() - centerX);
const radiusY = Math.max(centerY, this.getHeight() - centerY);
return radiusX * radiusX + radiusY * radiusY;
}
/**
* Return at least all the hit boxes that overlap a given area.
*
@@ -2478,11 +2463,25 @@ namespace gdjs {
//First check if bounding circle are too far.
const o1centerX = obj1.getCenterX();
const o1centerY = obj1.getCenterY();
const obj1BoundingRadius = Math.sqrt(obj1.getSqBoundingRadius());
const obj1BoundingRadius = Math.sqrt(
computeSqBoundingRadius(
obj1.getWidth(),
obj1.getHeight(),
o1centerX,
o1centerY
)
);
const o2centerX = obj2.getCenterX();
const o2centerY = obj2.getCenterY();
const obj2BoundingRadius = Math.sqrt(obj2.getSqBoundingRadius());
const obj2BoundingRadius = Math.sqrt(
computeSqBoundingRadius(
obj2.getWidth(),
obj2.getHeight(),
o2centerX,
o2centerY
)
);
const o1AbsoluteCenterX = obj1.getDrawableX() + o1centerX;
const o1AbsoluteCenterY = obj1.getDrawableY() + o1centerY;
@@ -2543,7 +2542,12 @@ namespace gdjs {
// First check if bounding circles are too far
const objCenterX = this.getCenterX();
const objCenterY = this.getCenterY();
const objSqBoundingRadius = this.getSqBoundingRadius();
const objSqBoundingRadius = computeSqBoundingRadius(
this.getWidth(),
this.getHeight(),
objCenterX,
objCenterY
);
const rayCenterWorldX = (x + endX) / 2;
const rayCenterWorldY = (y + endY) / 2;

View File

@@ -6,7 +6,6 @@
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.
@@ -43,15 +42,11 @@ namespace gdjs {
// Set to `new gdjs.Profiler()` to have profiling done on the scene.
_onProfilerStopped: null | ((oldProfiler: gdjs.Profiler) => void) = null;
private _frameIndex: integer = 0;
_cachedGameResolutionWidth: integer;
_cachedGameResolutionHeight: integer;
_frameIndex: integer = 0;
_layersCameraCoordinates: Record<string, SearchArea> = {};
_layerObjectManagers = new Map<string, ObjectManager>();
_cameraObjects: Record<string, Array<RuntimeObject>> = {};
/**
* @param runtimeGame The game associated to this scene.
*/
@@ -88,43 +83,6 @@ 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.
@@ -482,26 +440,6 @@ 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)
@@ -511,74 +449,50 @@ 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) {
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);
if (this._frameIndex === 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) {
rendererObject.visible = false;
}
}
}
for (const layerName in this._cameraObjects) {
for (const object of this._cameraObjects[layerName]) {
const rendererObject = object.getRendererObject();
if (rendererObject) {
rendererObject.visible = false;
}
}
}
for (const layerName in this._layers.items) {
const cameraAABB = this._layersCameraCoordinates[layerName];
let cameraObjects = this._cameraObjects[layerName];
if (cameraObjects === undefined) {
cameraObjects = [];
this._cameraObjects[layerName] = cameraObjects;
}
if (!cameraAABB) {
continue;
}
const layerObjectManager = this._layerObjectManagers.get(layerName);
if (!layerObjectManager) {
continue;
}
cameraObjects.length = 0;
layerObjectManager.search(cameraAABB, cameraObjects);
for (const object of cameraObjects) {
const rendererObject = object.getRendererObject();
if (rendererObject) {
rendererObject.visible = !object.isHidden();
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]
);
}
// Update effects, only for visible objects.
if (rendererObject.visible) {

View File

@@ -457,14 +457,21 @@ namespace gdjs {
}
isNeedingLifecycleFunctions(): boolean {
return (
super.isNeedingLifecycleFunctions() ||
(!this.isAnimationPaused() &&
this._animations[this._currentAnimation].directions[
this._currentDirection
].frames.length > 1 &&
!this.hasAnimationEnded())
);
if (super.isNeedingLifecycleFunctions()) {
return true;
}
if (
this.isAnimationPaused() ||
this.hasAnimationEnded() ||
this._currentAnimation >= this._animations.length
) {
return false;
}
const animation = this._animations[this._currentAnimation];
if (this._currentDirection > animation.directions.length) {
return false;
}
return animation.directions[this._currentDirection].frames.length > 1;
}
/**

View File

@@ -13,6 +13,9 @@ declare type float = number;
/** A point in cartesian space. */
declare type FloatPoint = [number, number];
/** A Hastable with the picked objects lists. */
declare type ObjectsLists = Hashtable<gdjs.RuntimeObject[]>;
/**
* Represents the context of the events function (or the behavior method),
* if any. If the JavaScript code is running in a scene, this will be undefined (so you can't use this in a scene).

View File

@@ -36,7 +36,6 @@ module.exports = function (config) {
//GDJS game engine files: (Order is important)
'./newIDE/app/resources/GDJS/Runtime/libs/jshashtable.js',
'./newIDE/app/resources/GDJS/Runtime/ObjectsLists.js',
'./newIDE/app/resources/GDJS/Runtime/logger.js',
'./newIDE/app/resources/GDJS/Runtime/gd.js',
'./newIDE/app/resources/GDJS/Runtime/AsyncTasksManager.js',
@@ -55,7 +54,6 @@ module.exports = function (config) {
'./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',
@@ -81,7 +79,6 @@ module.exports = function (config) {
'./newIDE/app/resources/GDJS/Runtime/events-tools/inputtools.js',
'./newIDE/app/resources/GDJS/Runtime/events-tools/networktools.js',
'./newIDE/app/resources/GDJS/Runtime/events-tools/objecttools.js',
'./newIDE/app/resources/GDJS/Runtime/events-tools/ObjectPositionTools.js',
'./newIDE/app/resources/GDJS/Runtime/events-tools/cameratools.js',
'./newIDE/app/resources/GDJS/Runtime/events-tools/soundtools.js',
'./newIDE/app/resources/GDJS/Runtime/events-tools/storagetools.js',

View File

@@ -13,37 +13,28 @@ describe('gdjs.evtTools.object', function () {
expect(
gdjs.evtTools.object.getPickedInstancesCount(
gdjs.evtTools.objectsLists.newFrom(
{
MyObjectA: [objectA1, objectA2],
MyObjectB: [objectB1],
},
true
)
Hashtable.newFrom({
MyObjectA: [objectA1, objectA2],
MyObjectB: [objectB1],
})
)
).to.be(3);
expect(
gdjs.evtTools.object.getPickedInstancesCount(
gdjs.evtTools.objectsLists.newFrom(
{
MyObjectA: [],
MyObjectB: [],
},
true
)
Hashtable.newFrom({
MyObjectA: [],
MyObjectB: [],
})
)
).to.be(0);
// Also test the deprecated name for this function:
expect(
gdjs.evtTools.object.pickedObjectsCount(
gdjs.evtTools.objectsLists.newFrom(
{
MyObjectA: [objectA1, objectA2],
MyObjectB: [objectB1],
},
true
)
Hashtable.newFrom({
MyObjectA: [objectA1, objectA2],
MyObjectB: [objectB1],
})
)
).to.be(3);
});
@@ -61,58 +52,43 @@ describe('gdjs.evtTools.object', function () {
expect(
gdjs.evtTools.object.getSceneInstancesCount(
runtimeScene,
gdjs.evtTools.objectsLists.newFrom(
{
MyObjectA: [objectA1],
MyObjectB: [objectB1],
},
true
)
Hashtable.newFrom({
MyObjectA: [objectA1],
MyObjectB: [objectB1],
})
)
).to.be(2 + 1);
expect(
gdjs.evtTools.object.getSceneInstancesCount(
runtimeScene,
gdjs.evtTools.objectsLists.newFrom(
{
MyObjectA: [objectA1],
MyObjectB: [],
},
true
)
Hashtable.newFrom({
MyObjectA: [objectA1],
MyObjectB: [],
})
)
).to.be(2 + 1);
expect(
gdjs.evtTools.object.getSceneInstancesCount(
runtimeScene,
gdjs.evtTools.objectsLists.newFrom(
{
MyObjectA: [objectA1],
},
true
)
Hashtable.newFrom({
MyObjectA: [objectA1],
})
)
).to.be(2);
expect(
gdjs.evtTools.object.getSceneInstancesCount(
runtimeScene,
gdjs.evtTools.objectsLists.newFrom(
{
MyObjectA: [],
},
false
)
Hashtable.newFrom({
MyObjectA: [],
})
)
).to.be(2);
expect(
gdjs.evtTools.object.getSceneInstancesCount(
runtimeScene,
gdjs.evtTools.objectsLists.newFrom(
{
MyObjectC: [],
},
false
)
Hashtable.newFrom({
MyObjectC: [],
})
)
).to.be(0);
});
@@ -130,12 +106,9 @@ describe('gdjs.evtTools.object', function () {
runtimeScene.createObject('MyObjectA');
// 1 of 2 instances are picked.
const pickedObjectList = gdjs.evtTools.objectsLists.newFrom(
{
MyObjectA: [objectA1],
},
true
);
const pickedObjectList = Hashtable.newFrom({
MyObjectA: [objectA1],
});
const newObjectA = gdjs.evtTools.object.createObjectOnScene(
runtimeScene,
@@ -161,12 +134,9 @@ describe('gdjs.evtTools.object', function () {
runtimeScene.createObject('MyObjectA');
// 0 of 2 instances are picked.
const pickedObjectList = gdjs.evtTools.objectsLists.newFrom(
{
MyObjectA: [],
},
true
);
const pickedObjectList = Hashtable.newFrom({
MyObjectA: [],
});
const newObjectA = gdjs.evtTools.object.createObjectOnScene(
runtimeScene,
@@ -191,12 +161,9 @@ describe('gdjs.evtTools.object', function () {
const objectA2 = runtimeScene.createObject('MyObjectA');
// All instances are picked.
const pickedObjectList = gdjs.evtTools.objectsLists.newFrom(
{
MyObjectA: [objectA1, objectA2],
},
false
);
const pickedObjectList = Hashtable.newFrom({
MyObjectA: [objectA1, objectA2],
});
const newObjectA = gdjs.evtTools.object.createObjectOnScene(
runtimeScene,
@@ -224,13 +191,10 @@ describe('gdjs.evtTools.object', function () {
runtimeScene.createObject('MyObjectB');
// 2 of 3 instances are picked.
const pickedObjectList = gdjs.evtTools.objectsLists.newFrom(
{
MyObjectA: [objectA1],
MyObjectB: [objectB1],
},
true
);
const pickedObjectList = Hashtable.newFrom({
MyObjectA: [objectA1],
MyObjectB: [objectB1],
});
const newObjectA = gdjs.evtTools.object.createObjectOnScene(
runtimeScene,
@@ -260,13 +224,10 @@ describe('gdjs.evtTools.object', function () {
const objectB1 = runtimeScene.createObject('MyObjectB');
// All instances are picked.
const pickedObjectList = gdjs.evtTools.objectsLists.newFrom(
{
MyObjectA: [objectA1, objectA2],
MyObjectB: [objectB1],
},
false
);
const pickedObjectList = Hashtable.newFrom({
MyObjectA: [objectA1, objectA2],
MyObjectB: [objectB1],
});
const newObjectA = gdjs.evtTools.object.createObjectOnScene(
runtimeScene,

View File

@@ -0,0 +1,78 @@
// @ts-check
describe.only('gdjs.RuntimeScene active objects tests', () => {
const spriteConfiguration = {
name: 'MySprite',
type: 'Sprite',
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,
},
],
},
],
},
],
}
it('can recycle a sprite without duplication in the active objects', () => {
const game = gdjs.getPixiRuntimeGame();
const scene = new gdjs.TestRuntimeScene(game);
scene.registerObject(spriteConfiguration);
let object = scene.createObject('MySprite');
object.resetTimer("MyTimer");
scene.renderAndStep(1000 / 60);
expect(scene._activeInstances.length).to.be(1);
object.deleteFromScene(scene);
scene.renderAndStep(1000 / 60);
expect(scene._activeInstances.length).to.be(0);
// The object is not destroyed because it is recycled.
expect(object.getLifecycleSleepState()._onWakingUpCallbacks.length).to.be(1);
let object2 = scene.createObject('MySprite');
expect(object === object2).to.be(true);
object.resetTimer("MyTimer");
expect(scene._activeInstances.length).to.be(1);
expect(object.getLifecycleSleepState()._onWakingUpCallbacks.length).to.be(1);
});
it('can keep an object awake to handle its timer', () => {
const game = gdjs.getPixiRuntimeGame();
const scene = new gdjs.TestRuntimeScene(game);
scene.registerObject(spriteConfiguration);
let object = scene.createObject('MySprite');
object.resetTimer("MyTimer");
for (let index = 0; index < 60; index++) {
scene.renderAndStep(1000 / 60);
expect(scene._activeInstances.length).to.be(1);
}
object.removeTimer("MyTimer");
for (let index = 0; index < 59; index++) {
scene.renderAndStep(1000 / 60);
expect(scene._activeInstances.length).to.be(1);
}
scene.renderAndStep(1000 / 60);
expect(scene._activeInstances.length).to.be(0);
});
});

View File

@@ -402,7 +402,7 @@ describe('gdjs.SceneStack', () => {
gdjs._unregisterCallback(onRuntimeScenePaused);
});
it.only('can start a layout which assets loading didn\'t stated yet and wait them to finish', async () => {
it('can start a layout which assets loading didn\'t stated yet and wait them to finish', async () => {
const mockedResourceManager = new gdjs.MockedResourceManager();
//@ts-ignore
const runtimeGame = gdjs.getPixiRuntimeGame(gameSettingsWithHeavyResource);

View File

@@ -565,37 +565,6 @@ const getPickedInstancesCount = (objectsLists) => {
return count;
};
/**
* @param {any} objectsContext
* @param {Hashtable<RuntimeObject[]>} objectsLists
*/
const getVisibleInstancesCount = (objectsContext, objectsLists) => {
let count = 0;
const objectNames = [];
objectsLists.keys(objectNames);
const uniqueObjectNames = new Set(objectNames);
for (const objectName of uniqueObjectNames) {
const visibleObjects = objectsContext.getObjects(objectName);
if (visibleObjects) {
count += visibleObjects.length;
}
}
return count;
};
/**
* @param {Hashtable<RuntimeObject[]>} objectsLists
*/
const clearObjectLists = (objectsLists) => {
const lists = [];
objectsLists.values(lists);
for (let i = 0, len = lists.length; i < len; ++i) {
lists[i].length = 0;
}
};
/** A minimal implementation of gdjs.RuntimeScene for testing. */
class RuntimeScene {
constructor(sceneData) {
@@ -737,27 +706,6 @@ class LongLivedObjectsList {
}
}
const clearObjectsLists = (objectsLists) => {
for (const k in objectsLists.items) {
if (objectsLists.items.hasOwnProperty(k)) {
objectsLists.items[k].length = 0;
}
}
};
const addObject = (
objectsLists,
objectName,
object
) => {
if (!objectsLists.isPicked) {
// A picking starts from empty lists.
clearObjectsLists(objectsLists);
objectsLists.isPicked = true;
}
objectsLists.get(objectName).push(object);
};
/**
* Create a minimal mock of GDJS with a RuntimeScene (`gdjs.RuntimeScene`),
* supporting setting a variable, using "Trigger Once" conditions
@@ -781,8 +729,6 @@ function makeMinimalGDJSMock(options) {
createObjectOnScene,
getSceneInstancesCount,
getPickedInstancesCount,
getVisibleInstancesCount,
clearObjectLists,
},
runtimeScene: {
wait: () => new FakeAsyncTask(),
@@ -791,9 +737,6 @@ function makeMinimalGDJSMock(options) {
common: {
resolveAsyncEventsFunction: ({ task }) => task.resolve(),
},
objectsLists: {
addObject,
}
},
registerBehavior: (behaviorTypeName, Ctor) => {
behaviorCtors[behaviorTypeName] = Ctor;

View File

@@ -380,6 +380,41 @@ const generateAllExtensionsSections = extensions => {
return extensionSectionsContent;
};
/**
* @param {Array<any>} extensions The extension (gdEventsFunctionsExtension)
*/
const generateExtensionsMkDocsDotPagesFile = async (extensions) => {
const extensionsByCategory = sortKeys(
groupBy(extensions, pair => pair.getCategory() || 'General')
);
let pagesList = '';
for (const category in extensionsByCategory) {
pagesList += ` - ${category}:\n`;
const extensions = extensionsByCategory[category];
for (const extension of extensions) {
const folderName = getExtensionFolderName(extension.getName());
pagesList += ` - ${extension.getFullName()}: ${folderName}\n`;
}
}
const dotPagesContent = `nav:
- index.md
- search.md
- tiers.md
- Create your own extensions:
- Create a new extension : create.md
- best-practices.md
- share-extension.md
${pagesList} - ...
`;
const extensionsDotPagesFilePath = path.join(extensionsRootPath, '.pages');
await fs.writeFile(extensionsDotPagesFilePath, dotPagesContent);
console.info(` File generated: ${extensionsDotPagesFilePath}`);
};
const generateExtensionsList = async gd => {
let content = `## Extensions list
@@ -400,11 +435,11 @@ Here are listed all the extensions available in GDevelop. The list is divided in
header => header.tier === 'community'
);
const reviewedExtensions = reviewedExtensionShortHeaders.map(
header => project.getEventsFunctionsExtension(header.name)
const reviewedExtensions = reviewedExtensionShortHeaders.map(header =>
project.getEventsFunctionsExtension(header.name)
);
const communityExtensions = communityExtensionShortHeaders.map(
header => project.getEventsFunctionsExtension(header.name)
const communityExtensions = communityExtensionShortHeaders.map(header =>
project.getEventsFunctionsExtension(header.name)
);
content += '## Reviewed extensions\n\n';
@@ -454,6 +489,12 @@ does or inspect its content before using it.
);
}
content += generateAllExtensionsSections(communityExtensions);
await generateExtensionsMkDocsDotPagesFile([
...reviewedExtensions,
...communityExtensions,
]);
project.delete();
return content;
};