본문으로 건너뛰기

dotgimon 제작기 04. 8px 메뉴 아이콘과 선택 표시 조정

·10 min read·4 / 32

dotgimon의 메뉴 아이콘은 64×64 LCD 위아래에 놓인 8px짜리 그림이다. 먹이와 훈련, 대전처럼 기능은 서로 다른데 작은 칸에 넣으면 비슷한 덩어리로 보였다. 아이콘 외형과 선택 표시를 함께 손봤다.

음식 아이콘은 닭다리, 통고기, 햄을 거쳐 다시 닭다리로 바뀌었다. 그림을 더 자세하게 그리는 것만으로 해결하지 않고, 선택하지 않은 메뉴가 얼마나 흐리게 남는지와 입력을 멈춘 뒤 포커스를 어떻게 정리할지도 바꿨다.

테두리를 없애니 겹치는 도형이 눈에 띄었다

아이콘은 iconPaths.ts에 24×24 기준 SVG 패스로 들어 있었다. Skia가 이를 읽어 LCD의 8px 칸에 맞춰 줄였다. 선 두께 1.3도 논리 픽셀로 환산하면 약 0.43px라 작은 수치 차이가 그림의 인상을 바꿨다.

기존 선택 표시는 아이콘 바깥의 둥근 테두리였다. 이 박스를 없애고 선택한 아이콘은 진하게, 나머지는 불투명도 0.3으로 흐리게 그렸다. LCD에서 꺼진 부분이 희미하게 남는 모습에 맞추려 했다.

서브패스마다 불투명도를 낮추자 겹치는 부분이 더 진하게 남았다. 아이콘 하나가 여러 도형을 포개 만든 그림이라, 알파를 각각 적용하면 같은 자리가 여러 번 칠해졌다. 아이콘 전체를 한 레이어로 그린 뒤 한 번만 불투명도를 적용하도록 바꿨다.

<Group
  layer={i === menu.cursor ? undefined : <Paint opacity={ICON_DIM} />}
  transform={at(i)}
>
  {glyphSubs(subs)}
</Group>

Group의 layer에 Paint를 넘기면 자식들을 한 이미지로 그린 뒤 효과를 적용할 수 있다. 패스가 겹쳐도 최종 아이콘 전체에 같은 농도가 걸리게 했다. 아이콘 배치 계산과 서브패스를 그리는 함수도 이 과정에서 따로 묶었다.

전구 아이콘은 채운 모양에서 외곽선으로 바꾸었다. CLEAN의 똥 아이콘과 덩어리가 비슷해 보이는 것을 줄이려는 변경이었다. 전구 몸체와 소켓의 선 두께도 따로 조정했다.

메뉴를 만지다 멈추면 포커스도 사라지게 했다

선택을 농도로만 나타내니 사용자가 메뉴를 만지다 놓은 뒤에도 아이콘 하나가 계속 진하게 남았다. 펫이 걷는 화면으로 돌아와도 마지막에 고른 메뉴가 눈에 들어왔다. 입력 없이 15초가 지나면 커서를 풀도록 했다.

버튼을 누를 때마다 inputAt을 현재 시각으로 갱신하고, MAIN에서만 무입력 시간을 확인했다.

const IDLE_OFF_MS = 15_000
 
if (
  s.screen === 'MAIN' &&
  s.cursor >= 0 &&
  now - this.inputAt >= IDLE_OFF_MS
) {
  s.cursor = -1
}

선택 메뉴 안에서 시간이 지났다는 이유만으로 커서가 사라지지는 않게 했다. MAIN에서 해제되면 모든 메뉴가 같은 옅은 농도로 돌아왔다. 명시적인 취소와 무입력 해제가 각각 포커스를 정리했다.

React에 넘기는 메뉴 상태도 같이 살폈다. useGame은 표시 여부와 커서, 깜빡임을 키로 비교해 바뀔 때만 setMenu를 불렀다. 호출 알림을 추가하면서 그 키에 attention도 넣었다. 상태 필드를 추가해도 비교에서 빼먹으면 화면에는 변화가 전달되지 않았다.

