플레이어와 월드 간의 순환 종속성을 피하는 방법은 무엇입니까?


60

위, 아래, 왼쪽 및 오른쪽으로 움직일 수있는 2D 게임을 만들고 있습니다. 본질적으로 두 가지 게임 로직 객체가 있습니다.

  • 플레이어 : 세계에 대한 입장
  • 월드 : 맵과 플레이어를 그립니다.

지금까지 월드플레이어에 의존합니다 (즉, 참조가 있습니다). 플레이어 캐릭터를 그릴 위치와 그릴 맵의 위치를 ​​알아 내기위한 위치가 필요합니다.

이제 플레이어가 벽을 통과 할 수 없도록 충돌 감지를 추가하고 싶습니다.

내가 생각할 수있는 가장 간단한 방법은 플레이어 가 의도 한 움직임이 가능한지 세계에 묻도록 하는 것입니다. 그러나 그것은 PlayerWorld 사이에 순환 의존성을 유발할 것입니다 (즉, 각각은 다른 것에 대한 참조를 보유합니다). 내가 생각해 낸 유일한 방법은 월드플레이어를 움직이게하는 것입니다.

최선의 선택은 무엇입니까? 아니면 가치가없는 순환 의존성을 피하고 있습니까?


4
순환 의존성이 왜 나쁜 것이라고 생각합니까? stackoverflow.com/questions/1897537/…
Fuhrmanator

@Fuhrmanator 나는 그것들이 일반적으로 나쁜 것이라고 생각하지 않지만, 코드를 소개하기 위해 코드에서 좀 더 복잡하게 만들어야합니다.
futlib

나는 우리의 작은 토론에 대한 게시물을 화나게했지만 새로운 것은 없다 : yannbane.com/2012/11/… ...
jcora

답변:


61

세상은 스스로를 그려서는 안됩니다. 렌더러는 월드를 그려야합니다. 플레이어는 스스로를 끌어 당기지 않아야합니다. 렌더러는 월드를 기준으로 플레이어를 그려야합니다.

플레이어는 월드에 충돌 감지를 요청해야합니다. 또는 충돌은 정적 세계뿐만 아니라 다른 액터에 대해서도 충돌 감지를 확인하는 별도의 클래스로 처리해야합니다.

전 세계가 플레이어를 전혀 인식하지 않아야한다고 생각합니다. 신의 대상이 아닌 저수준의 기본 체 여야합니다. 플레이어는 아마도 간접적으로 (충돌 감지 또는 대화 형 객체 확인 등) 일부 World 메서드를 호출해야 할 것입니다.


25
@ snake5- "can"과 "should"에는 차이가 있습니다. 무엇이든 그릴 수 있습니다. 그러나 드로잉을 다루는 코드를 변경해야하는 경우 드로잉하는 "Anything"을 검색하는 대신 "Renderer"클래스로 이동하는 것이 훨씬 쉽습니다. "구획화에 대한 집착"은 "응집력"의 또 다른 단어입니다.
Nate

16
@ Mr.Beast, 아니, 그는하지 않습니다. 그는 좋은 디자인을 옹호하고 있습니다. 한 번의 실수로 모든 것을 깨뜨리는 것은 의미가 없습니다.
jcora

23
우와, 나는 그것이 그런 반응을 일으킬 것이라고 생각하지 않았다 :) 나는 대답에 추가 할 것이 없지만, 왜 그것을 주 었는지 설명 할 수있다-그것이 더 간단하다고 생각하기 때문에. '적절한'또는 '올바른'이 아닙니다. 나는 그렇게 들리기를 원하지 않았다. 너무 많은 책임을 가진 클래스를 다루는 것을 발견하면 기존 코드를 읽을 수있게하는 것보다 분할 속도가 빠르기 때문에 나에게 더 간단합니다. 나는 이해할 수있는 덩어리로 된 코드를 좋아하고 @ futlib과 같은 문제에 대한 반응으로 리팩터링합니다.
Liosan

12
@ snake5 더 많은 클래스를 추가하면 프로그래머에게 오버 헤드가 추가된다고 말하는 것은 종종 내 경험에서 완전히 잘못되었습니다. 제 생각에는 유익한 이름과 잘 정의 된 책임을 가진 10x100 라인 클래스 는 단일 1000 라인 갓 클래스보다 프로그래머가 읽기 쉽고 오버 헤드적습니다 .
Martin

