서바이벌 게임의 핵심은 자동 공격이다.
플레이어가 직접 공격 버튼을 누르지 않아도 무기가 알아서 발사된다. 이걸 구현하는 게 이번 커밋의 목표였다.
무기를 두 가지로 분류했다.
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은 무기 자체는 유지되고 데미지만 처리한다.
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는 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 그룹에 속하고, 생성 위치는 플레이어 이동 방향 기준으로 앞/뒤를 계산한다.
몬스터가 죽을 때 폭발 이펙트가 재생된다.
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 타이밍을 잡을 필요가 없다.
모든 공격 이벤트는 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으로 분리한 게 나중에 확장할 때 깔끔했다.
새 무기 타입을 추가할 때 어느 그룹에 넣을지만 결정하면 충돌 처리는 자동으로 따라온다.