오늘 한 것

  • 지난 시간에 배운 정의 데이터 / 가변 상태 / 파생 데이터를 시간과 주식에 적용
  • 게임 시작일을 2000년 1월 1일, 한 턴을 7일로 설정
  • 다음 주 버튼으로 턴을 진행하고 날짜를 갱신
  • 주식의 변하지 않는 정의와 플레이 중 변하는 현재 상태를 분리
  • 삼성전자, SK하이닉스, 현대자동차 3종의 주식 정의 데이터 추가
  • 한 턴이 지날 때마다 각 주가가 직전 가격 기준 ±10% 범위에서 변하도록 구현
  • 새 게임에서는 시간과 주가를 초기화하고, 은행에 다녀오면 현재 상태를 유지

이번 내용은 새로운 데이터 구조를 배우는 시간이 아니라, 지난 시간에 배운 게임 데이터 3분류를 실제 게임 도메인에 적용하는 응용편이다.

1. 시간도 게임 데이터다

게임의 시간은 현실처럼 저절로 흐르지 않는다. 현재 게임에서는 플레이어가 다음 주 버튼을 눌렀을 때 current_turn이 1 증가한다.

1
2
3
current_turn = 0  →  2000년 1월 1일 · 1주차
current_turn = 1  →  2000년 1월 8일 · 2주차
current_turn = 2  →  2000년 1월 15일 · 3주차

시간 관련 데이터는 다음처럼 분류한다.

분류이유
시작일 2000-01-01정의 데이터게임 제작 시 정하고 플레이 중 바뀌지 않음
턴당 7일정의 데이터시간 진행 규칙
current_turn가변 상태플레이 중 계속 증가함
현재 날짜파생 데이터시작일과 현재 턴으로 계산 가능
화면의 주차파생 데이터current_turn + 1로 계산 가능

현재 날짜를 상태로 저장하지 않는 이유

턴과 날짜를 둘 다 저장하면 한쪽만 갱신되어 서로 어긋날 수 있다.

1
2
3
# 잘못된 상태
current_turn = 2
current_date = 2000-01-08  ← 1턴의 날짜에 머물러 있음

따라서 실제로 저장하는 시간 상태는 current_turn 하나뿐이다.

1
2
현재 날짜 = 시작일 + current_turn × 7일
화면 주차 = current_turn + 1

GameState.current_date()는 Godot의 Time 싱글톤을 이용해 이 계산 결과를 달력 형식의 Dictionary로 돌려준다.

턴은 현재 단계에서 새로운 구조가 아니다

현재 구현에서 턴은 여러 가변 상태 중 게임 시간의 기준으로 선택한 값이다.

1
2
money += 100
current_turn += 1

구조적으로 둘 다 가변 상태를 변경하는 코드다. 나중에 시간, 주가, 회사 실적, 뉴스, 생활비 등 여러 시스템의 갱신 순서를 한곳에서 관리하게 되면 턴 루프나 업데이트 파이프라인이라는 새로운 구조로 발전할 수 있다. 현재는 그 단계가 아니다.

2. 초기 주가와 현재 주가

처음에는 기준 가격이라는 표현을 사용했지만 의미가 모호했다. 전일 종가나 적정 가격도 기준 가격이라고 부를 수 있고, 이 값들은 플레이 중 변할 수 있다.

이번에 필요한 값은 새 게임의 현재 주가를 만드는 원본이므로 initial_price, 즉 초기 주가라고 이름 붙였다.

1
2
초기 주가: 게임 제작자가 정한 새 게임의 시작값
현재 주가: 현재 플레이에서 턴마다 변하는 값
회사명초기 주가
삼성전자10,000원
SK하이닉스5,000원
현대자동차20,000원

이 가격은 데이터 구조 학습을 위한 임의의 게임 값이며 실제 주가를 나타내지 않는다.

3. StockDef — 변하지 않는 정의

StockDef는 주식 한 종의 정의 데이터 스키마다.

1
2
3
4
5
class_name StockDef
extends Resource

@export var company_name: String = "이름 없는 회사"
@export var initial_price: int = 0

실제 값은 .tres 파일 3개에 저장한다.

1
2
3
4
5
StockDef.gd                   ← 스키마 1개
tutorials/data/stocks/
├── samsung_electronics.tres ← 삼성전자 / 10,000원
├── sk_hynix.tres             ← SK하이닉스 / 5,000원
└── hyundai_motor.tres        ← 현대자동차 / 20,000원

MachineDef와 같은 데이터 주도 구조다. 회사 종류를 늘릴 때 주가 계산 코드를 복사하지 않고 StockDef 데이터만 추가할 수 있다.

4. StockState — 현재 플레이의 가변 상태

.tresinitial_price를 턴마다 직접 수정하면 정의 데이터와 현재 상태가 섞인다. 그래서 현재 주가는 별도의 StockState 객체가 가진다.

1
2
3
4
5
class_name StockState
extends RefCounted

var definition: StockDef
var current_price: int

두 객체의 관계는 다음과 같다.

