[230319 - TIL] Dart 기본문법을 이용한 턴제 게임만들기

Dongwoo Kim·2023년 3월 19일
0

TIL / WIL

목록 보기
96/113

1. 개요

그동안 배운 Dart문법을 이용해서 1시간 가량 간단한 턴제 게임을 만들어보았다. 전체적인 맥락은 내일배움캠프에서 처음 웹개발을 배울 때 python으로 만들었던 1:1 격투게임을 생각하면서 클래스와 공격, 수비 등을 구현해보았다.

사용해본 문법

abstract class, final, enum,
required, ?, ??, named constructor,
@override, random, super 등등


2. 추상화 클래스

player는 각자 하나의 직업을 가지게된다. 직업은 탱커, 근딜, 원딜, 서포터 등등이 존재할 수 있지만 닉네임, 체력, 공격력 등등 공통적인 요소를 추상화 클래스를 통해 구현하였다.

import 'dart:math';

enum Status { normal, attack, defense, jump, die }

abstract class Job {
  final String nickname;  // 닉네임
  int hp;                 // 체력
  int attack;             // 공격력
  int defense;            // 방어력
  double criticalChance;  // 크리티컬확률
  
  Status status;          // 현재상태 - 일반, 공격중, 수비중, 점프중

  Job({
    required this.nickname,
    required this.hp,
    required this.attack,
    required this.defense,
    double? criticalChance
  }) : status = Status.normal,
       criticalChance = criticalChance ?? 0.1;

  /// 크리티컬이 발생했는지 확인하는 함수
  bool isCritical(){
    var random = Random();
    var result = random.nextDouble() < criticalChance;
    return result;
  }
  
  
  /// 공격 실행 함수
  /// [target] 공격 당하는 상대방 인스턴스
  void attackTarget(Job target) {
    print('$nickname attacks ${target.nickname}');

    // 상대방이 수비 중이면 공격 실패
    if (target.status == Status.defense) {
      print('${target.nickname} defenses $nickname\'s attack');
    } 
    // 공격시 크리티컬확률에 따라 2배 데미지 
    else {
      var damageValue = attack;
      if (isCritical()) {
        damageValue *= 2;
        print('critical!!!');
      }

      target.damage(damageValue);
    }
  }

  // 수비 실행 추상화 함수
  void defend();

  /// 데미지 받는 함수
  /// [value] 받은 데미지 값
  void damage(int value) {
    // 방어도만큼 데미지 감소
    int damageValue = value - defense;
    if (damageValue < 0) damageValue = 0;
    
    hp -= damageValue;
    
    print('$nickname takes $damageValue damage');
    if (hp <= 0) {
      status = Status.die;
      print('$nickname is defeated');
    }
  }
}

3. 직업별 특징 구현

시간관계상 탱커와 서포터의 특징만 따로 구현을 해보았다. 탱커는 수비할 경우 추가로 수비력이 올라가고, 서포터는 자기 체력을 채울 수 있다. 또한 서포터는 고유스킬로 힐을 할 수 있도록 만들어봤다.

class Tanker extends Job {
  Tanker(String nickname) 
    : super(
      nickname: nickname,
      hp: 100,
      attack: 10,
      defense: 5
    );

  
  void defend() {
    // 탱커는 수비시 추가 방어력을 얻는다. (최대 8)
    if (defense < 8) {
      defense += 1;
    }
    
    status = Status.defense;
    print('$nickname defend');
  }
  
  void taunt() {
    // 탱커만 가지고 있는 특수 스킬
  }
}

class MeleeDealer extends Job {
  MeleeDealer(String nickname) 
    : super(
      nickname: nickname,
      hp: 80,
      attack: 15,
      defense: 3,
      criticalChance: 0.2
    );

  
  void defend() {
    // 근거리 딜러의 방어 코드
  }

  void comboAttack() {
    // 근거리 딜러만 가지고 있는 특수 스킬
  }
}

class RangedDealer extends Job {
  RangedDealer(String nickname) 
    : super(
      nickname: nickname,
      hp: 60,
      attack: 20,
      defense: 1,
      criticalChance: 0.3
    );
  
  
  void defend() {
    // 원거리 딜러의 방어 코드
  }

  void criticalHit() {
    // 원거리 딜러만 가지고 있는 특수 스킬
  }
}

