add attack training dummy (for QueryAABB example)

This commit is contained in:
Quillraven
2025-06-08 20:09:13 +02:00
parent d2e7b83f98
commit 33f986a82d
40 changed files with 654 additions and 126 deletions

View File

@@ -7,6 +7,7 @@ import com.badlogic.gdx.InputMultiplexer;
import com.badlogic.gdx.InputProcessor;
import com.badlogic.gdx.Screen;
import com.badlogic.gdx.assets.loaders.resolvers.InternalFileHandleResolver;
import com.badlogic.gdx.graphics.FPSLogger;
import com.badlogic.gdx.graphics.GL20;
import com.badlogic.gdx.graphics.OrthographicCamera;
import com.badlogic.gdx.graphics.g2d.Batch;
@@ -33,6 +34,7 @@ public class GdxGame extends Game {
private OrthographicCamera camera;
private Viewport viewport;
private GLProfiler glProfiler;
private FPSLogger fpsLogger;
private InputMultiplexer inputMultiplexer;
private final Map<Class<? extends Screen>, Screen> screenCache = new HashMap<>();
@@ -51,6 +53,7 @@ public class GdxGame extends Game {
glProfiler = new GLProfiler(Gdx.graphics);
glProfiler.enable();
fpsLogger = new FPSLogger();
addScreen(new LoadingScreen(this));
setScreen(LoadingScreen.class);
@@ -81,9 +84,8 @@ public class GdxGame extends Game {
super.render();
Gdx.graphics.setTitle("Mystic Tutorial " +
"- Draw Calls: " + glProfiler.getDrawCalls() + " " +
"- FPS: " + Gdx.graphics.getFramesPerSecond());
Gdx.graphics.setTitle("Mystic Tutorial - Draw Calls: " + glProfiler.getDrawCalls());
fpsLogger.log();
}
@Override

View File

@@ -5,6 +5,8 @@ import com.badlogic.gdx.ai.fsm.State;
import com.badlogic.gdx.ai.msg.Telegram;
import io.github.com.quillraven.component.Animation2D;
import io.github.com.quillraven.component.Animation2D.AnimationType;
import io.github.com.quillraven.component.Attack;
import io.github.com.quillraven.component.Damaged;
import io.github.com.quillraven.component.Fsm;
import io.github.com.quillraven.component.Move;
@@ -20,6 +22,18 @@ public enum AnimationState implements State<Entity> {
Move move = Move.MAPPER.get(entity);
if (move != null && !move.getDirection().isZero()) {
Fsm.MAPPER.get(entity).getAnimationFsm().changeState(WALK);
return;
}
Attack attack = Attack.MAPPER.get(entity);
if (attack != null && attack.isAttacking()) {
Fsm.MAPPER.get(entity).getAnimationFsm().changeState(ATTACK);
return;
}
Damaged damaged = Damaged.MAPPER.get(entity);
if (damaged != null) {
Fsm.MAPPER.get(entity).getAnimationFsm().changeState(DAMAGED);
}
}
@@ -51,6 +65,54 @@ public enum AnimationState implements State<Entity> {
public void exit(Entity entity) {
}
@Override
public boolean onMessage(Entity entity, Telegram telegram) {
return false;
}
},
ATTACK {
@Override
public void enter(Entity entity) {
Animation2D.MAPPER.get(entity).setType(AnimationType.ATTACK);
}
@Override
public void update(Entity entity) {
Attack attack = Attack.MAPPER.get(entity);
if (attack.canAttack()) {
Fsm.MAPPER.get(entity).getAnimationFsm().changeState(IDLE);
}
}
@Override
public void exit(Entity entity) {
}
@Override
public boolean onMessage(Entity entity, Telegram telegram) {
return false;
}
},
DAMAGED {
@Override
public void enter(Entity entity) {
Animation2D.MAPPER.get(entity).setType(AnimationType.DAMAGED);
}
@Override
public void update(Entity entity) {
Animation2D animation2D = Animation2D.MAPPER.get(entity);
if (animation2D.isFinished()) {
Fsm.MAPPER.get(entity).getAnimationFsm().changeState(IDLE);
}
}
@Override
public void exit(Entity entity) {
}
@Override
public boolean onMessage(Entity entity, Telegram telegram) {
return false;

View File

@@ -85,9 +85,16 @@ public class Animation2D implements Component {
return this.stateTime;
}
public boolean isFinished() {
return animation.isAnimationFinished(stateTime);
}
public enum AnimationType {
IDLE,
WALK;
WALK,
ATTACK,
DAMAGED,
;
private final String atlasKey;

View File

@@ -0,0 +1,39 @@
package io.github.com.quillraven.component;
import com.badlogic.ashley.core.Component;
import com.badlogic.ashley.core.ComponentMapper;
public class Attack implements Component {
public static final ComponentMapper<Attack> MAPPER = ComponentMapper.getFor(Attack.class);
private float damage;
private float damageDelay;
private float attackTimer;
public Attack(float damage, float damageDelay) {
this.damage = damage;
this.damageDelay = damageDelay;
this.attackTimer = 0f;
}
public boolean canAttack() {
return this.attackTimer == 0f;
}
public boolean isAttacking() {
return this.attackTimer > 0f;
}
public void startAttack() {
this.attackTimer = this.damageDelay;
}
public void decAttackTimer(float deltaTime) {
attackTimer = Math.max(0f, attackTimer - deltaTime);
}
public float getDamage() {
return damage;
}
}

View File

@@ -0,0 +1,22 @@
package io.github.com.quillraven.component;
import com.badlogic.ashley.core.Component;
import com.badlogic.ashley.core.ComponentMapper;
public class Damaged implements Component {
public static final ComponentMapper<Damaged> MAPPER = ComponentMapper.getFor(Damaged.class);
private float damage;
public Damaged(float damage) {
this.damage = damage;
}
public void addDamage(float amount) {
this.damage += amount;
}
public float getDamage() {
return damage;
}
}

View File

@@ -19,8 +19,10 @@ import io.github.com.quillraven.audio.AudioService;
import io.github.com.quillraven.input.GameControllerState;
import io.github.com.quillraven.input.KeyboardController;
import io.github.com.quillraven.system.AnimationSystem;
import io.github.com.quillraven.system.AttackSystem;
import io.github.com.quillraven.system.CameraSystem;
import io.github.com.quillraven.system.ControllerSystem;
import io.github.com.quillraven.system.DamagedSystem;
import io.github.com.quillraven.system.FacingSystem;
import io.github.com.quillraven.system.FsmSystem;
import io.github.com.quillraven.system.LifeSystem;
@@ -67,7 +69,13 @@ public class GameScreen extends ScreenAdapter {
this.engine.addSystem(new PhysicMoveSystem());
this.engine.addSystem(new PhysicSystem(physicWorld, 1 / 60f));
this.engine.addSystem(new FacingSystem());
this.engine.addSystem(new AttackSystem(physicWorld));
this.engine.addSystem(new FsmSystem());
// DamagedSystem must run after FsmSystem to correctly
// detect when a damaged animation should be played.
// This is done by checking if an entity has a Damaged component,
// and this component is removed in the DamagedSystem.
this.engine.addSystem(new DamagedSystem());
this.engine.addSystem(new TriggerSystem(audioService));
this.engine.addSystem(new LifeSystem(this.viewModel));
this.engine.addSystem(new AnimationSystem(game.getAssetService()));

View File

@@ -0,0 +1,95 @@
package io.github.com.quillraven.system;
import com.badlogic.ashley.core.Entity;
import com.badlogic.ashley.core.Family;
import com.badlogic.ashley.systems.IteratingSystem;
import com.badlogic.gdx.math.Rectangle;
import com.badlogic.gdx.math.Vector2;
import com.badlogic.gdx.physics.box2d.Body;
import com.badlogic.gdx.physics.box2d.Fixture;
import com.badlogic.gdx.physics.box2d.PolygonShape;
import com.badlogic.gdx.physics.box2d.Shape.Type;
import com.badlogic.gdx.physics.box2d.World;
import com.badlogic.gdx.utils.Array;
import com.badlogic.gdx.utils.GdxRuntimeException;
import io.github.com.quillraven.component.Attack;
import io.github.com.quillraven.component.Damaged;
import io.github.com.quillraven.component.Facing;
import io.github.com.quillraven.component.Facing.FacingDirection;
import io.github.com.quillraven.component.Life;
import io.github.com.quillraven.component.Physic;
public class AttackSystem extends IteratingSystem {
public static final Rectangle attackAABB = new Rectangle();
private final World world;
private final Vector2 tmpVertex;
private Body attackerBody;
private float attackDamage;
public AttackSystem(World world) {
super(Family.all(Attack.class, Facing.class, Physic.class).get());
this.world = world;
this.tmpVertex = new Vector2();
this.attackerBody = null;
this.attackDamage = 0f;
}
@Override
protected void processEntity(Entity entity, float deltaTime) {
Attack attack = Attack.MAPPER.get(entity);
if (attack.canAttack()) return;
attack.decAttackTimer(deltaTime);
if (attack.canAttack()) {
FacingDirection facingDirection = Facing.MAPPER.get(entity).getDirection();
attackerBody = Physic.MAPPER.get(entity).getBody();
PolygonShape attackPolygonShape = getAttackFixture(attackerBody, facingDirection);
updateAttackAABB(attackerBody.getPosition(), attackPolygonShape);
this.attackDamage = attack.getDamage();
world.QueryAABB(this::attackCallback, attackAABB.x, attackAABB.y, attackAABB.width, attackAABB.height);
}
}
private boolean attackCallback(Fixture fixture) {
Body body = fixture.getBody();
if (body.equals(attackerBody)) return true;
if (!(body.getUserData() instanceof Entity entity)) return true;
Life life = Life.MAPPER.get(entity);
if (life == null) {
return true;
}
Damaged damaged = Damaged.MAPPER.get(entity);
if (damaged == null) {
entity.add(new Damaged(this.attackDamage));
} else {
damaged.addDamage(this.attackDamage);
}
return true;
}
private void updateAttackAABB(Vector2 bodyPosition, PolygonShape attackPolygonShape) {
attackPolygonShape.getVertex(0, tmpVertex);
tmpVertex.add(bodyPosition);
attackAABB.setPosition(tmpVertex.x, tmpVertex.y);
attackPolygonShape.getVertex(2, tmpVertex);
tmpVertex.add(bodyPosition);
attackAABB.setSize(tmpVertex.x, tmpVertex.y);
}
private PolygonShape getAttackFixture(Body body, FacingDirection direction) {
Array<Fixture> fixtureList = body.getFixtureList();
String fixtureName = "attack_sensor_" + direction.getAtlasKey();
for (Fixture fixture : fixtureList) {
if (fixtureName.equals(fixture.getUserData()) && Type.Polygon.equals(fixture.getShape().getType())) {
return (PolygonShape) fixture.getShape();
}
}
throw new GdxRuntimeException("Entity has no polygon attack sensor with userData '" + fixtureName + "'");
}
}

View File

@@ -4,6 +4,7 @@ import com.badlogic.ashley.core.Entity;
import com.badlogic.ashley.core.Family;
import com.badlogic.ashley.systems.IteratingSystem;
import io.github.com.quillraven.GdxGame;
import io.github.com.quillraven.component.Attack;
import io.github.com.quillraven.component.Controller;
import io.github.com.quillraven.component.Move;
import io.github.com.quillraven.input.Command;
@@ -30,6 +31,7 @@ public class ControllerSystem extends IteratingSystem {
case DOWN -> moveEntity(entity, 0f, -1f);
case LEFT -> moveEntity(entity, -1f, 0f);
case RIGHT -> moveEntity(entity, 1f, 0f);
case SELECT -> startEntityAttack(entity);
case CANCEL -> game.setScreen(MenuScreen.class);
}
}
@@ -46,6 +48,13 @@ public class ControllerSystem extends IteratingSystem {
controller.getReleasedCommands().clear();
}
private void startEntityAttack(Entity entity) {
Attack attack = Attack.MAPPER.get(entity);
if (attack != null && attack.canAttack()) {
attack.startAttack();
}
}
private void moveEntity(Entity entity, float dx, float dy) {
Move move = Move.MAPPER.get(entity);
if (move != null) {

View File

@@ -0,0 +1,25 @@
package io.github.com.quillraven.system;
import com.badlogic.ashley.core.Entity;
import com.badlogic.ashley.core.Family;
import com.badlogic.ashley.systems.IteratingSystem;
import io.github.com.quillraven.component.Damaged;
import io.github.com.quillraven.component.Life;
public class DamagedSystem extends IteratingSystem {
public DamagedSystem() {
super(Family.all(Damaged.class).get());
}
@Override
protected void processEntity(Entity entity, float deltaTime) {
Damaged damaged = Damaged.MAPPER.get(entity);
entity.remove(Damaged.class);
Life life = Life.MAPPER.get(entity);
if (life != null) {
life.addLife(-damaged.getDamage());
}
}
}

View File

@@ -2,6 +2,9 @@ package io.github.com.quillraven.system;
import com.badlogic.ashley.core.EntitySystem;
import com.badlogic.gdx.graphics.Camera;
import com.badlogic.gdx.graphics.Color;
import com.badlogic.gdx.graphics.glutils.ShapeRenderer;
import com.badlogic.gdx.math.Rectangle;
import com.badlogic.gdx.physics.box2d.Box2DDebugRenderer;
import com.badlogic.gdx.physics.box2d.World;
import com.badlogic.gdx.utils.Disposable;
@@ -9,10 +12,12 @@ import com.badlogic.gdx.utils.Disposable;
public class PhysicDebugRenderSystem extends EntitySystem implements Disposable {
private final World physicWorld;
private final Box2DDebugRenderer box2DDebugRenderer;
private final ShapeRenderer shapeRenderer;
private final Camera camera;
public PhysicDebugRenderSystem(World physicWorld, Camera camera) {
this.box2DDebugRenderer = new Box2DDebugRenderer();
this.shapeRenderer = new ShapeRenderer();
this.physicWorld = physicWorld;
this.camera = camera;
setProcessing(false);
@@ -21,10 +26,22 @@ public class PhysicDebugRenderSystem extends EntitySystem implements Disposable
@Override
public void update(float deltaTime) {
this.box2DDebugRenderer.render(physicWorld, camera.combined);
this.shapeRenderer.setProjectionMatrix(camera.combined);
this.shapeRenderer.begin(ShapeRenderer.ShapeType.Line);
this.shapeRenderer.setColor(Color.RED);
Rectangle attackAABB = AttackSystem.attackAABB;
this.shapeRenderer.rect(
attackAABB.x,
attackAABB.y,
attackAABB.width - attackAABB.x,
attackAABB.height - attackAABB.y);
this.shapeRenderer.end();
}
@Override
public void dispose() {
this.box2DDebugRenderer.dispose();
this.shapeRenderer.dispose();
}
}

View File

@@ -10,6 +10,7 @@ import com.badlogic.gdx.physics.box2d.Body;
import com.badlogic.gdx.physics.box2d.Contact;
import com.badlogic.gdx.physics.box2d.ContactImpulse;
import com.badlogic.gdx.physics.box2d.ContactListener;
import com.badlogic.gdx.physics.box2d.Fixture;
import com.badlogic.gdx.physics.box2d.Manifold;
import com.badlogic.gdx.physics.box2d.World;
import io.github.com.quillraven.component.Physic;
@@ -95,26 +96,28 @@ public class PhysicSystem extends IteratingSystem implements EntityListener, Con
@Override
public void beginContact(Contact contact) {
Object userDataA = contact.getFixtureA().getBody().getUserData();
Object userDataB = contact.getFixtureB().getBody().getUserData();
Fixture fixtureA = contact.getFixtureA();
Object userDataA = fixtureA.getBody().getUserData();
Fixture fixtureB = contact.getFixtureB();
Object userDataB = fixtureB.getBody().getUserData();
if (!(userDataA instanceof Entity entityA) || !(userDataB instanceof Entity entityB)) {
return;
}
playerTriggerContact(entityA, entityB);
playerTriggerContact(entityA, fixtureA, entityB, fixtureB);
}
private static void playerTriggerContact(Entity entityA, Entity entityB) {
private static void playerTriggerContact(Entity entityA, Fixture fixtureA, Entity entityB, Fixture fixtureB) {
Trigger trigger = Trigger.MAPPER.get(entityA);
boolean isPlayer = Player.MAPPER.get(entityB) != null;
boolean isPlayer = Player.MAPPER.get(entityB) != null && !fixtureB.isSensor();
if (trigger != null && isPlayer) {
trigger.setTriggeringEntity(entityB);
return;
}
trigger = Trigger.MAPPER.get(entityB);
isPlayer = Player.MAPPER.get(entityA) != null;
isPlayer = Player.MAPPER.get(entityA) != null && !fixtureA.isSensor();
if (trigger != null && isPlayer) {
trigger.setTriggeringEntity(entityA);
}

View File

@@ -17,6 +17,7 @@ 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.BodyDef.BodyType;
import com.badlogic.gdx.physics.box2d.Fixture;
import com.badlogic.gdx.physics.box2d.FixtureDef;
import com.badlogic.gdx.physics.box2d.World;
import com.badlogic.gdx.utils.GdxRuntimeException;
@@ -25,6 +26,7 @@ import io.github.com.quillraven.asset.AssetService;
import io.github.com.quillraven.asset.AtlasAsset;
import io.github.com.quillraven.component.Animation2D;
import io.github.com.quillraven.component.Animation2D.AnimationType;
import io.github.com.quillraven.component.Attack;
import io.github.com.quillraven.component.CameraFollow;
import io.github.com.quillraven.component.Controller;
import io.github.com.quillraven.component.Facing;
@@ -92,7 +94,6 @@ public class TiledAshleyConfigurator {
Entity entity = this.engine.createEntity();
TiledMapTile tile = tileMapObject.getTile();
TextureRegion textureRegion = getTextureRegion(tile);
String classType = tile.getProperties().get("type", "", String.class);
float sortOffsetY = tile.getProperties().get("sortOffsetY", 0, Integer.class);
sortOffsetY *= GdxGame.UNIT_SCALE;
int z = tile.getProperties().get("z", 1, Integer.class);
@@ -103,9 +104,10 @@ public class TiledAshleyConfigurator {
tileMapObject.getScaleX(), tileMapObject.getScaleY(),
sortOffsetY,
entity);
BodyType bodyType = getObjectBodyType(tile);
addEntityPhysic(
tile.getObjects(),
"Prop".equals(classType) ? BodyType.StaticBody : BodyType.DynamicBody,
bodyType,
Vector2.Zero,
entity);
addEntityAnimation(tile, entity);
@@ -114,6 +116,7 @@ public class TiledAshleyConfigurator {
addEntityCameraFollow(tileMapObject, entity);
addEntityLife(tile, entity);
addEntityPlayer(tileMapObject, entity);
addEntityAttack(tile, entity);
entity.add(new Facing(FacingDirection.DOWN));
entity.add(new Fsm(entity));
entity.add(new Graphic(textureRegion, Color.WHITE.cpy()));
@@ -122,6 +125,24 @@ public class TiledAshleyConfigurator {
this.engine.addEntity(entity);
}
private BodyType getObjectBodyType(TiledMapTile tile) {
String classType = tile.getProperties().get("type", "", String.class);
if ("Prop".equals(classType)) {
return BodyType.StaticBody;
}
String bodyTypeStr = tile.getProperties().get("bodyType", "DynamicBody", String.class);
return BodyType.valueOf(bodyTypeStr);
}
private void addEntityAttack(TiledMapTile tile, Entity entity) {
float damage = tile.getProperties().get("damage", 0f, Float.class);
if (damage == 0f) return;
float damageDelay = tile.getProperties().get("damageDelay", 0f, Float.class);
entity.add(new Attack(damage, damageDelay));
}
private void addEntityPlayer(TiledMapTileMapObject tileMapObject, Entity entity) {
if ("Player".equals(tileMapObject.getName())) {
entity.add(new Player());
@@ -227,7 +248,8 @@ public class TiledAshleyConfigurator {
body.setUserData(userData);
for (MapObject object : mapObjects) {
FixtureDef fixtureDef = TiledPhysics.fixtureDefOf(object, scaling, relativeTo);
body.createFixture(fixtureDef);
Fixture fixture = body.createFixture(fixtureDef);
fixture.setUserData(object.getName());
fixtureDef.shape.dispose();
}
return body;