diff --git a/examples/starting-3d-camera-monitoring/starting-3d-camera-monitoring.json b/examples/starting-3d-camera-monitoring/starting-3d-camera-monitoring.json index 0f59574ed..33e755e83 100644 --- a/examples/starting-3d-camera-monitoring/starting-3d-camera-monitoring.json +++ b/examples/starting-3d-camera-monitoring/starting-3d-camera-monitoring.json @@ -1717,7 +1717,7 @@ ], "events": [ { - "folded": true, + "folded": false, "type": "BuiltinCommonInstructions::Comment", "color": { "b": 109, @@ -1730,7 +1730,7 @@ "comment": "Disable editor lighting" }, { - "folded": true, + "folded": false, "type": "BuiltinCommonInstructions::Standard", "conditions": [], "actions": [ @@ -1790,7 +1790,7 @@ ] }, { - "folded": true, + "folded": false, "type": "BuiltinCommonInstructions::Comment", "color": { "b": 109, @@ -1803,7 +1803,7 @@ "comment": "Setting object room variables" }, { - "folded": true, + "folded": false, "type": "BuiltinCommonInstructions::ForEach", "object": "RoomArea", "conditions": [ @@ -1858,7 +1858,7 @@ "type": "BuiltinCommonInstructions::Group", "events": [ { - "folded": true, + "folded": false, "type": "BuiltinCommonInstructions::Comment", "color": { "b": 109, @@ -1871,7 +1871,7 @@ "comment": "Change camera" }, { - "folded": true, + "folded": false, "type": "BuiltinCommonInstructions::Standard", "conditions": [ { @@ -1905,7 +1905,7 @@ ], "events": [ { - "folded": true, + "folded": false, "type": "BuiltinCommonInstructions::Standard", "conditions": [ { @@ -1933,7 +1933,7 @@ ] }, { - "folded": true, + "folded": false, "type": "BuiltinCommonInstructions::Standard", "conditions": [ { @@ -1962,7 +1962,7 @@ ] }, { - "folded": true, + "folded": false, "type": "BuiltinCommonInstructions::Comment", "color": { "b": 109, @@ -1975,7 +1975,7 @@ "comment": "Camera transition and effect" }, { - "folded": true, + "folded": false, "type": "BuiltinCommonInstructions::Standard", "conditions": [ { @@ -2056,7 +2056,7 @@ ], "events": [ { - "folded": true, + "folded": false, "type": "BuiltinCommonInstructions::Standard", "conditions": [ { @@ -2123,7 +2123,7 @@ ] }, { - "folded": true, + "folded": false, "type": "BuiltinCommonInstructions::Standard", "conditions": [ { @@ -2183,7 +2183,7 @@ "type": "BuiltinCommonInstructions::Group", "events": [ { - "folded": true, + "folded": false, "type": "BuiltinCommonInstructions::Standard", "conditions": [ { @@ -2208,7 +2208,7 @@ ] }, { - "folded": true, + "folded": false, "type": "BuiltinCommonInstructions::Standard", "conditions": [ { @@ -2246,7 +2246,7 @@ ], "events": [ { - "folded": true, + "folded": false, "type": "BuiltinCommonInstructions::Comment", "color": { "b": 109, @@ -2259,7 +2259,7 @@ "comment": "Setting clock time" }, { - "folded": true, + "folded": false, "type": "BuiltinCommonInstructions::Standard", "conditions": [], "actions": [ @@ -2286,7 +2286,7 @@ ], "events": [ { - "folded": true, + "folded": false, "type": "BuiltinCommonInstructions::Standard", "conditions": [ { @@ -2314,7 +2314,7 @@ ] }, { - "folded": true, + "folded": false, "type": "BuiltinCommonInstructions::Standard", "conditions": [ { @@ -2342,7 +2342,7 @@ ] }, { - "folded": true, + "folded": false, "type": "BuiltinCommonInstructions::Standard", "conditions": [], "actions": [ @@ -2594,6 +2594,7 @@ "type": "gameplay", "source": [ "await harness.goToScene('Game Scene');", + "harness.setTimeoutMs(120000);", "await harness.stepFrames(40);", "const near=(a,b)=>Math.abs(a-b)<0.1;", "const sameAngle=(a,b)=>Math.abs(((a-b+540)%360)-180)<0.1;", @@ -2638,6 +2639,7 @@ "type": "gameplay", "source": [ "await harness.goToScene('Game Scene');", + "harness.setTimeoutMs(120000);", "await harness.stepFrames(40);", "const near=(a,b)=>Math.abs(a-b)<0.1;", "const sameAngle=(a,b)=>Math.abs(((a-b+540)%360)-180)<0.1;", diff --git a/examples/starting-3d-camera-monitoring/theme-slots.json b/examples/starting-3d-camera-monitoring/theme-slots.json new file mode 100644 index 000000000..646e8b16a --- /dev/null +++ b/examples/starting-3d-camera-monitoring/theme-slots.json @@ -0,0 +1,16 @@ +{ + "version": 1, + "objects": { + "scene:Game Scene/Walls": "env.wall", + "scene:Game Scene/Floor": "env.ground.tiled", + "scene:Game Scene/Furniture": "env.crate" + }, + "effects": {}, + "ignoredObjects": [ + "scene:Game Scene/Camera", + "scene:Game Scene/Glass", + "scene:Game Scene/RoomArea", + "object:Light3D::SpotLight3D/Placeholder", + "object:Light3D::PointLight3D/Placeholder" + ] +} diff --git a/examples/starting-first-person-shooter-enemies/README.md b/examples/starting-first-person-shooter-enemies/README.md index 609b0dd43..b0854f666 100644 --- a/examples/starting-first-person-shooter-enemies/README.md +++ b/examples/starting-first-person-shooter-enemies/README.md @@ -1,9 +1,3 @@ # 3D FPS game with Enemies -A 3D physics-based first-person shooter, where the player can look around and turn using the camera, as well as jump and fall within the environment. The scene includes a ground surface, solid platforms the player can land on, and pushable physics boxs that reacts realistically to movement. - -Enemies roam around the playable area using pathfinding to navigate around obstacles, as they try to find the player. Their behavior is controlled by a simple state machine. The enemies use raycasting to check if the player is in their line of sight, and when one detects the player, it stops pathfinding and switches to a chase state. If an enemy collides with the player, the game resets. - -The player holds a 3D gun model that can be fired. Firing uses raycasting to detect where the bullet hits, creating a 3D particle effect at the point of impact. Each shot plays a sound effect and causes the screen to shake. - -This setup works well for any action game where the player is able to shoot and kill enemies. The game supports both keyboard/mouse controls and mobile touch controls. \ No newline at end of file +A 3D physics-based first-person shooter, where the player can look around, walk, jump and fall, on a ground with a platform reached by ramps and pushable physics boxes. The player holds a 3D gun: firing uses raycasting to detect where the bullet hits, with a 3D particle effect at the impact, a sound and a screen shake. Enemies patrol using navmesh pathfinding, chase the player when they see them (raycasting), and are thrown back and killed when shot. If an enemy catches the player, the game resets. This setup works well for any action game where the player shoots and kills enemies. The game supports both keyboard/mouse controls and mobile touch controls. diff --git a/examples/starting-first-person-shooter-enemies/assets/Skybox_Back.png b/examples/starting-first-person-shooter-enemies/assets/Skybox_Back.png index 16e34f3da..aa0891ef2 100644 Binary files a/examples/starting-first-person-shooter-enemies/assets/Skybox_Back.png and b/examples/starting-first-person-shooter-enemies/assets/Skybox_Back.png differ diff --git a/examples/starting-first-person-shooter-enemies/assets/Skybox_Bottom.png b/examples/starting-first-person-shooter-enemies/assets/Skybox_Bottom.png index aa0891ef2..16e34f3da 100644 Binary files a/examples/starting-first-person-shooter-enemies/assets/Skybox_Bottom.png and b/examples/starting-first-person-shooter-enemies/assets/Skybox_Bottom.png differ diff --git a/examples/starting-first-person-shooter-enemies/assets/Skybox_Front.png b/examples/starting-first-person-shooter-enemies/assets/Skybox_Front.png index ce5ae5d6f..018d5faac 100644 Binary files a/examples/starting-first-person-shooter-enemies/assets/Skybox_Front.png and b/examples/starting-first-person-shooter-enemies/assets/Skybox_Front.png differ diff --git a/examples/starting-first-person-shooter-enemies/assets/Skybox_Left.png b/examples/starting-first-person-shooter-enemies/assets/Skybox_Left.png index 497b38fe8..e4053f388 100644 Binary files a/examples/starting-first-person-shooter-enemies/assets/Skybox_Left.png and b/examples/starting-first-person-shooter-enemies/assets/Skybox_Left.png differ diff --git a/examples/starting-first-person-shooter-enemies/assets/Skybox_Right.png b/examples/starting-first-person-shooter-enemies/assets/Skybox_Right.png index a026e8faf..d14c192b1 100644 Binary files a/examples/starting-first-person-shooter-enemies/assets/Skybox_Right.png and b/examples/starting-first-person-shooter-enemies/assets/Skybox_Right.png differ diff --git a/examples/starting-first-person-shooter-enemies/assets/Skybox_Top.png b/examples/starting-first-person-shooter-enemies/assets/Skybox_Top.png index 377476a78..e83ef6a1b 100644 Binary files a/examples/starting-first-person-shooter-enemies/assets/Skybox_Top.png and b/examples/starting-first-person-shooter-enemies/assets/Skybox_Top.png differ diff --git a/examples/starting-first-person-shooter-enemies/assets/StartingCollisionArea.png b/examples/starting-first-person-shooter-enemies/assets/StartingCollisionArea.png deleted file mode 100644 index 443292ad3..000000000 Binary files a/examples/starting-first-person-shooter-enemies/assets/StartingCollisionArea.png and /dev/null differ diff --git a/examples/starting-first-person-shooter-enemies/starting-first-person-shooter-enemies.json b/examples/starting-first-person-shooter-enemies/starting-first-person-shooter-enemies.json index 435f37b36..3a6da8536 100644 --- a/examples/starting-first-person-shooter-enemies/starting-first-person-shooter-enemies.json +++ b/examples/starting-first-person-shooter-enemies/starting-first-person-shooter-enemies.json @@ -14,7 +14,7 @@ "orientation": "landscape", "packageName": "com.example.gamename", "pixelsRounding": false, - "projectUuid": "9edc4cea-3ab0-48ef-ac50-2b22ec45ae68", + "projectUuid": "1aee5f1c-4305-41f3-974d-17f9d43bd2f6", "scaleMode": "linear", "sizeOnStartupMode": "", "templateSlug": "starting-first-person", @@ -262,14 +262,6 @@ "name": "assets/Skybox_Right.png", "smoothed": true, "userAdded": false - }, - { - "file": "assets/StartingCollisionArea.png", - "kind": "image", - "metadata": "", - "name": "assets/StartingCollisionArea.png", - "smoothed": true, - "userAdded": false } ] }, @@ -959,36 +951,6 @@ "numberProperties": [], "stringProperties": [], "initialVariables": [] - }, - { - "angle": 0, - "customSize": true, - "height": 1, - "layer": "", - "name": "PathfindingObstacle", - "persistentUuid": "42bf5101-5e19-4173-9286-729cf80cd2ad", - "width": 1152, - "x": 64, - "y": 0, - "zOrder": 0, - "numberProperties": [], - "stringProperties": [], - "initialVariables": [] - }, - { - "angle": 0, - "customSize": true, - "height": 16, - "layer": "", - "name": "PathfindingObstacle", - "persistentUuid": "0b7620d7-4ec1-404b-a177-1148d7a37e28", - "width": 1152, - "x": 64, - "y": -464, - "zOrder": 0, - "numberProperties": [], - "stringProperties": [], - "initialVariables": [] } ], "objects": [ @@ -1000,12 +962,6 @@ "variables": [], "effects": [], "behaviors": [ - { - "name": "NavMeshObstacle", - "type": "NavMeshPathfinding::NavMeshObstacleBehavior", - "shape": "Box", - "meshShapeResourceName": "" - }, { "name": "Physics3D", "type": "Physics3D::Physics3DBehavior", @@ -1033,6 +989,13 @@ "massCenterOffsetY": 0, "massCenterOffsetZ": 0, "massOverride": 0 + }, + { + "name": "NavMeshObstacle", + "type": "NavMeshPathfinding::NavMeshObstacleBehavior", + "shape": "Box", + "meshShapeResourceName": "", + "obstacleOnly": false } ], "content": { @@ -1102,6 +1065,13 @@ "massCenterOffsetY": 0, "massCenterOffsetZ": 0, "massOverride": 0 + }, + { + "name": "NavMeshObstacle", + "type": "NavMeshPathfinding::NavMeshObstacleBehavior", + "shape": "Box", + "meshShapeResourceName": "", + "obstacleOnly": false } ], "content": { @@ -1143,12 +1113,6 @@ "variables": [], "effects": [], "behaviors": [ - { - "name": "PathfindingObstacle", - "type": "PathfindingBehavior::PathfindingObstacleBehavior", - "cost": 2, - "impassable": true - }, { "isFolded": true, "name": "Physics3D", @@ -1177,6 +1141,13 @@ "massCenterOffsetY": 0, "massCenterOffsetZ": 0, "massOverride": 0 + }, + { + "name": "NavMeshObstacle", + "type": "NavMeshPathfinding::NavMeshObstacleBehavior", + "shape": "Box", + "meshShapeResourceName": "", + "obstacleOnly": true } ], "content": { @@ -1573,31 +1544,36 @@ "persistentUuid": "1b008b3c-c7a5-4199-9f5c-cf5b0cbfef41", "type": "string", "value": "Chasing" + }, + { + "name": "DestinationX", + "type": "number", + "value": 0 + }, + { + "name": "DestinationY", + "type": "number", + "value": 0 } ], "effects": [], "behaviors": [ { - "name": "Pathfinding", - "type": "PathfindingBehavior::PathfindingBehavior", - "maxSpeed": 180, - "acceleration": 400, - "allowDiagonals": true, - "angleOffset": 0, + "name": "NavMeshCharacter", + "type": "NavMeshPathfinding::NavMeshCharacterBehavior", + "acceleration": 1500, + "maxSpeed": 120, "angularMaxSpeed": 180, - "cellHeight": 16, - "cellWidth": 16, - "extraBorder": 0, - "gridOffsetX": 0, - "gridOffsetY": 0, "rotateObject": true, - "smoothingMaxCellGap": 1 + "angleOffset": 0, + "radius": 0, + "avoidanceSightRange": 120 }, { "name": "Physics3D", "type": "Physics3D::Physics3DBehavior", "object3D": "Object3D", - "bodyType": "Dynamic", + "bodyType": "Kinematic", "bullet": false, "fixedRotation": true, "shape": "Capsule", @@ -1899,52 +1875,6 @@ ] } } - }, - { - "assetStoreId": "", - "name": "PathfindingObstacle", - "persistentUuid": "e73ee1f0-3ce9-483f-a141-3e3449409016", - "type": "Scene3D::Cube3DObject", - "variables": [], - "effects": [], - "behaviors": [ - { - "name": "PathfindingObstacle", - "type": "PathfindingBehavior::PathfindingObstacleBehavior", - "cost": 2, - "impassable": true - } - ], - "content": { - "width": 100, - "height": 100, - "depth": 100, - "enableTextureTransparency": false, - "facesOrientation": "Y", - "frontFaceResourceName": "assets/StartingCollisionArea.png", - "backFaceResourceName": "assets/StartingCollisionArea.png", - "backFaceUpThroughWhichAxisRotation": "X", - "leftFaceResourceName": "assets/StartingCollisionArea.png", - "rightFaceResourceName": "assets/StartingCollisionArea.png", - "topFaceResourceName": "assets/StartingCollisionArea.png", - "bottomFaceResourceName": "assets/StartingCollisionArea.png", - "frontFaceVisible": true, - "backFaceVisible": true, - "leftFaceVisible": true, - "rightFaceVisible": true, - "topFaceVisible": true, - "bottomFaceVisible": true, - "frontFaceResourceRepeat": true, - "backFaceResourceRepeat": true, - "leftFaceResourceRepeat": true, - "rightFaceResourceRepeat": true, - "topFaceResourceRepeat": true, - "bottomFaceResourceRepeat": true, - "materialType": "StandardWithoutMetalness", - "tint": "255;255;255", - "isCastingShadow": false, - "isReceivingShadow": false - } } ], "objectsFolderStructure": { @@ -1959,18 +1889,6 @@ { "objectName": "Enemy" }, - { - "objectName": "PushableBox" - }, - { - "objectName": "PathfindingObstacle" - }, - { - "objectName": "Obstacle" - }, - { - "objectName": "Ground" - }, { "objectName": "ControlsToggle" }, @@ -1993,6 +1911,20 @@ "objectName": "JumpButton" } ] + }, + { + "folderName": "World", + "children": [ + { + "objectName": "Ground" + }, + { + "objectName": "Obstacle" + }, + { + "objectName": "PushableBox" + } + ] } ] }, @@ -2017,19 +1949,11 @@ "parameters": [ "Player" ] - }, - { - "type": { - "value": "Hide" - }, - "parameters": [ - "PathfindingObstacle" - ] } ], "events": [ { - "folded": true, + "folded": false, "type": "BuiltinCommonInstructions::Standard", "conditions": [], "actions": [ @@ -2070,7 +1994,7 @@ ] }, { - "folded": true, + "folded": false, "type": "BuiltinCommonInstructions::Standard", "conditions": [], "actions": [ @@ -2133,7 +2057,7 @@ "type": "BuiltinCommonInstructions::Group", "events": [ { - "folded": true, + "folded": false, "type": "BuiltinCommonInstructions::Standard", "conditions": [ { @@ -2246,7 +2170,7 @@ ], "events": [ { - "folded": true, + "folded": false, "type": "BuiltinCommonInstructions::Standard", "conditions": [ { @@ -2288,7 +2212,19 @@ ], "events": [ { - "folded": true, + "type": "BuiltinCommonInstructions::Comment", + "color": { + "b": 109, + "g": 230, + "r": 255, + "textB": 0, + "textG": 0, + "textR": 0 + }, + "comment": "While it walks, the navmesh moves the enemy: its physics body is kinematic, so the player bumps into it without pushing it. When it is shot, it becomes dynamic, to be thrown by the impulse at the impact and to fall." + }, + { + "folded": false, "type": "BuiltinCommonInstructions::Standard", "conditions": [ { @@ -2320,10 +2256,20 @@ }, "parameters": [ "Enemy", - "Pathfinding", + "NavMeshCharacter", "no" ] }, + { + "type": { + "value": "Physics3D::Physics3DBehavior::SetBodyType" + }, + "parameters": [ + "Enemy", + "Physics3D", + "\"Dynamic\"" + ] + }, { "type": { "value": "Physics3D::Physics3DBehavior::SetFixedRotation" @@ -2386,7 +2332,7 @@ "type": "BuiltinCommonInstructions::Group", "events": [ { - "folded": true, + "folded": false, "type": "BuiltinCommonInstructions::Comment", "color": { "b": 109, @@ -2399,7 +2345,7 @@ "comment": "Patrolling logic" }, { - "folded": true, + "folded": false, "type": "BuiltinCommonInstructions::ForEach", "object": "Enemy", "conditions": [ @@ -2447,11 +2393,22 @@ "Enemy", "\"PositionChange\"" ] + }, + { + "type": { + "value": "NavMeshPathfinding::NavMeshCharacterBehavior::SetMaxSpeed" + }, + "parameters": [ + "Enemy", + "NavMeshCharacter", + "=", + "120" + ] } ] }, { - "folded": true, + "folded": false, "type": "BuiltinCommonInstructions::Standard", "conditions": [ { @@ -2480,91 +2437,135 @@ "actions": [ { "type": { - "value": "PathfindingBehavior::SetDestination" + "value": "NavMeshPathfinding::MoveTo" }, "parameters": [ "Enemy", - "Pathfinding", - "", + "NavMeshCharacter", "Player.CenterX()", - "Player.CenterY()" + "Player.CenterY()", + "Player.Object3D::Z()" ] } ], "events": [ { - "folded": true, + "folded": false, "type": "BuiltinCommonInstructions::Standard", "conditions": [ { "type": { "inverted": true, - "value": "PathfindingBehavior::PathFound" + "value": "NavMeshPathfinding::PathFound" }, "parameters": [ "Enemy", - "Pathfinding" + "NavMeshCharacter" ] } ], "actions": [ { "type": { - "value": "PathfindingBehavior::SetDestination" + "value": "SetNumberObjectVariable" }, "parameters": [ "Enemy", - "Pathfinding", - "", - "Player.CenterX()+RandomInRange(-32,32)", + "DestinationX", + "=", + "Player.CenterX()+RandomInRange(-32,32)" + ] + }, + { + "type": { + "value": "SetNumberObjectVariable" + }, + "parameters": [ + "Enemy", + "DestinationY", + "=", "Player.CenterY()+RandomInRange(-32,32)" ] + }, + { + "type": { + "value": "NavMeshPathfinding::MoveTo" + }, + "parameters": [ + "Enemy", + "NavMeshCharacter", + "Enemy.DestinationX", + "Enemy.DestinationY", + "Player.Object3D::Z()" + ] } ], "events": [ { - "folded": true, + "folded": false, "type": "BuiltinCommonInstructions::Standard", "conditions": [ { "type": { "inverted": true, - "value": "PathfindingBehavior::PathFound" + "value": "NavMeshPathfinding::PathFound" }, "parameters": [ "Enemy", - "Pathfinding" + "NavMeshCharacter" ] } ], "actions": [ { "type": { - "value": "PathfindingBehavior::SetDestination" + "value": "SetNumberObjectVariable" }, "parameters": [ "Enemy", - "Pathfinding", - "", - "RandomInRange(Ground.BoundingBoxLeft(), Ground.BoundingBoxRight())", + "DestinationX", + "=", + "RandomInRange(Ground.BoundingBoxLeft(), Ground.BoundingBoxRight())" + ] + }, + { + "type": { + "value": "SetNumberObjectVariable" + }, + "parameters": [ + "Enemy", + "DestinationY", + "=", "RandomInRange(Ground.BoundingBoxTop(), Ground.BoundingBoxBottom())" ] + }, + { + "type": { + "value": "NavMeshPathfinding::MoveTo" + }, + "parameters": [ + "Enemy", + "NavMeshCharacter", + "Enemy.DestinationX", + "Enemy.DestinationY", + "Ground.Object3D::Z() + Ground.Object3D::Depth()" + ] } ] } ] }, { - "folded": true, + "folded": false, "type": "BuiltinCommonInstructions::Standard", "conditions": [ { "type": { - "value": "PathfindingBehavior::PathFound" + "value": "NavMeshPathfinding::PathFound" }, "parameters": [ "Enemy", - "Pathfinding" + "NavMeshCharacter" ] } ], @@ -2583,7 +2584,7 @@ ] }, { - "folded": true, + "folded": false, "type": "BuiltinCommonInstructions::Comment", "color": { "b": 109, @@ -2596,7 +2597,7 @@ "comment": "Chasing logic" }, { - "folded": true, + "folded": false, "type": "BuiltinCommonInstructions::ForEach", "object": "Enemy", "conditions": [ @@ -2680,7 +2681,7 @@ ] }, { - "folded": true, + "folded": false, "type": "BuiltinCommonInstructions::Standard", "conditions": [ { @@ -2698,7 +2699,7 @@ "actions": [], "events": [ { - "folded": true, + "folded": false, "type": "BuiltinCommonInstructions::Standard", "conditions": [ { @@ -2711,54 +2712,105 @@ "actions": [ { "type": { - "value": "PathfindingBehavior::SetDestination" + "value": "NavMeshPathfinding::NavMeshCharacterBehavior::SetMaxSpeed" }, "parameters": [ "Enemy", - "Pathfinding", - "", - "Enemy.X()", - "Enemy.Y()" + "NavMeshCharacter", + "=", + "180" + ] + }, + { + "type": { + "value": "NavMeshPathfinding::MoveTo" + }, + "parameters": [ + "Enemy", + "NavMeshCharacter", + "Player.CenterX()", + "Player.CenterY()", + "Player.Object3D::Z()" + ] + }, + { + "type": { + "value": "ResetObjectTimer" + }, + "parameters": [ + "Enemy", + "\"Repath\"" ] } ] }, { - "folded": true, - "type": "BuiltinCommonInstructions::Standard", - "conditions": [], + "folded": false, + "type": "BuiltinCommonInstructions::ForEach", + "conditions": [ + { + "type": { + "value": "CompareObjectTimer" + }, + "parameters": [ + "Enemy", + "\"Repath\"", + ">=", + "0.5" + ] + }, + { + "type": { + "value": "BuiltinCommonInstructions::CompareNumbers" + }, + "parameters": [ + "DistanceBetweenPositions(Enemy.NavMeshCharacter::DestinationX(), Enemy.NavMeshCharacter::DestinationY(), Player.CenterX(), Player.CenterY())", + ">", + "40" + ] + } + ], "actions": [ { "type": { - "value": "Physics3D::Physics3DBehavior::ApplyForceTowardPosition" + "value": "NavMeshPathfinding::NavMeshCharacterBehavior::SetMaxSpeed" }, "parameters": [ "Enemy", - "Physics3D", - "60", - "Player.CenterX()", - "Player.CenterY()", - "Enemy.Object3D::Z()" + "NavMeshCharacter", + "=", + "180" ] }, { "type": { - "value": "RotateTowardPosition" + "value": "NavMeshPathfinding::MoveTo" }, "parameters": [ "Enemy", + "NavMeshCharacter", "Player.CenterX()", "Player.CenterY()", - "180", - "" + "Player.Object3D::Z()" + ] + }, + { + "type": { + "value": "ResetObjectTimer" + }, + "parameters": [ + "Enemy", + "\"Repath\"" ] } - ] + ], + "object": "Enemy", + "events": [] } ] }, { - "folded": true, + "folded": false, "type": "BuiltinCommonInstructions::Comment", "color": { "b": 109, @@ -2771,7 +2823,7 @@ "comment": "Player dies" }, { - "folded": true, + "folded": false, "type": "BuiltinCommonInstructions::Standard", "conditions": [ { @@ -2825,7 +2877,7 @@ "type": "BuiltinCommonInstructions::Group", "events": [ { - "folded": true, + "folded": false, "type": "BuiltinCommonInstructions::Standard", "conditions": [ { @@ -2841,7 +2893,7 @@ "actions": [], "events": [ { - "folded": true, + "folded": false, "type": "BuiltinCommonInstructions::Comment", "color": { "b": 109, @@ -2854,7 +2906,7 @@ "comment": "Set to Touch" }, { - "folded": true, + "folded": false, "type": "BuiltinCommonInstructions::Standard", "conditions": [ { @@ -2919,7 +2971,7 @@ ] }, { - "folded": true, + "folded": false, "type": "BuiltinCommonInstructions::Comment", "color": { "b": 109, @@ -2932,7 +2984,7 @@ "comment": "Set to Keyboard" }, { - "folded": true, + "folded": false, "type": "BuiltinCommonInstructions::Standard", "conditions": [ { @@ -3011,7 +3063,7 @@ "type": "BuiltinCommonInstructions::Group", "events": [ { - "folded": true, + "folded": false, "type": "BuiltinCommonInstructions::Standard", "conditions": [ { @@ -3040,7 +3092,7 @@ "actions": [], "events": [ { - "folded": true, + "folded": false, "type": "BuiltinCommonInstructions::Standard", "conditions": [ { @@ -3070,7 +3122,7 @@ ] }, { - "folded": true, + "folded": false, "type": "BuiltinCommonInstructions::Standard", "conditions": [ { @@ -3092,7 +3144,7 @@ "actions": [], "events": [ { - "folded": true, + "folded": false, "type": "BuiltinCommonInstructions::Standard", "conditions": [ { @@ -3160,7 +3212,7 @@ "type": "BuiltinCommonInstructions::Group", "events": [ { - "folded": true, + "folded": false, "type": "BuiltinCommonInstructions::Standard", "conditions": [ { @@ -3303,6 +3355,7 @@ "frontFaceResourceName": "assets/Skybox_Front.png", "leftFaceResourceName": "assets/Skybox_Left.png", "rightFaceResourceName": "assets/Skybox_Right.png", + "top": "Z+", "topFaceResourceName": "assets/Skybox_Top.png" }, "booleanParameters": {} @@ -3433,10 +3486,6 @@ "name": "MultitouchButton", "type": "SpriteMultitouchJoystick::MultitouchButton" }, - { - "name": "NavMeshObstacle", - "type": "NavMeshPathfinding::NavMeshObstacleBehavior" - }, { "name": "Object3D", "type": "Scene3D::Base3DBehavior" @@ -3445,14 +3494,6 @@ "name": "Opacity", "type": "OpacityCapability::OpacityBehavior" }, - { - "name": "Pathfinding", - "type": "PathfindingBehavior::PathfindingBehavior" - }, - { - "name": "PathfindingObstacle", - "type": "PathfindingBehavior::PathfindingObstacleBehavior" - }, { "name": "Physics3D", "type": "Physics3D::Physics3DBehavior", @@ -3484,6 +3525,21 @@ { "name": "Tween", "type": "Tween::TweenBehavior" + }, + { + "name": "NavMeshCharacter", + "type": "NavMeshPathfinding::NavMeshCharacterBehavior", + "cellSize": 10, + "cellDepth": 2, + "slopeMaxAngle": 40, + "stairHeightMax": 20, + "walkableRadius": -1, + "speedScaleY": 1, + "walkableDepth": 150 + }, + { + "name": "NavMeshObstacle", + "type": "NavMeshPathfinding::NavMeshObstacleBehavior" } ] } @@ -3500,6 +3556,7 @@ "type": "gameplay", "source": [ "await harness.goToScene('Game Scene');", + "harness.setTimeoutMs(120000);", "const probe = await harness.resetSceneAndProbeControls('Player', ['w','a','s','d'], {frames:20});", "for (const key of ['w','a','s','d']) {", " const p = probe.keys[key];", @@ -3542,6 +3599,7 @@ "type": "gameplay", "source": [ "await harness.goToScene('Game Scene');", + "harness.setTimeoutMs(120000);", "const enemies = harness.getNearby('Enemy','Player',3000);", "harness.assert(enemies.length>0,'The scene contains enemies that can be tested');", "if (!enemies.length) return;", @@ -3578,6 +3636,114 @@ "await harness.takeScreenshot('Enemy removed');", "harness.releaseAllInputs();" ] + }, + { + "name": "A shot enemy is pushed away by the impact", + "description": "Puts an enemy in front of the player and shoots it: the impulse at the impact throws it back, tumbling.", + "type": "gameplay", + "source": [ + "await harness.goToScene('Game Scene');", + "// Faster in the software renderer used to run tests (grass is thousands of blades, trees are drawn in batches it is slow with).", + "harness.setGameResolutionSize(160, 90);", + "for (const name of [\"MobileControls\",\"UI\",\"Crosshair Layer\"]) {", + " if (!harness.getCurrentRuntimeScene().hasLayer(name)) continue;", + " const layer = harness.getCurrentRuntimeScene().getLayer(name);", + " layer.setCameraX(640);", + " layer.setCameraY(360);", + " layer.setCameraZoom(160 / 1280);", + "}", + "harness.getCurrentRuntimeScene().getLayer('').setEffectBooleanParameter('3D Global Light', 'isCastingShadow', false);", + "harness.setTimeoutMs(60000);", + "const DT = { dtMs: 50 };", + "await harness.stepFrames(4, DT);", + "const player = harness.getObjects('Player')[0];", + "const [monster, ...otherMonsters] = harness.getObjects('Enemy');", + "for (const other of otherMonsters) harness.removeObject(other.id);", + "// The player looks toward -Y: the enemy stands in front of the crosshair.", + "const monsterX = player.centerX;", + "const monsterY = player.centerY - 250;", + "harness.setObjectPosition(monster.id, monsterX, monsterY, 0);", + "harness.setMousePositionScreen(harness.getGameResolutionWidth() / 2, harness.getGameResolutionHeight() / 2);", + "await harness.stepFrames(3, DT);", + "const runtimeMonster = harness.getRuntimeObject(monster.id);", + "const distanceToPlayer = () => Math.hypot(runtimeMonster.getCenterXInScene() - player.centerX, runtimeMonster.getCenterYInScene() - player.centerY);", + "const startDistance = distanceToPlayer();", + "harness.setMouseButtonPressed(true);", + "await harness.stepFrames(1, DT);", + "harness.setMouseButtonPressed(false);", + "let mostTipped = 0;", + "for (let frame = 0; frame < 12; frame++) {", + " await harness.stepFrames(1, DT);", + " mostTipped = Math.max(mostTipped, Math.abs(runtimeMonster.getRotationX()) + Math.abs(runtimeMonster.getRotationY()));", + "}", + "console.log('distance to the player: ' + startDistance.toFixed(0) + ' -> ' + distanceToPlayer().toFixed(0) + ', most tipped: ' + mostTipped.toFixed(0));", + "harness.assert(runtimeMonster.getVariables().get('State').getAsString() === 'Dying', 'The enemy was shot.');", + "harness.assert(distanceToPlayer() > startDistance + 60, 'The enemy was thrown back (' + startDistance.toFixed(0) + ' -> ' + distanceToPlayer().toFixed(0) + ').');", + "harness.assert(mostTipped > 30, 'The enemy tumbles (' + mostTipped.toFixed(0) + ' degrees).');" + ] + }, + { + "name": "A patrolling enemy walks toward the player", + "description": "Puts an enemy far from the player, out of sight: it patrols toward the player on the navmesh, staying on the ground.", + "type": "gameplay", + "source": [ + "await harness.goToScene('Game Scene');", + "// Faster in the software renderer used to run tests.", + "harness.setGameResolutionSize(160, 90);", + "harness.getCurrentRuntimeScene().getLayer('').setEffectBooleanParameter('3D Global Light', 'isCastingShadow', false);", + "harness.setTimeoutMs(60000);", + "const DT = { dtMs: 50 };", + "const player = harness.getObjects('Player')[0];", + "const [enemy, ...otherEnemies] = harness.getObjects('Enemy');", + "for (const other of otherEnemies) harness.removeObject(other.id);", + "// Behind the platform, more than 600 pixels away from the player: it patrols.", + "harness.setObjectPosition(enemy.id, 1600, -700, 0);", + "await harness.stepFrames(2, DT);", + "const getEnemy = () => harness.getObjects('Enemy').find((one) => one.id === enemy.id);", + "const distanceToPlayer = () => Math.hypot(getEnemy().centerX - player.centerX, getEnemy().centerY - player.centerY);", + "const startDistance = distanceToPlayer();", + "const getState = () => getEnemy().variables.find((variable) => variable.name === 'State').value;", + "harness.assert(getState() === 'Patrolling', 'The enemy patrols (' + getState() + ').');", + "// The patrol picks a new destination after 3 seconds, then every 3 seconds.", + "await harness.stepFrames(160, DT);", + "const endDistance = distanceToPlayer();", + "console.log('distance to the player: ' + startDistance.toFixed(0) + ' -> ' + endDistance.toFixed(0) + ', z=' + getEnemy().z.toFixed(1));", + "harness.assert(endDistance < startDistance - 400, 'The enemy walked toward the player (' + startDistance.toFixed(0) + ' -> ' + endDistance.toFixed(0) + ').');" + ] + }, + { + "name": "An enemy that sees the player chases and catches them", + "description": "Puts an enemy in front of the player: it sees the player, runs to them, and catching the player restarts the scene.", + "type": "gameplay", + "source": [ + "await harness.goToScene('Game Scene');", + "// Faster in the software renderer used to run tests.", + "harness.setGameResolutionSize(160, 90);", + "harness.getCurrentRuntimeScene().getLayer('').setEffectBooleanParameter('3D Global Light', 'isCastingShadow', false);", + "harness.setTimeoutMs(60000);", + "const DT = { dtMs: 50 };", + "await harness.stepFrames(2, DT);", + "const scene = harness.getCurrentRuntimeScene();", + "const player = harness.getObjects('Player')[0];", + "const [enemy, ...otherEnemies] = harness.getObjects('Enemy');", + "for (const other of otherEnemies) harness.removeObject(other.id);", + "harness.setObjectPosition(player.id, 615, 900, 0);", + "harness.setObjectPosition(enemy.id, 640, 650, 0);", + "const navMeshCharacter = harness.getRuntimeObject(enemy.id).getBehavior('NavMeshCharacter');", + "let highestZ = 0;", + "let mostNodes = 0;", + "let caught = false;", + "for (let frame = 0; frame < 80; frame++) {", + " await harness.stepFrames(1, DT);", + " caught = harness.getCurrentRuntimeScene() !== scene;", + " if (caught) break;", + " const now = harness.getObjects('Enemy').find((one) => one.id === enemy.id);", + " highestZ = Math.max(highestZ, now.z);", + " mostNodes = Math.max(mostNodes, navMeshCharacter.getNodeCount());", + "}", + "console.log('caught=' + caught + ' highestZ=' + highestZ.toFixed(1) + ' mostNodes=' + mostNodes);", + "harness.assert(caught, 'The enemy caught the player, which restarted the scene.');" + ] } ], "eventsFunctionsExtensions": [ @@ -4569,8 +4735,8 @@ "iconUrl": "data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTgiPz4NCjwhLS0gR2VuZXJhdG9yOiBBZG9iZSBJbGx1c3RyYXRvciAyMy4wLjMsIFNWRyBFeHBvcnQgUGx1Zy1JbiAuIFNWRyBWZXJzaW9uOiA2LjAwIEJ1aWxkIDApICAtLT4NCjxzdmcgdmVyc2lvbj0iMS4xIiBpZD0iSWNvbnMiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiIHg9IjBweCIgeT0iMHB4Ig0KCSB2aWV3Qm94PSIwIDAgMzIgMzIiIHN0eWxlPSJlbmFibGUtYmFja2dyb3VuZDpuZXcgMCAwIDMyIDMyOyIgeG1sOnNwYWNlPSJwcmVzZXJ2ZSI+DQo8c3R5bGUgdHlwZT0idGV4dC9jc3MiPg0KCS5zdDB7ZmlsbDpub25lO3N0cm9rZTojMDAwMDAwO3N0cm9rZS13aWR0aDoyO3N0cm9rZS1saW5lY2FwOnJvdW5kO3N0cm9rZS1saW5lam9pbjpyb3VuZDtzdHJva2UtbWl0ZXJsaW1pdDoxMDt9DQo8L3N0eWxlPg0KPGNpcmNsZSBjbGFzcz0ic3QwIiBjeD0iMTYiIGN5PSIxNiIgcj0iMTMiLz4NCjxwb2x5bGluZSBjbGFzcz0ic3QwIiBwb2ludHM9IjI4LjQsMTIgMjAsMTIgMjAsMy42ICIvPg0KPHBvbHlsaW5lIGNsYXNzPSJzdDAiIHBvaW50cz0iMjAsMjguNCAyMCwyMCAyOC40LDIwICIvPg0KPHBvbHlsaW5lIGNsYXNzPSJzdDAiIHBvaW50cz0iMy42LDIwIDEyLDIwIDEyLDI4LjQgIi8+DQo8cG9seWxpbmUgY2xhc3M9InN0MCIgcG9pbnRzPSIxMiwzLjYgMTIsMTIgMy42LDEyICIvPg0KPHBvbHlnb24gY2xhc3M9InN0MCIgcG9pbnRzPSIxNiw2IDE2LjcsNyAxNS4zLDcgIi8+DQo8cG9seWdvbiBjbGFzcz0ic3QwIiBwb2ludHM9IjE2LDI2IDE1LjMsMjUgMTYuNywyNSAiLz4NCjxwb2x5Z29uIGNsYXNzPSJzdDAiIHBvaW50cz0iNiwxNiA3LDE1LjMgNywxNi43ICIvPg0KPHBvbHlnb24gY2xhc3M9InN0MCIgcG9pbnRzPSIyNiwxNiAyNSwxNi43IDI1LDE1LjMgIi8+DQo8L3N2Zz4NCg==", "name": "SpriteMultitouchJoystick", "previewIconUrl": "https://resources.gdevelop-app.com/assets/Icons/Line Hero Pack/Master/SVG/Videogames/Videogames_controller_joystick_arrows_direction.svg", - "shortDescription": "Joysticks or buttons for touchscreens.", - "version": "1.9.0", + "shortDescription": "On-screen multitouch joystick and buttons.", + "version": "1.9.2", "description": [ "Multitouch joysticks are objects showing a joystick on the screen, useful for mobile. They work like a physical gamepad:", "- 4 or 8 directions", @@ -5372,7 +5538,7 @@ "objectGroups": [] }, { - "description": "the direction index (left = 1, bottom = 1, right = 2, top = 3) for an angle (in degrees).", + "description": "the direction index (right = 0, bottom = 1, left = 2, top = 3) for an angle (in degrees).", "fullName": "Angle to 4-way index", "functionType": "ExpressionAndCondition", "name": "AngleTo4Way", @@ -5407,7 +5573,7 @@ "objectGroups": [] }, { - "description": "the direction index (left = 1, bottom-left = 1... top-left = 7) for an angle (in degrees).", + "description": "the direction index (right = 0, bottom-right = 1... top-right = 7) for an angle (in degrees).", "fullName": "Angle to 8-way index", "functionType": "ExpressionAndCondition", "name": "AngleTo8Way", @@ -10363,7 +10529,7 @@ "parameters": [ "Object", "PhysicsCar3D", - "SpriteMultitouchJoystick::StickForceX(ControllerIdentifier, \"Primary\")" + "SpriteMultitouchJoystick::StickForceX(ControllerIdentifier, SteerJoystickIdentifier)" ] } ] @@ -10393,7 +10559,7 @@ "parameters": [ "Object", "PhysicsCar3D", - "-SpriteMultitouchJoystick::StickForceY(ControllerIdentifier, \"Secondary\")" + "-SpriteMultitouchJoystick::StickForceY(ControllerIdentifier, SpeedJoystickIdentifier)" ] } ] @@ -11144,6 +11310,7 @@ "areaMinX": 0, "areaMinY": 0, "areaMinZ": 0, + "assetStoreTag": "multitouch joysticks", "defaultName": "Joystick", "description": "Joystick for touchscreens.", "fullName": "Multitouch Joystick", @@ -12577,15 +12744,13 @@ "propertyName": "ShouldBeHiddenWhenReleased" } ] - }, - "variants": [] + } } ] }, { "author": "PANDAKO-GitHub", "category": "Input", - "dimension": "", "extensionNamespace": "", "fullName": "Mouse Pointer Lock", "gdevelopVersion": ">=5.5.222", @@ -12593,8 +12758,8 @@ "iconUrl": "data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTgiPz4NCjwhLS0gR2VuZXJhdG9yOiBBZG9iZSBJbGx1c3RyYXRvciAyMy4wLjMsIFNWRyBFeHBvcnQgUGx1Zy1JbiAuIFNWRyBWZXJzaW9uOiA2LjAwIEJ1aWxkIDApICAtLT4NCjxzdmcgdmVyc2lvbj0iMS4xIiBpZD0iSWNvbnMiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiIHg9IjBweCIgeT0iMHB4Ig0KCSB2aWV3Qm94PSIwIDAgMzIgMzIiIHN0eWxlPSJlbmFibGUtYmFja2dyb3VuZDpuZXcgMCAwIDMyIDMyOyIgeG1sOnNwYWNlPSJwcmVzZXJ2ZSI+DQo8c3R5bGUgdHlwZT0idGV4dC9jc3MiPg0KCS5zdDB7ZmlsbDpub25lO3N0cm9rZTojMDAwMDAwO3N0cm9rZS13aWR0aDoyO3N0cm9rZS1saW5lY2FwOnJvdW5kO3N0cm9rZS1saW5lam9pbjpyb3VuZDtzdHJva2UtbWl0ZXJsaW1pdDoxMDt9DQo8L3N0eWxlPg0KPHBvbHlsaW5lIGNsYXNzPSJzdDAiIHBvaW50cz0iMTksMTkgMjQsMTkgMjQsMjQgIi8+DQo8cG9seWxpbmUgY2xhc3M9InN0MCIgcG9pbnRzPSI2LDIzIDExLDIzIDExLDE4ICIvPg0KPHBhdGggY2xhc3M9InN0MCIgZD0iTTI0LDE5LjRjLTAuNywwLjgtMS40LDEuNi0yLjIsMi40Yy03LDctMTUuMywxMC4yLTE4LjUsN3MtMC4xLTExLjUsNy0xOC41czE1LjMtMTAuMiwxOC41LTcNCgljMS40LDEuNCwxLjYsMy42LDAuOCw2LjMiLz4NCjxwYXRoIGNsYXNzPSJzdDAiIGQ9Ik0xMSwyMi41Yy0wLjMtMC4yLTAuNS0wLjUtMC44LTAuOGMtNy03LTEwLjItMTUuMy03LTE4LjVzMTEuNS0wLjEsMTguNSw3czEwLjIsMTUuMyw3LDE4LjUNCgljLTEuNywxLjctNC44LDEuNi04LjQsMC4xIi8+DQo8L3N2Zz4NCg==", "name": "MousePointerLock", "previewIconUrl": "https://resources.gdevelop-app.com/assets/Icons/Line Hero Pack/Master/SVG/Virtual Reality/Virtual Reality_360_rotate_vr_movement.svg", - "shortDescription": "This behavior removes the limit on the distance the mouse can move and hides the cursor.", - "version": "0.3.0", + "shortDescription": "Lock and hide mouse pointer for unlimited movement (e.g., FPS mouse look).", + "version": "0.3.1", "description": [ "This behavior removes the limit on the distance the mouse can move and hides the cursor.", "", @@ -13046,7 +13211,7 @@ "functionType": "Condition", "name": "isEmulatingPointerLock", "private": true, - "sentence": "The mouse pointer is lock is emulated", + "sentence": "The mouse pointer lock is emulated", "events": [ { "type": "BuiltinCommonInstructions::Standard", @@ -13363,68 +13528,12 @@ "objectGroups": [] } ], - "eventsFunctionsFolderStructure": { - "folderName": "__ROOT", - "children": [ - { - "functionName": "onFirstSceneLoaded" - }, - { - "functionName": "onScenePostEvents" - }, - { - "functionName": "RequestPointerLock" - }, - { - "functionName": "ExitPointerLock" - }, - { - "functionName": "isPointerLocked" - }, - { - "functionName": "isPointerActuallyLocked" - }, - { - "functionName": "isEmulatingPointerLock" - }, - { - "functionName": "IsMoving" - }, - { - "functionName": "MovementX" - }, - { - "functionName": "SetMovementX" - }, - { - "functionName": "MovementY" - }, - { - "functionName": "SetMovementY" - }, - { - "functionName": "TouchX" - }, - { - "functionName": "TouchY" - }, - { - "functionName": "TouchSpeedFactor" - }, - { - "functionName": "SetTouchSpeedFactor" - } - ] - }, "eventsBasedBehaviors": [ { "description": "Control camera rotations with a mouse.", "fullName": "First person camera mouse mapper", - "helpPath": "", - "iconUrl": "", "name": "FirstPersonPointerMapper", "objectType": "", - "previewIconUrl": "", "eventsFunctions": [ { "fullName": "", @@ -13699,7 +13808,6 @@ "fullName": "", "functionType": "ActionWithOperator", "getterName": "HorizontalSpeed", - "group": "First person camera mouse mapper configuration", "name": "SetHorizontalSpeed", "sentence": "", "events": [ @@ -13780,7 +13888,6 @@ "fullName": "", "functionType": "ActionWithOperator", "getterName": "VerticalSpeed", - "group": "First person camera mouse mapper configuration", "name": "SetVerticalSpeed", "sentence": "", "events": [ @@ -13861,7 +13968,6 @@ "fullName": "", "functionType": "ActionWithOperator", "getterName": "VerticalAngleMin", - "group": "First person camera mouse mapper configuration", "name": "SetVerticalAngleMin", "sentence": "", "events": [ @@ -13942,7 +14048,6 @@ "fullName": "", "functionType": "ActionWithOperator", "getterName": "VerticalAngleMax", - "group": "First person camera mouse mapper configuration", "name": "SetVerticalAngleMax", "sentence": "", "events": [ @@ -14023,7 +14128,6 @@ "fullName": "", "functionType": "ActionWithOperator", "getterName": "OffsetZ", - "group": "First person camera mouse mapper configuration", "name": "SetOffsetZ", "sentence": "", "events": [ @@ -14060,57 +14164,6 @@ "objectGroups": [] } ], - "eventsFunctionsFolderStructure": { - "folderName": "__ROOT", - "children": [ - { - "functionName": "doStepPreEvents" - }, - { - "folderName": "Layers and cameras", - "children": [ - { - "functionName": "LookFromObjectEyes" - } - ] - }, - { - "folderName": "First person camera mouse mapper configuration", - "children": [ - { - "functionName": "HorizontalSpeed" - }, - { - "functionName": "SetHorizontalSpeed" - }, - { - "functionName": "VerticalSpeed" - }, - { - "functionName": "SetVerticalSpeed" - }, - { - "functionName": "VerticalAngleMin" - }, - { - "functionName": "SetVerticalAngleMin" - }, - { - "functionName": "VerticalAngleMax" - }, - { - "functionName": "SetVerticalAngleMax" - }, - { - "functionName": "OffsetZ" - }, - { - "functionName": "SetOffsetZ" - } - ] - } - ] - }, "propertyDescriptors": [ { "value": "", @@ -14164,32 +14217,7 @@ "name": "ShouldLock" } ], - "propertiesFolderStructure": { - "folderName": "__ROOT", - "children": [ - { - "propertyName": "Object3D" - }, - { - "propertyName": "HorizontalSpeed" - }, - { - "propertyName": "VerticalSpeed" - }, - { - "propertyName": "VerticalAngleMin" - }, - { - "propertyName": "VerticalAngleMax" - }, - { - "propertyName": "OffsetZ" - }, - { - "propertyName": "ShouldLock" - } - ] - } + "sharedPropertyDescriptors": [] } ], "eventsBasedObjects": [] @@ -29427,12 +29455,14 @@ "name": "ParticleEmitter3D", "previewIconUrl": "https://asset-resources.gdevelop.io/public-resources/Icons/f2e5a34bf465f781866677762d385d6c8e9e8d203383f2df9a3b7e0fad6a2cb5_fire.svg", "shortDescription": "Display a large number of particles in 3D to create visual effects in a 3D game.", - "version": "3.1.1", + "version": "3.2.1", "description": [ - "3D particle emitters let you create and display many small particles to simulate visual effects in your game — like fire, explosions, smoke, or dust.", + "3D particle emitters let you create and display many small particles to simulate visual effects in your game — like fire, explosions, smoke, dust, magic, rain or snow.", "", "The parameters of the object can be configured in multiple different ways to create a specific desired effect. GDevelop will give a set of a pre-configured 3D particle emitters that you should try first.", "", + "Particles are emitted along the local Z axis of the emitter: upward by default. Rotate the instance to aim the emission (for instance a rotation of 180° on the X axis emits downward, 90° on the Y axis emits toward X). The size of the instance has no effect: particles are emitted from a single point, or from a disc when the \"Emission zone radius\" property is set (useful for rain or snow falling over a whole area).", + "", "Use these 3D particle emitters in 3D games. For 2D games or particles that appear on a game's 2D user interface, use the 2D particle emitter object instead." ], "origin": { @@ -29446,7 +29476,10 @@ "fire", "smoke", "splash", - "light" + "light", + "rain", + "snow", + "weather" ], "authorIds": [ "IWykYNRvhCZBN3vEgKEbBPOR3Oc2" @@ -29532,6 +29565,8 @@ " cone;", " /** @type {THREE.LineSegments} */", " centerLine;", + " /** @type {THREE.LineSegments} */", + " zoneCircle;", " /** @type {THREE.Mesh} */", " originBox;", "", @@ -29575,6 +29610,23 @@ " );", " this.add(this.centerLine);", "", + " const zoneCircleGeometry = new THREE.BufferGeometry();", + " const zoneCirclePositions = [];", + " for (let i = 0, j = 1, l = 32; i < l; i++ , j++) {", + " const p1 = (i / l) * Math.PI * 2;", + " const p2 = (j / l) * Math.PI * 2;", + " zoneCirclePositions.push(", + " Math.cos(p1), Math.sin(p1), 0,", + " Math.cos(p2), Math.sin(p2), 0", + " );", + " }", + " zoneCircleGeometry.setAttribute('position', new THREE.Float32BufferAttribute(zoneCirclePositions, 3));", + " this.zoneCircle = new THREE.LineSegments(", + " zoneCircleGeometry,", + " new THREE.LineBasicMaterial({ fog: false, toneMapped: false })", + " );", + " this.add(this.zoneCircle);", + "", " this.originBox = new THREE.Mesh(", " new THREE.BoxGeometry(16, 16, 16),", " new THREE.MeshBasicMaterial({ fog: false, toneMapped: false })", @@ -29590,6 +29642,8 @@ " this.cone.material.dispose();", " this.centerLine.geometry.dispose();", " this.centerLine.material.dispose();", + " this.zoneCircle.geometry.dispose();", + " this.zoneCircle.material.dispose();", " this.originBox.geometry.dispose();", " this.originBox.material.dispose();", " }", @@ -29598,6 +29652,11 @@ " const coneWidth = coneLength * Math.sin(gdjs.toRad(this.object._getSpayConeAngle()));", " const coneHeight = coneLength * Math.cos(gdjs.toRad(this.object._getSpayConeAngle()));", " this.cone.scale.set(coneWidth, coneWidth, coneHeight);", + " const zoneRadius = this.object._getZoneRadius();", + " this.zoneCircle.visible = zoneRadius > 0;", + " if (zoneRadius > 0) {", + " this.zoneCircle.scale.set(zoneRadius, zoneRadius, 1);", + " }", " const startColor = gdjs.rgbOrHexStringToNumber(this.object._getStartColor())", " const endColor = gdjs.rgbOrHexStringToNumber(this.object._getEndColor());", " this.cone.material.color.set(endColor);", @@ -29638,6 +29697,11 @@ " constructor(inverseWorldScale, particleSystem, colorOverLife, sizeOverLife, applyForce, widthOverLength) {", " this.inverseWorldScale = inverseWorldScale;", " this.particleSystem = particleSystem;", + " this.sprayConeAngle = 0;", + " this.zoneRadius = 0;", + " this.startSpeedMin = 0;", + " this.startSpeedMax = 0;", + " this.gravity = 0;", " this.colorOverLife = colorOverLife;", " this.sizeOverLife = sizeOverLife;", " this.applyForce = applyForce;", @@ -29667,7 +29731,7 @@ " if (!startLength) {", " return;", " }", - " startLength.a = startLengthMin * this.inverseWorldScale;", + " startLength.a = startLengthMin;", " }", "", " /**", @@ -29678,7 +29742,7 @@ " if (!startLength) {", " return;", " }", - " startLength.b = startLengthMax * this.inverseWorldScale;", + " startLength.b = startLengthMax;", " }", "", " /**", @@ -29769,14 +29833,31 @@ " * @param startSpeedMin {number}", " */", " setStartSpeedMin(startSpeedMin) {", - " this.particleSystem.startSpeed.a = startSpeedMin * this.inverseWorldScale;", + " this.startSpeedMin = startSpeedMin;", + " this.updateStartSpeed();", " }", "", " /**", " * @param startSpeedMax {number}", " */", " setStartSpeedMax(startSpeedMax) {", - " this.particleSystem.startSpeed.b = startSpeedMax * this.inverseWorldScale;", + " this.startSpeedMax = startSpeedMax;", + " this.updateStartSpeed();", + " }", + "", + " /**", + " * Velocities of particles in world space are in the Three.js world units,", + " * so they need the world scale. Velocities of relative particles are in", + " * the emitter units, converted by the emitter transformation.", + " */", + " getVelocityScale() {", + " return this.particleSystem.worldSpace ? this.inverseWorldScale : 1;", + " }", + "", + " updateStartSpeed() {", + " const velocityScale = this.getVelocityScale();", + " this.particleSystem.startSpeed.a = this.startSpeedMin * velocityScale;", + " this.particleSystem.startSpeed.b = this.startSpeedMax * velocityScale;", " }", "", " /**", @@ -29805,22 +29886,41 @@ " */", " setParticlesRelative(areParticlesRelative) {", " this.particleSystem.worldSpace = !areParticlesRelative;", + " this.updateStartSpeed();", + " this.updateGravity();", " }", "", " /**", " * @param sprayConeAngle {number}", " */", " setSprayConeAngle(sprayConeAngle) {", - " if (0 < sprayConeAngle && sprayConeAngle <= 180) {", - " if (!this.particleSystem.emitterShape.angle) {", + " this.sprayConeAngle = sprayConeAngle;", + " this.updateEmitterShape();", + " }", + "", + " /**", + " * @param zoneRadius {number}", + " */", + " setZoneRadius(zoneRadius) {", + " this.zoneRadius = zoneRadius;", + " this.updateEmitterShape();", + " }", + "", + " updateEmitterShape() {", + " // The emitter transformation converts the shape to the Three.js world units.", + " const zoneRadius = Math.max(0, this.zoneRadius);", + " // Without a zone, a spray angle of 0 emits in every direction from a point.", + " // With a zone, it emits straight from every point of the disc.", + " const isCone = this.sprayConeAngle <= 180 && (0 < this.sprayConeAngle || 0 < zoneRadius);", + " if (isCone) {", + " if (this.particleSystem.emitterShape.type !== 'cone') {", " this.particleSystem.emitterShape = new ConeEmitter({ radius: 0.1, angle: Math.PI / 8 });", " }", - " this.particleSystem.emitterShape.angle = sprayConeAngle * Math.PI / 180;", + " this.particleSystem.emitterShape.angle = Math.max(0, this.sprayConeAngle) * Math.PI / 180;", + " this.particleSystem.emitterShape.radius = zoneRadius || 0.1;", " }", - " else {", - " if (this.particleSystem.emitterShape.angle) {", - " this.particleSystem.emitterShape = new PointEmitter();", - " }", + " else if (this.particleSystem.emitterShape.type !== 'point') {", + " this.particleSystem.emitterShape = new PointEmitter();", " }", " }", "", @@ -29843,7 +29943,12 @@ " * @param gravity {number}", " */", " setGravity(gravity) {", - " this.applyForce.magnitude.value = gravity * this.inverseWorldScale;", + " this.gravity = gravity;", + " this.updateGravity();", + " }", + "", + " updateGravity() {", + " this.applyForce.magnitude.value = this.gravity * this.getVelocityScale();", " }", "", " /**", @@ -36374,7 +36479,7 @@ "areaMinZ": 0, "assetStoreTag": "3d particles", "defaultName": "ParticleEmitter", - "description": "Display a large number of particles to create visual effects.", + "description": "Display a large number of particles to create visual effects (fire, explosions, smoke, magic, rain, snow...). Particles are emitted along the emitter's local Z axis (upward by default): rotate the instance to aim them (180° on the X axis emits downward). The instance size has no effect: use the \"Emission zone radius\" property to emit from a whole area.", "fullName": "3D particle emitter", "helpPath": "", "iconUrl": "", @@ -36610,7 +36715,9 @@ " layer.getRenderer().add3DRendererObject(particleSystem.emitter);", "}", "", - "particleSystem.emitter.updateMatrixWorld(true);", + "// Update the parents too, so that the first particles are emitted with the", + "// scale of the layer (the 3D renderer world scale).", + "particleSystem.emitter.updateWorldMatrix(true, true);", "", "", "// See doStepPostEvents", @@ -36983,6 +37090,17 @@ "" ] }, + { + "type": { + "value": "ParticleEmitter3D::ParticleEmitter3D::SetZoneRadius" + }, + "parameters": [ + "Object", + "=", + "Object.ZoneRadius()", + "" + ] + }, { "type": { "value": "ParticleEmitter3D::ParticleEmitter3D::SetGravity" @@ -38964,12 +39082,12 @@ "objectGroups": [] }, { - "description": "the spay cone angle of the object.", - "fullName": "Spay cone angle", + "description": "the spray cone angle of the object.", + "fullName": "Spray cone angle", "functionType": "ExpressionAndCondition", "group": "3D particle emitter configuration", "name": "SpayConeAngle", - "sentence": "the spay cone angle", + "sentence": "the spray cone angle", "events": [ { "type": "BuiltinCommonInstructions::Standard", @@ -39047,6 +39165,90 @@ ], "objectGroups": [] }, + { + "description": "the emission zone radius of the object: particles are emitted from a disc of this radius (0 emits from a single point).", + "fullName": "Emission zone radius", + "functionType": "ExpressionAndCondition", + "group": "3D particle emitter configuration", + "name": "ZoneRadius", + "sentence": "the emission zone radius", + "events": [ + { + "type": "BuiltinCommonInstructions::Standard", + "conditions": [], + "actions": [ + { + "type": { + "value": "SetReturnNumber" + }, + "parameters": [ + "ZoneRadius" + ] + } + ] + } + ], + "expressionType": { + "type": "expression" + }, + "parameters": [ + { + "description": "Object", + "name": "Object", + "supplementaryInformation": "ParticleEmitter3D::ParticleEmitter3D", + "type": "object" + } + ], + "objectGroups": [] + }, + { + "fullName": "", + "functionType": "ActionWithOperator", + "getterName": "ZoneRadius", + "group": "3D particle emitter configuration", + "name": "SetZoneRadius", + "sentence": "", + "events": [ + { + "type": "BuiltinCommonInstructions::Standard", + "conditions": [], + "actions": [ + { + "type": { + "value": "SetNumberVariable" + }, + "parameters": [ + "ZoneRadius", + "=", + "Value" + ] + } + ] + }, + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "const object = objects[0];\r", + "const zoneRadius = eventsFunctionContext.getArgument(\"Value\");\r", + "\r", + "object.__particleEmitterAdapter.setZoneRadius(zoneRadius);\r", + "" + ], + "parameterObjects": "Object", + "useStrict": true, + "eventsSheetExpanded": true + } + ], + "parameters": [ + { + "description": "Object", + "name": "Object", + "supplementaryInformation": "ParticleEmitter3D::ParticleEmitter3D", + "type": "object" + } + ], + "objectGroups": [] + }, { "description": "the blending of the object.", "fullName": "Blending", @@ -40100,6 +40302,12 @@ { "functionName": "SetSpayConeAngle" }, + { + "functionName": "ZoneRadius" + }, + { + "functionName": "SetZoneRadius" + }, { "functionName": "ShouldAutodestruct" }, @@ -40247,7 +40455,8 @@ "unit": "PixelSpeed", "label": "Start min speed", "group": "Speed", - "name": "StartSpeedMin" + "name": "StartSpeedMin", + "description": "Initial speed of the particles, in the emission direction (the local Z axis of the emitter)." }, { "value": "100", @@ -40276,22 +40485,34 @@ "type": "Number", "unit": "Second", "label": "Emission duration", - "name": "Duration" + "name": "Duration", + "description": "0 emits forever." }, { "value": "", "type": "Boolean", "label": "Particles move with the emitter", "group": "Speed", - "name": "AreParticlesRelative" + "name": "AreParticlesRelative", + "description": "When checked, the particles already emitted follow the emitter when it moves or rotates. Leave it unchecked for rain, snow or smoke that must stay where it was emitted while the emitter follows the camera or a character." }, { "value": "30", "type": "Number", "unit": "DegreeAngle", - "label": "Spay cone angle", + "label": "Spray cone angle", + "group": "Position", + "name": "SpayConeAngle", + "description": "Spread of the particles around the emission direction, which is the local Z axis of the emitter (upward by default, rotate the instance to aim it). 0 or more than 180 emits in every direction, unless an emission zone radius is set: then 0 emits straight." + }, + { + "value": "0", + "type": "Number", + "unit": "Pixel", + "label": "Emission zone radius", + "description": "Particles are emitted from a disc of this radius, perpendicular to the emission direction. 0 emits from a single point. Use a large radius (like 1000) for rain or snow falling over a whole area.", "group": "Position", - "name": "SpayConeAngle" + "name": "ZoneRadius" }, { "value": "Additive", @@ -40320,7 +40541,8 @@ "value": "None" } ], - "name": "Blending" + "name": "Blending", + "description": "Additive makes particles glow (fire, magic, energy). Use Normal for rain, snow, smoke or dust. Only applied when the object is created." }, { "value": "Y-", @@ -40337,7 +40559,8 @@ "value": "Z+" } ], - "name": "GravityTop" + "name": "GravityTop", + "description": "Z+: gravity pulls particles down along the Z axis (the usual choice in 3D games). Y-: gravity pulls particles toward Y+." }, { "value": "0", @@ -40345,13 +40568,15 @@ "unit": "PixelAcceleration", "label": "Gravity", "group": "Speed", - "name": "Gravity" + "name": "Gravity", + "description": "Acceleration pulling the particles in the direction given by the gravity top. 0 disables gravity." }, { "value": "true", "type": "Boolean", "label": "Delete when emission ends", - "name": "ShouldAutodestruct" + "name": "ShouldAutodestruct", + "description": "Delete the object once the emission duration is over and every particle disappeared. Uncheck it and set the emission duration to 0 for a continuous effect like fire, rain or snow." }, { "value": "Center-center", @@ -40392,20 +40617,22 @@ { "value": "120", "type": "Number", - "unit": "Pixel", + "unit": "Dimensionless", "label": "Start min length", "group": "Trail", "advanced": true, - "name": "TrailStartLengthMin" + "name": "TrailStartLengthMin", + "description": "Number of past positions kept to draw the trail, one per frame: the visible length is this number multiplied by the distance traveled by the particle each frame." }, { "value": "120", "type": "Number", - "unit": "Pixel", + "unit": "Dimensionless", "label": "Start max length", "group": "Trail", "advanced": true, - "name": "TrailStartLengthMax" + "name": "TrailStartLengthMax", + "description": "Number of past positions kept to draw the trail, one per frame: the visible length is this number multiplied by the distance traveled by the particle each frame." }, { "value": "Billboard", @@ -40422,7 +40649,8 @@ } ], "advanced": true, - "name": "RenderMode" + "name": "RenderMode", + "description": "\"Image facing the camera\" draws each particle as an image. \"Image following a trail\" stretches the image along the path of each particle (sparks, rain streaks)." }, { "value": "", @@ -40540,6 +40768,9 @@ { "propertyName": "SpayConeAngle" }, + { + "propertyName": "ZoneRadius" + }, { "propertyName": "Z" }, @@ -40555,8 +40786,7 @@ "propertyName": "ParentOrigin" } ] - }, - "variants": [] + } } ] } diff --git a/examples/starting-first-person-shooter-enemies/theme-slots.json b/examples/starting-first-person-shooter-enemies/theme-slots.json new file mode 100644 index 000000000..d2e84383d --- /dev/null +++ b/examples/starting-first-person-shooter-enemies/theme-slots.json @@ -0,0 +1,28 @@ +{ + "version": 1, + "objects": { + "scene:Game Scene/Ground": { + "front": "env.ground", + "left": "env.wall", + "right": "env.wall", + "top": "env.wall", + "bottom": "env.wall" + }, + "scene:Game Scene/Obstacle": { + "front": "env.wall", + "left": "env.wall", + "right": "env.wall", + "top": "env.wall", + "bottom": "env.wall" + }, + "scene:Game Scene/PushableBox": "env.crate", + "scene:Game Scene/Enemy": "character.enemy", + "scene:Game Scene/Gun": "weapon.handheld" + }, + "effects": { + "effect:Game Scene//SkyBox": "sky.day" + }, + "ignoredObjects": [ + "scene:Game Scene/Player" + ] +} diff --git a/scripts/generate-database.js b/scripts/generate-database.js index 0e4a46e60..dd0359f79 100644 --- a/scripts/generate-database.js +++ b/scripts/generate-database.js @@ -16,6 +16,7 @@ const allLicenses = require('./lib/licenses.json'); const args = require('minimist')(process.argv.slice(2)); const { loadSerializedProject } = require('./lib/LocalProjectOpener'); const { writeProjectJSONFile } = require('./lib/LocalProjectWriter'); +const { getExampleGDevelopVersion } = require('./lib/ExampleGDevelopVersion'); const { loadThemeSlotsVocabulary, checkStarterThemeSlots, @@ -527,11 +528,12 @@ const extractExamples = async ( ), license: fileWithMetadata.license, projectFileUrl: getResourceUrl(fileWithMetadata.path), - gdevelopVersion: usedExtensions - .map((exampleUsedExtension) => exampleUsedExtension.name) - .includes('Physics3D') - ? '>=5.5.220' - : '', //TODO: set to the GDevelop version used to author the example? + gdevelopVersion: getExampleGDevelopVersion( + projectObject, + usedExtensions.map( + (exampleUsedExtension) => exampleUsedExtension.name + ) + ), }; return example; diff --git a/scripts/lib/ExampleGDevelopVersion.js b/scripts/lib/ExampleGDevelopVersion.js new file mode 100644 index 000000000..0135c923f --- /dev/null +++ b/scripts/lib/ExampleGDevelopVersion.js @@ -0,0 +1,55 @@ +// @ts-check + +/** + * The actions and conditions that need a GDevelop more recent than the one + * needed by the extensions used by an example. + * @type {Object.} + */ +const gdevelopVersionsByInstructionType = { + // The "Body type" action of the 3D physics. + 'Physics3D::Physics3DBehavior::SetBodyType': '>=5.6.285', +}; + +/** + * @param {*} value A part of a project, as read from its file. + * @param {Set} instructionTypes Filled with the types of the actions + * and conditions found in it. + */ +const collectInstructionTypes = (value, instructionTypes) => { + if (Array.isArray(value)) { + for (const item of value) collectInstructionTypes(item, instructionTypes); + return; + } + if (!value || typeof value !== 'object') return; + if ( + value.type && + typeof value.type.value === 'string' && + Array.isArray(value.parameters) + ) { + instructionTypes.add(value.type.value); + } + for (const key in value) + collectInstructionTypes(value[key], instructionTypes); +}; + +/** + * The versions of GDevelop able to open an example, like ">=5.5.220", or an + * empty string for any version. + * TODO: set to the GDevelop version used to author the example? + * @param {*} projectObject The project, as read from its file. + * @param {Array} usedExtensionNames + * @returns {string} + */ +const getExampleGDevelopVersion = (projectObject, usedExtensionNames) => { + /** @type {Set} */ + const instructionTypes = new Set(); + collectInstructionTypes(projectObject, instructionTypes); + for (const instructionType in gdevelopVersionsByInstructionType) { + if (instructionTypes.has(instructionType)) { + return gdevelopVersionsByInstructionType[instructionType]; + } + } + return usedExtensionNames.includes('Physics3D') ? '>=5.5.220' : ''; +}; + +module.exports = { getExampleGDevelopVersion }; diff --git a/scripts/lib/__tests__/ExampleGDevelopVersion.spec.js b/scripts/lib/__tests__/ExampleGDevelopVersion.spec.js new file mode 100644 index 000000000..cd601e3c7 --- /dev/null +++ b/scripts/lib/__tests__/ExampleGDevelopVersion.spec.js @@ -0,0 +1,90 @@ +// @ts-check +const { getExampleGDevelopVersion } = require('../ExampleGDevelopVersion'); + +/** + * @param {string} type + * @param {Array} parameters + */ +const createFakeInstruction = (type, parameters) => ({ + type: { value: type }, + parameters, +}); + +/** + * @param {Array<*>} actions + * @param {Array<*>} [subEvents] + */ +const createFakeEvent = (actions, subEvents = []) => ({ + type: 'BuiltinCommonInstructions::Standard', + conditions: [], + actions, + events: subEvents, +}); + +/** + * @param {{ sceneEvents?: Array<*>, extensionFunctionEvents?: Array<*> }} events + */ +const createFakeProjectObject = ({ + sceneEvents = [], + extensionFunctionEvents = [], +}) => ({ + layouts: [{ name: 'Game Scene', objects: [], events: sceneEvents }], + externalEvents: [], + eventsFunctionsExtensions: [ + { + name: 'MyExtension', + eventsFunctions: [ + { name: 'MyFunction', events: extensionFunctionEvents }, + ], + }, + ], +}); + +const setBodyTypeAction = createFakeInstruction( + 'Physics3D::Physics3DBehavior::SetBodyType', + ['Enemy', 'Physics3D', '"Dynamic"'] +); +const applyImpulseAction = createFakeInstruction( + 'Physics3D::Physics3DBehavior::ApplyImpulse', + ['Enemy', 'Physics3D', '0', '0', '0'] +); + +describe('getExampleGDevelopVersion', () => { + it('needs a GDevelop with the "Body type" action when a sub-event of a scene uses it', () => { + const projectObject = createFakeProjectObject({ + sceneEvents: [ + createFakeEvent([], [createFakeEvent([setBodyTypeAction])]), + ], + }); + + expect(getExampleGDevelopVersion(projectObject, ['Physics3D'])).toBe( + '>=5.6.285' + ); + }); + + it('needs a GDevelop with the "Body type" action when an extension uses it', () => { + const projectObject = createFakeProjectObject({ + extensionFunctionEvents: [createFakeEvent([setBodyTypeAction])], + }); + + expect(getExampleGDevelopVersion(projectObject, ['Physics3D'])).toBe( + '>=5.6.285' + ); + }); + + it('needs a GDevelop with the 3D physics for the other 3D physics examples', () => { + const projectObject = createFakeProjectObject({ + sceneEvents: [createFakeEvent([applyImpulseAction])], + }); + + expect(getExampleGDevelopVersion(projectObject, ['Physics3D'])).toBe( + '>=5.5.220' + ); + }); + + it('accepts any GDevelop for the other examples', () => { + const projectObject = createFakeProjectObject({}); + + expect(getExampleGDevelopVersion(projectObject, ['Sprite'])).toBe(''); + }); +});