Skip to content
ShaneMcIvor edited this page Sep 4, 2025 · 1 revision

Introduction

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.

Key Features

  • 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

Usage

Hero

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;
}

}`

Projectile

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
});

}`

UML Diagram

image

More Information

For more information visit this README file.

Clone this wiki locally