거대한 플레이어 클래스를 피하려면 어떻게해야합니까?


47

게임에는 거의 항상 플레이어 클래스가 있습니다. 플레이어는 일반적으로 게임에서 많은 것을 할 수 있습니다. 즉,이 클래스는 플레이어가 할 수있는 각 기능을 지원하기 위해 많은 변수로 막대합니다. 각 조각은 자체적으로 상당히 작지만 결합하면 수천 줄의 코드로 끝나고 변경하기 위해 필요하고 무서운 것을 찾기가 어려워집니다. 기본적으로 전체 게임을 제어하는 ​​일반적인 방법으로이 문제를 어떻게 피할 수 있습니까?


26
여러 파일 또는 하나의 파일, 코드는 어딘가에 가야합니다. 게임은 복잡합니다. 필요한 것을 찾으려면 좋은 분석법 이름과 설명 주석을 작성하십시오. 변경하기 위해 두려워하지 마십시오. 테스트 만하면됩니다. 그리고 백업 작업 :
크리스 맥팔랜드

7
어딘가로 가야하지만 유연성과 유지 관리에서 코드 디자인이 중요합니다. 수천 줄의 클래스 또는 코드 그룹을 갖는 것이 나에게도 영향을 미치지 않습니다.
user441521

17
@ChrisMcFarland는 백업을 권장하지 않으며, 버전 코드 XD를 제안합니다.
GameDeveloper

1
@ChrisMcFarland GameDeveloper에 동의합니다. Git, svn, TFS 등과 같은 버전 제어 기능을 사용하면 큰 변경 사항을 훨씬 쉽게 취소하고 실수로 프로젝트를 삭제하거나 하드웨어 오류 또는 파일 손상과 같은 것을 쉽게 복구 할 수 있기 때문에 개발이 훨씬 쉬워집니다.
Nzall

3
@ TylerH : 나는 매우 동의하지 않습니다. 백업은 많은 탐색 적 변경 사항을 함께 병합 할 수 없으며, 유용한 메타 데이터와 가까운 곳을 변경 세트와 연결하거나 다중 개발자 워크 플로우를 활성화 할 수 없습니다. 매우 강력한 특정 시점 백업 시스템처럼 버전 제어를 사용할 수 있지만 이러한 도구의 가능성이 많이 없습니다.
Phoshi

답변:


68

일반적으로 엔터티 구성 요소 시스템을 사용합니다 (엔터티 구성 요소 시스템은 구성 요소 기반 아키텍처 임). 또한 다른 개체를 더 쉽게 만들 수 있으며 적 / NPC가 플레이어와 동일한 구성 요소를 가질 수 있습니다.

이 접근 방식은 객체 지향 접근 방식과 정확히 반대 방향으로 진행됩니다. 게임의 모든 것은 실체입니다. 엔터티는 게임 메커니즘이 내장되어 있지 않은 경우에 불과합니다. 구성 요소 목록과이를 조작하는 방법이 있습니다.

예를 들어 플레이어에는 위치 구성 요소, 애니메이션 구성 요소 및 입력 구성 요소가 있으며 사용자가 공간을 누르면 플레이어가 점프하기를 원합니다.

플레이어 엔터티에 점프 구성 요소를 제공하면이를 달성 할 수 있습니다.이 구성 요소를 호출하면 애니메이션 구성 요소가 점프 애니메이션으로 변경되고 플레이어가 위치 구성 요소에서 양의 y 속도를 갖도록합니다. 입력 컴포넌트에서 스페이스 키를 듣고 점프 컴포넌트를 호출합니다. (이것은 단지 예일뿐입니다. 이동을위한 컨트롤러 구성 요소가 있어야합니다).

이를 통해 코드를 더 작고 재사용 가능한 모듈로 나누고보다 체계적인 프로젝트를 만들 수 있습니다.


의견은 긴 토론을위한 것이 아닙니다. 이 대화는 채팅 으로 이동 되었습니다 .
MichaelHouse

8
이동해야하는 의견 이동은 이해하지만 답변의 정확성에 도전하는 의견은 이동하지 마십시오. 분명해야합니까?
bug-a-lot

21

게임은 이것에서 독특하지 않습니다. 신 클래스는 모든 곳에서 반 패턴입니다.

일반적인 해결책은 큰 클래스를 작은 클래스의 트리로 분류하는 것입니다. 플레이어에 인벤토리가있는 경우 인벤토리 관리를의 일부로 만들지 마십시오 class Player. 대신을 만드십시오 class Inventory. 이것은의 멤버 class Player이지만 내부적 class Inventory으로 많은 코드를 래핑 할 수 있습니다.

또 다른 예 : 플레이어 캐릭터는 NPC와 관계가있을 수 있으므로 객체와 객체를 class Relation모두 참조 하지만 둘 다에 속하지 않을 수 있습니다.PlayerNPC


