노무현 대통령 배너

Strategy Pattern 스트래티지 패턴으로 액션어드밴쳐 게임 만들기

by on 5.17, 2009, under Design Pattern

zelda_and_link헤드퍼스트 디자인 패턴 책에는 오리를 이용하여 스트래티지 패턴을 설명하였습니다. 스트래티지 패턴은 OOP의 중요한 원칙 중 하나인 “상속보다는 합성” 원칙을 직접 표현하고 있기 때문에 반복학습을 통한 확실한 이해를 하기 위해 오리가 아닌 액션어드벤쳐 게임을 주제로 삼아 이 패턴을 다시한번 설명하도록 하겠습니다.

전체 클래스와 인터페이스를 한눈에 보기 위해 starUML을 이용해 클래스 다이어그램을 작성해 보았습니다.

Strategy Pattern - Action Adventure Game

Strategy Pattern - Action Adventure Game

먼저 Character 클래스는 Knight 와 Wizard 클래스의 추상층입니다. 각 직업들이 가지는 공통적인 특성을 정의하여 서브클래스에서 확장(상속) 사용하도록 합니다.

모든 캐릭터들은 무기를 사용할 수 있고 갑옷을 입을 수 있기 때문에 이 두가지의 기능을 커다란 알고리즘으로 분류하여 각각 인터페이스를 만듭니다.
무기를 사용하는 것은 useWeapon() 메서드를 호출하여 무기를 사용하는 모습을 그래픽으로 처리하게 하고, 갑옷을 착용하면 갑옷 종류에 따라 다른 보너스 hp 를 얻도록 할 계획입니다.

아래의 IArmor 인터페이스를 보면 wearBonus() 메서드의 리턴값이 int 로 되어 있음을 볼 수 있습니다. 이것은 갑옷 종류에 따른 hp 보너스 포인트를 리턴할 수 있도록 한 것입니다.

0
1
2
3
4
5
6
package
{
	public interface IArmor
	{
		function wearBonus():int;
	}
}

IArmor 인터페이스를 구상하는 클래스중 하나인 Plate 클래스를 보면 인터페이스에 있는 메서드를 구현하고 있고 wearBonus() 메서드에서 int 값 50을 리턴하고 있습니다.

0
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package
{
	public class Plate implements IArmor
	{
		public function Plate ()
		{
			trace( "판금갑옷을 착용하였습니다." )
		}
 
		public function wearBonus():int
		{
			trace( "현재 hp에 + 50" )
			return 50;
		}
	}
}

이런식으로 아무것도 입지 않은 Naked 클래스를 포함하여, 필요한 갑옷마다 hp 보너스를 설정하여 인터페이스를 구현합니다.

그럼 이제 IWeapon 과 IArmor 인터페이스를 사용하게 되는 Character 클래스를 보면 모든 캐릭터들이 공통적으로 필요한 변수를 설정하고 몇가지에는 getter 를 설정하였습니다. 각 메서드에 대한 설명은 주석으로 달아놓았습니다.

0
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
package
{
	public class Character
	{
		protected var weapon:IWeapon;
		protected var armor:IArmor;
		protected var _hp:int = 0;
		protected var _name:String;
		protected var _characterClass:String;
 
		public function Character ()
		{
			trace( "-----새로운 캐릭터를 생성합니다.-----" )
		}
 
		//무기를 장비함
		public function equipWeapon( $weapon:IWeapon ):void
		{
			weapon = $weapon;
		}
 
		//무기를 사용함
		public function weaponAction():void
		{
			weapon.useWeapon()
		}
 
		//갑옷을 입음
		public function equipArmor( $armor:IArmor ):void
		{
			armor = $armor;
 
			//해당 갑옷의 보너스 hp를 리턴받아 캐릭터의 hp에 더함
			_hp += armor.wearBonus();
 
			//방어력, 이동속도 등 다른 착용효과 구현 가능
		}
 
		//현재 장비하고 있는 무기를 리턴
		public function getObjectWeapon():IWeapon
		{
			return weapon
		}
 
		//캐릭터 이름, 직업, hp 현황을 리턴
		public function getStat():String
		{
			return _name + "(" + _characterClass + ") 현재 hp : " + _hp
		}
 
		public function get hp():int { return _hp; }
 
		public function get name():String { return _name; }
 
		public function get characterClass():String { return _characterClass; }
	}
}

