Show GN: 3일만에 antigravity로 만들어본 타워디펜스
(tower.dsp.ai.kr)원래는 claude code와 cursor를 쓰다가 작년 퇴사 후 11월부터 antigravity로 갈아타고나서 이것저것 토이 웹서비스 프로젝트를 하루마다 만들고 있었는데, 갑자기 3일전(1/2)에 타워디펜스 게임을 만들어 보고 싶어졌습니다.
원래 웹frontend개발자라 앱으로 감싸기는 귀찮고, 그냥 데스크탑 브라우저 환경에서만 동작하게 하고, 캔버스 기반 동작 및 그림파일 작업은 너무 시간이 오래걸릴것 같아서 이모지를 최대한 활용하자는 아이디어로 시작했습니다.
하루만에 끝내려고 했는데, 만들다보니 재미있어서 이것저것 수정/추가하다보니 3일이나 걸렸네요. ㅠㅜ
기본적인 기술스택은 다음과 같습니다.
- Frontend: Next.js 14+ (App Router), TypeScript, HTML5 Canvas
- Backend: FastAPI, Python 3.9+, Pydantic
- Database: Supabase (PostgreSQL)
- Deployment: Vercel
이런저런 수정을 거쳐 최종적으로 게임로직은 다음과 같이 결정되었습니다.
1. 타워 공격력
타워는 머지(Merge)를 통해 레벨이 오를 때마다 사거리(Range)가 10씩 증가하지만, 공격력 효율은 1.8배로 조정됩니다 (밸런스).
-
공식:
Damage = floor(BaseDamage * (1.8 ^ (Level - 1)))- Lv 1: 10
- Lv 2: 18
- Lv 3: 32
- Lv 4: 57
2. 적 등장 패턴 (Wave Logic)
단조로움을 없애기 위해 다양한 웨이브 패턴이 적용됩니다.
- 일반 웨이브 (Horde Mix): 현재 티어 적(70%)과 이전 티어 적(30%)이 섞여서 등장합니다.
-
보스 웨이브 (Every 6th): 보스는 시대의 최강 적 1마리가 등장하며, 주변에 쫄병들이 호위합니다.
- 보스 웨이브의 적 수:
Wave / 6(웨이브 6에 1마리, 웨이브 12에 2마리...)
- 보스 웨이브의 적 수:
4. 골드 보상 (Active)
적을 처치하거나 웨이브를 클리어하면 골드를 획득합니다.
ps. 모바일 환경은 크게 고려하지 않았습니다. 터치이벤트를 추가했으나 폴드나 패드에서는 잘 되지만 작은 화면에서의 고려는 그다지 안했습니다~
ps. 개선점 및 버그는 댓글로 알려주시면 시간날때마다 반영하겠습니다.
저도 최근에 게임 만들어보다가 https://verse8.io 써봤는데, 이미지랑 2d 스프라이트 애니메이션까지 직접 만들어줘서 에셋 작업에 시간 안 쓰고 게임 로직에만 집중할 수 있더라고요. 거기다 플랫폼에서 바로 배포까지 되니까 별도로 호스팅 신경 쓸 필요 없어서 정말 편했습니다.
재밌네요. 이모지가 적극적으로 쓰인게 굉장히 새롭게 느껴져요.
제가 타워디펜스 게임을 자주 해보지 않아가지고 이게 맞는 의견일지 잘 모르겠는데, 웨이브 타임과 준비 타임이 나눠져 있지 않아서 웨이브가 오는 순간에도 타워의 위치를 바꿀 수 있게 되어있는게 이상하게 느껴집니다.
극단적으로는 위치를 계속 바꿔가면서 매 순간 공격하게 할 수 있어서 사거리가 무의미하다고 느껴집니다.
재밌게 해 주셔서 감사합니다.
저도 만들면서 위치를 이동하는게 이상하다고 느껴졌었는데, 그게 나름대로 이 게임의 장점이자 동적이고 컨트롤할수 있는 요소가 될수 있어 보여서 그대로 놔뒀습니다.
사실 빨리 옮기려면 타워를 머지하지 않으면 정말 힘들거든요.
옮기는 데에 대한 패널티를 추가로 둘지 등 게임 밸런스는 계속 고민할 포인트인것 같습니다.
안녕하세요 재밌게 플레이했습니다, 보안관련 피드백 드립니다.
- 현재 HMAC 시크릿(서명용 키)이 프론트엔드에 노출되어 있습니다
- 프론트에서 직접적으로 gold, wave, lives, towers 를 변경할수있습니다
vercel 과 supabase 요금제문제가 있을수있어서 알려드립니다
const state = {
towers: [],
gold: 9999,
wave: 1,
lives: 1
};
const state_raw = JSON.stringify(state);
const user_id = "11111";
const secret = "11111";
(async () => {
const key = await crypto.subtle.importKey(
"raw",
new TextEncoder().encode(secret),
{ name: "HMAC", hash: "SHA-256" },
false,
["sign"]
);
const signatureBuffer = await crypto.subtle.sign(
"HMAC",
key,
new TextEncoder().encode(state_raw)
);
const signature = Array.from(new Uint8Array(signatureBuffer))
.map(b => b.toString(16).padStart(2, "0")).join("");
fetch("https://tower.dsp.ai.kr/api/sync", {
method: "POST",
headers: {
"Content-Type": "application/json",
"Accept": "*/*"
},
body: JSON.stringify({
user_id,
state_raw,
signature
})
})
.then(res => res.text())
.then(console.log)
.catch(console.error);
})();