예, 나는 이것을하는 방법에 대한 아이디어를 찾고있었습니다. 사고 방식은 작은 조각 기능이 많기 때문에 코딩하는 동안 자연스럽지 않습니다. 어쨌든 그 작은 조각을 분해하는 것은 자연스럽지 않습니다. 그러나 모든 작은 기능들이 플레이어 클래스를 크게 만들기 시작한다는 것은 분명합니다.
user441521

1
사람들은 일반적으로 무언가가 게임의 다른 모든 클래스 / 객체를 포함하고 관리 할 때 신 클래스 또는 신 객체라고 말합니다.
Bálint

11

1) 플레이어 : 상태 머신 + 컴포넌트 기반 아키텍처.

Player의 일반적인 구성 요소 : HealthSystem, MovementSystem, InventorySystem, ActionSystem. 그것들은 모두 같은 클래스 class HealthSystem입니다.

내가 사용하지 않는 것이 좋습니다 Update()플레이어가 중독됩니다 그리고 당신은 그를 필요 -. (그것은 하나의 경우 당신은 또한 생각할 수있다 당신이 거의 발생하지, 거기에 일부 작업 프레임마다 그것을 필요로하지 않는 일반적인 경우에 아무 의미가 의료 시스템에 업데이트가 없을 수 있습니다 때때로 건강을 잃으려면-여기에 코 루틴을 사용하는 것이 좋습니다. 또 다른 사람은 건강이나 달리는 힘을 지속적으로 재생성합니다. 현재 건강이나 힘을 취하고 시간이 오면 그 수준까지 채우기 위해 코 루틴을 호출하십시오. 그는 손상을 입었거나 다시 달리기 시작했습니다 .

상태 : LootState, RunState, WalkState, AttackState, IDLEState

모든 상태는에서 상속됩니다 interface IState. IState우리의 경우에는 예를 들어 4 가지 방법이 있습니다.Loot() Run() Walk() Attack()

또한 class InputController사용자의 모든 입력을 확인할 수 있는 곳이 있습니다.

실제 예를 들어 : InputController플레이어가을 누르는지 확인한 WASD or arrows다음을 누르십시오 Shift. 그는 단지 누른 경우 WASD우리가 부르는 _currentPlayerState.Walk();이 happends 때 우리가 currentPlayerState동일하게 WalkState에 다음 WalkState.Walk() 이 경우에 - 우리는 모든 구성 요소가이 상태에 필요한있다 MovementSystem, 그래서 우리는 플레이어의 움직임을 public void Walk() { _playerMovementSystem.Walk(); }- 당신은 우리가 여기서 무엇을 가지고 볼? 우리는 두 번째 행동 계층을 가지고 있으며 코드 유지 관리 및 디버깅에 매우 좋습니다.

이제 두 번째 경우 : 우리가 WASD+를 Shift눌렀다면? 그러나 우리의 이전 상태는 WalkState입니다. 이 경우에 Run()호출됩니다 InputController(이를 섞지 말고 + 때문에 체크인 하지 않기 때문에 Run()호출됩니다 ). 우리가 호출하는 경우 에 - 우리는 우리가 전환해야 알고 에 우리가 그렇게 할 의 와 다른 상태로 지금은이 방법 안에 다시 전화를하지만 우리는 행동이 프레임을 잃고 싶지 않기 때문에. 그리고 지금 우리는 전화 합니다.WASDShiftInputControllerWalkState_currentPlayerState.Run();WalkState_currentPlayerStateRunStateRun()WalkState_playerMovementSystem.Run();

그러나 LootState플레이어가 버튼 을 놓을 때까지 걷거나 뛸 수없는 경우는 어떻습니까? 버튼 글쎄이 경우 우리는 예를 들어, 약탈을 시작했을 때 E우리가 전화를 누른 _currentPlayerState.Loot();우리가 전환 LootState의이 호출 전화를 지금. 예를 들어 범위 내에서 전리품이 있는지 확인하기 위해 collsion 메소드를 호출합니다. 그리고 우리는 애니메이션이 있거나 애니메이션이 시작되는 곳에서 코 루틴을 호출하고 플레이어가 여전히 버튼을 누르고 있는지, 코 루틴이 끊어지지 않았는지 여부를 확인합니다. 하지만 플레이어가 누르면 WASD어떻게 될까요? - _currentPlayerState.Walk();라고 불리지 만 여기에 상태 머신에 대한 예쁜 점이 있습니다.LootState.Walk()우리는 아무것도하지 않거나 기능처럼 할 수있는 빈 방법을 가지고 있습니다. 플레이어들은 말합니다. "이봐, 난 아직 이걸 잃지 않았어, 기다려?" 약탈이 끝나면 우리는로 바뀝니다 IDLEState.

또한 class BaseState : IState이러한 모든 기본 메소드 동작이 구현되어 있지만 클래스 유형으로 작성할 수있는 다른 스크립트를 수행 virtual할 수 override있습니다 class LootState : BaseState.


구성 요소 기반 시스템은 훌륭합니다. 시스템에 대해 귀찮게하는 유일한 것은 인스턴스입니다. 가비지 수집기에 더 많은 메모리와 작업이 필요합니다. 예를 들어 적의 인스턴스가 1000 개인 경우 모두 4 개의 구성 요소가 있습니다. 1000 개 대신 4000 개 개체. Mb 단일 게임 개체에 포함 된 모든 구성 요소를 고려하면 성능 테스트를 실행하지 않았습니다.


2) 상속 기반 아키텍처. 구성 요소를 완전히 제거 할 수는 없지만 깨끗하고 작동하는 코드를 원한다면 실제로 불가능합니다. 또한 적절한 경우에 사용하기를 강력히 권장하는 디자인 패턴을 사용하려는 경우 (과도하게 사용하지 마십시오.이를 과잉 생성이라고합니다).

