1. 개요
RPG 콘솔 게임 프로젝트를 진행하면서 오류가 발생했던 상황과 고민했던 부분에 대해 서술했습니다.
2. 트러블 슈팅
1) Monster 객체 생성 문제
먼저 Monster 클래스의 속성과 생성자에 대해 설명하겠습니다.
class Monster {
String name;
int health;
int attack = 0;
int defense = 0;
Monster(this.name, this.health, this.maxAttack);
}
Monster 객체를 생성하려면 이름, 체력, 공격력이 필요하며,
이 데이터는 외부 파일 데이터에서 가져오게 됩니다.
이를 위해 파일에서 몬스터의 데이터를 불러오는 loadMonsterStatsAsync 함수를 새로 생성하였습니다.
이 파일은 monsters.txt 파일입니다.
Batman,30,20
Spiderman,20,30
Superman,30,10
각 줄마다 몬스터의 이름, 체력, 공격력이 쉼표로 구분되어 있습니다.
아래 코드는 monsters.txt 파일을 읽어 캐릭터의 문자열 데이터를 반환하는 loadMonsterStatsAsync 함수입니다.
List<Monster> loadMonsterStatsAsync() {
try {
final file = File('resource/monsters.txt');
var lines = file.readAsLinesSync();
return [];
} on PathNotFoundException {
throw ('파일의 지정된 경로를 찾을 수 없습니다.');
} catch (e) {
throw ('몬스터 데이터를 불러오는 데 실패했습니다: $e');
}
}
문자열 데이터를 각 줄마다 분리한 후, 각 줄을 name, health, attack으로 분리하여 Monster 객체를 생성해야합니다.
여기서 '파일 데이터를 어떻게 몬스터마다 name, health, attack으로 분리하지'라는 문제도 생겼지만,
이 문제는 튜터님께서 파일을 각 한줄씩 읽어 리스트로 반환하는 readAsLineSync()를 사용하라고 하셨습니다.
monsters.txt는 총 3줄이기에 3개의 길이를 가진 리스트가 만들어집니다.
하지만 여기서 '이렇게 분리하는 작업을 어디서 진행할 것인가'에 대한 고민이 많았습니다.
loadMonsterStatsAsync 함수에서 분리한다면 이 분리된 데이터를 리스트 형태로 반환해야되나?
String 형태로 반환해야되나? String 형태로 반환하면 또 거기거 분리해야 할 것 같은데?
그렇다고 main 함수에서 데이터를 분리하기에는 main 함수의 관심사와는 거리가 멀지 않나?
데이터의 분리와 객체 생성 위치에 대해 고민하다가, 튜터님의 조언을 구하게 되었습니다.
2) Monster 객체 생성 방식 개선
이 문제를 해결하기 위해 선택한 방법은 factory 생성자를 사용하는 것이었습니다.
content(문자열 데이터)을 매개변수로 받아 각각 데이터 health, attack, defense로 분리 한 후, Monster 객체를 생성하는 방식입니다.
class Monster {
String name;
int health;
int maxAttack;
int attack = 0;
int defense = 0;
Monster(this.name, this.health, this.maxAttack);
factory Monster.fromPlainText(String content) {
final stats = content.split(',');
String name = stats[0].toString();
int health = int.parse(stats[1]);
int maxAttack = int.parse(stats[2]);
return Monster(name, health, maxAttack);
}
}
이 factory 생성자는 데이터 가공을 객체 생성 내부에서 처리할 수 있습니다.
이제 loadMonsterStatsAsync 함수는 다음과 같이 파일 데이터를 순회하며 Monster 객체를 생성할 수 있습니다.
List<Monster> loadMonsterStatsAsync() {
try {
final file = File('resource/monsters.txt');
var lines = file.readAsLinesSync();
List<Monster> monsterList = [];
for (int i = 0; i < lines.length; i++) {
monsterList.add(Monster.fromPlainText(lines[i]));
}
return monsterList;
} on PathNotFoundException {
throw ('파일의 지정된 경로를 찾을 수 없습니다.');
} catch (e) {
throw ('몬스터 데이터를 불러오는 데 실패했습니다: $e');
}
}
loadMonsterStatsAsync에서 파일의 데이터를 한 줄 씩 읽어 Monster 객체를 생성 후, 리스트에 담습니다.
그리고 main함수에서는 리스트만 받아오면 됩니다.
void main() {
List<Monster> monster = loadMonsterStatsAsync();
}
처음엔 factory 생성자가 익숙하지 않았지만, 이번 프로젝트를 통해 개념과 필요성을 제대로 이해하게 되었습니다.
조만간 이를 따로 포스팅으로 정리할 예정입니다.
3) Rondom 범위 오류
코드를 실행하다 어느 순간에 RangeError가 발생하는 것이었습니다.
일정한 패턴이 아닌 불규칙한 패턴으로 오류 없이 넘어갈 때도 있었고, 처음부터 오류가 나는 경우도 있었습니다.