그리고 Character 클래스를 확장한 Knight 클래스에서는 Character 클래스에서 protected 로 선언하여 상속한 변수들에 초기값을 입력하여 캐릭터를 생성하게 됩니다.

0
1
2
3
4
5
6
7
8
9
10
11
12
13
14
package
{
	public class Knight extends Character
	{
		public function Knight ( $name:String )
		{
			_hp = 100;
			_name = $name;
			_characterClass = "기사"
			trace( name , " (이)라는 이름을 가진 ", characterClass ," 캐릭터를 생성하였습니다." )
			weapon = new Fist();
			armor = new Naked();
		}
	}
}

여기까지 하면 주먹( new Fist(); ) 이라는 무기와 아무것도 걸치지 않은 맨몸( new Naked(); )의 hp 100짜리 기사 캐릭터가 생성 됩니다.

IArmor를 짚어나가고 있으므로 그 부분을 집중해서 보도록 하겠는데요, 여기서 중요한 부분은 Character 클래스에서 IArmor 데이터형으로 인스턴스 변수를 생성한 armor 에[01] new Naked(); 라는 갑옷를 대입하여 현재 갑옷상태를 아무것도 입지 않은 상태로 초기 지정하였습니다. 즉, 추상층[02] 에서는 상위 형(type)인 인터페이스 형(type)으로 변수 선언을 하고, 구상층에서 상속받은 그 변수에 구체적인 형태를 대입하는 것입니다. 이렇게 하는 이유는 아래 호스트 코드에서 살펴보도록 하겠습니다.

0
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
package
{
	import flash.display.Sprite;
 
	public class Main extends Sprite
	{
		public function Main():void
		{
			var player1:Character = new Knight( "세계의끝" );
							//출력 : -----새로운 캐릭터를 생성합니다.-----
							//출력 : 세계의끝  (이)라는 이름을 가진  기사  캐릭터를 생성하였습니다.
							//출력 : 맨주먹입니다. (무기가 없습니다)
							//출력 : 아무것도 착용하지 않았습니다.
			trace( player1.getStat() );		//출력 : 세계의끝(기사) 현재 hp : 100
			player1.weaponAction();		//출력 : 주먹을 휘두릅니다.
			player1.equipWeapon( new Sword() );	//출력 : Sword 를 장비하였습니다.
			player1.weaponAction();		//출력 : Sword 를 휘두릅니다
			player1.equipArmor( new Plate() );	//출력 : 판금갑옷을 착용하였습니다.
							//출력 : 현재 hp에 + 50
			trace( player1.getStat() );		//출력 : 세계의끝(기사) 현재 hp : 150
			trace( player1.getObjectWeapon() );	//출력 : [object Sword]
 
			var player2:Character = new Wizard( "댄스댄스댄스" );
							//출력 : -----새로운 캐릭터를 생성합니다.-----
							//출력 : 댄스댄스댄스  (이)라는 이름을 가진  마법사  캐릭터를 생성하였습니다.
							//출력 : 맨주먹입니다. (무기가 없습니다)
							//출력 : 아무것도 착용하지 않았습니다.
			trace( player2.getStat() );		//출력 : 댄스댄스댄스(마법사) 현재 hp : 80
			player2.weaponAction();		//출력 : 주먹을 휘두릅니다.
			player2.equipWeapon( new Staff() );	//출력 : Staff 를 장비하였습니다.
			player2.weaponAction();		//출력 : Staff 에서 마법을 발사합니다.
			player2.equipArmor( new Leather() );	//출력 : 가죽갑옷을 착용하였습니다.
							//출력 : 현재 hp에 + 30
			trace( player2.getStat() );		//출력 : 댄스댄스댄스(마법사) 현재 hp : 110
			trace( player2.getObjectWeapon() );	//출력 : [object Staff]
		}
	}
}

8 번 라인을 보면, 위에서 언급한 것과 마찬가지로 new Knight( “세계의끝” ) 를 상위 type 인
var player1:Character 로 받고 있는데요, 이런 형태의 캐스팅을 하게 되면, 인스턴스 변수를 사용할 때에 그게 어떤 구체적인 형태인지 호스트코드에서 알 필요 없이 알아서 사용됩니다.
위의 player1은 type 이 Character 지만 Knight 클래스라는 것을 player1 은 이미 알고 있다는 의미 입니다. 마찬가지로 Character 클래스의 equipArmor() 메서드에서 armor.wearBonus() 를 하면 캐릭터가 뭘 입었는 몰라도,[03] 보너스 hp는 조사하면 다 나온다는 것이죠.