게임에서 종료하는 데 필요한 모든 속성을 가진 Player 클래스가 있다고 상상해보십시오. 그것은 건강, 마나 또는 에너지를 가지고 있고, 움직이고, 달리고, 능력을 사용하고, 인벤토리를 가지고, 아이템을 만들거나, 아이템을 전리품으로 만들 수 있으며, 심지어 바리케이드 나 포탑을 만들 수도 있습니다.

우선, 인벤토리, 크래프팅, 이동, 건물은 컴포넌트 기반이어야한다고 생각합니다. 왜냐하면 AddItemToInventoryArray()플레이어는 PutItemToInventory()이전에 설명한 방법을 호출 할 수있는 방법을 가질 수 있습니다. 다른 레이어에 따라 몇 가지 조건을 추가하십시오).

건물의 또 다른 예. 플레이어는와 같은 것을 호출 할 수 OpenBuildingWindow()있지만 Building나머지는 모두 처리해야합니다. 사용자가 특정 건물을 짓기로 결정하면 필요한 정보를 모두 플레이어에게 전달 Build(BuildingInfo someBuildingInfo)하고 플레이어는 필요한 모든 애니메이션으로 정보 를 만들기 시작합니다.

SOLID-OOP 원칙. S-단일 책임 : 이전 예에서 본 것. 예,하지만 상속은 어디에 있습니까?

여기 : 다른 개체가 플레이어의 건강 및 기타 특성을 처리해야합니까? 나는 그렇게 생각하지 않는다. 건강이없는 선수는있을 수 없으며, 존재하는 경우 상속하지 않습니다. 예를 들어, 우리는이 IDamagable, LivingEntity, IGameActor, GameActor. IDamagable물론 있습니다 TakeDamage().

class LivinEntity : IDamagable {

   private float _health; // For fields that are the same between Instances I would use Flyweight Pattern.

   public void TakeDamage() {
       ....
   }
}

class GameActor : LivingEntity, IGameActor {
    // Here goes state machine and other attached components needed.
}

class Player : GameActor {
   // Inventory, Building, Crafting.... components.
}

따라서 여기서는 실제로 구성 요소를 상속과 분리 할 수 ​​없었지만 보시다시피 혼합 할 수 있습니다. 예를 들어 다른 유형의 시스템이 있고 필요한 것보다 더 많은 코드를 작성하지 않으려는 경우와 같이 건물 시스템의 일부 기본 클래스를 만들 수도 있습니다. 실제로 우리는 다른 유형의 건물을 가질 수 있으며 실제로 구성 요소를 기반으로하는 좋은 방법은 없습니다!

OrganicBuilding : Building, TechBuilding : Building. 일반적인 작업이나 건물 속성을 위해 2 개의 구성 요소를 작성하고 코드를 두 번 작성할 필요가 없습니다. 그런 다음 다르게 추가하면 상속의 힘과 나중에 다형성과 캡슐화를 사용할 수 있습니다.


나는 사이에 무언가를 사용하는 것이 좋습니다. 구성 요소를 과도하게 사용하지 마십시오.


나는 게임 프로그래밍 패턴 에 대한이 책을 읽는 것이 좋습니다 -그것은 웹에서 무료입니다.


나는 오늘 밤 늦게 파고 있지만 참고로 나는 화합을 사용하지 않으므로 괜찮은 것을 조정해야합니다.
user441521