RangeError 메세지를 해석하면 범위 오류(최대): 양수여야 하며 <= 2^32: 포함 범위가 아님 1..4294967296: 라고 나옵니다.
즉, 범위는 양수이며, 0이 아니어야 한다는 뜻이죠.
이 RangeError를 발생하는 곳은 한 곳밖에 없다고 생각했습니다.
바로 Random 함수를 사용하는 코드입니다.
Random().nextInt()는 최대 범위를 지정하여 그 안에서 랜덤값을 뽑게 됩니다.
그런데 이 과정에서 뭔가 문제가 발생했다는 뜻이 되겠죠.
아래는 문제가 되는 코드로, 각 턴마다 몬스터의 공격을 랜덤으로 다르게 설정했습니다.
monster의 attak값은 게임을 시작한 후, getRandomMonster로 전투를 진행할 몬스터를 뽑을 때, 랜덤으로 값을 지정합니다.
class Monster {
String name;
int health;
int attack;
int defense = 0;
void attackCharacter(Character character) {
print('$name이(가) ${character.name}에게 $attack의 데미지를 입혔습니다.');
character.health -= attack - character.defense;
attack = Random().nextInt(attack - character.defense) + character.defense;
}
}
하지만 여기서 attack값이 랜덤으로 5가 나오는 상황이 발생한다면 어떻게 될까요?
character의 defense값은 항상 5로 고정되어 있습니다.
attack = Random().nextInt(attack - character.defense) + character.defense;
Random().nextInt(5 - 5) + character.defense;
Random().nextInt(0) + character.defense;
Random().nextInt() 설명을 보면 0보다 큰 수를 넣어야 한다고 합니다.
그래서 이런 오류가 발생한 것이죠.
2) Random 범위 오류 해결
이를 해결하기 위해 maxAttack이라는 인스턴스 인스턴스 변수를 새로 생성하였습니다.
class Monster {
String name;
int health;
// maxAttack 새로 생성
int maxAttack;
int attack = 0;
int defense = 0;
void attackCharacter(Character character) {
print('$name이(가) ${character.name}에게 $attack의 데미지를 입혔습니다.');
character.health -= attack - character.defense;
attack =
Random().nextInt(maxAttack - character.defense) + character.defense;
}
}
몬스터가 캐릭터를 공격할 때마다 maxAttack을 기반으로 랜덤 값을 생성하도록 수정하였습니다.
3. 프로젝트 소개
이 프로젝트는 콘솔 환경에서 작동하는 RPG Game입니다.
플레이어는 캐릭터를 생성하고, 다양한 몬스터들과 차례로 전투를 하며 살아남는 것이 목표입니다.
1) 주요 기능
① 캐릭터 생성 및 로딩
사용자가 이름을 입력하면 characters.txt에서 해당 캐릭터의 스탯 정보를 로드합니다.
캐릭터는 heal, attack, defense를 가집니다.
② 몬스터 데이터 로딩
monsters.txt에서 여러 마리의 몬스터 정보를 로드합니다.
몬스터는 이름, 체력, 최대 공격력을 가지며, 전투 시 랜덤으로 등장합니다.
③ 전투 시스템
턴제로 전투가 진행됩니다.
먼저 플레이어의 턴이 시작되며, 행동을 선택합니다.
1: 공격
2: 방어 (몬스터 공격력의 10%만큼 체력 회복)
3: 아이템 사용 (공격력 2배, 단 1회 제한)
행동 선택 후, 몬스터의 턴이 시작됩니다.
몬스터는 랜덤 공격력으로 공격하며, 초기 방어력은 0이지만, 3턴마다 방어력이 2씩 증가합니다.
④ 아이템 시스템
아이템은 한 번만 사용 가능하며, 사용 시 공격력이 2배로 증가합니다.
한 번 공격 후 다시 원래의 공격력으로 돌아갑니다.
⑤ 게임 저장 기능
전투 도중이나 게임이 끝난 후, 결과를 result.txt에 저장합니다.
캐릭터 이름, 남은 체력, 승패 여부를 저장하며,
아직 전투가 끝나지 않았다면 남은 몬스터들의 정보도 저장합니다.
⑥ . 기타 기능
전투 후 계속 싸울지 여부를 선택할 수 있으며,
30% 확률로 게임 시작 시 캐릭터 체력이 10 (보너스 회복) 회복됩니다.
2) 프로젝트 구조
프로젝트 구조는 크게 세가지로 나뉩니다.
domain/usecase의 character.dart, monster.dart, game.dart파일이 있고,
resource 폴더의 characters.txt, monsters.txt파일,
utils 폴더의 input.dart, load_character.dart, load_monster.dart 파일,
마지막으로 rpg_game.dart (main) 파일이 있습니다.
/bin
├─ utils/
│ └─ input.dart
├─ domain/
│ └─ usecase/
│ ├─ character.dart
│ ├─ monster.dart
│ └─ game.dart
└─ main.dart
/resource
├─ characters.txt
├─ monsters.txt
└─ result.txt
4. 참고자료
https://dart.dev/language/constructors#factory-constructors
https://stackoverflow.com/questions/17476718/how-do-get-a-random-element-from-a-list-in-dart
'TIL' 카테고리의 다른 글
[TIL] 250326 LeetCode 문제 풀이, StatefulWidget, StatelessWidget, 개인 과제 피드백 (0) | 2025.03.26 |
---|---|
[TIL] 250321 LeetCode 풀이, 개인 과제 제출, 트러블 슈팅 (0) | 2025.03.21 |
[TIL] 250319 LeetCode 풀이, 개인 과제 (0) | 2025.03.19 |
[TIL] 250318 LeetCode 풀이, 주석 정리, 개인 과제 (0) | 2025.03.18 |
[TIL] 250317 LeetCode 27번, 예외처리 (2) | 2025.03.17 |
1. 개요
RPG 콘솔 게임 프로젝트를 진행하면서 오류가 발생했던 상황과 고민했던 부분에 대해 서술했습니다.
2. 트러블 슈팅
1) Monster 객체 생성 문제
먼저 Monster 클래스의 속성과 생성자에 대해 설명하겠습니다.
class Monster {
String name;
int health;
int attack = 0;
int defense = 0;
Monster(this.name, this.health, this.maxAttack);
}
Monster 객체를 생성하려면 이름, 체력, 공격력이 필요하며,
이 데이터는 외부 파일 데이터에서 가져오게 됩니다.
이를 위해 파일에서 몬스터의 데이터를 불러오는 loadMonsterStatsAsync 함수를 새로 생성하였습니다.
이 파일은 monsters.txt 파일입니다.
Batman,30,20
Spiderman,20,30
Superman,30,10
각 줄마다 몬스터의 이름, 체력, 공격력이 쉼표로 구분되어 있습니다.
아래 코드는 monsters.txt 파일을 읽어 캐릭터의 문자열 데이터를 반환하는 loadMonsterStatsAsync 함수입니다.
List<Monster> loadMonsterStatsAsync() {
try {
final file = File('resource/monsters.txt');
var lines = file.readAsLinesSync();
return [];
} on PathNotFoundException {
throw ('파일의 지정된 경로를 찾을 수 없습니다.');
} catch (e) {
throw ('몬스터 데이터를 불러오는 데 실패했습니다: $e');
}
}
문자열 데이터를 각 줄마다 분리한 후, 각 줄을 name, health, attack으로 분리하여 Monster 객체를 생성해야합니다.
여기서 '파일 데이터를 어떻게 몬스터마다 name, health, attack으로 분리하지'라는 문제도 생겼지만,
이 문제는 튜터님께서 파일을 각 한줄씩 읽어 리스트로 반환하는 readAsLineSync()를 사용하라고 하셨습니다.
monsters.txt는 총 3줄이기에 3개의 길이를 가진 리스트가 만들어집니다.
하지만 여기서 '이렇게 분리하는 작업을 어디서 진행할 것인가'에 대한 고민이 많았습니다.
loadMonsterStatsAsync 함수에서 분리한다면 이 분리된 데이터를 리스트 형태로 반환해야되나?
String 형태로 반환해야되나? String 형태로 반환하면 또 거기거 분리해야 할 것 같은데?
그렇다고 main 함수에서 데이터를 분리하기에는 main 함수의 관심사와는 거리가 멀지 않나?
데이터의 분리와 객체 생성 위치에 대해 고민하다가, 튜터님의 조언을 구하게 되었습니다.
2) Monster 객체 생성 방식 개선
이 문제를 해결하기 위해 선택한 방법은 factory 생성자를 사용하는 것이었습니다.
content(문자열 데이터)을 매개변수로 받아 각각 데이터 health, attack, defense로 분리 한 후, Monster 객체를 생성하는 방식입니다.
class Monster {
String name;
int health;
int maxAttack;
int attack = 0;
int defense = 0;
Monster(this.name, this.health, this.maxAttack);
factory Monster.fromPlainText(String content) {
final stats = content.split(',');
String name = stats[0].toString();
int health = int.parse(stats[1]);
int maxAttack = int.parse(stats[2]);
return Monster(name, health, maxAttack);
}
}
이 factory 생성자는 데이터 가공을 객체 생성 내부에서 처리할 수 있습니다.
이제 loadMonsterStatsAsync 함수는 다음과 같이 파일 데이터를 순회하며 Monster 객체를 생성할 수 있습니다.
List<Monster> loadMonsterStatsAsync() {
try {
final file = File('resource/monsters.txt');
var lines = file.readAsLinesSync();
List<Monster> monsterList = [];
for (int i = 0; i < lines.length; i++) {
monsterList.add(Monster.fromPlainText(lines[i]));
}
return monsterList;
} on PathNotFoundException {
throw ('파일의 지정된 경로를 찾을 수 없습니다.');
} catch (e) {
throw ('몬스터 데이터를 불러오는 데 실패했습니다: $e');
}
}
loadMonsterStatsAsync에서 파일의 데이터를 한 줄 씩 읽어 Monster 객체를 생성 후, 리스트에 담습니다.
그리고 main함수에서는 리스트만 받아오면 됩니다.
void main() {
List<Monster> monster = loadMonsterStatsAsync();
}
처음엔 factory 생성자가 익숙하지 않았지만, 이번 프로젝트를 통해 개념과 필요성을 제대로 이해하게 되었습니다.
조만간 이를 따로 포스팅으로 정리할 예정입니다.
3) Rondom 범위 오류
코드를 실행하다 어느 순간에 RangeError가 발생하는 것이었습니다.
일정한 패턴이 아닌 불규칙한 패턴으로 오류 없이 넘어갈 때도 있었고, 처음부터 오류가 나는 경우도 있었습니다.

