From 30698d8acb4ed14255d2117786e220f46c4334e4 Mon Sep 17 00:00:00 2001 From: Isaac Lichter Date: Sun, 30 May 2021 17:02:19 -0400 Subject: [PATCH 1/4] feat: generic collision mesh creation system --- .../org/destinationsol/SolApplication.java | 11 +- .../body/components/BodyLinked.java | 21 ++ .../body/systems/BodyHandlerSystem.java | 3 + .../game/CollisionMeshLoader.java | 2 +- .../CollisionMeshBuilderSystem.java | 310 ++++++++++++++++++ .../rendering/RenderableElement.java | 4 + 6 files changed, 347 insertions(+), 4 deletions(-) create mode 100644 engine/src/main/java/org/destinationsol/meshBuilder/CollisionMeshBuilderSystem.java diff --git a/engine/src/main/java/org/destinationsol/SolApplication.java b/engine/src/main/java/org/destinationsol/SolApplication.java index dce03586d..92e6b55b2 100644 --- a/engine/src/main/java/org/destinationsol/SolApplication.java +++ b/engine/src/main/java/org/destinationsol/SolApplication.java @@ -284,6 +284,7 @@ private void draw() { element.tint = Color.YELLOW; element.setSize(size.size); element.graphicsOffset = new Vector2(); + element.density = 10; Renderable graphicsComponent = new Renderable(); graphicsComponent.elements.add(element); @@ -294,10 +295,14 @@ private void draw() { Health health = new Health(); health.currentHealth = 1; - EntityRef entityRef = entitySystemManager.getEntityManager().createEntity(graphicsComponent, position, size, - new Angle(), new Velocity(), new AsteroidMesh(), health, new DropsMoneyOnDestruction(), new CreatesRubbleOnDestruction()); + BodyLinked bodyLinked = new BodyLinked(); + bodyLinked.setJsonSchemaFileName("engine:schemaCollisionMesh"); + bodyLinked.setJsonPath("engine:asteroids"); + + EntityRef entityRef = entitySystemManager.getEntityManager().createEntity(graphicsComponent, position, + size, bodyLinked, new Angle(), new Velocity(), health, + new DropsMoneyOnDestruction(), new CreatesRubbleOnDestruction()); - entityRef.setComponent(new BodyLinked()); entityCreated = true; } } diff --git a/engine/src/main/java/org/destinationsol/body/components/BodyLinked.java b/engine/src/main/java/org/destinationsol/body/components/BodyLinked.java index e202aa5f6..2394745ad 100644 --- a/engine/src/main/java/org/destinationsol/body/components/BodyLinked.java +++ b/engine/src/main/java/org/destinationsol/body/components/BodyLinked.java @@ -30,6 +30,9 @@ public class BodyLinked implements Component { private float mass; + private String jsonPath = ""; + private String jsonSchemaFileName = ""; + /** * Sets the mass of the entity. This is called every tick by the {@link BodyHandlerSystem}. */ @@ -46,8 +49,26 @@ public float getMass() { return mass; } + public String getJsonPath() { + return jsonPath; + } + + public void setJsonPath(String jsonPath) { + this.jsonPath = jsonPath; + } + + public String getJsonSchemaFileName() { + return jsonSchemaFileName; + } + + public void setJsonSchemaFileName(String jsonSchemaFileName) { + this.jsonSchemaFileName = jsonSchemaFileName; + } + @Override public void copy(BodyLinked other) { this.mass = other.getMass(); + this.jsonPath = other.getJsonPath(); + this.jsonSchemaFileName = other.jsonSchemaFileName; } } diff --git a/engine/src/main/java/org/destinationsol/body/systems/BodyHandlerSystem.java b/engine/src/main/java/org/destinationsol/body/systems/BodyHandlerSystem.java index 5e88cd26f..777ff30f9 100644 --- a/engine/src/main/java/org/destinationsol/body/systems/BodyHandlerSystem.java +++ b/engine/src/main/java/org/destinationsol/body/systems/BodyHandlerSystem.java @@ -65,6 +65,9 @@ public class BodyHandlerSystem implements EventReceiver { public EventResult onBodyUpdate(BodyUpdateEvent event, EntityRef entity) { createBodyIfNonexistent(entity); + if (!referenceToBodyObjects.containsKey(entity)) { + return EventResult.CANCEL; + } Body body = referenceToBodyObjects.get(entity); BodyLinked bodyLinkedComponent = entity.getComponent(BodyLinked.class).get(); bodyLinkedComponent.setMass(body.getMass()); diff --git a/engine/src/main/java/org/destinationsol/game/CollisionMeshLoader.java b/engine/src/main/java/org/destinationsol/game/CollisionMeshLoader.java index 149d7d649..e12b9308c 100644 --- a/engine/src/main/java/org/destinationsol/game/CollisionMeshLoader.java +++ b/engine/src/main/java/org/destinationsol/game/CollisionMeshLoader.java @@ -338,7 +338,7 @@ public static class RigidBodyModel { public static class PolygonModel { public final List vertices = new ArrayList<>(); - private Vector2[] tmpArray; // Used to avoid allocation in attachFixture() + public Vector2[] tmpArray; // Used to avoid allocation in attachFixture() } public static class CircleModel { diff --git a/engine/src/main/java/org/destinationsol/meshBuilder/CollisionMeshBuilderSystem.java b/engine/src/main/java/org/destinationsol/meshBuilder/CollisionMeshBuilderSystem.java new file mode 100644 index 000000000..19985c9e0 --- /dev/null +++ b/engine/src/main/java/org/destinationsol/meshBuilder/CollisionMeshBuilderSystem.java @@ -0,0 +1,310 @@ +/* + * Copyright 2020 The Terasology Foundation + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.destinationsol.meshBuilder; + +import com.badlogic.gdx.math.MathUtils; +import com.badlogic.gdx.math.Vector2; +import com.badlogic.gdx.physics.box2d.*; +import org.destinationsol.Const; +import org.destinationsol.assets.json.Validator; +import org.destinationsol.body.components.BodyLinked; +import org.destinationsol.body.events.BodyCreatedEvent; +import org.destinationsol.body.events.GenerateBodyEvent; +import org.destinationsol.common.In; +import org.destinationsol.entitysystem.EntitySystemManager; +import org.destinationsol.entitysystem.EventReceiver; +import org.destinationsol.game.CollisionMeshLoader; +import org.destinationsol.game.UpdateAwareSystem; +import org.destinationsol.location.components.Angle; +import org.destinationsol.location.components.Position; +import org.destinationsol.rendering.RenderableElement; +import org.destinationsol.rendering.components.Renderable; +import org.destinationsol.size.components.Size; +import org.json.JSONArray; +import org.json.JSONObject; +import org.terasology.gestalt.entitysystem.entity.EntityRef; +import org.terasology.gestalt.entitysystem.event.EventResult; +import org.terasology.gestalt.entitysystem.event.ReceiveEvent; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; + +/** + * This system creates a {@link Body} for an entity with a {@link BodyLinked} component. + *

+ * The system loads the collision mesh data from a specified JSON file (if the data hasn't already been loaded), and + * then builds a {@link Body} using that data. + *

+ * Bodies should only be created during an update sent by an {@link UpdateAwareSystem}. Attempting to create a body at + * any other time may cause the game to crash. + */ +public class CollisionMeshBuilderSystem implements EventReceiver { + + private HashMap storedData = new HashMap<>(); + + @In + private World world; + + @In + private EntitySystemManager entitySystemManager; + + // Reusable objects, used for constructing Body instances + private final List vectorPool = new ArrayList<>(); + Vector2 reusableVector = new Vector2(); + private final PolygonShape reusablePolygonShape = new PolygonShape(); + private final CircleShape reusableCircleShape = new CircleShape(); + + @ReceiveEvent(components = {BodyLinked.class, Size.class, Position.class, Angle.class, Renderable.class}) + public EventResult onGenerateBodyEvent(GenerateBodyEvent event, EntityRef entity) { + BodyLinked bodyLinkedComponent = entity.getComponent(BodyLinked.class).get(); + String jsonPath = bodyLinkedComponent.getJsonPath(); + if (!storedData.containsKey(jsonPath)) { + loadDataFromFile(bodyLinkedComponent, jsonPath); + } + CollisionMeshLoader.Model model = storedData.get(jsonPath); + if (model == null) { + return EventResult.CANCEL; + } + + float size = entity.getComponent(Size.class).get().size; + Vector2 position = entity.getComponent(Position.class).get().position; + float angle = entity.getComponent(Angle.class).get().getAngle(); + ArrayList renderableElements = entity.getComponent(Renderable.class).get().elements; + + return createBody(entity, size, position, angle, renderableElements, model); + } + + /** + * This loads all the collision mesh data from a file. + */ + private void loadDataFromFile(BodyLinked bodyLinkedComponent, String jsonPath) { + String schemaFileName = bodyLinkedComponent.getJsonSchemaFileName(); + + if (schemaFileName.equals("engine:schemaCollisionMesh")) { + JSONObject rootNode = Validator.getValidatedJSON(jsonPath, schemaFileName); + JSONArray rbNode = rootNode.getJSONArray("rigidBodies"); + for (int i = 0; i < rbNode.length(); i++) { + JSONObject jsonObject = rbNode.getJSONObject(i); + readIndividualMeshData(jsonObject, jsonPath, jsonObject.getString("name")); + } + } else { + readIndividualMeshData(Validator.getValidatedJSON(jsonPath, schemaFileName), jsonPath, jsonPath); + } + } + + /** + * This reads a particular collision mesh's data from a given file, and stores it in a model. + */ + private void readIndividualMeshData(JSONObject rbNode, String jsonPath, String identifier) { + CollisionMeshLoader.RigidBodyModel rbModel = new CollisionMeshLoader.RigidBodyModel(); + rbModel.name = identifier; + + JSONObject originNode = rbNode.getJSONObject("origin"); + rbModel.origin.x = (float) originNode.getDouble("x"); + rbModel.origin.y = 1 - (float) originNode.getDouble("y"); + + // Polygons + JSONArray polygonNodeArray = rbNode.getJSONArray("polygons"); + for (int i = 0; i < polygonNodeArray.length(); i++) { + JSONArray polygonNode = polygonNodeArray.getJSONArray(i); + CollisionMeshLoader.PolygonModel polygonModel = new CollisionMeshLoader.PolygonModel(); + rbModel.polygons.add(polygonModel); + + for (int j = 0; j < polygonNode.length(); j++) { + JSONObject vertexNode = polygonNode.getJSONObject(j); + float x = (float) vertexNode.getDouble("x"); + float y = 1 - (float) vertexNode.getDouble("y"); + polygonModel.vertices.add(new Vector2(x, y)); + } + + polygonModel.tmpArray = new Vector2[polygonModel.vertices.size()]; + } + + // Shapes + JSONArray shapeNodeArray = rbNode.getJSONArray("shapes"); + for (int i = 0; i < shapeNodeArray.length(); i++) { + JSONObject shapeNode = shapeNodeArray.getJSONObject(i); + String type = shapeNode.getString("type"); + if (!type.equals("POLYGON")) { + continue; + } + + CollisionMeshLoader.PolygonModel shapeModel = new CollisionMeshLoader.PolygonModel(); + rbModel.shapes.add(shapeModel); + JSONArray vertices = shapeNode.getJSONArray("vertices"); + for (int j = 0; j < vertices.length(); j++) { + JSONObject vertexNode = vertices.getJSONObject(j); + float x = (float) vertexNode.getDouble("x"); + float y = 1 - (float) vertexNode.getDouble("y"); + shapeModel.vertices.add(new Vector2(x, y)); + } + + shapeModel.tmpArray = new Vector2[shapeModel.vertices.size()]; + } + + // Circles + JSONArray circles = rbNode.getJSONArray("circles"); + for (int i = 0; i < circles.length(); i++) { + CollisionMeshLoader.CircleModel circleModel = new CollisionMeshLoader.CircleModel(); + rbModel.circles.add(circleModel); + + JSONObject circleNode = circles.getJSONObject(i); + + circleModel.center.x = (float) circleNode.getDouble("cx"); + circleModel.center.y = 1 - (float) circleNode.getDouble("cy"); + circleModel.radius = (float) circleNode.getDouble("r"); + } + + CollisionMeshLoader.Model model = new CollisionMeshLoader.Model(); + model.rigidBodies.put(rbModel.name, rbModel); + storedData.put(jsonPath, model); + } + + /** + * Creates a {@link Body} for an entity from a JSON-defined {@link CollisionMeshLoader.Model}. + */ + private EventResult createBody(EntityRef entity, float size, Vector2 position, float angle, ArrayList renderableElements, CollisionMeshLoader.Model model) { + //This creates an entity with a generic Body. The fixtures, which provide the collision meshes, are attached later. + BodyDef bodyDef = new BodyDef(); + bodyDef.type = BodyDef.BodyType.DynamicBody; + bodyDef.angle = angle * MathUtils.degRad; + bodyDef.angularDamping = 0; + bodyDef.position.set(position); + bodyDef.linearDamping = 0; + Body body = world.createBody(bodyDef); + + //This sets a reference to an entity in the Body, so that the entity can be retrieved from the body during collision handling. + body.setUserData(entity); + + //This attaches collision meshes to the Body of an entity, based on its graphics. + for (RenderableElement element : renderableElements) { + FixtureDef fixtureDef = new FixtureDef(); + fixtureDef.density = element.density; + fixtureDef.friction = Const.FRICTION; + attachFixture(body, element.texture.name, fixtureDef, size, model); + + /* + * This calculates the offset of the renderable element from "the origin" (as defined in the JSON that the + * CollisionMeshLoader reads from). + * The origin is where the center of the object should be. This system creates Fixtures (collision meshes) + * using that information, so the sprites need to be adjusted to overlay the mesh properly. + * LibGDX draws sprites from the bottom left corner. Since the position information is from the center, it + * needs to be adjusted to be at the bottom left of the sprite. To do so, (.5, .5) is subtracted from the + * origin. (The coordinates are scaled to range from zero to one, so (.5, .5) represents the center.) + * The originInformation is the information that was read from the JSON, which is used to calculate the + * graphics offset information. + */ + CollisionMeshLoader.RigidBodyModel rbModel = model.rigidBodies.get(element.texture.name); + if (rbModel == null) { + reusableVector.set(.5f, .5f); + } else { + reusableVector.set(rbModel.origin); + } + reusableVector.scl(size); + element.graphicsOffset = reusableVector.cpy(); + + } + + entitySystemManager.sendEvent(new BodyCreatedEvent(body), entity); + + return EventResult.CONTINUE; + } + + /** + * Creates and applies the fixtures defined in the editor. The name + * parameter is used to retrieve the right fixture from the loaded file. + *

+ *

+ * The body reference point (the red cross in the tool) is by default + * located at the bottom left corner of the image. This reference point + * will be put right over the BodyDef position point. Therefore, you should + * place this reference point carefully to let you place your body in your + * world easily with its BodyDef.position point. Note that to draw an image + * at the position of your body, you will need to know this reference point. + *

+ *

+ * Also, saved shapes are normalized. As shown in the tool, the width of + * the image is considered to be always 1 meter. Thus, you need to provide + * a scale factor so the polygons get resized according to your needs. + * + * @param body The Box2d body you want to attach the fixture to. + * @param name The name of the fixture you want to load. + * @param fd The fixture parameters to apply to the created body fixture. + * @param scale The desired scale of the body. The default width is 1. + */ + private boolean attachFixture(Body body, String name, FixtureDef fd, float scale, CollisionMeshLoader.Model model) { + CollisionMeshLoader.RigidBodyModel rbModel = model.rigidBodies.get(name); + if (rbModel == null) { + return false; + } + + Vector2 origin = reusableVector.set(rbModel.origin).scl(scale); + + for (CollisionMeshLoader.PolygonModel polygon : rbModel.polygons) { + Vector2[] points = polygon.tmpArray; + + int pointCount = points.length; + for (int i = 0; i < pointCount; i++) { + Vector2 origPoint = polygon.vertices.get(pointCount - i - 1); + points[i] = newVector(origPoint).scl(scale); + points[i].sub(origin); + } + + reusablePolygonShape.set(points); + fd.shape = reusablePolygonShape; + body.createFixture(fd); + + for (Vector2 point : points) { + free(point); + } + } + + for (CollisionMeshLoader.CircleModel circle : rbModel.circles) { + Vector2 center = newVector(circle.center).scl(scale).sub(origin); + float radius = circle.radius * scale; + + reusableCircleShape.setPosition(center); + reusableCircleShape.setRadius(radius); + fd.shape = reusableCircleShape; + body.createFixture(fd); + + free(center); + } + + return true; + } + + /** + * Returns a vector from the vector pool if one exists, or creates a new one if the pool is empty. If a vector is + * passed in, the returned vector is assigned the same values as the original vector. + */ + private Vector2 newVector(Vector2 vector) { + Vector2 res = vectorPool.isEmpty() ? new Vector2() : vectorPool.remove(0); + if (vector != null) { + res.set(vector); + } + return res; + } + + /** + * Adds a vector to the vector pool. + */ + private void free(Vector2 vector) { + vectorPool.add(vector); + } + +} diff --git a/engine/src/main/java/org/destinationsol/rendering/RenderableElement.java b/engine/src/main/java/org/destinationsol/rendering/RenderableElement.java index e0df1d201..76cd364f1 100644 --- a/engine/src/main/java/org/destinationsol/rendering/RenderableElement.java +++ b/engine/src/main/java/org/destinationsol/rendering/RenderableElement.java @@ -59,6 +59,9 @@ public class RenderableElement { */ public Vector2 graphicsOffset; + /** Represents the density of this element. */ + public float density; + //TODO this should be automatically called when the Size component is changed, e.g. the entity shrinks or grows /** @@ -86,6 +89,7 @@ public void copy(RenderableElement other) { this.height = other.getHeight(); this.tint = other.tint.cpy(); this.graphicsOffset = other.graphicsOffset.cpy(); + this.density = other.density; } public float getWidth() { From 0c509f1c432caaffd8c79d02ededdf00636bc2cd Mon Sep 17 00:00:00 2001 From: Isaac Lichter Date: Sun, 30 May 2021 17:38:23 -0400 Subject: [PATCH 2/4] feat: deleted redundant asteroid body creation system --- .../org/destinationsol/SolApplication.java | 1 - .../asteroids/components/AsteroidMesh.java | 21 ---- .../systems/AsteroidBodyCreationSystem.java | 117 ------------------ 3 files changed, 139 deletions(-) delete mode 100644 engine/src/main/java/org/destinationsol/asteroids/components/AsteroidMesh.java delete mode 100644 engine/src/main/java/org/destinationsol/asteroids/systems/AsteroidBodyCreationSystem.java diff --git a/engine/src/main/java/org/destinationsol/SolApplication.java b/engine/src/main/java/org/destinationsol/SolApplication.java index 92e6b55b2..42a6e3c88 100644 --- a/engine/src/main/java/org/destinationsol/SolApplication.java +++ b/engine/src/main/java/org/destinationsol/SolApplication.java @@ -26,7 +26,6 @@ import org.destinationsol.assets.Assets; import org.destinationsol.assets.music.OggMusicManager; import org.destinationsol.assets.sound.OggSoundManager; -import org.destinationsol.asteroids.components.AsteroidMesh; import org.destinationsol.body.components.BodyLinked; import org.destinationsol.common.SolColor; import org.destinationsol.common.SolMath; diff --git a/engine/src/main/java/org/destinationsol/asteroids/components/AsteroidMesh.java b/engine/src/main/java/org/destinationsol/asteroids/components/AsteroidMesh.java deleted file mode 100644 index 426590804..000000000 --- a/engine/src/main/java/org/destinationsol/asteroids/components/AsteroidMesh.java +++ /dev/null @@ -1,21 +0,0 @@ -/* - * Copyright 2020 The Terasology Foundation - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.destinationsol.asteroids.components; - -import org.terasology.gestalt.entitysystem.component.EmptyComponent; - -public class AsteroidMesh extends EmptyComponent { -} diff --git a/engine/src/main/java/org/destinationsol/asteroids/systems/AsteroidBodyCreationSystem.java b/engine/src/main/java/org/destinationsol/asteroids/systems/AsteroidBodyCreationSystem.java deleted file mode 100644 index 7b8c5b18c..000000000 --- a/engine/src/main/java/org/destinationsol/asteroids/systems/AsteroidBodyCreationSystem.java +++ /dev/null @@ -1,117 +0,0 @@ -/* - * Copyright 2020 The Terasology Foundation - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.destinationsol.asteroids.systems; - -import com.badlogic.gdx.math.MathUtils; -import com.badlogic.gdx.math.Vector2; -import com.badlogic.gdx.physics.box2d.Body; -import com.badlogic.gdx.physics.box2d.BodyDef; -import com.badlogic.gdx.physics.box2d.FixtureDef; -import com.badlogic.gdx.physics.box2d.World; -import org.destinationsol.Const; -import org.destinationsol.asteroids.components.AsteroidMesh; -import org.destinationsol.body.events.BodyCreatedEvent; -import org.destinationsol.body.events.GenerateBodyEvent; -import org.destinationsol.common.In; -import org.destinationsol.entitysystem.EntitySystemManager; -import org.destinationsol.entitysystem.EventReceiver; -import org.destinationsol.game.CollisionMeshLoader; -import org.destinationsol.game.UpdateAwareSystem; -import org.destinationsol.location.components.Angle; -import org.destinationsol.location.components.Position; -import org.destinationsol.rendering.RenderableElement; -import org.destinationsol.rendering.components.Renderable; -import org.destinationsol.size.components.Size; -import org.terasology.gestalt.entitysystem.entity.EntityRef; -import org.terasology.gestalt.entitysystem.event.EventResult; -import org.terasology.gestalt.entitysystem.event.ReceiveEvent; - -import java.util.ArrayList; - -/** - * This system creates a {@link Body} for an entity with a {@link AsteroidMesh} component. - *

- * The way that {@link CollisionMeshLoader} is designed, it needs to be given a JSON fileName that points to a specific - * type of mesh, such as asteroid meshes, which doesn't lend itself to modular body creation. - * TODO Once CollisionMeshLoader has been refactored to be modular, this should be replaced by a generic body creation system. - *

- * Bodies should only be created during an update sent by an {@link UpdateAwareSystem}. Attempting to create a body at - * any other time may cause the game to crash. - */ -public class AsteroidBodyCreationSystem implements EventReceiver { - - private static final float DENSITY = 10f; - - @In - private EntitySystemManager entitySystemManager; - - @In - private World world; - - private final CollisionMeshLoader collisionMeshLoader = new CollisionMeshLoader("engine:asteroids"); - - @ReceiveEvent(components = {AsteroidMesh.class, Size.class, Position.class, Angle.class, Renderable.class}) - public EventResult onGenerateBody(GenerateBodyEvent event, EntityRef entity) { - - float size = entity.getComponent(Size.class).get().size; - Vector2 position = entity.getComponent(Position.class).get().position; - float angle = entity.getComponent(Angle.class).get().getAngle(); - ArrayList renderableElements = entity.getComponent(Renderable.class).get().elements; - - //This creates an entity with a generic Body. The fixtures, which provide the collision meshes, are attached later. - BodyDef bd = new BodyDef(); - bd.type = BodyDef.BodyType.DynamicBody; - bd.angle = angle * MathUtils.degRad; - bd.angularDamping = 0; - bd.position.set(position); - bd.linearDamping = 0; - Body body = world.createBody(bd); - - //This sets a reference to an entity in the Body, so that the entity can be retrieved from the body during collision handling. - body.setUserData(entity); - - //This attaches collision meshes to the Body of an entity, based on its graphics. - for (RenderableElement element : renderableElements) { - FixtureDef fixtureDef = new FixtureDef(); - fixtureDef.density = DENSITY; - fixtureDef.friction = Const.FRICTION; - collisionMeshLoader.attachFixture(body, element.texture.name, fixtureDef, size); - - calculateGraphicsOffset(element); - } - - entitySystemManager.sendEvent(new BodyCreatedEvent(body), entity); - return EventResult.CONTINUE; - } - - /** - * This calculates the offset of the renderable element from "the origin" (as defined in the JSON that the - * CollisionMeshLoader reads from). - * The origin is where the center of the object should be, which is relevant for physics handling. The - * CollisionMeshLoader creates Fixtures (collision meshes) using that information, so the sprites need to be - * adjusted to overlay the mesh properly. - * LibGDX draws sprites from the bottom left corner. Since the position information is from the center, it - * needs to be adjusted to be at the bottom left of the sprite. To do so, (.5, .5) is subtracted from the origin. - * (The coordinates are scaled to range from zero to one, so (.5, .5) represents the center.) - * The originInformation is the information that was read from the JSON, which is used to calculate the graphics - * offset information. - */ - //TODO separate this method into a separate system once CollisionMeshLoader is modular - private void calculateGraphicsOffset(RenderableElement element) { - Vector2 originInformation = collisionMeshLoader.getOrigin(element.texture.name, 1); - element.graphicsOffset = new Vector2(originInformation.x - .5f, originInformation.y - .5f); - } -} From fe7c6ea82517a381d0ce4c763cc86e6504809a40 Mon Sep 17 00:00:00 2001 From: Isaac Lichter Date: Mon, 11 Oct 2021 12:06:04 -0400 Subject: [PATCH 3/4] feat: use collision mesh builder for rubble --- .../rubble/systems/RubbleCreationSystem.java | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/engine/src/main/java/org/destinationsol/rubble/systems/RubbleCreationSystem.java b/engine/src/main/java/org/destinationsol/rubble/systems/RubbleCreationSystem.java index af3e8cc3a..82088004f 100644 --- a/engine/src/main/java/org/destinationsol/rubble/systems/RubbleCreationSystem.java +++ b/engine/src/main/java/org/destinationsol/rubble/systems/RubbleCreationSystem.java @@ -25,7 +25,6 @@ import org.destinationsol.entitysystem.EntitySystemManager; import org.destinationsol.entitysystem.EventReceiver; import org.destinationsol.game.Rubble; -import org.destinationsol.game.RubbleBuilder; import org.destinationsol.game.SolGame; import org.destinationsol.game.drawables.DrawableLevel; import org.destinationsol.health.components.Health; @@ -56,9 +55,6 @@ public class RubbleCreationSystem implements EventReceiver { public static final float MAX_SCALE = .12f; private static final float MAX_SPD = 40f; - @In - private RubbleBuilder rubbleBuilder; - @In private SolGame solGame; @@ -147,7 +143,10 @@ private void buildRubblePieces(Position pos, Velocity vel, Angle angle, Size siz EntityRef entityRef = entitySystemManager.getEntityManager().createEntity(graphicsComponent, positionComponent, velocityComponent, angle, sizeComponent, new RubbleMesh()); SolMath.free(velocity); - entityRef.setComponent(new BodyLinked()); + BodyLinked bodyLinked = new BodyLinked(); + bodyLinked.setJsonSchemaFileName("engine:schemaCollisionMesh"); + bodyLinked.setJsonPath("engine:miscCollisionMeshes"); + entityRef.setComponent(bodyLinked); } } } From 15993017bbe7e9fbd44fa5f7e282f35018c36405 Mon Sep 17 00:00:00 2001 From: Nicholas Bates <19882546+NicholasBatesNZ@users.noreply.github.com> Date: Sat, 15 Aug 2026 10:13:53 +1200 Subject: [PATCH 4/4] fix: make the collision mesh builder work on current develop Follow-up fixes to the merged collision mesh builder work, needed because develop has moved on since the original change was written. * CollisionMeshBuilderSystem used @In field injection for World and EntitySystemManager. EventReceiverServiceRegistry now hands every EventReceiver to the gestalt DI container, which only populates @Inject members and never touches @In fields, so both would have been null at runtime. Switch to @Inject with an @Inject constructor and resolve the World lazily via ObjectManager.getWorld(), matching the pattern the other body creation systems use. * Delete RubbleMesh and RubbleBodyCreationSystem, which were added to develop after this work was written. Rubble now carries BodyLinked, so keeping them would have built two physics bodies per rubble piece. Removing the per-type body systems is the point of this change. The fixture density they hardcoded is preserved on the RenderableElement. * Accumulate every rigid body of a collision mesh file into one cached model instead of replacing the cache entry per rigid body, which left only the last entry of the file usable. * Compute the graphics offset as origin - (.5, .5), as the surrounding comment describes. RenderingSystem already scales the offset by the entity size, so scaling it here applied the size twice and skipped the centering correction. Co-authored-by: IsaacLic Co-Authored-By: Claude Opus 5 --- .../CollisionMeshBuilderSystem.java | 37 +++--- .../rubble/components/RubbleMesh.java | 26 ---- .../systems/RubbleBodyCreationSystem.java | 116 ------------------ .../rubble/systems/RubbleCreationSystem.java | 6 +- 4 files changed, 27 insertions(+), 158 deletions(-) delete mode 100644 engine/src/main/java/org/destinationsol/rubble/components/RubbleMesh.java delete mode 100644 engine/src/main/java/org/destinationsol/rubble/systems/RubbleBodyCreationSystem.java diff --git a/engine/src/main/java/org/destinationsol/meshBuilder/CollisionMeshBuilderSystem.java b/engine/src/main/java/org/destinationsol/meshBuilder/CollisionMeshBuilderSystem.java index 19985c9e0..01ccc27c2 100644 --- a/engine/src/main/java/org/destinationsol/meshBuilder/CollisionMeshBuilderSystem.java +++ b/engine/src/main/java/org/destinationsol/meshBuilder/CollisionMeshBuilderSystem.java @@ -17,16 +17,20 @@ import com.badlogic.gdx.math.MathUtils; import com.badlogic.gdx.math.Vector2; -import com.badlogic.gdx.physics.box2d.*; +import com.badlogic.gdx.physics.box2d.Body; +import com.badlogic.gdx.physics.box2d.BodyDef; +import com.badlogic.gdx.physics.box2d.CircleShape; +import com.badlogic.gdx.physics.box2d.FixtureDef; +import com.badlogic.gdx.physics.box2d.PolygonShape; import org.destinationsol.Const; import org.destinationsol.assets.json.Validator; import org.destinationsol.body.components.BodyLinked; import org.destinationsol.body.events.BodyCreatedEvent; import org.destinationsol.body.events.GenerateBodyEvent; -import org.destinationsol.common.In; import org.destinationsol.entitysystem.EntitySystemManager; import org.destinationsol.entitysystem.EventReceiver; import org.destinationsol.game.CollisionMeshLoader; +import org.destinationsol.game.ObjectManager; import org.destinationsol.game.UpdateAwareSystem; import org.destinationsol.location.components.Angle; import org.destinationsol.location.components.Position; @@ -39,6 +43,7 @@ import org.terasology.gestalt.entitysystem.event.EventResult; import org.terasology.gestalt.entitysystem.event.ReceiveEvent; +import javax.inject.Inject; import java.util.ArrayList; import java.util.HashMap; import java.util.List; @@ -56,11 +61,11 @@ public class CollisionMeshBuilderSystem implements EventReceiver { private HashMap storedData = new HashMap<>(); - @In - private World world; + @Inject + protected ObjectManager objectManager; - @In - private EntitySystemManager entitySystemManager; + @Inject + protected EntitySystemManager entitySystemManager; // Reusable objects, used for constructing Body instances private final List vectorPool = new ArrayList<>(); @@ -68,6 +73,10 @@ public class CollisionMeshBuilderSystem implements EventReceiver { private final PolygonShape reusablePolygonShape = new PolygonShape(); private final CircleShape reusableCircleShape = new CircleShape(); + @Inject + public CollisionMeshBuilderSystem() { + } + @ReceiveEvent(components = {BodyLinked.class, Size.class, Position.class, Angle.class, Renderable.class}) public EventResult onGenerateBodyEvent(GenerateBodyEvent event, EntityRef entity) { BodyLinked bodyLinkedComponent = entity.getComponent(BodyLinked.class).get(); @@ -94,22 +103,24 @@ public EventResult onGenerateBodyEvent(GenerateBodyEvent event, EntityRef entity private void loadDataFromFile(BodyLinked bodyLinkedComponent, String jsonPath) { String schemaFileName = bodyLinkedComponent.getJsonSchemaFileName(); + CollisionMeshLoader.Model model = storedData.computeIfAbsent(jsonPath, path -> new CollisionMeshLoader.Model()); + if (schemaFileName.equals("engine:schemaCollisionMesh")) { JSONObject rootNode = Validator.getValidatedJSON(jsonPath, schemaFileName); JSONArray rbNode = rootNode.getJSONArray("rigidBodies"); for (int i = 0; i < rbNode.length(); i++) { JSONObject jsonObject = rbNode.getJSONObject(i); - readIndividualMeshData(jsonObject, jsonPath, jsonObject.getString("name")); + readIndividualMeshData(jsonObject, model, jsonObject.getString("name")); } } else { - readIndividualMeshData(Validator.getValidatedJSON(jsonPath, schemaFileName), jsonPath, jsonPath); + readIndividualMeshData(Validator.getValidatedJSON(jsonPath, schemaFileName), model, jsonPath); } } /** - * This reads a particular collision mesh's data from a given file, and stores it in a model. + * This reads a particular collision mesh's data from a given file, and adds it to the given model. */ - private void readIndividualMeshData(JSONObject rbNode, String jsonPath, String identifier) { + private void readIndividualMeshData(JSONObject rbNode, CollisionMeshLoader.Model model, String identifier) { CollisionMeshLoader.RigidBodyModel rbModel = new CollisionMeshLoader.RigidBodyModel(); rbModel.name = identifier; @@ -169,9 +180,7 @@ private void readIndividualMeshData(JSONObject rbNode, String jsonPath, String i circleModel.radius = (float) circleNode.getDouble("r"); } - CollisionMeshLoader.Model model = new CollisionMeshLoader.Model(); model.rigidBodies.put(rbModel.name, rbModel); - storedData.put(jsonPath, model); } /** @@ -185,7 +194,7 @@ private EventResult createBody(EntityRef entity, float size, Vector2 position, f bodyDef.angularDamping = 0; bodyDef.position.set(position); bodyDef.linearDamping = 0; - Body body = world.createBody(bodyDef); + Body body = objectManager.getWorld().createBody(bodyDef); //This sets a reference to an entity in the Body, so that the entity can be retrieved from the body during collision handling. body.setUserData(entity); @@ -214,7 +223,7 @@ private EventResult createBody(EntityRef entity, float size, Vector2 position, f } else { reusableVector.set(rbModel.origin); } - reusableVector.scl(size); + reusableVector.sub(.5f, .5f); element.graphicsOffset = reusableVector.cpy(); } diff --git a/engine/src/main/java/org/destinationsol/rubble/components/RubbleMesh.java b/engine/src/main/java/org/destinationsol/rubble/components/RubbleMesh.java deleted file mode 100644 index 7c8bbd307..000000000 --- a/engine/src/main/java/org/destinationsol/rubble/components/RubbleMesh.java +++ /dev/null @@ -1,26 +0,0 @@ -/* - * Copyright 2020 The Terasology Foundation - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.destinationsol.rubble.components; - -import org.destinationsol.body.events.GenerateBodyEvent; -import org.destinationsol.rubble.systems.RubbleBodyCreationSystem; -import org.terasology.gestalt.entitysystem.component.EmptyComponent; - -/** - * This class directs the {@link GenerateBodyEvent} to the {@link RubbleBodyCreationSystem} - */ -public class RubbleMesh extends EmptyComponent { -} diff --git a/engine/src/main/java/org/destinationsol/rubble/systems/RubbleBodyCreationSystem.java b/engine/src/main/java/org/destinationsol/rubble/systems/RubbleBodyCreationSystem.java deleted file mode 100644 index ae8f40f17..000000000 --- a/engine/src/main/java/org/destinationsol/rubble/systems/RubbleBodyCreationSystem.java +++ /dev/null @@ -1,116 +0,0 @@ -/* - * Copyright 2020 The Terasology Foundation - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.destinationsol.rubble.systems; - -import com.badlogic.gdx.math.MathUtils; -import com.badlogic.gdx.math.Vector2; -import com.badlogic.gdx.physics.box2d.Body; -import com.badlogic.gdx.physics.box2d.BodyDef; -import com.badlogic.gdx.physics.box2d.FixtureDef; -import com.badlogic.gdx.physics.box2d.World; -import org.destinationsol.Const; -import org.destinationsol.body.events.BodyCreatedEvent; -import org.destinationsol.body.events.GenerateBodyEvent; -import org.destinationsol.entitysystem.EntitySystemManager; -import org.destinationsol.entitysystem.EventReceiver; -import org.destinationsol.game.CollisionMeshLoader; -import org.destinationsol.game.ObjectManager; -import org.destinationsol.location.components.Angle; -import org.destinationsol.location.components.Position; -import org.destinationsol.rendering.RenderableElement; -import org.destinationsol.rendering.components.Renderable; -import org.destinationsol.rubble.components.RubbleMesh; -import org.destinationsol.size.components.Size; -import org.terasology.gestalt.entitysystem.entity.EntityRef; -import org.terasology.gestalt.entitysystem.event.EventResult; -import org.terasology.gestalt.entitysystem.event.ReceiveEvent; - -import javax.inject.Inject; -import java.util.ArrayList; - -/** - * This system creates a {@link Body} for an entity with a {@link RubbleMesh} component. - */ -public class RubbleBodyCreationSystem implements EventReceiver { - - private static final float DENSITY = 3f; - - @Inject - protected EntitySystemManager entitySystemManager; - - @Inject - protected ObjectManager objectManager; - - private final CollisionMeshLoader collisionMeshLoader = new CollisionMeshLoader("engine:miscCollisionMeshes"); - - @Inject - public RubbleBodyCreationSystem() { - - } - - @ReceiveEvent(components = {RubbleMesh.class, Size.class, Position.class, Angle.class, Renderable.class}) - public EventResult onGenerateBody(GenerateBodyEvent event, EntityRef entity) { - - float size = entity.getComponent(Size.class).get().size; - Vector2 position = entity.getComponent(Position.class).get().position; - float angle = entity.getComponent(Angle.class).get().getAngle(); - ArrayList renderableElements = entity.getComponent(Renderable.class).get().elements; - - //This creates an entity with a generic Body. The fixtures, which provide the collision meshes, are attached later. - BodyDef bd = new BodyDef(); - bd.type = BodyDef.BodyType.DynamicBody; - bd.angle = angle * MathUtils.degRad; - bd.angularDamping = 0; - bd.position.set(position); - bd.linearDamping = 0; - Body body = objectManager.getWorld().createBody(bd); - - //This sets a reference to an entity in the Body, so that the entity can be retrieved from the body during collision handling. - body.setUserData(entity); - - //This attaches collision meshes to the Body of an entity, based on its graphics. - for (RenderableElement element : renderableElements) { - FixtureDef fixtureDef = new FixtureDef(); - fixtureDef.density = DENSITY; - fixtureDef.friction = Const.FRICTION; - collisionMeshLoader.attachFixture(body, element.texture.name, fixtureDef, size); - - calculateGraphicsOffset(element); - } - - entitySystemManager.sendEvent(new BodyCreatedEvent(body), entity); - - return EventResult.CONTINUE; - } - - /** - * This calculates the offset of the renderable element from "the origin" (as defined in the JSON that the - * CollisionMeshLoader reads from). - * The origin is where the center of the object should be, which is relevant for physics handling. The - * CollisionMeshLoader creates Fixtures (collision meshes) using that information, so the sprites need to be - * adjusted to overlay the mesh properly. - * LibGDX draws sprites from the bottom left corner. Since the position information is from the center, it - * needs to be adjusted to be at the bottom left of the sprite. To do so, (.5, .5) is subtracted from the origin. - * (The coordinates are scaled to range from zero to one, so (.5, .5) represents the center.) - * The originInformation is the information that was read from the JSON, which is used to calculate the graphics - * offset information. - */ - //TODO separate this method into a separate system once CollisionMeshLoader is modular - private void calculateGraphicsOffset(RenderableElement element) { - Vector2 originInformation = collisionMeshLoader.getOrigin(element.texture.name, 1); - element.graphicsOffset = new Vector2(originInformation.x - .5f, originInformation.y - .5f); - } -} diff --git a/engine/src/main/java/org/destinationsol/rubble/systems/RubbleCreationSystem.java b/engine/src/main/java/org/destinationsol/rubble/systems/RubbleCreationSystem.java index da64435a5..2cbafe374 100644 --- a/engine/src/main/java/org/destinationsol/rubble/systems/RubbleCreationSystem.java +++ b/engine/src/main/java/org/destinationsol/rubble/systems/RubbleCreationSystem.java @@ -36,7 +36,6 @@ import org.destinationsol.rendering.RenderableElement; import org.destinationsol.rendering.components.Renderable; import org.destinationsol.rubble.components.CreatesRubbleOnDestruction; -import org.destinationsol.rubble.components.RubbleMesh; import org.destinationsol.size.components.Size; import org.destinationsol.stasis.components.Stasis; import org.terasology.gestalt.entitysystem.entity.EntityRef; @@ -57,6 +56,8 @@ public class RubbleCreationSystem implements EventReceiver { public static final float MIN_SCALE = .1f; public static final float MAX_SCALE = .3f; private static final float MAX_SPD = 40f; + /** The density used for the collision meshes of rubble pieces. */ + private static final float DENSITY = 3f; @Inject protected RubbleBuilder rubbleBuilder; @@ -123,6 +124,7 @@ private void buildRubblePieces(Position pos, Velocity vel, Angle angle, Size siz element.relativePosition = new Vector2(); element.tint = Color.WHITE; + element.density = DENSITY; Renderable graphicsComponent = new Renderable(); graphicsComponent.elements.add(element); @@ -149,7 +151,7 @@ private void buildRubblePieces(Position pos, Velocity vel, Angle angle, Size siz velocityComponent.velocity = velocity; EntityRef entityRef = entitySystemManager.getEntityManager().createEntity(graphicsComponent, positionComponent, - sizeComponent, angle, velocityComponent, new RubbleMesh(), health); + sizeComponent, angle, velocityComponent, health); if (scaledSize > MIN_DIVISIBLE_SIZE) { entityRef.setComponent(new CreatesRubbleOnDestruction());