7
무엇을 그리는 것에 대한 메모로서, Renderer일종의 것이 필요하지만, 각각의 것들이 어떻게 렌더링 되는지에 대한 논리 가에 의해 처리되는 것은 아니며 Renderer, 그려야 할 각 것은 아마도 IDrawable또는 IRenderable(또는 사용중인 언어와 동등한 인터페이스). Renderer내가 생각하기에 세계는 그럴 수 있지만, 특히 이미 IRenderable그 자체 라면 그 책임을 넘어서는 것처럼 보인다 .
zzzzBov

35

일반적인 렌더링 엔진이 이러한 작업을 처리하는 방법은 다음과 같습니다.

공간 안에 물체가있는 위치와 물체가 그려지는 방법에는 근본적인 차이가 있습니다.

  1. 객체 그리기

    일반적으로이를 수행하는 렌더러 클래스가 있습니다. 단순히 객체 (Model)를 가져 와서 화면에 그립니다. drawSprite (Sprite), drawLine (..), drawModel (Model)과 같은 메소드가 필요할 때마다 사용할 수 있습니다. 렌더러이므로이 모든 작업을 수행해야합니다. 또한 아래에있는 모든 API를 사용하므로 OpenGL을 사용하는 렌더러와 DirectX를 사용하는 렌더러를 사용할 수 있습니다. 게임을 다른 플랫폼으로 이식하려면 새 렌더러를 작성하여 사용하십시오. "그것"은 쉽다.

  2. 객체 이동

    각 객체는 SceneNode 라고 부르는 것에 부착됩니다 . 당신은 구성을 통해 이것을 달성합니다. SceneNode에는 객체가 포함되어 있습니다. 그게 다야. SceneNode 란 무엇입니까? 객체의 모든 변형 (위치, 회전, 스케일) (일반적으로 다른 SceneNode 기준)을 실제 객체와 함께 포함하는 간단한 클래스입니다.

  3. 객체 관리

    SceneNode는 어떻게 관리됩니까? SceneManager를 통해 . 이 클래스는 장면의 모든 SceneNode를 만들고 추적합니다. 특정 SceneNode (일반적으로 "Player"또는 "Table"과 같은 문자열 이름으로 식별) 또는 모든 노드 목록을 요청할 수 있습니다.

  4. 세계를 그리기

    이것은 지금까지 분명합니다. 씬의 모든 SceneNode를 살펴보고 렌더러가 올바른 위치에 그리도록하십시오. 렌더러가 렌더링하기 전에 객체의 변형을 저장하도록하여 올바른 위치에 그릴 수 있습니다.

  5. 충돌 감지

    항상 사소한 것은 아닙니다. 일반적으로 공간의 특정 지점에있는 물체 또는 광선이 교차 할 물체에 대해 장면을 쿼리 할 수 ​​있습니다. 이런 식으로 플레이어의 움직임 방향으로 광선을 만들고 장면 관리자에게 광선이 가장 먼저 교차하는 물체를 물어볼 수 있습니다. 그런 다음 플레이어를 새로운 위치로 옮기거나, 더 작은 양만큼 움직이거나 (충돌하는 물체 옆에 놓기 위해) 움직이지 않도록 선택할 수 있습니다. 이러한 쿼리는 별도의 클래스에서 처리해야합니다. SceneManager에 SceneNode 목록을 요청해야하지만 SceneNode가 공간의 한 점을 덮는 지 또는 광선과 교차하는지 여부를 결정하는 것은 또 다른 작업입니다. SceneManager는 노드 만 작성하고 저장합니다.

그렇다면 플레이어는 무엇이며 세상은 무엇입니까?

Player는 SceneNode를 포함하는 클래스 일 수 있으며, 여기에는 렌더링 할 모델이 포함됩니다. 장면 노드의 위치를 ​​변경하여 플레이어를 이동시킵니다. 세계는 단순히 SceneManager의 인스턴스입니다. SceneNodes를 통해 모든 객체를 포함합니다. 장면의 현재 상태를 쿼리하여 충돌 감지를 처리합니다.

이는 대부분의 엔진에서 발생하는 일에 대한 완전하고 정확한 설명과는 거리가 먼데, 기본 사항을 이해하는 데 도움이되고 SOLID에 의해 밑줄이 표시된 OOP 원칙을 준수하는 것이 중요한 이유가됩니다 . 코드를 재구성하기가 너무 어렵거나 실제로 도움이되지 않는다는 생각에 자신을 사임하지 마십시오. 신중하게 코드를 디자인하면 앞으로 더 많은 것을 얻을 수 있습니다.