RangeError 메세지를 해석하면 범위 오류(최대): 양수여야 하며 <= 2^32: 포함 범위가 아님 1..4294967296: 라고 나옵니다.
즉, 범위는 양수이며, 0이 아니어야 한다는 뜻이죠.
이 RangeError를 발생하는 곳은 한 곳밖에 없다고 생각했습니다.
바로 Random 함수를 사용하는 코드입니다.
Random().nextInt()는 최대 범위를 지정하여 그 안에서 랜덤값을 뽑게 됩니다.
그런데 이 과정에서 뭔가 문제가 발생했다는 뜻이 되겠죠.
아래는 문제가 되는 코드로, 각 턴마다 몬스터의 공격을 랜덤으로 다르게 설정했습니다.
monster의 attak값은 게임을 시작한 후, getRandomMonster로 전투를 진행할 몬스터를 뽑을 때, 랜덤으로 값을 지정합니다.
class Monster {
String name;
int health;
int attack;
int defense = 0;
void attackCharacter(Character character) {
print('$name이(가) ${character.name}에게 $attack의 데미지를 입혔습니다.');
character.health -= attack - character.defense;
attack = Random().nextInt(attack - character.defense) + character.defense;
}
}
하지만 여기서 attack값이 랜덤으로 5가 나오는 상황이 발생한다면 어떻게 될까요?
character의 defense값은 항상 5로 고정되어 있습니다.
attack = Random().nextInt(attack - character.defense) + character.defense;
Random().nextInt(5 - 5) + character.defense;
Random().nextInt(0) + character.defense;
Random().nextInt() 설명을 보면 0보다 큰 수를 넣어야 한다고 합니다.
그래서 이런 오류가 발생한 것이죠.
2) Random 범위 오류 해결
이를 해결하기 위해 maxAttack이라는 인스턴스 인스턴스 변수를 새로 생성하였습니다.
class Monster {
String name;
int health;
// maxAttack 새로 생성
int maxAttack;
int attack = 0;
int defense = 0;
void attackCharacter(Character character) {
print('$name이(가) ${character.name}에게 $attack의 데미지를 입혔습니다.');
character.health -= attack - character.defense;
attack =
Random().nextInt(maxAttack - character.defense) + character.defense;
}
}
몬스터가 캐릭터를 공격할 때마다 maxAttack을 기반으로 랜덤 값을 생성하도록 수정하였습니다.
3. 프로젝트 소개
이 프로젝트는 콘솔 환경에서 작동하는 RPG Game입니다.
플레이어는 캐릭터를 생성하고, 다양한 몬스터들과 차례로 전투를 하며 살아남는 것이 목표입니다.
1) 주요 기능
① 캐릭터 생성 및 로딩
사용자가 이름을 입력하면 characters.txt에서 해당 캐릭터의 스탯 정보를 로드합니다.
캐릭터는 heal, attack, defense를 가집니다.
② 몬스터 데이터 로딩
monsters.txt에서 여러 마리의 몬스터 정보를 로드합니다.
몬스터는 이름, 체력, 최대 공격력을 가지며, 전투 시 랜덤으로 등장합니다.
③ 전투 시스템
턴제로 전투가 진행됩니다.
먼저 플레이어의 턴이 시작되며, 행동을 선택합니다.
1: 공격
2: 방어 (몬스터 공격력의 10%만큼 체력 회복)
3: 아이템 사용 (공격력 2배, 단 1회 제한)
행동 선택 후, 몬스터의 턴이 시작됩니다.
몬스터는 랜덤 공격력으로 공격하며, 초기 방어력은 0이지만, 3턴마다 방어력이 2씩 증가합니다.
④ 아이템 시스템
아이템은 한 번만 사용 가능하며, 사용 시 공격력이 2배로 증가합니다.
한 번 공격 후 다시 원래의 공격력으로 돌아갑니다.
⑤ 게임 저장 기능
전투 도중이나 게임이 끝난 후, 결과를 result.txt에 저장합니다.
캐릭터 이름, 남은 체력, 승패 여부를 저장하며,
아직 전투가 끝나지 않았다면 남은 몬스터들의 정보도 저장합니다.
⑥ . 기타 기능
전투 후 계속 싸울지 여부를 선택할 수 있으며,
30% 확률로 게임 시작 시 캐릭터 체력이 10 (보너스 회복) 회복됩니다.
2) 프로젝트 구조
프로젝트 구조는 크게 세가지로 나뉩니다.
domain/usecase의 character.dart, monster.dart, game.dart파일이 있고,
resource 폴더의 characters.txt, monsters.txt파일,
utils 폴더의 input.dart, load_character.dart, load_monster.dart 파일,
마지막으로 rpg_game.dart (main) 파일이 있습니다.
/bin
├─ utils/
│ └─ input.dart
├─ domain/
│ └─ usecase/
│ ├─ character.dart
│ ├─ monster.dart
│ └─ game.dart
└─ main.dart
/resource
├─ characters.txt
├─ monsters.txt
└─ result.txt
4. 참고자료
https://dart.dev/language/constructors#factory-constructors
https://stackoverflow.com/questions/17476718/how-do-get-a-random-element-from-a-list-in-dart
'TIL' 카테고리의 다른 글
[TIL] 250326 LeetCode 문제 풀이, StatefulWidget, StatelessWidget, 개인 과제 피드백 (0) | 2025.03.26 |
---|---|
[TIL] 250321 LeetCode 풀이, 개인 과제 제출, 트러블 슈팅 (0) | 2025.03.21 |
[TIL] 250319 LeetCode 풀이, 개인 과제 (0) | 2025.03.19 |
[TIL] 250318 LeetCode 풀이, 주석 정리, 개인 과제 (0) | 2025.03.18 |
[TIL] 250317 LeetCode 27번, 예외처리 (2) | 2025.03.17 |