오, sry 여기에 Unity 태그가 있다고 생각했습니다. 유일한 것은 MonoBehavior입니다-Unity 에디터에서 씬의 모든 인스턴스에 대한 기본 클래스 일뿐입니다. Physics.OverlapSphere ()에 관해서는-프레임 중에 구 충돌체를 생성하고 그것이 접촉하는 것을 확인하는 메소드입니다. 코 루틴은 가짜 업데이트와 비슷하며 플레이어 PC에서 통화를 fps보다 적은 양으로 줄일 수 있습니다. Start ()-인스턴스가 생성 될 때 한 번만 호출되는 메서드입니다. 다른 모든 것은 다른 곳에 적용해야합니다. 다음 부분에서는 Unity에서 아무 것도 사용하지 않습니다. 스리 이것이 무언가를 분명히하기를 바랍니다.
솔직한 달 _Max_

아이디어를 이해하기 전에 Unity를 사용했습니다. 나는 코 루틴을 가지고있는 Lua를 사용하고 있으므로 번역이 상당히 잘되어야합니다.
user441521

이 답변은 Unity 태그가 없다는 점을 고려하면 너무 Unity에 고유 한 것으로 보입니다. 좀 더 일반적으로 만들고 화합의 예를 더 많이 만들면 훨씬 좋은 대답이 될 것입니다.
파라

@ CandidMoon 그래, 더 낫다.
파라

4

이 문제에 대한 총알은 없지만 다양한 접근 방식이 있으며, 거의 모두 '문제의 분리'원칙을 중심으로 이루어집니다. 다른 답변은 이미 널리 사용되는 구성 요소 기반 접근 방식에 대해 설명했지만 구성 요소 기반 솔루션 대신 또는 구성 요소 기반 솔루션과 함께 사용할 수있는 다른 접근 방식이 있습니다. 엔터티 컨트롤러 접근 방식은이 문제에 대한 선호 솔루션 중 하나이므로 논의 할 것입니다.

첫째, Player수업의 개념은 처음에 오도의 소지가 있습니다. 많은 사람들이 플레이어 캐릭터, NPC 캐릭터 및 몬스터 / 에미지를 다른 클래스로 생각하는 경향이 있습니다. 실제로 모든 사람들이 공통점이 많을 때 : 그들은 모두 화면에 그려지고 모두 움직입니다. 모두 재고 등이 있습니다

이러한 사고 방식은 플레이어 캐릭터, 비 플레이어 캐릭터 및 몬스터 / 에니 미가 모두 다르게 취급 Entity되지 않고 ' s' 로 취급되는 접근 방식으로 이어집니다 . 당연히, 그들은 다르게 행동해야합니다-플레이어 캐릭터는 입력을 통해 제어되어야하며 npcs는 ai가 필요합니다.

이것에 대한 해결책 Controller은을 제어하는 ​​데 사용되는 클래스 를 갖는 것 Entity입니다. 이렇게하면 모든 강력한 논리가 컨트롤러에 들어가고 모든 데이터와 공통성이 엔터티에 저장됩니다.

또한, 서브 클래스 ControllerInputController하고 AIController, 상기 플레이어를 효과적으로 제어 할 수있는 Entity방안에있다. 이 방법은 또한 네트워크 스트림의 명령을 통해 작동 하는 클래스 RemoteController또는 NetworkController클래스를 사용 하여 멀티 플레이어를 지원 합니다.

Controller조심하지 않으면 많은 논리가 하나로 합쳐질 수 있습니다. 이를 피하는 방법은을 Controller다른 것으로 구성 Controller하거나 Controller기능을의 다양한 속성에 의존 하게 만드는 것 Controller입니다. 예를 들어, AIControllerDecisionTree첨부 되어 있고 , a ( OnGround, 오름차순 및 내림차순 상태 의 상태 머신 포함) , 와 같은 PlayerCharacterController다양한 다른 요소로 구성 될 수 있습니다 . 이것의 추가 이점은 새로운 기능이 추가 될 때 새로운 기능을 추가 할 수 있다는 것입니다. 인벤토리 시스템없이 게임을 시작하고 게임을 추가하면 나중에 컨트롤러를 사용할 수 있습니다.ControllerMovementControllerJumpControllerInventoryUIControllerController


나는 이것의 아이디어를 좋아하지만 모든 코드를 컨트롤러 클래스로 전송하여 같은 문제가 발생하는 것으로 보입니다.
user441521

@ user441521 방금 추가 할 단락이 있다는 것을 깨달았지만 브라우저가 다운되면 잃어 버렸습니다. 지금 추가하겠습니다. 기본적으로 각기 다른 컨트롤러가 서로 다른 것을 처리 할 수 ​​있도록 서로 다른 컨트롤러를 통합 컨트롤러로 구성 할 수 있습니다. 예 : AggregateController.Controllers = {JumpController (키 바인드), MoveController (키 바인드), InventoryUIController (키 바인드, uisystem)}
Pharap
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.