+1-나는 내 게임 시스템을 이런 식으로 만드는 것을 발견했고 그것이 매우 유연하다는 것을 알았습니다.
Cypher

+1, 좋은 답변입니다. 내 자신보다 더 구체적이고 요점.
jcora

+1, 나는이 답변에서 많은 것을 배웠고 심지어 영감을주는 결말을 가지고있었습니다. 감사합니다 @rootlocus
joslinm

16

왜 피하고 싶습니까? 재사용 가능한 클래스를 만들려면 순환 종속성을 피해야합니다. 그러나 플레이어는 재사용이 필요한 클래스가 아닙니다. 세계없이 플레이어를 사용하고 싶습니까? 아마 아닙니다.

클래스는 기능 모음에 지나지 않습니다. 문제는 기능을 어떻게 나누는가하는 것입니다. 당신이해야 할 일을하십시오. 순환 퇴폐가 필요한 경우에도 마찬가지입니다. (OOP 기능도 마찬가지입니다. 목적에 맞는 방식으로 코드를 작성하고 패러다임을 맹목적으로 따르지 마십시오.)

편집
질문에 대답하기 위해 콜백을 사용하여 플레이어가 충돌 확인을 위해 월드를 알아야한다는 것을 피할 수 있습니다.

World::checkForCollisions()
{
  [...]
  foreach(entityA in entityList)
    foreach(entityB in entityList)
      if([... entityA and entityB have collided ...])
         entityA.onCollision(entityB);
}

Player::onCollision(other)
{
  [... react on the collision ...]
}

엔터티의 속도를 노출하면 질문에 설명 된 물리의 종류를 세계에서 처리 할 수 ​​있습니다.

World::calculatePhysics()
{ 
  foreach(entityA in entityList)
    foreach(entityB in entityList)
    {
      [... move entityA according to its velocity as far as possible ...]
      if([... entityA has collided with the world ...])
         entityA.onWorldCollision();
      [... calculate the movement of entityB in order to know if A has collided with B ...]
      if([... entityA and entityB have collided ...])
         entityA.onCollision(entityB);
    }
}

그러나 조만간 세계에 대한 의존성이 필요할 것입니다. 즉, 세계의 기능이 필요할 때마다 가장 가까운 적의 위치를 ​​알고 싶습니까? 다음 난간이 얼마나 멀리 떨어져 있는지 알고 싶습니까? 의존성입니다.


4
+1 순환 종속성은 실제로 문제가되지 않습니다. 이 단계에서는 걱정할 이유가 없습니다. 게임이 성장하고 코드가 성숙 해지면 어쨌든 하위 클래스에서 해당 플레이어 및 월드 클래스를 리팩터링하고, 적절한 컴포넌트 기반 시스템, 입력 처리를위한 클래스, 렌더링 등을하는 것이 좋습니다. 시작, 문제 없습니다.
Laurent Couvidou

4
-1, 순환 의존성을 도입하지 않는 유일한 이유는 아닙니다. 그것들을 소개하지 않으면 시스템을 쉽게 확장하고 변경할 수 있습니다.
jcora

4
@Bane 그 접착제 없이는 아무것도 코딩 할 수 없습니다. 차이점은 얼마나 많은 간접 지정을 추가 하느냐입니다. Game-> World-> Entity 클래스가 있거나 Game-> World, SoundManager, InputManager, PhysicsEngine, ComponentManager 클래스가있는 경우 모든 (구문 적) 오버 헤드와 그로 인한 복잡성으로 인해 가독성이 떨어집니다. 그리고 한 시점에서 서로 상호 작용할 구성 요소가 필요합니다. 그리고 그것은 하나의 접착제 클래스가 많은 클래스로 나누어 진 모든 것보다 일을 더 쉽게 만드는 지점입니다.
API-Beast

3
아닙니다, 당신은 골대를 움직이고 있습니다. 물론 무언가 를 불러야 render(World)합니다. 논쟁은 모든 코드가 하나의 클래스 안에 포함되어야하는지 또는 코드가 논리 및 기능 단위로 나뉘어 야하는지 여부에 관한 것이며, 유지 관리, 확장 및 관리가 더 쉽습니다. BTW, 구성 요소 관리자, 물리 엔진 및 입력 관리자를 재사용하면 행운을 빕니다.
jcora