1
2
3
4
5
StockDef                         StockState
──────────────────────────────────────────────
삼성전자 / 초기 10,000원   ←참조─  현재 10,300원
SK하이닉스 / 초기 5,000원  ←참조─  현재 4,800원
현대자동차 / 초기 20,000원 ←참조─  현재 20,500원
  • StockDef: 파일로 저장하고 여러 곳이 공유하는 불변 원본이므로 Resource
  • StockState: 현재 플레이에서만 사용하는 가벼운 객체이므로 RefCounted

백엔드로 비유하면 StockDef는 종목 마스터이고 StockState는 현재 게임 세이브에 들어갈 종목별 레코드다. 회사명을 StockState에 다시 복사하지 않고 정의 객체를 참조하므로, 회사명의 원본은 한 곳에만 존재한다.

새 게임에서는 정의 데이터로 현재 상태를 생성한다.

1
2
definition = stock_definition
current_price = definition.initial_price

5. 턴마다 현재 주가 변경

GameState.advance_turn()은 현재 턴을 증가시킨 뒤 세 주식의 현재 가격을 갱신한다.

1
2
3
4
5
6
7
8
9
다음 주 버튼
current_turn += 1
각 StockState의 current_price 변경
stock_prices_changed 시그널
주가 라벨 갱신

현재 가격은 -10%부터 +10% 사이의 임의 비율로 변한다.

1
다음 가격 = 직전 현재 가격 × (1 + 임의 변화율)

중요한 점은 초기 주가가 아니라 직전 현재 주가를 기준으로 계산한다는 것이다.

1
2
# 직전 가격으로부터 변화가 누적됨
10,000 → 10,300 → 9,900 → 10,500

초기 주가를 기준으로 매번 계산하면 이전 턴의 변화가 다음 턴으로 이어지지 않는다. 계산 결과는 정수로 반올림하고 주가가 1원보다 작아지지 않도록 제한한다.

6. 상태의 수명

current_turnstock_states는 Autoload인 GameState에 있다. 따라서 Game 씬에서 은행 씬으로 이동해도 객체가 사라지지 않는다.

1
2
3
Game → Bank → Game
현재 턴 유지
현재 주가 유지

반면 타이틀에서 새 게임을 시작하면 다음 순서로 초기화한다.

1
2
3
4
money = 500
current_turn = 0
기존 stock_states 제거
Game 씬에서 StockDef 3개로 StockState 다시 생성

같은 데이터라도 어느 노드가 소유하는지에 따라 수명이 결정된다는 지난 시간의 내용을 다시 적용한 것이다.

7. 데이터 분류 전수 조사

정의 데이터

위치
게임 시작일GameState.START_DATE
한 턴의 일수GameState.DAYS_PER_TURN
회사명StockDef.company_name과 각 .tres
초기 주가StockDef.initial_price와 각 .tres
주가 변화 범위 ±10%_update_stock_prices()의 게임 규칙

가변 상태

위치새 게임은행 왕복
현재 턴GameState.current_turn0으로 초기화유지
주식별 현재 주가GameState.stock_states초기 주가로 초기화유지

파생 데이터

무엇으로부터 계산하는가
현재 날짜시작일, 현재 턴, 턴당 일수
화면 주차current_turn + 1
시간 라벨현재 날짜와 화면 주차
주가 라벨회사명, 현재 주가, 초기 주가

현재 구현에는 등락액과 등락률이 없지만 필요할 때 저장하지 않고 계산할 수 있다.

1
2
초기 대비 등락액 = 현재 주가 - 초기 주가
초기 대비 등락률 = (현재 주가 - 초기 주가) / 초기 주가 × 100

단, 지난주 대비 등락률은 현재 주가와 초기 주가만으로 계산할 수 없다. 과거 가격을 보관해야 하므로 가격 이력을 추가한 뒤 파생 데이터로 계산해야 한다.

8. 구현 파일

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
tutorials/
├── data/stocks/
│   ├── samsung_electronics.tres  # 삼성전자 정의
│   ├── sk_hynix.tres              # SK하이닉스 정의
│   └── hyundai_motor.tres         # 현대자동차 정의
├── scenes/
│   └── Game.tscn                  # 날짜, 다음 주 버튼, 주가 목록 UI
└── scripts/
    ├── StockDef.gd                # 불변 정의 데이터 스키마
    ├── StockState.gd              # 주식별 현재 가격 상태
    ├── GameState.gd               # 턴, 날짜 계산, 주식 상태와 가격 갱신
    ├── Game.gd                    # 버튼 연결과 시간·주가 UI 갱신
    └── Title.gd                   # 새 게임의 시간·주식 상태 초기화

9. 실습

  1. 버튼을 눌러 시간이 변하는 모습

다음 주 버튼으로 턴과 날짜가 진행되는 모습

  1. 시간이 지남에 따라 주가가 랜덤하게 변하는 모습

턴이 지날 때마다 주가가 랜덤하게 변하는 모습

다음에 이어서 볼 것 (아이디어)

  • 주가 이력을 저장하고 현재 주가와 지난주 주가의 차이 이해하기
  • 가격 이력에서 등락액과 등락률을 파생 데이터로 계산하기
  • 보유 수량과 평균 매입가를 추가해 매수·매도 상태 설계하기
  • 턴에 반응하는 시스템이 많아질 때 갱신 순서를 관리하는 턴 루프 공부하기

이전 글: 게임 만들기 - Godot 데이터 설계 배우기