캐릭터의 이름과 공격력을 코드에 직접 적으면, 숫자를 고칠 때마다 프로그래머가 코드를 고쳐야 합니다. 이 수업에서는 게임 데이터를 SQLite 데이터베이스에 담고, Unity 에서 읽고, 자주 쓰는 값은 메모리에 담아 두는 과정까지 실습했습니다.
2022년 1학기 한국애니메이션고등학교 2학년 「게임 개발과 미들웨어」 2강 「게임 데이터 처리 #1」에서 가르친 내용입니다.
데이터베이스는 무엇을 하고, 어떻게 쓰나

- T1. 기획 데이터 생성기획자가 엑셀에서 값을 만듭니다
- T2. 미들웨어Unity 에서 불러올 수 있는 형식으로 바꾸는 데이터 변환기
- T3. 게임 데이터 로드게임이 실행될 때 읽어 화면과 전투에 씁니다
엑셀은 사람이 고치기 좋은 형식이고, 게임은 빠르게 찾아 읽기 좋은 형식이 필요합니다. 두 형식을 잇는 가운데 단계가 이 수업의 제목에 들어 있는 미들웨어입니다. 이 단계가 있으면 기획자는 엑셀만 고치고, 프로그래머는 읽는 코드를 만들어 두면 됩니다.
클라이언트 데이터베이스 고르기
Microsoft Access
같은 회사의 엑셀 시트를 Access DB 로 쉽게 바꿀 수 있습니다. C# API 로 연결할 수 있어 모바일 기기에서도 씁니다.
SQLite3
Unity, Unreal 같은 멀티 플랫폼 엔진에 라이브러리를 넣어 여러 플랫폼에서 씁니다. 커뮤니티 지원이 활발해 지금 가장 많이 쓰는 클라이언트 DB 입니다.
JSON · XML · Binary
데이터를 표준 형식 파일에 저장해 두고 필요할 때 읽습니다. 업계 표준 형식을 써도 되고, 직접 정한 바이너리 형식을 쓰기도 합니다.
수업에서는 SQLite 를 골랐습니다. 데이터베이스 파일이 그대로 게임에 들어가고, SQL 로 원하는 행을 골라 읽을 수 있기 때문입니다.
도구 세 가지
| 도구 | 하는 일 |
|---|---|
| SQLite3 | 명령 프롬프트(터미널)에서 데이터베이스를 만들고 관리하는 유틸리티입니다 |
| SQLite Browser | 창 화면으로 데이터베이스를 봅니다. 터미널보다 쉽게 표를 만들고 고칠 수 있습니다 |
| SQLite C# Library | Unity 에서 SQLite 를 읽는 라이브러리입니다. 프로젝트의 Assets/Plugins 아래에 플러그인 파일이, Sources 아래에 SQLite.cs 가 들어갑니다 |
SQLite 콘솔로 표 만들기
첫 실습은 명령 프롬프트에서 데이터베이스 파일을 만드는 것이었습니다. sqlite3 파일이름 으로 실행하면 파일이 없을 때 새로 만듭니다.
C:\SQLite> sqlite3 ClientGameData.db
SQLite version 3.x
Enter ".help" for instructions
sqlite> .databases
seq name file
0 main C:\SQLite\ClientGameData.db다음은 캐릭터 표를 만들고, 두 캐릭터를 넣고, 읽어 보는 차례였습니다.
sqlite> CREATE TABLE "Characters" (
"ID" INTEGER,
"Name" TEXT,
"AttackPoint" INTEGER,
"HealthPoint" INTEGER,
PRIMARY KEY("ID")
);
sqlite> .tables
Characters
sqlite> INSERT INTO Characters (ID, Name, AttackPoint, HealthPoint) VALUES (1, '제임스', 4, 4);
sqlite> INSERT INTO Characters (ID, Name, AttackPoint, HealthPoint) VALUES (2, '루이스', 5, 6);
sqlite> SELECT Name, AttackPoint, HealthPoint FROM Characters;
제임스|4|4
루이스|5|6
sqlite> SELECT Name, AttackPoint, HealthPoint FROM Characters WHERE ID=1;
제임스|4|4| 명령 | 하는 일 |
|---|---|
| CREATE TABLE | 표를 만들고 칸(열)의 이름과 형식을 정합니다. PRIMARY KEY 는 행을 구별하는 기본 키입니다 |
| INSERT INTO | 표에 행을 넣습니다 |
| SELECT … FROM | 표에서 원하는 칸을 읽습니다. WHERE 를 붙이면 조건에 맞는 행만 고릅니다 |
| UPDATE … SET | 이미 있는 행의 값을 고칩니다 |
| .tables · .databases | SQL 이 아닌 SQLite 콘솔 명령입니다. 표와 데이터베이스 목록을 봅니다 |
같은 일을 SQLite Browser 로도 해 봤습니다. UPDATE Characters SET Name='소피아' WHERE ID=2; 를 실행하고 표를 보면 루이스가 소피아로 바뀐 것이 눈으로 보입니다. 콘솔로 원리를 익히고, 실제 작업은 Browser 로 하는 순서입니다.
Unity 에서 읽기
Unity 가 데이터베이스 파일을 읽으려면 파일이 있어야 할 위치에 있어야 합니다. 쓰기와 읽기가 모두 되는 위치는 Application.persistentDataPath 입니다. Debug.Log(Application.persistentDataPath); 로 위치를 찍어 보고 그 폴더로 파일을 옮겼습니다.
다음은 표의 행을 C# 클래스로 옮기는 일입니다. 칸 이름과 같은 이름의 속성을 만들면, 라이브러리가 SELECT 결과를 그대로 객체에 채워 줍니다. 1편에서 배운 객체가 여기서 다시 나옵니다. 데이터베이스의 행이 곧 객체가 됩니다.
public class CharacterRow
{
[PrimaryKey]
public int ID { get; set; }
public string Name { get; set; }
public int AttackPoint { get; set; }
public int HealthPoint { get; set; }
}
public List<CharacterRow> GetCharacter(int id)
{
SQLiteConnection conn = GetConnection();
List<CharacterRow> result = null;
try
{
SQLiteCommand command = conn.CreateCommand(string.Format(
"SELECT ID, Name, AttackPoint, HealthPoint FROM Characters WHERE ID={0};", id));
result = command.ExecuteQuery<CharacterRow>();
}
catch (Exception ee)
{
Debug.LogError(ee.Message);
Debug.LogError(ee.StackTrace);
}
return result;
}// GameScene.cs
List<GameDB.CharacterRow> result = gameDB.GetCharacter(2);
if (result != null && result.Count > 0)
{
heroCharacter.SetName(result[0].Name);
}
// Character.cs
public void SetName(string characterName)
{
myNameText.text = characterName;
}- 공격력과 생명력을 띄울 Text 두 개를 Scene 에 추가합니다.
- 두 Text 를 Character 오브젝트에 연결합니다.
- Character.cs 에 SetAttackPoint(int point), SetHealthPoint(int point) 를 만듭니다.
- DB 에서 불러온 공격력과 생명력을 캐릭터에 넣습니다.
2번 캐릭터를 불러오면 화면에 공격력 5, 생명력 6 이 나오면 성공입니다.
자주 읽는 값은 메모리에

