개인의 기록
  • 소개
  • 프로젝트
  • 글
  • 링크

© 2026 newgirok

← Lion Survival

전투 시스템 — 자동 공격과 이펙트

2026년 5월 20일
phaser3combateffectssprite

서바이벌 게임의 핵심은 자동 공격이다.

플레이어가 직접 공격 버튼을 누르지 않아도 무기가 알아서 발사된다. 이걸 구현하는 게 이번 커밋의 목표였다.

Dynamic vs Static 무기 그룹

무기를 두 가지로 분류했다.

Dynamic — 발사체. 날아가다가 몬스터에 맞으면 사라진다.

  • Beam — 가장 가까운 몬스터를 향해 발사되는 광선

Static — 고정형. 플레이어 주변에 붙어서 지속 피해를 준다.

  • Claw — 플레이어 앞뒤에서 순서대로 발동
  • Catnip — 플레이어 위치에 지속적으로 깔리는 범위기

PlayingScene에서 두 그룹을 따로 관리한다.

this.m_weaponDynamic = this.add.group();
this.m_weaponStatic = this.add.group();

// 충돌 처리 분리
this.physics.add.overlap(this.m_weaponDynamic, this.m_mobs, (weapon, mob) => {
  mob.hitByDynamic(weapon, weapon.m_damage);
});
this.physics.add.overlap(this.m_weaponStatic, this.m_mobs, (weapon, mob) => {
  mob.hitByStatic(weapon.m_damage);
});

Dynamic 충돌에선 weapon을 같이 넘긴다. 맞은 무기를 destroy해야 하니까. Static은 무기 자체는 유지되고 데미지만 처리한다.

Beam — 가장 가까운 적을 향해

constructor(scene, startingPosition, damage, scale) {
  super(scene, startingPosition[0], startingPosition[1], "beam");
  this.SPEED = 100;
  this.DURATION = 1500;

  scene.m_weaponDynamic.add(this); // Dynamic 그룹에 등록
  scene.m_beamSound.play();

  this.m_damage = damage;
  this.setVelocity();  // 가장 가까운 몬스터 방향으로 속도 설정
  this.setAngle();     // 스프라이트 회전

  // 1.5초 후 자동 소멸
  scene.time.addEvent({
    delay: this.DURATION,
    callback: () => this.destroy(),
    loop: false,
  });
}

setVelocity() {
  const closest = this.scene.m_closest;
  if (!closest) { this.setVelocityY(-250); return; }

  const dx = closest.x - this.x;
  const dy = closest.y - this.y;
  const r = Math.sqrt(dx * dx + dy * dy) / 2;
  this.body.velocity.x = (dx / r) * this.SPEED;
  this.body.velocity.y = (dy / r) * this.SPEED;
}

scene.m_closest는 PlayingScene.update에서 매 프레임 갱신된다. Beam이 생성되는 시점의 가장 가까운 적을 향해 날아간다.

벡터를 정규화할 때 거리로 나누는 게 핵심이다. 이걸 안 하면 가까이 있는 적한테는 느리게, 멀리 있는 적한테는 빠르게 날아간다.

Claw — 앞뒤 연속 공격

Claw는 attackManager에서 500ms 간격으로 앞뒤를 번갈아 생성한다.

// attackManager.js
function doAttackOneSet(scene, attackType, damage, scale) {
  if (attackType === "claw") {
    // 앞쪽 발톱 → 500ms 후 뒤쪽 발톱
    new Claw(scene, damage, scale, 1);
    scene.time.addEvent({
      delay: 500,
      callback: () => new Claw(scene, damage, scale, -1),
    });
  }
}

Claw는 Static 그룹에 속하고, 생성 위치는 플레이어 이동 방향 기준으로 앞/뒤를 계산한다.

Explosion — 몬스터 사망 이펙트

몬스터가 죽을 때 폭발 이펙트가 재생된다.

export default class Explosion extends Phaser.Physics.Arcade.Sprite {
  constructor(scene, x, y) {
    super(scene, x, y, "explosion");
    scene.add.existing(this);
    scene.physics.world.enableBody(this);
    this.setDepth(20);

    // 애니메이션 재생 후 자동 제거
    this.play("explode");
    this.on("animationcomplete", () => this.destroy());
  }
}

animationcomplete 이벤트로 애니메이션이 끝나면 자동으로 소멸한다. 수동으로 destroy 타이밍을 잡을 필요가 없다.

attackManager — 공격 이벤트 관리

모든 공격 이벤트는 scene.m_attackEvents 객체에 저장한다.

export function addAttackEvent(scene, attackType, damage, scale, repeatGap) {
  scene.m_attackEvents[attackType] = {
    event: scene.time.addEvent({
      delay: repeatGap,
      callback: () => doAttackOneSet(scene, attackType, damage, scale),
      loop: true,
    }),
    damage,
    scale,
    repeatGap,
  };
}

레벨업 때 setAttackScale(scene, "beam", 2)로 스케일을 바꾸거나, setAttackDamage로 데미지를 올린다. 이벤트 객체를 직접 참조해서 값을 수정하는 방식이다.

무기를 Dynamic/Static으로 분리한 게 나중에 확장할 때 깔끔했다.

새 무기 타입을 추가할 때 어느 그룹에 넣을지만 결정하면 충돌 처리는 자동으로 따라온다.

← 이전 글캐릭터 구현 — Player와 Mob
다음 글 →경험치와 레벨업 — 아이템과 UI