1
@Bane 새로운 클래스 인 btw를 도입하는 것보다 논리적 청크로 분할하는 다른 방법이 있습니다. 새 기능을 추가하거나 파일을 주석 블록으로 구분 된 여러 섹션으로 나눌 수도 있습니다. 단순하게 유지한다고해서 코드가 엉망이되는 것은 아닙니다.
API-Beast

13

현재 설계는 SOLID 설계 의 첫 번째 원칙에 위배되는 것 같습니다 .

"단일 책임 원칙"이라고하는이 첫 번째 원칙은 일반적으로 디자인에 항상 해를 끼치는 모 놀리 식의 모든 것을 생성하지 않기 위해 따라야 할 훌륭한 지침입니다.

구체화하기 위해 World객체는 게임 상태를 업데이트 및 유지하고 모든 것을 그리는 책임이 있습니다.

렌더링 코드가 변경되거나 변경되면 어떻게됩니까? 실제로 렌더링과 관련이없는 두 클래스를 모두 업데이트해야하는 이유는 무엇입니까? Liosan이 이미 말했듯이 Renderer.


이제 실제 질문에 대답하기 위해 ...

이를 수행하는 방법에는 여러 가지가 있으며, 이는 분리하는 한 가지 방법 일뿐입니다.

  1. 세계는 플레이어가 무엇인지 모른다.
    • Object그러나 플레이어가 있는의 목록이 있지만 플레이어 클래스에 의존하지 않습니다 (상속을 사용하여 달성하십시오).
  2. 플레이어는 일부에 의해 업데이트됩니다 InputManager.
  3. 세계는 움직임과 충돌 감지를 처리하여 적절한 물리적 변경 사항을 적용하고 객체에 업데이트를 보냅니다.
    • 예를 들어, 객체 A와 객체 B가 충돌하면 세계가이를 알리고 스스로 처리 할 수 ​​있습니다.
    • 세상은 여전히 ​​물리를 다룰 것입니다 (디자인이 그런 경우).
    • 그런 다음 두 객체 모두 충돌이 관심이 있는지 여부를 확인할 수 있습니다. 예를 들어, 물체 A가 플레이어이고 물체 B가 스파이크 인 경우 플레이어는 자신에게 피해를 줄 수 있습니다.
    • 그러나 이것은 다른 방법으로 해결할 수 있습니다.
  4. Renderer모든 개체를 그립니다.

세계는 플레이어가 무엇인지 모르지만 플레이어가 충돌하는 객체 중 하나 인 경우 플레이어의 속성을 알아야하는 충돌 감지를 처리한다고 말합니다.
Markus von Broady

상속, 세계는 일반적인 방식으로 설명 할 수있는 어떤 종류의 대상을 알고 있어야합니다. 문제는 월드가 플레이어에 대한 참조만을 가지고 있다는 것이 아니라 클래스로 의존 할 수 있다는 것입니다 (즉, health이 인스턴스 만 Player가지고 있는 필드를 사용합니다 ).
jcora

아, 당신은 세계가 플레이어를 참조하지 않는다는 것을 의미합니다. 필요한 경우 플레이어와 함께 ICollidable 인터페이스를 구현하는 객체 배열 만 있습니다.
Markus von Broady

2
+1 좋은 답변입니다. 그러나 "좋은 소프트웨어 디자인이 중요하지 않다고 말하는 모든 사람들을 무시하십시오". 흔한. 아무도 그렇게 말하지 않았다.
Laurent Couvidou

2
편집했습니다! 어쨌든 불필요 해 보였습니다 ...
jcora

1

플레이어는 충돌 감지와 같은 것들에 대해 세계에 문의해야합니다. 순환 의존성을 피하는 방법은 월드가 플레이어에 의존하지 않는 것입니다. 월드는 드로잉 자체의 위치를 ​​알아야합니다. 아마도 추적 할 엔티티에 대한 참조를 보유 할 수있는 Camera 오브젝트에 대한 참조를 통해 더 멀리 추상화되기를 원할 것입니다.

순환 참조의 관점에서 피하고 싶은 것은 서로에 대한 참조를 많이 보유하는 것이 아니라 코드에서 서로를 명시 적으로 참조하는 것입니다.


1

서로 다른 두 가지 유형의 객체가 서로를 요청할 수 있습니다. 메소드를 호출하기 위해 다른 것에 대한 참조를 보유해야하기 때문에 서로 의존합니다.

