8.5 객체지향으로 개발하기 1
배틀시티 게임의 객체지향형 버전을 만든다는 것은 구조적으로 어떠한 형태를 띄어야 하는 것일까? 파이게임제로 라이브러리를 베이스로 사용하고 있기 때문에 필수 콜백함수들(draw, update 함수 등)을 통한 게임루프의 구조는 그대로 가져갈 수 밖에 없다는 걸 전제해야 한다. 그러나, 기존의 절차지향 버전에서 존재했던 (글로벌)함수들 기반의 절자지향형 코드들은 이제 객체형태로 변형되어 그 객체간의 협력만으로 게임이 잘 동작하게 만드는 것이 목표라 하겠다.
저 그럼 배틀시티 게임에 필요한 객체식별부터 시작해 보자. 우리 게임무대에 등장인물들은 배우(Actor)객체 중심으로 찾아보면 쉬운데, "주인공 탱크", "적 탱크", "벽 더미", "대포알", "폭발장면"이 있다. 크게 구분해 보면, "주인공", "적", "장애물", "무기" 라는 역할을 한다고 말할 수 있다. 우리의 고민은 여기서 다시 시작된다. 이 게임을 예제수준에서 먼저 절차지향형으로 개발해 본 딱 그정도만 기능구현하고 더이상 추가구현 없는 전혀 건드리지 않을(?) 게임으로 생각하는지, 아니면 개인적인 의욕이 있어 이 게임을 더 개선해서 더 발전된 형태의 버전업이 되는 게임, 예를들어 적으로서 탱크만이 아닌 다양한 형태의 다른 적이 등장한다던지, 내가 사용하는 무기도 대포알 뿐만 아니라 다른 형태의 공격무기도 갖게 할 것인지, 지금은 장매물이 벽 더미이지만, 그 외의 다른 형태의 장애물도 생각하는지 등등에 따라 당장의 상속의 깊이에서부터 객체지향 설계의 스케일이 달라지기 때문이다.
스케일을 너무 크게 잡고 설계 하는 부분에 대해서는 애시당초 이 책이 고려한 학습 난이도에 비해 높고, 분량이 많아져서 기회가 되면 이 책의 다음 레벨의 책에서 더 진도를 나가기로 하고, 이 책에서는 적절한 수준으로 타협하여 설계하도록 하겠다. 그럼, 먼저 주인공과 적의 역할만 놓고 생각해보자. 주인공과 적의 목표는 이동하면서 서로 상대방을 포쏘기 공격해 맞추는 것이다. 이 때 둘의 사용하는 무기(대포알)가 동일하고 이동하는 방법 역시 전후좌우의 직진이동만 할 수 있어 둘 다 똑같은 제약을 갖는다. 그렇다. 탱크들를 객체로 만들어 활용하면 좋겠다는 생각이 들었다면 맞게 생각한 것이다. 그리고, 주인공 탱크나 적 탱크나 유사점이 너무 많아 우리 지난 시간에 배운 것처럼 유사 객체들 사이에서 공통특징으로 일반화 할 수 있는 이 지점이 바로, 객체지향설계에 있어서 부모-자식간의 상속(Inheritance) 을 고려해 보면 어떨까라는 생각이 들었다면 이 역시 맞는 결정이다.
그럼, 이를 또 한편으로 탱크객체가 해야할 책임(외부의 객체의 사용자에게 제공하게 될 정보 또는 요청에 의한 행동)에 대해서 고민해보자. 그런데 책임들을 정하기에 앞서 먼저는 객체들 간에 협력에 대해 생각하는게 우선인데, 협력에 대해 고민하면 자연스럽게 객체의 책임을 이끌어 내기 때문이다. 즉, 어떤 객체가 탱크객체에게 어떤 행동의 요구를 할 것인가에 대한 것을 고민해보자. 가장 본질적인 요구는 각 객체을 생성(creation)하는 것에서부터 출발할 수 있다. 각 객체를 첨부터 무조건 다 만들어 놓고 시작하는게 아니라, 특정 객체가 특정 객체의 생성을 특정 시점에 요구할 수 있는 것이다. 예를들어 스페이스 바를 눌러 대포를 쏘는 시점에 "총알" 라는 객체가 생성되고 날아가기 시작하면 되는 것처럼 말이다.
게임이 맨 처음 시작했을 때 화면에 맨 처음 등장하는 객체들은 무엇인가? "주인공 탱크", "적 탱크", "벽 더미" 이다. 그리고, 객체는 아닌 단순 이미지인 배경 이미지가 존재해야 할 것이다. 그럼, 이들 객체의 생성은 누가할 것인가? 엔트리 블록코딩에서는 엔트리(시스템) 자체가 내부적으로 자동으로 해주었다면, 이젠 그것도 우리가 직접 코딩으로 해야하는 것이다. 객체들이 화면에 그려지기 위해서는 draw 콜백함수 안에서 각 객체의 draw 메소드를 호출해야 할 것이고, 그 말은 draw 콜백함수 이전에 최소한 생성이 되어 있어야 하는 것을 전제한다. 다음의 battle_city_oop.py 라고 명명한 파일에서 방금 언급된 내용의 기본적인 코딩을 해보도록 하겠다.
🔢 벽 더미 생성코드는 기존의 절차지향 버전에서의 것과 동일하여 설명이 필요없다. 18-25라인까지 주인공 탱크와 3대의 적 탱크를 생성하고 있는데, 특이한 것은 기존에는 배우객체를 생성할 때 Actor 클래스를 통해 생성했었는데, 이제는 우리가 직접 만든 사용자 객체인 MyTank, EnemyTank라는 클래스를 사용해 생성하고 있다는 것이다. 그 사용자 객체들은 battle_city_oop.py 파일 안에 함께 코딩할 수도 있지만, 한 파일 안에 너무 많은 코드를 적는 것은 지양되고, 의미있게 여러 파일로 나눠 모듈화(modulization) 하는 것이 더 큰 프로젝트를 관리하는데 효과적이며 일반적이다. 따라서, 우리의 사용자 객체들은 아래처럼 actors.py 라는 파일 안에 존재하며, 이를 battle_city_oop.py 안에서 불러 사용하기 위해 2번 라인에서 from-import 문을 사용해 actors 모듈(이를 통해 우리는 파일명이 곧 모듈명이 된다는 것을 알 수 있음) 안에 클래스들을 import 하고 있다.
🔢 이해를 돕기 위해 상세구현 없는 식별된 객체들의 아주 기본적인 뼈대로부터 시작하는게 좋겠다. 먼저 탱크 객체부터 차근히 고민해보자. 탱크 객체는 Actor객체를 상속했기 때문에 기존의 Actor처럼 간주될 수 있기 때문에 기존의 일반적인 배우 객체의 생성 때처럼 객체의 외관 이미지와 최초 화면상의 위치정보의 두 파라미터 값으로 생성할 수 있으나, 잘 고민해보면 게임 시작시 적 객체와 주인공 객체가 서로 마주보는 상태로 게임이 시작해야 하므로, 객체생성시 초기 파마미터로 아예 angle 속성값도 함께 넘겨 초기화하면 좋겠다. 참고로 angle 속성값은 원래 오리지널 Actor 객체의 속성엔 존재하지 않지만 actors.py 상단에서 pgzhelper 모듈을 import 했기 때문에 Actor객체 안에 원래 존해하는 추가 속성처럼 인식이 가능하게 되었다는 것을 잘 알고 있을 것으로 안다. 총 3개의 파라미터에서 img_name, pos 값은 부모객체인 오리지널 Actor객체의 초기화에 사용되도록 값을 넘기고, angle값은 파생객체의 자신의 속성값으로 설정한다. 그럼 지금까지 언급된 내용을 코드로 표현해 보면 다음과 같다.
🔢 그 다음으로는 탱크 이동에 관한 move 메소드의 구현을 고민해 보자. 부모는 자식들이 모두 공유하는 공통된 기능에 대한 것을 구현하는 것이 의미 있는데 그래야만 부모를 상속한 자녀가 부모 것을 그대로 쓰면 되기 때문이다. 현재까지 찾아진 탱크의 공통동작이라고 할 수 있는 것은 근본적인 탱크 이동에 대한 것으로 이 move 메소드가 호출될 때 마다 현재 탱크가 바라보고 있는 방향을 기준으로 정해진 크기만큼 직진이동 한다는 것이다. 이 부분까지 포함해 다시 전체적인 코드로 표현하면 다음과 같다.
탱크의 이동에 대해 우리가 간과한(?)이 하나 있는데, 탱크이동에 제약이 있다는 것이다. 이 제약은 기존 절차지향으로 개발할 때도 다 인지가 되었던 것으로 탱크가 벽을 통과해 이동할 수 없고, 보이는 전체 게임화면을 넘어 이동해서도 안된다. 두 제약은 이동제약이란 측면에선 유사하지만 실제 구현관점에선 화면경계 제약은 단순 범위제약이고, 벽 관련 제약은 두 객체(탱크와 벽)의 충돌확인 후 이동의 제약이라 서로 내용이 다르다.
여기서 전자의 경우는 꼭 탱크객체에게만 유효할까? 다른 객체에는 필요없을까? 를 생각해보면, '총알' 객체에게도 필요한 부분이다. 왜냐하면, 총알객체가 화면경계 밖을 넘어서자마자 객체를 삭제(메모리에서 제거)해야 하기 때문이다. 이렇게 모든 객체에게는 필요하지 않지만, 일부 객체에겐 필요한 기능을 구현하기 위해 객체지향 프로그래밍에서 반드시 알아야하는 또다른 개념이 하나 등장하는데 추상 클래스(abstract class) 또는 인터페이스(interface)라는 개념을 배워야 할 시점이 왔다. 이 부분은 이론적 이해가 필요해 내용이 길어질 수 있으므로 다음 장에서 좀 더 상세히 설하도록 하겠다.
Last updated