-
Notifications
You must be signed in to change notification settings - Fork 0
Hero
The Hero Factory and Projectile Factory classes contain the functions required to create the Hero and Projectile, whilst the folder source/core/src/main/com/csse3200/game/projectile contains the classes to implement the functionality on the Projectile and the folder source/core/src/main/com/csse3200/game/hero contains the functionality of the Hero. In this document, we will go over the creation and behaviour of the hero.
- Hero Placement: The hero will be dragged and dropped to be
- Rotation: The hero rotates to follow the mouse
- Attack: The hero will shoot projectiles in the direction of the mouse that cause damage to the enemy when they make contact
The hero will be able to be placed any (non-path) spot on the map once. Once the hero has been placed, it cannot be moved again. ` public void create() { // Setup input multiplexer prevProcessor = Gdx.input.getInputProcessor(); if (prevProcessor instanceof InputMultiplexer) { muxSnapshot = (InputMultiplexer) prevProcessor; createdNewMux = false; } else { muxSnapshot = new InputMultiplexer(); if (prevProcessor != null) muxSnapshot.addProcessor(prevProcessor); Gdx.input.setInputProcessor(muxSnapshot); createdNewMux = true; }
input = new InputAdapter() {
@Override
public boolean keyDown(int keycode) {
if (keycode == Input.Keys.ENTER || keycode == Input.Keys.NUMPAD_ENTER) {
// ESC cancels the current preview but does not exit placement mode
Gdx.app.postRunnable(() -> {
removeGhost();
placed = false;
});
logger.info("Hero preview cancelled (placement mode still active).");
return true;
}
return false;
}
@Override
public boolean touchDown(int screenX, int screenY, int pointer, int button) {
if (pointer != 0) return false; // Ignore multi-touch
if (button == Input.Buttons.RIGHT) {
// Right-click: generate preview if none exists
if (ghostEntity == null) {
GridPoint2 cell = screenToGrid(screenX, screenY);
if (cell == null) return true;
// Optional: check if terrain.isPlaceable(cell)
spawnGhost(cell);
previewCell = cell;
logger.info("Preview hero at ({}, {})", cell.x, cell.y);
return true;
}
return false;
}
if (button == Input.Buttons.LEFT) {
// Left-click: confirm placement if clicking the preview cell
if (ghostEntity != null && previewCell != null && hitGhostByScreen(screenX, screenY)) {
final GridPoint2 cell = new GridPoint2(previewCell);
placed = true;
try {
if (onPlace != null) onPlace.accept(cell);
} finally {
Gdx.app.postRunnable(() -> {
removeGhost();
detachInput(true);
});
}
logger.info("Hero confirmed at ({}, {})", cell.x, cell.y);
return true;
}
return false;
}
return false;
}
};
// Insert at highest priority
muxSnapshot.addProcessor(0, input);
logger.info("Right-click to preview, left-click to confirm, ESC to cancel preview.");
}`
All hero logic is implemented in HeroTurretAttackComponent.java, including aiming, shooting and cooldown. Each update cycle, the tower will rotate to point towards the mouse pointer. Then, if the cooldown has finished, the hero will attack by spawning a projectile that travels towards in the direction of the mouse pointer, otherwise the code will continue. A maximum of one projectile will be generated each update cycle. `public void update() { if (entity == null) return;
// Use Gdx deltaTime; fallback to 1/60 if unavailable
float dt = Gdx.graphics != null ? Gdx.graphics.getDeltaTime() : (1f / 60f);
if (cdTimer > 0f) {
cdTimer -= dt;
}
// Calculate aiming direction
Vector2 firePos = getEntityCenter(entity);
if (!computeAimDirection(firePos, dir)) return;
// Rotate sprite towards aim direction (RotatingTextureRenderComponent)
RotatingTextureRenderComponent rot = entity.getComponent(RotatingTextureRenderComponent.class);
if (rot != null) {
float angleDeg = dir.angleDeg() + SPRITE_FACING_OFFSET_DEG;
rot.setRotation(angleDeg);
}
// Only fire when cooldown has finished
if (cdTimer <= 0f) {
float vx = dir.x * bulletSpeed;
float vy = dir.y * bulletSpeed;
final Entity bullet = ProjectileFactory.createBullet(
bulletTexture, firePos, vx, vy, bulletLife, damage
);
var es = ServiceLocator.getEntityService();
if (es != null) {
// Avoid modifying entity collection during iteration
Gdx.app.postRunnable(() -> es.register(bullet));
} else {
Gdx.app.error("HeroTurret", "EntityService is null; skip bullet spawn this frame");
}
cdTimer = cooldown;
}
}`
The projectile is created by the HeroTurretAttackComponent and further behaviour is implemented in the ProjectileComponent, DestroyOnHitComponent and TouchAttackComponent classes. Once created, the projectile travels in a straight line towards the mouse pointer and disappears when it reaches the edge of the screen or comes into contact with an enemy. If a projectile touches an enemy, it will deal the damage specified in HeroTurretAttackComponent to that enemy. `public void update() { float dt = ServiceLocator.getTimeSource().getDeltaTime(); timer -= dt;
if (timer <= 0f) {
if (dead) return;
dead = true;
// 1) Stop physics immediately to prevent further collisions this frame
if (physics != null && physics.getBody() != null) {
physics.getBody().setLinearVelocity(0, 0);
physics.getBody().setActive(false);
}
// 2) Defer disposal to next frame (Entity.dispose() will unregister internally)
com.badlogic.gdx.Gdx.app.postRunnable(() -> {
try {
entity.dispose(); // Will call ServiceLocator.getEntityService().unregister(this)
} catch (Exception ignored) {
// Swallow to avoid crashing render thread; entity will be cleaned up by service.
}
});
}`
`private void onCollisionStart(Fixture me, Fixture other) { // Only handle contacts involving this entity's hitbox if (hitbox == null || hitbox.getFixture() != me) return;
// Process only if the other fixture matches the target layer
if (!PhysicsLayer.contains(targetLayer, other.getFilterData().categoryBits)) return;
// Allow entry only once
if (!scheduled.compareAndSet(false, true)) return;
// Schedule destruction after event dispatch, to avoid concurrent modification
Gdx.app.postRunnable(() -> {
entity.dispose(); // Entity.dispose() will internally unregister
});
}`
For more information visit this README file.