월드가 플레이어에게 요청함으로써 순환 의존성을 피할 수 있지만 플레이어는 월드에 요청할 수 없으며 그 반대도 마찬가지입니다. 이런 식으로 월드는 플레이어를 언급하지만 플레이어는 월드를 참조 할 필요는 없습니다. 혹은 그 반대로도. 그러나 이것은 세계가 플레이어에게 물어볼 것이 있는지 물어보고 다음 전화에서 알려야하기 때문에 문제를 해결하지 못할 것입니다 ...

따라서이 "문제"를 실제로 해결할 수 없으며 걱정할 필요가 없습니다. 최대한 바보 같은 디자인을 유지하십시오.


0

플레이어와 월드에 대한 세부 정보를 제거하면 두 객체 사이에 순환 종속성을 도입하지 않으려는 간단한 경우가 있습니다 (언어에 따라 중요하지 않을 수도 있지만 Fuhrmanator의 의견 링크를 참조하십시오). 이 문제와 비슷한 문제에 적용 할 수있는 매우 간단한 구조 솔루션이 두 가지 이상 있습니다.

1) 세계적 수준 싱글 톤 패턴을 소개하십시오 . 이를 통해 플레이어 (및 다른 모든 객체)는 값 비싼 검색이나 영구적으로 유지되는 링크없이 월드 객체를 쉽게 찾을 수 있습니다. 이 패턴의 요점은 클래스가 해당 클래스의 유일한 인스턴스에 대한 정적 참조를 가지고 있다는 것입니다.이 인스턴스는 객체의 인스턴스화에 따라 설정되고 삭제되면 지워집니다.

개발 언어와 원하는 복잡성에 따라이 클래스를 수퍼 클래스 또는 인터페이스로 쉽게 구현하고 프로젝트에서 둘 이상을 기대하지 않는 많은 주요 클래스에 재사용 할 수 있습니다.

2) 개발중인 언어가 지원하는 언어라면 (많은 언어), 약한 참조를 사용하십시오 . 이것은 가비지 콜렉션과 같은 것에 영향을 미치지 않는 참조입니다. 이 경우 정확히 유용합니다. 참조가 약한 객체가 여전히 존재하는지 여부를 가정하지 마십시오.

특별한 경우 플레이어는 세계에 대한 약한 참조를 보유 할 수 있습니다. (단일 톤과 마찬가지로) 이것의 장점은 각 프레임을 어떻게 든 월드 오브젝트를 검색 할 필요가 없거나 가비지 콜렉션과 같은 순환 참조의 영향을받는 프로세스를 방해 할 영구 참조를 가질 필요가 없다는 것입니다.


0

다른 사람이 말했듯이, 당신의 생각 World은에 노력하고있다 : 너무 많은 한 일을하고 경기 포함 Map(별개의 법인이어야 함) Renderer같은 시간에.

따라서 , 가능하면 라는 객체를 만들고 GameMap지도 수준 데이터를 저장합니다. 현재지도와 상호 작용하는 함수를 작성하십시오.

그런 다음 객체 필요 Renderer합니다. 당신은 할 수 이 있도록 Renderer객체 양쪽 물건 포함 GameMap 하고 Player(뿐만 아니라 Enemies), 또한 그것들을 그립니다.


-6

변수를 멤버로 추가하지 않으면 순환 종속성을 피할 수 있습니다. 플레이어 또는 이와 유사한 것에 정적 CurrentWorld () 함수를 사용하십시오. 월드에서 이미 구현 된 것과 다른 인터페이스를 발명하지 마십시오. 이것은 완전히 불필요합니다.

순환 참조로 인한 문제를 효과적으로 막기 위해 플레이어 객체를 파괴하기 전에 / 제거하는 동안 참조를 파괴 할 수도 있습니다.


1
난 너랑 같이있어. OOP가 너무 과대 평가되었습니다. 튜토리얼과 교육은 기본 제어 흐름을 학습 한 후 빠르게 OO로 넘어갑니다. OO 프로그램은 일반적으로 절차 코드보다 속도가 느립니다. 객체간에 관료주의가 있기 때문에 포인터 액세스가 많기 때문에 캐시 누락이 발생할 수 있습니다. 게임은 작동하지만 매우 느립니다. 캐시 누락을 피하기 위해 모든 것에 대해 일반 전역 배열과 수동으로 최적화 된 미세 조정 기능을 사용하는 매우 빠르고 기능이 풍부한 실제 게임입니다. 성능이 10 배 증가 할 수 있습니다.
Calmarius
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.