Skip to content

Commit 9f29236

Browse files
committed
세그먼트 이동 알고리즘을 chase-following에서 path-following으로 변경
1 parent 084c4cc commit 9f29236

3 files changed

Lines changed: 128 additions & 36 deletions

File tree

‎game-server/src/game/engine.ts‎

Lines changed: 84 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -123,10 +123,18 @@ function updateWormRotation(worm: Worm, targetDirection: { x: number; y: number
123123
}
124124
}
125125

126+
/**
127+
* 지렁이의 경로 기록을 초기화합니다.
128+
*/
129+
export function initializePositionHistory(worm: Worm): { x: number; y: number }[] {
130+
// 세그먼트 위치를 역순으로 저장 (과거 → 최신 순서)
131+
return worm.segments.map((s) => ({ x: s.x, y: s.y })).reverse();
132+
}
133+
126134
/**
127135
* 지렁이 머리의 위치를 업데이트합니다.
128136
*/
129-
function updateWormHead(worm: Worm, deltaTime: number): void {
137+
function updateWormHead(worm: Worm, deltaTime: number, positionHistory: { x: number; y: number }[]): void {
130138
const dirX = worm.direction.x;
131139
const dirY = worm.direction.y;
132140
const magnitude = Math.sqrt(dirX * dirX + dirY * dirY);
@@ -144,27 +152,67 @@ function updateWormHead(worm: Worm, deltaTime: number): void {
144152
head.x += normalizedDirX * speed * deltaTime;
145153
head.y += normalizedDirY * speed * deltaTime;
146154
}
155+
156+
// 머리의 새 위치를 경로 기록에 추가 (배열 끝 = 최신)
157+
const head = worm.segments[0];
158+
positionHistory.push({ x: head.x, y: head.y });
147159
}
148160

149161
/**
150-
* 지렁이의 몸통 세그먼트들이 머리를 따라오도록 업데이트합니다.
162+
* 지렁이의 몸통 세그먼트들이 머리의 경로를 따라가도록 업데이트합니다.
163+
* 경로 기록을 역순(최신→과거)으로 걸어가며 각 세그먼트를 i × SEGMENT_SPACING 거리 지점에 배치합니다.
151164
*/
152-
function updateWormSegments(worm: Worm): void {
153-
// 세그먼트들 따라오게 하기
154-
for (let i = 1; i < worm.segments.length; i++) {
155-
const prev = worm.segments[i - 1];
156-
const curr = worm.segments[i];
157-
158-
const dx = prev.x - curr.x;
159-
const dy = prev.y - curr.y;
160-
const distance = Math.hypot(dx, dy);
161-
162-
if (distance > GAME_CONSTANTS.SEGMENT_SPACING) {
163-
const moveX = (dx / distance) * (distance - GAME_CONSTANTS.SEGMENT_SPACING);
164-
const moveY = (dy / distance) * (distance - GAME_CONSTANTS.SEGMENT_SPACING);
165-
166-
curr.x += moveX;
167-
curr.y += moveY;
165+
function updateWormSegments(worm: Worm, positionHistory: { x: number; y: number }[]): void {
166+
if (positionHistory.length < 2) return;
167+
168+
let segmentIndex = 1;
169+
let targetDistance = GAME_CONSTANTS.SEGMENT_SPACING;
170+
let accumulatedDistance = 0;
171+
172+
// 경로 기록을 역순(최신→과거)으로 순회
173+
for (let j = positionHistory.length - 1; j > 0 && segmentIndex < worm.segments.length; j--) {
174+
const p1 = positionHistory[j]; // 최신 쪽
175+
const p2 = positionHistory[j - 1]; // 과거 쪽
176+
const dx = p2.x - p1.x;
177+
const dy = p2.y - p1.y;
178+
const pathSegmentLength = Math.hypot(dx, dy);
179+
180+
// 이 경로 구간 내에 배치할 수 있는 세그먼트가 있는지 확인
181+
while (segmentIndex < worm.segments.length && accumulatedDistance + pathSegmentLength >= targetDistance) {
182+
const remaining = targetDistance - accumulatedDistance;
183+
const t = pathSegmentLength > 0 ? remaining / pathSegmentLength : 0;
184+
185+
worm.segments[segmentIndex].x = p1.x + dx * t;
186+
worm.segments[segmentIndex].y = p1.y + dy * t;
187+
188+
segmentIndex++;
189+
targetDistance = segmentIndex * GAME_CONSTANTS.SEGMENT_SPACING;
190+
}
191+
192+
accumulatedDistance += pathSegmentLength;
193+
}
194+
195+
// 경로가 부족한 경우 남은 세그먼트는 경로의 가장 오래된 위치에 배치
196+
if (segmentIndex < worm.segments.length) {
197+
const lastPoint = positionHistory[0];
198+
for (let i = segmentIndex; i < worm.segments.length; i++) {
199+
worm.segments[i].x = lastPoint.x;
200+
worm.segments[i].y = lastPoint.y;
201+
}
202+
}
203+
204+
// 경로 기록 트리밍 - 필요한 길이 + 여유분만 유지
205+
const maxNeededDistance = worm.segments.length * GAME_CONSTANTS.SEGMENT_SPACING;
206+
let totalDist = 0;
207+
for (let j = positionHistory.length - 1; j > 0; j--) {
208+
totalDist += Math.hypot(
209+
positionHistory[j].x - positionHistory[j - 1].x,
210+
positionHistory[j].y - positionHistory[j - 1].y,
211+
);
212+
if (totalDist > maxNeededDistance + GAME_CONSTANTS.SEGMENT_SPACING * 2) {
213+
// j-1 이전의 기록은 불필요하므로 제거
214+
positionHistory.splice(0, j - 1);
215+
break;
168216
}
169217
}
170218
}
@@ -176,18 +224,22 @@ function updateSingleWorm(
176224
worm: Worm,
177225
deltaTime: number,
178226
targetDirections: Map<string, { x: number; y: number }>,
227+
positionHistories: Map<string, { x: number; y: number }[]>,
179228
): void {
180229
// 부드러운 회전 로직 적용
181230
const targetDirection = targetDirections.get(worm.id);
182231
if (targetDirection) {
183232
updateWormRotation(worm, targetDirection, deltaTime);
184233
}
185234

186-
// 머리 위치 업데이트
187-
updateWormHead(worm, deltaTime);
235+
const positionHistory = positionHistories.get(worm.id);
236+
if (!positionHistory) return;
237+
238+
// 머리 위치 업데이트 + 경로 기록 추가
239+
updateWormHead(worm, deltaTime, positionHistory);
188240

189-
// 몸통 세그먼트들 업데이트
190-
updateWormSegments(worm);
241+
// 몸통 세그먼트들 업데이트 (경로 기반)
242+
updateWormSegments(worm, positionHistory);
191243
}
192244

193245
/**
@@ -204,6 +256,7 @@ export function updateWorld(
204256
foods: Map<string, Food>,
205257
targetDirections: Map<string, { x: number; y: number }>,
206258
botMovementStrategies: Map<string, MovementStrategy>,
259+
positionHistories: Map<string, { x: number; y: number }[]>,
207260
): void {
208261
const allWorms = Array.from(worms.values());
209262

@@ -214,7 +267,7 @@ export function updateWorld(
214267
}
215268

216269
// 개별 지렁이 상태 업데이트
217-
updateSingleWorm(worm, deltaTime, targetDirections);
270+
updateSingleWorm(worm, deltaTime, targetDirections, positionHistories);
218271
}
219272
}
220273

@@ -294,6 +347,7 @@ function removeDeadPlayer(
294347
playerId: string,
295348
worms: Map<string, Worm>,
296349
targetDirections: Map<string, { x: number; y: number }>,
350+
positionHistories: Map<string, { x: number; y: number }[]>,
297351
): void {
298352
const worm = worms.get(playerId);
299353
if (worm && worm.isDead && worm.type === WormType.Player) {
@@ -302,6 +356,7 @@ function removeDeadPlayer(
302356
// 플레이어 상태 제거
303357
worms.delete(playerId);
304358
targetDirections.delete(playerId);
359+
positionHistories.delete(playerId);
305360
}
306361
}
307362

@@ -313,15 +368,16 @@ export function handleKilledWorms(
313368
worms: Map<string, Worm>,
314369
targetDirections: Map<string, { x: number; y: number }>,
315370
botMovementStrategies: Map<string, MovementStrategy>,
371+
positionHistories: Map<string, { x: number; y: number }[]>,
316372
): string[] {
317373
const removedPlayerIds: string[] = [];
318374

319375
for (const [wormId, worm] of worms) {
320376
if (worm.isDead) {
321377
if (worm.type === WormType.Bot) {
322-
respawnBot(wormId, worms, targetDirections, botMovementStrategies);
378+
respawnBot(wormId, worms, targetDirections, botMovementStrategies, positionHistories);
323379
} else if (worm.type === WormType.Player) {
324-
removeDeadPlayer(wormId, worms, targetDirections);
380+
removeDeadPlayer(wormId, worms, targetDirections, positionHistories);
325381
removedPlayerIds.push(wormId);
326382
}
327383
}
@@ -335,6 +391,7 @@ function respawnBot(
335391
worms: Map<string, Worm>,
336392
targetDirections: Map<string, { x: number; y: number }>,
337393
botMovementStrategies: Map<string, MovementStrategy>,
394+
positionHistories: Map<string, { x: number; y: number }[]>,
338395
): void {
339396
// 기존 봇 데이터 제거
340397
const bot = worms.get(botId);
@@ -344,6 +401,7 @@ function respawnBot(
344401
worms.delete(botId);
345402
targetDirections.delete(botId);
346403
botMovementStrategies.delete(botId);
404+
positionHistories.delete(botId);
347405

348406
// 새로운 랜덤 타입의 봇 생성
349407
const numericBotTypes = Object.values(BotType).filter((v) => typeof v === "number") as BotType[];
@@ -356,6 +414,7 @@ function respawnBot(
356414
worms.set(botId, newBot);
357415
targetDirections.set(botId, { x: newBot.direction.x, y: newBot.direction.y });
358416
botMovementStrategies.set(botId, createMovementStrategy(botType));
417+
positionHistories.set(botId, initializePositionHistory(newBot));
359418

360419
console.log(`🤖 Bot ${botId} respawned as type ${botType}`);
361420
}

‎game-server/src/index.ts‎

Lines changed: 36 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import {
1313
handleSprintFoodDrop,
1414
handleKilledWorms,
1515
handleMapBoundaryExceedingWorms,
16+
initializePositionHistory,
1617
} from "./game/engine";
1718
import { setupSocketHandlers } from "./socket/handlers";
1819
import { registerWithLobby } from "./lobby/lobbyApi";
@@ -61,6 +62,7 @@ function initializeBots(
6162
worms: Map<string, Worm>,
6263
targetDirections: Map<string, { x: number; y: number }>,
6364
botMovementStrategies: Map<string, MovementStrategy>,
65+
positionHistories: Map<string, { x: number; y: number }[]>,
6466
): Worm[] {
6567
const createdBots: Worm[] = [];
6668

@@ -72,6 +74,7 @@ function initializeBots(
7274
worms.set(bot.id, bot);
7375
targetDirections.set(bot.id, { x: bot.direction.x, y: bot.direction.y });
7476
botMovementStrategies.set(bot.id, createMovementStrategy(botType));
77+
positionHistories.set(bot.id, initializePositionHistory(bot));
7578

7679
createdBots.push(bot);
7780
}
@@ -86,6 +89,7 @@ function removeAllBots(
8689
worms: Map<string, Worm>,
8790
targetDirections: Map<string, { x: number; y: number }>,
8891
botMovementStrategies: Map<string, MovementStrategy>,
92+
positionHistories: Map<string, { x: number; y: number }[]>,
8993
): void {
9094
const botIds: string[] = [];
9195

@@ -101,6 +105,7 @@ function removeAllBots(
101105
worms.delete(botId);
102106
targetDirections.delete(botId);
103107
botMovementStrategies.delete(botId);
108+
positionHistories.delete(botId);
104109
}
105110

106111
console.log(`🤖 Removed ${botIds.length} bots - no players online`);
@@ -114,6 +119,7 @@ function manageBots(
114119
worms: Map<string, Worm>,
115120
targetDirections: Map<string, { x: number; y: number }>,
116121
botMovementStrategies: Map<string, MovementStrategy>,
122+
positionHistories: Map<string, { x: number; y: number }[]>,
117123
playerCount: number,
118124
): Worm[] {
119125
let botCount = 0;
@@ -126,13 +132,13 @@ function manageBots(
126132
if (playerCount === 0) {
127133
// 서버에 연결된 플레이어가 없으면 모든 봇 제거
128134
if (botCount > 0) {
129-
removeAllBots(worms, targetDirections, botMovementStrategies);
135+
removeAllBots(worms, targetDirections, botMovementStrategies, positionHistories);
130136
}
131137
return [];
132138
} else if (botCount === 0) {
133139
// 플레이어가 있는데 봇이 없으면 봇 생성
134140
console.log(`🤖 Creating bots - ${playerCount} players online`);
135-
return initializeBots(worms, targetDirections, botMovementStrategies);
141+
return initializeBots(worms, targetDirections, botMovementStrategies, positionHistories);
136142
}
137143

138144
return [];
@@ -168,21 +174,28 @@ function updateAndBroadcastGameState(
168174
foods: Map<string, Food>,
169175
targetDirections: Map<string, { x: number; y: number }>,
170176
botMovementStrategies: Map<string, MovementStrategy>,
177+
positionHistories: Map<string, { x: number; y: number }[]>,
171178
): void {
172179
// 봇 관리 (주기적으로 체크) - 새로 생성된 봇들 수집
173-
const newlyCreatedBots = manageBots(worms, targetDirections, botMovementStrategies, io.engine.clientsCount);
180+
const newlyCreatedBots = manageBots(
181+
worms,
182+
targetDirections,
183+
botMovementStrategies,
184+
positionHistories,
185+
io.engine.clientsCount,
186+
);
174187

175188
// 먹이 업데이트 (부족한 먹이 추가)
176189
updateFoods(foods);
177190

178191
// 부활 처리 및 제거된 플레이어 수집
179-
const removedPlayerIds = handleKilledWorms(worms, targetDirections, botMovementStrategies);
192+
const removedPlayerIds = handleKilledWorms(worms, targetDirections, botMovementStrategies, positionHistories);
180193

181194
// 스프린트 중 먹이 떨어뜨리기 처리
182195
handleSprintFoodDrop(worms, foods, deltaTime);
183196

184197
// 지렁이 상태 업데이트
185-
updateWorld(deltaTime, worms, foods, targetDirections, botMovementStrategies);
198+
updateWorld(deltaTime, worms, foods, targetDirections, botMovementStrategies, positionHistories);
186199

187200
// 맵 경계 초과 지렁이 처리
188201
const mapBoundaryExceedingWorms = handleMapBoundaryExceedingWorms(worms, foods);
@@ -238,6 +251,7 @@ function createGameLoop(
238251
foods: Map<string, Food>,
239252
targetDirections: Map<string, { x: number; y: number }>,
240253
botMovementStrategies: Map<string, MovementStrategy>,
254+
positionHistories: Map<string, { x: number; y: number }[]>,
241255
): () => void {
242256
let lastTickTime = Date.now();
243257

@@ -246,7 +260,15 @@ function createGameLoop(
246260
const deltaTime = (now - lastTickTime) / 1000;
247261
lastTickTime = now;
248262

249-
updateAndBroadcastGameState(io, deltaTime, worms, foods, targetDirections, botMovementStrategies);
263+
updateAndBroadcastGameState(
264+
io,
265+
deltaTime,
266+
worms,
267+
foods,
268+
targetDirections,
269+
botMovementStrategies,
270+
positionHistories,
271+
);
250272

251273
let nextSleepTime = GAME_CONSTANTS.TICK_MS - (Date.now() - lastTickTime); // 다음 틱까지 대기 시간 계산
252274
if (nextSleepTime < 0) {
@@ -301,11 +323,17 @@ async function main() {
301323
*/
302324
const botMovementStrategies = new Map<string, MovementStrategy>();
303325

326+
/**
327+
* 각 지렁이의 경로 기록을 저장하는 맵 (path-following용)
328+
* Key: wormId, Value: 경로 기록 배열 (과거 → 최신 순서)
329+
*/
330+
const positionHistories = new Map<string, { x: number; y: number }[]>();
331+
304332
// Socket.IO 이벤트 핸들러 설정
305-
setupSocketHandlers(io, worms, foods, targetDirections);
333+
setupSocketHandlers(io, worms, foods, targetDirections, positionHistories);
306334

307335
// 게임 루프 시작
308-
const gameLoop = createGameLoop(io, worms, foods, targetDirections, botMovementStrategies);
336+
const gameLoop = createGameLoop(io, worms, foods, targetDirections, botMovementStrategies, positionHistories);
309337
gameLoop();
310338

311339
// 서버 종료 시그널 처리

0 commit comments

Comments
 (0)