/* * GDevelop JS Platform * Copyright 2013-2016 Florian Rival (Florian.Rival@gmail.com). All rights reserved. * This project is released under the MIT License. */ namespace gdjs { export namespace evtTools { export namespace object { /** * Keep only the specified object in the lists of picked objects. * * @param objectsLists The lists of objects to trim * @param runtimeObject The object to keep in the lists */ export const pickOnly = function ( objectsLists: ObjectsLists, runtimeObject: gdjs.RuntimeObject ) { for (const listName in objectsLists.items) { if (objectsLists.items.hasOwnProperty(listName)) { const list = objectsLists.items[listName]; //Be sure not to lose the reference to the original array if (list.indexOf(runtimeObject) === -1) { list.length = 0; } else { list.length = 0; //Be sure not to lose the reference to the original array list.push(runtimeObject); } } } }; /** * Do a test on two tables of objects so as to pick only the pair of objects for which the test is true. * * Note that the predicate method is not called strictly for each pair: When considering a pair of objects, if * these objects have already been marked as picked, the predicate method won't be called again. * * Cost (Worst case, predicate being always false): * Cost(Setting property 'picked' of NbObjList1+NbObjList2 objects to false) * + Cost(predicate)*NbObjList1*NbObjList2 * + Cost(Testing NbObjList1+NbObjList2 booleans) * + Cost(Removing NbObjList1+NbObjList2 objects from all the lists) * * Cost (Best case, predicate being always true): * Cost(Setting property 'picked' of NbObjList1+NbObjList2 objects to false) * + Cost(predicate)*(NbObjList1+NbObjList2) * + Cost(Testing NbObjList1+NbObjList2 booleans) * * * @param predicate The predicate function is called with the two objects to compare, and an optional argument `extraArg` * @param objectsLists1 The first lists of objects * @param objectsLists2 The second lists of objects * @param inverted If `inverted` == true, only the objects of the first table are filtered. * @param extraArg (optional) This argument should be used to avoid declaring the predicate as a closure that would be created and destroyed at each call to twoListsTest (potentially multiple time per frame). */ export const twoListsTest = function ( predicate: ( object1: gdjs.RuntimeObject, object2: gdjs.RuntimeObject, extraArg: any ) => boolean, objectsLists1: ObjectsLists, objectsLists2: ObjectsLists, inverted: boolean, extraArg: any ) { let isTrue = false; const objects1Lists = gdjs.staticArray( gdjs.evtTools.object.twoListsTest ); objectsLists1.values(objects1Lists); const objects2Lists = gdjs.staticArray2( gdjs.evtTools.object.twoListsTest ); objectsLists2.values(objects2Lists); for (let i = 0, leni = objects1Lists.length; i < leni; ++i) { let arr = objects1Lists[i]; for (let k = 0, lenk = arr.length; k < lenk; ++k) { arr[k].pick = false; } } for (let i = 0, leni = objects2Lists.length; i < leni; ++i) { let arr = objects2Lists[i]; for (let k = 0, lenk = arr.length; k < lenk; ++k) { arr[k].pick = false; } } //Launch the function for each object of the first list with each object //of the second list. for (let i = 0, leni = objects1Lists.length; i < leni; ++i) { const arr1 = objects1Lists[i]; for (let k = 0, lenk = arr1.length; k < lenk; ++k) { let atLeastOneObject = false; for (let j = 0, lenj = objects2Lists.length; j < lenj; ++j) { const arr2 = objects2Lists[j]; for (let l = 0, lenl = arr2.length; l < lenl; ++l) { if (arr1[k].pick && arr2[l].pick) { continue; } //Avoid unnecessary costly call to predicate. if ( arr1[k].id !== arr2[l].id && predicate(arr1[k], arr2[l], extraArg) ) { if (!inverted) { isTrue = true; //Pick the objects arr1[k].pick = true; arr2[l].pick = true; } atLeastOneObject = true; } } } if (!atLeastOneObject && inverted) { //For example, the object is not overlapping any other object. isTrue = true; arr1[k].pick = true; } } } //Trim not picked objects from lists. for (let i = 0, leni = objects1Lists.length; i < leni; ++i) { let arr = objects1Lists[i]; let finalSize = 0; for (let k = 0, lenk = arr.length; k < lenk; ++k) { let obj = arr[k]; if (arr[k].pick) { arr[finalSize] = obj; finalSize++; } } arr.length = finalSize; } if (!inverted) { for (let i = 0, leni = objects2Lists.length; i < leni; ++i) { let arr = objects2Lists[i]; let finalSize = 0; for (let k = 0, lenk = arr.length; k < lenk; ++k) { let obj = arr[k]; if (arr[k].pick) { arr[finalSize] = obj; finalSize++; } } arr.length = finalSize; } } return isTrue; }; /** * Filter objects to keep only the one that fullfil the predicate * * Objects that do not fullfil the predicate are removed from objects lists. * * @param predicate The function applied to each object: must return true if the object fulfill the predicate. * @param objectsLists The lists of objects to trim * @param negatePredicate If set to true, the result of the predicate is negated. * @param extraArg Argument passed to the predicate (along with the object). Useful for avoiding relying on temporary closures. * @return true if at least one object fulfill the predicate. */ export const pickObjectsIf = function ( predicate: Function, objectsLists: ObjectsLists, negatePredicate: boolean, extraArg: any ): boolean { let isTrue = false; const lists = gdjs.staticArray(gdjs.evtTools.object.pickObjectsIf); objectsLists.values(lists); // Pick only objects that are fulfilling the predicate. for (let i = 0, leni = lists.length; i < leni; ++i) { const arr = lists[i]; for (let k = 0, lenk = arr.length; k < lenk; ++k) { const object = arr[k]; // @ts-ignore if (negatePredicate ^ predicate(object, extraArg)) { isTrue = true; object.pick = true; } else { object.pick = false; } } } // Trim not picked objects from lists. for (let i = 0, leni = lists.length; i < leni; ++i) { gdjs.evtTools.object.filterPickedObjectsList(lists[i]); } return isTrue; }; /** * Filter in-place the specified array to remove objects for which * `pick` property is set to false. */ export const filterPickedObjectsList = function ( arr: gdjs.RuntimeObject[] ) { let finalSize = 0; for (let k = 0, lenk = arr.length; k < lenk; ++k) { const obj = arr[k]; if (obj.pick) { arr[finalSize] = obj; finalSize++; } } arr.length = finalSize; }; export const hitBoxesCollisionTest = function ( objectsLists1: ObjectsLists, objectsLists2: ObjectsLists, inverted: boolean, instanceContainer: gdjs.RuntimeInstanceContainer, ignoreTouchingEdges: boolean ) { return gdjs.evtTools.object.twoListsTest( gdjs.RuntimeObject.collisionTest, objectsLists1, objectsLists2, inverted, ignoreTouchingEdges ); }; export const _distanceBetweenObjects = function (obj1, obj2, distance) { return obj1.getSqDistanceToObject(obj2) <= distance; }; export const distanceTest = function ( objectsLists1: ObjectsLists, objectsLists2: ObjectsLists, distance: float, inverted: boolean ) { return gdjs.evtTools.object.twoListsTest( gdjs.evtTools.object._distanceBetweenObjects, objectsLists1, objectsLists2, inverted, distance * distance ); }; export const _movesToward = function (obj1, obj2, tolerance) { if (obj1.hasNoForces()) { return false; } let objAngle = Math.atan2( obj2.getDrawableY() + obj2.getCenterY() - (obj1.getDrawableY() + obj1.getCenterY()), obj2.getDrawableX() + obj2.getCenterX() - (obj1.getDrawableX() + obj1.getCenterX()) ); objAngle *= 180 / 3.14159; return ( Math.abs( gdjs.evtTools.common.angleDifference( obj1.getAverageForce().getAngle(), objAngle ) ) <= tolerance / 2 ); }; export const movesTowardTest = function ( objectsLists1: ObjectsLists, objectsLists2: ObjectsLists, tolerance: float, inverted: boolean ) { return gdjs.evtTools.object.twoListsTest( gdjs.evtTools.object._movesToward, objectsLists1, objectsLists2, inverted, tolerance ); }; // Deprecated export const _turnedToward = function (obj1, obj2, tolerance) { let objAngle = Math.atan2( obj2.getDrawableY() + obj2.getCenterY() - (obj1.getDrawableY() + obj1.getCenterY()), obj2.getDrawableX() + obj2.getCenterX() - (obj1.getDrawableX() + obj1.getCenterX()) ); objAngle *= 180 / 3.14159; return ( Math.abs( gdjs.evtTools.common.angleDifference(obj1.getAngle(), objAngle) ) <= tolerance / 2 ); }; // Deprecated export const turnedTowardTest = function ( objectsLists1, objectsLists2, tolerance, inverted ) { return gdjs.evtTools.object.twoListsTest( gdjs.evtTools.object._turnedToward, objectsLists1, objectsLists2, inverted, tolerance ); }; export const _isTurnedTowardObject = function ( obj1: gdjs.RuntimeObject, obj2: gdjs.RuntimeObject, tolerance: float ) { return ( Math.abs( gdjs.evtTools.common.angleDifference( obj1.getAngle(), obj1.getAngleToObject(obj2) ) ) <= tolerance ); }; export const isTurnedTowardObject = function ( objectsLists1: ObjectsLists, objectsLists2: ObjectsLists, tolerance: float, inverted: boolean ) { return gdjs.evtTools.object.twoListsTest( gdjs.evtTools.object._isTurnedTowardObject, objectsLists1, objectsLists2, inverted, tolerance ); }; export const pickAllObjects = function (objectsContext, objectsLists) { for (const name in objectsLists.items) { if (objectsLists.items.hasOwnProperty(name)) { const allObjects = objectsContext.getObjects(name); const objectsList = objectsLists.items[name]; gdjs.copyArray(allObjects, objectsList); } } return true; }; export const pickRandomObject = function ( instanceContainer: gdjs.RuntimeInstanceContainer, objectsLists: ObjectsLists ) { // Compute one many objects we have let objectsCount = 0; for (let listName in objectsLists.items) { if (objectsLists.items.hasOwnProperty(listName)) { let list = objectsLists.items[listName]; objectsCount += list.length; } } if (objectsCount === 0) { return false; } // Pick one random object let index = Math.floor(Math.random() * objectsCount); if (index >= objectsCount) { index = objectsCount - 1; } //Should never happen. // Find the object let startIndex = 0; let theChosenOne: gdjs.RuntimeObject | null = null; for (let listName in objectsLists.items) { if (objectsLists.items.hasOwnProperty(listName)) { let list = objectsLists.items[listName]; if (index - startIndex < list.length) { theChosenOne = list[index - startIndex]; break; } startIndex += list.length; } } // @ts-ignore gdjs.evtTools.object.pickOnly(objectsLists, theChosenOne); return true; }; export const pickNearestObject = function ( objectsLists: ObjectsLists, x: float, y: float, inverted: boolean = false ) { let bestObject = null; let best = 0; let first = true; const lists = gdjs.staticArray(gdjs.evtTools.object.pickNearestObject); objectsLists.values(lists); for (let i = 0, len = lists.length; i < len; ++i) { const list = lists[i]; for (let j = 0; j < list.length; ++j) { const object = list[j]; const distance = object.getSqDistanceToPosition(x, y); if (first || distance < best !== !!inverted) { best = distance; bestObject = object; } first = false; } } if (!bestObject) { return false; } gdjs.evtTools.object.pickOnly(objectsLists, bestObject); return true; }; export const raycastObject = function ( objectsLists: ObjectsLists, x: float, y: float, angle: float, dist: float, varX: gdjs.Variable, varY: gdjs.Variable, inverted: boolean ) { return gdjs.evtTools.object.raycastObjectToPosition( objectsLists, x, y, x + dist * Math.cos((angle * Math.PI) / 180.0), y + dist * Math.sin((angle * Math.PI) / 180.0), varX, varY, inverted ); }; export const raycastObjectToPosition = function ( objectsLists: ObjectsLists, x: float, y: float, endX: float, endY: float, varX: gdjs.Variable, varY: gdjs.Variable, inverted: boolean ) { let matchObject: gdjs.RuntimeObject | null = null; let testSqDist = inverted ? 0 : (endX - x) * (endX - x) + (endY - y) * (endY - y); let resultX = 0; let resultY = 0; const lists: RuntimeObject[][] = gdjs.staticArray( gdjs.evtTools.object.raycastObjectToPosition ); objectsLists.values(lists); for (let i = 0; i < lists.length; i++) { const list = lists[i]; for (let j = 0; j < list.length; j++) { const object = list[j]; const result = object.raycastTest(x, y, endX, endY, !inverted); if (result.collision) { if (!inverted && result.closeSqDist <= testSqDist) { testSqDist = result.closeSqDist; matchObject = object; resultX = result.closeX; resultY = result.closeY; } else { if (inverted && result.farSqDist >= testSqDist) { testSqDist = result.farSqDist; matchObject = object; resultX = result.farX; resultY = result.farY; } } } } } if (!matchObject) { return false; } gdjs.evtTools.object.pickOnly(objectsLists, matchObject); varX.setNumber(resultX); varY.setNumber(resultY); return true; }; /** * Do the work of creating a new object */ export const doCreateObjectOnScene = function ( objectsContext: EventsFunctionContext | gdjs.RuntimeScene, objectName: string, objectsLists: ObjectsLists, x: float, y: float, layerName: string ): gdjs.RuntimeObject | null { // objectsContext will either be the gdjs.RuntimeScene or, in an events function, the // eventsFunctionContext. We can't directly use runtimeScene because the object name could // be different than the real object name (this is the case in a function. The eventsFunctionContext // will take care of this in createObject). const obj = objectsContext.createObject(objectName); const layer = objectsContext.getLayer(layerName); if (obj !== null) { //Do some extra setup obj.setPosition(x, y); obj.setLayer(layerName); obj.setZOrder(layer.getDefaultZOrder()); //Let the new object be picked by next actions/conditions. if (objectsLists.containsKey(objectName)) { objectsLists.get(objectName).push(obj); } } return obj; }; /** * Allows events to create a new object on a scene. */ export const createObjectOnScene = function ( objectsContext: EventsFunctionContext | gdjs.RuntimeScene, objectsLists: ObjectsLists, x: float, y: float, layerName: string ): gdjs.RuntimeObject | null { return gdjs.evtTools.object.doCreateObjectOnScene( objectsContext, objectsLists.firstKey() as string, objectsLists, x, y, layerName ); }; /** * Allows events to create a new object on a scene. */ export const createObjectFromGroupOnScene = function ( objectsContext: EventsFunctionContext | gdjs.RuntimeScene, objectsLists: ObjectsLists, objectName: string, x: float, y: float, layerName: string ) { gdjs.evtTools.object.doCreateObjectOnScene( objectsContext, objectName, objectsLists, x, y, layerName ); }; /** * Return the number of instances in the specified lists of objects. */ export const getPickedInstancesCount = (objectsLists: ObjectsLists) => { let count = 0; const lists = gdjs.staticArray( gdjs.evtTools.object.getPickedInstancesCount ); objectsLists.values(lists); for (let i = 0, len = lists.length; i < len; ++i) { count += lists[i].length; } return count; }; /** * Return the number of instances of the specified objects living on the scene. */ export const getSceneInstancesCount = ( objectsContext: EventsFunctionContext | gdjs.RuntimeScene, objectsLists: ObjectsLists ) => { let count = 0; const objectNames = gdjs.staticArray( gdjs.evtTools.object.getSceneInstancesCount ); objectsLists.keys(objectNames); const uniqueObjectNames = new Set(objectNames); for (const objectName of uniqueObjectNames) { count += objectsContext.getInstancesCountOnScene(objectName); } return count; }; /** @deprecated */ export const pickedObjectsCount = getPickedInstancesCount; } } const logger = new gdjs.Logger('LongLivedObjectsLists'); /** * @category Core Engine > Object */ export type LongLivedObjectsListNetworkSyncData = { objectsLists: { [objectName: string]: Array; }; localVariablesContainers: Array>; }; /** * A container for objects lists that should last more than the current frame. * It automatically removes objects that were destroyed from the objects lists. * @category Core Engine > Object */ export class LongLivedObjectsList { private objectsLists = new Map>(); private localVariablesContainers: Array = []; private callbacks = new Map void>(); private parent: LongLivedObjectsList | null = null; /** * Create a new container for objects lists, inheriting from another one. This is * useful should we get the objects that have not been saved in this context (using * `addObject`) but saved in a parent context. * This avoids to save all object lists every time we create a new `LongLivedObjectsList`, * despite not all objects lists being used. * * @param parent * @returns */ static from(parent: LongLivedObjectsList): LongLivedObjectsList { const newList = new LongLivedObjectsList(); newList.parent = parent; return newList; } private getOrCreateList(objectName: string): RuntimeObject[] { if (!this.objectsLists.has(objectName)) this.objectsLists.set(objectName, []); return this.objectsLists.get(objectName)!; } getObjects(objectName: string): RuntimeObject[] { if (!this.objectsLists.has(objectName) && this.parent) return this.parent.getObjects(objectName); return this.objectsLists.get(objectName) || []; } addObject(objectName: string, runtimeObject: gdjs.RuntimeObject): void { const list = this.getOrCreateList(objectName); if (list.includes(runtimeObject)) return; list.push(runtimeObject); // Register callbacks for when the object is destroyed const onDestroy = () => this.removeObject(objectName, runtimeObject); this.callbacks.set(runtimeObject, onDestroy); runtimeObject.registerDestroyCallback(onDestroy); } removeObject(objectName: string, runtimeObject: gdjs.RuntimeObject): void { const list = this.getOrCreateList(objectName); const index = list.indexOf(runtimeObject); if (index === -1) return; list.splice(index, 1); // Properly remove callbacks to not leak the object runtimeObject.unregisterDestroyCallback( this.callbacks.get(runtimeObject)! ); this.callbacks.delete(runtimeObject); } restoreLocalVariablesContainers( variablesContainers: Array ): void { gdjs.copyArray(this.localVariablesContainers, variablesContainers); } backupLocalVariablesContainers( variablesContainers: Array ): void { gdjs.copyArray(variablesContainers, this.localVariablesContainers); } getNetworkSyncData( syncOptions: GetNetworkSyncDataOptions ): LongLivedObjectsListNetworkSyncData { const objectsLists: { [objectName: string]: Array; } = {}; for (const [objectName, runtimeObjects] of this.objectsLists.entries()) { const objectNetworkIds: Array = []; for (const runtimeObject of runtimeObjects) { const objectNetworkId = runtimeObject.getNetworkId(); if (!objectNetworkId) { logger.warn( 'Tried to get sync data of a LongLivedObjectsList and found an object without a network ID' ); continue; } objectNetworkIds.push(objectNetworkId); } objectsLists[objectName] = objectNetworkIds; } return { objectsLists, localVariablesContainers: this.localVariablesContainers.map( (container) => container.getNetworkSyncData(syncOptions) ), }; } updateFromNetworkSyncData( syncData: LongLivedObjectsListNetworkSyncData, runtimeScene: gdjs.RuntimeScene, syncOptions: UpdateFromNetworkSyncDataOptions ) { const { objectsLists, localVariablesContainers } = syncData; // Clear the current state. this.objectsLists.clear(); this.localVariablesContainers.length = 0; // Restore the list of objects. for (const [objectName, objectNetworkIds] of Object.entries( objectsLists )) { const runtimeObjects = runtimeScene.getObjects(objectName); if (!runtimeObjects) { logger.warn( 'Tried to update sync data of a LongLivedObjectsList but cannot find objects with name: ' + objectName ); continue; } const runtimeObjectsFromSyncData = runtimeObjects.filter( (runtimeObject) => { const runtimeObjectNetworkId = runtimeObject.getNetworkId(); return ( !!runtimeObjectNetworkId && objectNetworkIds.includes(runtimeObjectNetworkId) ); } ); for (const runtimeObject of runtimeObjectsFromSyncData) { this.addObject(objectName, runtimeObject); } } // Restore the local variables containers. this.localVariablesContainers = localVariablesContainers.map( (localVariablesContainer) => { const newContainer = new gdjs.VariablesContainer(); newContainer.updateFromNetworkSyncData( localVariablesContainer, syncOptions ); return newContainer; } ); } } }