비어 있던 칸에는 펫의 호출을 넣었다

조작 가능한 메뉴는 일곱 개였고 여덟 번째 칸은 비어 있었다. 여기에 펫이 배고프거나 아프고, 똥이 쌓이는 등 돌봄을 요구할 때 켜지는 아이콘을 넣었다. 선택해서 실행하는 메뉴가 아니므로 흐린 상태로 남기지 않고, 호출이 있을 때만 진하게 보이게 했다.

표시 조건은 메뉴가 보이는 MAIN 화면이고, 부화한 상태이며, needsAttention()이 참일 때였다. 먹거나 청소하고 치료하는 연출 중에는 다른 메뉴와 함께 숨겼다.

처음 그림은 종이었다. 이후 몬스터가 입을 벌리고 부르는 모습으로 바꾸었다. 머리와 귀, 눈, 입에 소리선을 더했는데, 줄어든 화면에서도 머리 외곽이 남도록 선 두께에 차이를 주었다. 아이콘의 기능은 호출 하나였지만 그림을 어떤 대상으로 표현할지는 여러 번 바뀌었다.

음식 모양은 벡터와 도트를 오갔다

닭다리에서는 살을 조금 세로로 긴 타원으로 만들고, 뼈 두께와 끝의 둥근 마디를 줄였다. 통고기 시안에서는 가로로 긴 덩어리 양쪽으로 뼈가 나오게 했다. 같은 작업 중 TRAIN은 트레이너가 내민 미트를 펫이 치는 모습으로, BATTLE은 두 파이터가 주먹을 맞대는 모습으로 바꿔 보았다.

햄 시안에서는 상단 메뉴 네 개를 도트로 다시 만들었다. 문자열 행의 #을 칸으로 보고 가로로 이어진 칸을 사각형 하나로 합치는 dots() 함수를 썼다. 데이터는 도트맵으로 적고 실제 렌더링은 fill 패스로 처리하는 방식이었다.

도트 버전은 오래 유지하지 않았다. 다시 SVG로 통일하면서 STATUS는 양팔저울, TRAIN은 아령, BATTLE은 교차검으로 정리했다. 음식은 나선 단면이 있는 햄을 거쳐 닭다리로 바뀌었다.

그림 내부의 빈 부분을 표현할 방법도 필요했다. 잉크 한 색만 쓰는 아이콘에 배경색으로 덧그릴 서브패스를 추가했다.

export interface IconSub {
  d: string
  stroke?: number
  bg?: boolean
}

bg가 참이면 잉크 대신 LCD 배경색으로 그렸다. 채운 도형 안에 나선이나 하이라이트를 넣을 수 있었다. 아이콘을 흐리게 할 때는 이 부분도 같은 레이어 안에서 처리했다.

최종 닭다리에는 살 덩어리, 뼈대, 뼈끝의 두 마디와 하이라이트를 남겼다. 살만 둥글게 그리면 음식 종류를 구별하기 어려웠고, 뼈끝 모양이 고기로 읽히는 단서가 되었다. 작은 칸에서 구별에 필요한 부분을 남기는 쪽으로 정리했다.

개발용 배속도 엔진에서 걷어냈다

화면 작업과 함께 엔진의 DEV_TIME_SCALE 사용을 없앴다. 육성 상수의 / TS를 제거하고 수면 시각도 실제 Date에서 읽게 했다. 배속 상수 정의는 남았지만 엔진에서는 더 이상 사용하지 않았다.

아이콘 변경과 배속 정리가 같은 작업 기록에 섞여 있었다. 그림 시안도 기능별로 분리되지 않은 곳이 있어 변경 의도를 따라가기가 어려웠다. 같은 화면을 만지는 작업이어도 외형 조정, 입력 처리, 시간 규칙은 서로 다른 판단이었다.

화면에서는 선택 테두리가 사라지고, 호출이 있을 때만 여덟 번째 아이콘이 켜졌다. 입력을 멈추면 선택도 풀렸다. 닭다리 모양을 고치는 동안 메뉴가 머무는 방식까지 같이 바뀌었다.