class Supporter extends Job {
  Supporter(String nickname) 
    : super(
      nickname: nickname,
      hp: 70,
      attack: 10,
      defense: 2
    );

  
  void defend() {
    // 서포터는 방어시 스스로 hp를 회복한다
    hp += 5;
    
    status = Status.defense;
    print('$nickname defend');
  }

  /// 서포터 고유 스킬 : target의 hp를 공격력+4만큼 회복한다. 크리티컬확률에 따라 2배 회복한다.
  /// [target] 해당 캐릭터
  void heal(Job target) {
    print('$nickname heals ${target.nickname}');
    var healValue = attack + 4;
    
    if (isCritical()) {
      healValue *= 2;
      print('critical!!!');
    }
    
    target.hp += healValue;    
    
    print('${target.nickname}\'s hp + $healValue');
  }
}

4. 턴제 실행

main 함수에서 (탱커 vs 서포터)로 턴제로 한번씩 행동을 주고받으며 결과를 프린트해보았다.

void main() {
  Tanker player1 = Tanker("dongwoo");
  Supporter player2 = Supporter("minji");

  var turn = 0;
  var random = Random();
  
  // 한턴씩 돌아가면서 행동실행
  while(true) {
    print('\n');
    print("==== turn - $turn =====");
    
    // player1의 행동 결정
    var pattern = random.nextDouble();
    if (pattern < 0.3) {
      // 30% 확률로 방어
      player1.defend();
    } else {
      // 70% 확률로 공격 
      player1.attackTarget(player2);
    }
    
    // 행동 결과 확인
    if (player2.status == Status.die) {
      print("player1 ${player1.nickname} win!!!!");
      break;
    } 
    
    // player2의 행동 결정
    pattern = random.nextDouble();
    if (pattern < 0.25) {
      player2.heal(player2);
    } else if (pattern < 0.5) {
      player2.defend();
    } else {
      player2.attackTarget(player1);
    }
    
    // 행동 결과 확인
    if (player1.status == Status.die) {
      print("player2 ${player2.nickname} win!!!!");
      break;
    } 
    
    // 무승부인지 확인
    if (turn > 100) {
      print("draw...");
      break;
    }
    
    // 다음 턴 진행
    turn += 1;
    print("player1 hp: ${player1.hp}");
    print("player2 hp: ${player2.hp}");
    player1.status = Status.normal;
    player2.status = Status.normal;
  }
}

5. 결과는?

각각 확률적으로 행동을 하기 때문에 실행할 때마다 결과가 다르게 나왔지만 대부분 탱커가 이기는 결과가 나왔다. 밸런스패치가 시급한 똥겜인것 같다.


6. 소감

일단 새로운 것을 배우는 것은 정말 즐겁다. 정말 별거 아니지만 오랜만에 처음 내배캠에서 처음 나만의 프로젝트를 만들 때 들었던 기분을 느낄 수 있었다. 내가 만들고 싶은 것을 정의하고, 생각을 정리해서 코드로 구현하고 결과물을 바로 확인할 수 있는 것은 개발이 가지는 정말 큰 매력이라고 생각한다.

이번에 Dart를 사용해본 소감을 정리해보자면 전체적으로는 사용하는데 크게 이질감이 들지는 않았다. 기존에 다른 언어들과 마찬가지로 문법적으로 크게 다른 점은 없었는데 몇가지 부분에서 낯설거나 특이하다고 생각이 들었던 부분은 있다.

named parameter, ?, ??

`named constructor`에서 사용했듯이 파라미터를 정의할 때 {}를 사용해서 `named_parameter`로 사용할 수 있는데 이점은 Dart만의 특징이라고 할 수 있다. 또한 파라미터를 optional하게 받고 싶으면 `?`와 `??` 문법을 사용할 수 있고, 그렇지않으면 `required`를 명시해줘야한다.

this

클래스 내부에서 자기자신을 가리킬 때는 this를 사용하는데, 클래스의 속성에 접근할 때는 별도로 같은 이름의 변수가 있지 않은 이상 this를 사용하지않아도 되는 것이 특이했다.

물론 this를 사용해도 문제는 없었지만 dart에서 사용하지않아도되는데 사용하고있다고 일종의 경고표시를 준다.

profile
kimphysicsman

0개의 댓글