같은 캐릭터를 읽을 때마다 SELECT 를 실행하면 매번 파일을 열고, SQL 을 해석하고, 행을 찾습니다. 게임에서는 같은 값을 아주 자주 읽기 때문에 이 비용이 쌓입니다.
그래서 게임이 시작할 때 모든 캐릭터를 목록에 담아 두는 인메모리 캐시를 만들었습니다.
- GameDB.cs 에
List<CharacterRow> preLoadedCharacters를 만듭니다. - Start 에서 모든 캐릭터를 SELECT 해
preLoadedCharacters.AddRange()로 담습니다. - GetCharacter(int) 는 목록에 있으면 목록에서 돌려주고, 없을 때 DB 에 SELECT 합니다.
List<CharacterRow> preLoadedCharacters = new List<CharacterRow>();
void Start()
{
SQLiteCommand command = GetConnection().CreateCommand(
"SELECT ID, Name, AttackPoint, HealthPoint FROM Characters;");
preLoadedCharacters.AddRange(command.ExecuteQuery<CharacterRow>());
}
public CharacterRow FindCharacter(int id)
{
CharacterRow cached = preLoadedCharacters.Find(row => row.ID == id);
if (cached != null) return cached; // 메모리에 있으면 그대로
List<CharacterRow> rows = GetCharacter(id); // 없을 때 DB 에 묻는다
return rows != null && rows.Count > 0 ? rows[0] : null;
}어디서든 같은 GameDB
캐릭터도, 무기도, 장면도 GameDB 를 씁니다. 저마다 GameDB 를 새로 만들면 캐시도 여러 벌이 되고 파일도 여러 번 열립니다. 그래서 GameDB 는 게임 전체에서 객체 하나를 함께 쓰도록 싱글턴으로 만들었습니다.
private static GameDB SingletonInstance = null;
public static GameDB GetSingleton()
{
if (SingletonInstance == null)
{
SingletonInstance = new GameDB();
}
return SingletonInstance;
}
// 쓰는 쪽 (GameScene.cs)
List<GameDB.CharacterRow> result = GameDB.GetSingleton().GetCharacter(2);처음 부를 때 만들어 두고, 다음부터는 만들어 둔 객체를 돌려줍니다. Character, Weapon, GameScene 이 모두 같은 GameDB 를 보게 됩니다.
데이터베이스에 있는 캐릭터를 모두 불러오기
마지막 실습은 데이터베이스에 들어 있는 캐릭터 수만큼 화면에 캐릭터를 만드는 것이었습니다. 캐릭터 모양은 프리팹(Prefab)으로 만들어 두고, 행마다 프리팹을 복제(Instantiate)해 이름과 능력치를 넣습니다.
public Character characterPrefab;
void Start()
{
float x = -4f;
foreach (GameDB.CharacterRow row in GameDB.GetSingleton().GetAllCharacters())
{
Character c = Instantiate(characterPrefab, new Vector3(x, 0, 0), Quaternion.identity);
c.SetName(row.Name);
c.SetAttackPoint(row.AttackPoint);
c.SetHealthPoint(row.HealthPoint);
x += 2f;
}
}이제 기획자가 데이터베이스에 캐릭터를 더 넣으면, 코드는 그대로인데 화면의 캐릭터가 늘어납니다. 데이터와 코드를 나눈 효과가 처음 눈에 보이는 순간입니다.
지금 100턴에서는
「졸업까지 100턴」은 엑셀 58개를 원본으로 두고, 도구가 클라이언트용 JSON 과 서버 DB 를 함께 만듭니다. 수업의 세 단계가 실제 서비스에서 어떻게 커졌는지는 구조 읽기 1편에서 볼 수 있습니다.
정리
- 기획, 변환, 로드. 사람이 고치는 형식과 게임이 읽는 형식 사이에 변환기를 둡니다.
- 표의 행은 객체가 됩니다. 칸 이름과 같은 속성을 가진 클래스로 SQL 결과를 받습니다.
- 자주 읽는 값은 메모리에, GameDB 는 하나만. 캐시와 싱글턴으로 같은 일을 반복하지 않습니다.
다음 편에서는 엑셀 데이터를 다듬어 데이터베이스로 옮기고, 여러 표를 설계하는 실습을 다룹니다.