ARCANE BREW 개발기 (5) - 가마솥과 포션 조제 시스템
목차 보기
시리즈 목차 보기5 / 7
ARCANE BREW 개발기
5 / 7
프로젝트의 심장부
재료 도감, 랜딩 페이지, 스타일 시스템 — 여기까지는 솔직히 "보여주기"에 가까웠다. 진짜 게임다운 경험은 가마솥에서 시작된다. 재료를 넣고, 불을 올리고, 저어서 물약을 만드는 과정. 이 인터랙션이 재미없으면 프로젝트 전체가 무너진다.
이번 편에서는 Canvas 기반 유체 시뮬레이션, 조제 상태 머신, 레시피 매칭 알고리즘까지 — 가마솥 시스템의 핵심을 다룬다.
🫧 Canvas 유체 시뮬레이션
가마솥의 물약은 정적인 색상 블록이 아니라, 출렁이고 끓어오르는 액체여야 했다. CauldronCanvas.vue에 Canvas API로 유체 시뮬레이션을 구현했다.
파동 효과
액체 표면의 출렁임은 두 개의 sin 함수를 합성해서 만들었다. 주파수와 위상이 다른 두 파동을 겹치면 자연스러운 불규칙 패턴이 생긴다. 핵심은 온도에 비례하는 진폭이다 — 불이 약하면 잔잔하고, 강하면 격렬하게 출렁인다.
const wave = Math.sin((x / w) * 6 + time * 2) * 3 * (temperature / 50)
+ Math.sin((x / w) * 10 + time * 3) * 1.5첫 번째 파동(*6)이 큰 출렁임, 두 번째(*10)가 잔물결 역할이다. temperature / 50을 진폭 계수로 쓰기 때문에, 온도 0도에서는 거의 평탄하고 100도에서는 진폭이 6px까지 커진다.
버블 시스템
끓는 물약에서 기포가 올라오는 연출이다. 온도가 높을수록 기포가 많이 생기고, 각 기포는 투명도가 서서히 줄어들며 상승한다.
const bubbleRate = Math.max(0, (temperature - 20) / 10)
if (Math.random() < bubbleRate * 0.1) {
bubbles.push({
x: w * 0.2 + Math.random() * w * 0.6,
y: h * 0.65 + Math.random() * h * 0.1,
r: 1 + Math.random() * 3,
vy: -(0.3 + Math.random() * 0.5 + temperature / 100),
opacity: 0.4 + Math.random() * 0.4,
color,
})
}20도 이하에서는 기포가 전혀 생기지 않는다. bubbleRate * 0.1로 프레임당 확률을 조절해서, 60도쯤 되면 적당히, 100도에서는 활발하게 보글거린다. 기포의 상승 속도(vy)에도 온도를 반영해서 뜨거울수록 빠르게 올라간다.
연기와 색상 블렌딩
온도가 60도를 넘으면 가마솥 위로 연기가 피어오른다. 방사형 그라디언트(createRadialGradient)로 렌더링하는데, 중심은 불투명하고 바깥은 완전히 투명한 원을 여러 개 겹치는 방식이다. 프레임마다 위치를 살짝 흔들어 자연스러운 연기 느낌을 만들었다.
재료를 투입하면 가마솥 액체의 색상이 바뀐다. blendColors 함수가 현재 가마솥 색상과 투입된 재료의 원소 색상을 혼합한다. 월광 이슬을 넣으면 은빛으로, 화염 석류석을 넣으면 붉게 물드는 식이다. 이 시각적 피드백이 조제 과정에 몰입감을 더해준다.
🔄 조제 상태 머신
조제 과정은 6단계 상태 머신으로 관리한다.
idle → selecting → heating → stirring → brewing → complete
BrewingPhase 타입으로 정의하고, 각 단계 전환마다 UI와 인터랙션이 달라진다. idle에서는 가마솥이 비어있고, selecting에서 재료를 슬롯에 배치하고, heating에서 온도를 올리고, stirring에서 저어주고, brewing에서 잠시 대기하면 complete로 전환된다.
각 단계를 명확히 분리한 덕분에, 단계별 Canvas 연출(불꽃 강도, 파티클 방출)과 UI 상태(버튼 활성화, 안내 텍스트)를 깔끔하게 연결할 수 있었다. 상태 전환 로직이 한 곳에 모여 있으니 디버깅도 수월했다.
🧪 레시피 매칭 알고리즘
슬롯에 배치된 재료 조합이 레시피와 일치하는지 판별하는 핵심 로직이다.
function brew(allRecipes: Recipe[]): BrewingResult {
const ingredientIds = slots.value.map(s => s.ingredientId).sort()
const matched = allRecipes.find(recipe => {
const recipeIds = recipe.ingredients.map(ri => ri.ingredientId).sort()
if (recipeIds.length !== ingredientIds.length) return false
return recipeIds.every((id, i) => id === ingredientIds[i])
})
// 품질 계산 — 온도 65도 근처 + 저어준 횟수에 의존
const tempScore = 100 - Math.abs(temperature.value - 65) * 1.5
const stirScore = Math.min(stirCount.value * 15, 100)
const quality = Math.max(10, Math.min(100, Math.round((tempScore + stirScore) / 2)))
// ...
}매칭 자체는 단순하다 — 양쪽 재료 ID 배열을 정렬해서 비교한다. 순서가 아닌 조합만 따지기 때문에 sort() 후 비교하는 방식을 선택했다. 22개 레시피를 선형 탐색하는 것도 이 규모에서는 전혀 문제가 되지 않는다.
흥미로운 부분은 품질 계산이다. 같은 레시피라도 온도와 교반 횟수에 따라 품질이 10~100으로 달라진다. 최적 온도는 65도 — 여기서 벗어나면 1.5씩 감점된다. 교반 횟수는 회당 15점씩, 최대 100점. 둘의 평균이 최종 품질이다. 플레이어에게 "조제 과정에서의 섬세함"을 요구하는 장치다.
📜 22개 레시피와 세계관
레시피는 5대 범주로 나뉜다. 치유 5개, 강화 5개, 변환 4개, 원소 5개, 금단 3개 — 총 22개다. 각 레시피에는 조합법, 결과 물약, 그리고 세계관 텍스트(lore)가 포함된다.
{
id: 'moonlit-restoration',
nameKo: '월광 회복의 영약',
category: 'healing',
ingredients: [
{ ingredientId: 'moonlight-dew', amount: 3 },
{ ingredientId: 'frost-lotus-petal', amount: 2 },
],
result: {
nameKo: '월광 회복의 영약',
color: '#b0c4de',
effects: ['체력 회복', '피로 해소'],
},
lore: '초대 점주 아르빈 크로스가 처음 기록한 가장 기초적인 치유 처방...',
}모든 레시피의 lore가 세계관과 연결된다. 누가 처음 만들었는지, 어떤 사건과 관련이 있는지 — 이 텍스트들이 게임을 단순한 조합 퍼즐에서 서사가 있는 경험으로 끌어올린다. 카테고리별로 난이도도 다르다. 치유 계열은 재료 2개로 쉽게 만들 수 있고, 금단 계열은 3~4개 재료에 최적 온도 유지까지 요구한다.
✨ 성공과 실패 연출
레시피 매칭에 성공하면 가마솥에서 해당 물약 색상의 파티클이 분출되고, 품질에 따라 발광 강도가 달라진다. 고품질 물약은 화면 전체에 잠깐 빛이 번지는 연출이 추가된다.
실패의 경우 — 매칭되는 레시피가 없으면 — 가마솥 내용물이 탁한 회색으로 변하며 연기가 피어오른다. 실패해도 재료를 돌려받지 못하기 때문에, 플레이어가 레시피 페이지를 참고하면서 신중하게 조합하도록 유도한다.
정리
가마솥 시스템은 Canvas 유체 시뮬레이션, 상태 머신, 매칭 알고리즘이 하나로 맞물려 돌아가는 구조다. sin 함수 두 개로 만든 출렁임이 이렇게 그럴듯할 줄은 나도 예상 못 했다. 온도, 교반, 재료 색상이 실시간으로 Canvas에 반영되면서 "진짜 무언가를 조제하고 있다"는 느낌을 만들어낸다.
다음 편에서는 지팡이 공방과 업적 시스템을 다룬다. SVG로 지팡이를 실시간 렌더링하고, 108가지 조합을 커스터마이징하고, 19개 업적으로 플레이어의 진행을 추적하는 시스템이다.