이러한 구조는 아래와 같은 절차지향 코드의 조건문을 상쇄한 효과를 냅니다.

0
1
2
3
4
5
6
7
8
9
10
function wearBonus():int {
	var bonus:int;
	if ( armor == "Plate" ) {
		bonus = 50;
	} else if ( armor == "Leather" ) {
		bonus = 30;
	} else {
		bonus = 0;
	}
	return bonus;
}

게다가 새로운 갑옷이 늘어날 때마다 else if 를 추가하지 않아도 됩니다. 무엇보다 확장에 유연하고 수정사항이 생겼을때 어디를 얼마만큼 고쳐야 하는지 명확하게 알 수 있게 됩니다.

zelda_link_to_the_past_stamp

Strategy Pattern : Action Adventure Game 액션스크립트 코드 다운로드 (239)

 

이 포스트를 쓰면서 계속 마계촌이 생각나서 검색하다 재미있는 것이 있어 마무리로 올려봤습니다.

마계촌의 후속작인 극마계촌의 PV 입니다.

이 글을 복사해서 퍼가시는건 허용하지 않습니다. 글의 주소를 다른곳에 알려주시는 것은 환영합니다.
  1. protected var armor:IArmor; 부분을 말합니다. []
  2. Character 클래스를 말합니다. []
  3. 보다 정확하게 말하면 뭘 입었는지 관계 없이 []

관련된 글

:, , , , , , , , , , , , , , ,

7 Comments for this entry

  • 동강No Gravatar

    잘 보았습니다.ㅎ 실제로 게임만들면 재미 있을것 같네요ㅎ

    • 세계의끝No Gravatar

      만드는 과정은 확실히 재미 있을것 같습니다.
      내공이 좀더 높아지면 한번 만들어 보려구요. ㅎㅎㅎ

  • No Gravatar

    감사합니다. 많은 도움 되었습니다.

  • No Gravatar

    안녕하세요.
    위의 Character 클래스에서
    public function getDamage( $dmg:int ):void
    {
    _hp -= $dmg;
    }
    를 추가하고
    본문(호스트코드?)에 player1.getDamage(50)을 하면 hp가 깎이잖아요

    근데 그걸 예를 들어 결투?하는 클래스(다른 클래스)가 따로 존재했을때
    getDamage를 실행하려면 어떻게 전달하는게 좋을까요? 이벤트를 쓰는게 좋을까요?
    아니면 다른 효율적인게 있나요?
    그것도 아니면 그걸 실행하는것 자체가 잘못된 설계인걸까요..?

    질문을 이해하셨는지 모르겠습니다. 제가 부족하니 질문도 부족하네요..
    플생사모에도 질문을 올렸는데 질문자체가 부실하네요

    • 세계의끝No Gravatar

      위의 경우라면 데미지를 서로 주고 받기까지 하는데 객체간 서로를 모를 수는 없다고 보고요,

      0
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      
      // Character 클래스
      public function weaponAction():void
      {
      	var targetPlayer:Character = getTarget();
      	var damage:int = weapon.useWeapon();
      	targetPlayer.setDamage( damage ); // 타겟에게 데미지를 줘야 하기때문에 setDamage 로 메서드명을 바꿨습니다.
      }
       
      public function getTarget():Character
      {
      	// 타켓시스템 등으로 상대 캐릭터를 선택
      }
       
      public function setDamage( $damage:int ):void
      {
      	_hp -= $damage;
      }

      IWeapon 인터페이스와 각 무기 클래스는 useWeapon() 메서드 에서 데미지 int 값을 리턴하도록 수정해야겠죠.
      getTarget() 메서드에서 상대 캐릭터를 선택하는 방법의 구현은 상황에 따라 다르게 구현하겠죠. 마우스나 탭 키로 타겟팅 하는 시스템이라면 유저가 해당 인터랙션을 취하는 순간 상태 캐릭터를 알 수 있겠죠. 논 타겟팅 시스템이라면 좀 다르게 해야겠지만 말이죠.

      료 님의 질문은 객체간 서로를 얼마만큼 알아야 하는가, 또는 어떤 방식으로 알아야 하는가에 대한 의문일텐데, 상황에 따라 다를 수 밖에 없다 라고 생각합니다.

Leave a Reply

Looking for something?

Use the form below to search the site:

Still not finding what you're looking for? Drop a comment on a post or contact us so we can take care of it!

Meta