Làm thế nào để tránh phụ thuộc vòng tròn giữa Người chơi và Thế giới?


60

Tôi đang làm việc trên một trò chơi 2D nơi bạn có thể di chuyển lên, xuống, sang trái và phải. Về cơ bản tôi có hai đối tượng logic trò chơi:

  • Người chơi: Có một vị trí liên quan đến thế giới
  • Thế giới: Vẽ bản đồ và người chơi

Cho đến nay, World phụ thuộc vào Người chơi (tức là có tham chiếu đến nó), cần vị trí của nó để tìm ra nơi để vẽ nhân vật người chơi và phần nào của bản đồ sẽ vẽ.

Bây giờ tôi muốn thêm phát hiện va chạm để người chơi không thể di chuyển qua các bức tường.

Cách đơn giản nhất tôi có thể nghĩ đến là để Người chơi hỏi Thế giới nếu chuyển động dự định là có thể. Nhưng điều đó sẽ giới thiệu một sự phụ thuộc vòng tròn giữa Người chơiThế giới (tức là mỗi người giữ một tham chiếu đến người khác), điều này có vẻ đáng để tránh. Cách duy nhất tôi nghĩ ra là để Thế giới di chuyển Người chơi , nhưng tôi thấy điều đó không trực quan.

lựa chọn tốt nhất của tôi là gì? Hoặc là tránh một phụ thuộc tròn không đáng?


4
Tại sao bạn nghĩ rằng một phụ thuộc tròn là một điều xấu? stackoverflow.com/questions/1897537/
hy

@Fuhrmanator Tôi không nghĩ chúng nói chung là một điều xấu, nhưng tôi phải làm cho mọi thứ phức tạp hơn một chút trong mã của mình để giới thiệu một thứ.
futlib

Tôi tức giận một bài viết về cuộc thảo luận nhỏ của chúng tôi, mặc dù không có gì mới: yannbane.com/2012/11/ Quy ...
jcora

Câu trả lời:


61

Thế giới không nên tự vẽ; Trình kết xuất sẽ vẽ Thế giới. Người chơi không nên tự vẽ; Trình kết xuất sẽ thu hút Người chơi so với Thế giới.

Người chơi nên hỏi Thế giới về phát hiện va chạm; hoặc có lẽ các vụ va chạm phải được xử lý bởi một lớp riêng biệt để kiểm tra phát hiện va chạm không chỉ đối với thế giới tĩnh mà còn đối với các tác nhân khác.

Tôi nghĩ rằng Thế giới có lẽ không nên biết về Người chơi; nó phải là một nguyên thủy cấp thấp không phải là một vật thể. Người chơi có thể sẽ cần phải gọi một số phương thức Thế giới, có thể gián tiếp (phát hiện va chạm hoặc kiểm tra các đối tượng tương tác, v.v.).


25
@ Snake5 - Có sự khác biệt giữa "có thể" và "nên". Bất cứ điều gì cũng có thể vẽ bất cứ điều gì - nhưng khi bạn cần thay đổi mã liên quan đến bản vẽ, việc đi đến lớp "Trình kết xuất" sẽ dễ dàng hơn nhiều so với tìm kiếm "Bất cứ điều gì" đang vẽ. "Nỗi ám ảnh về sự ngăn cách" là một từ khác của "sự gắn kết".
Nate

16
@ Mr.Beast, không, anh ấy không. Anh ấy ủng hộ thiết kế tốt. Việc nhồi nhét mọi thứ trong một sai lầm của một lớp học không có ý nghĩa gì.
jcora

23
Whoa, tôi không nghĩ nó sẽ gây ra phản ứng như vậy :) Tôi không có gì để thêm vào câu trả lời, nhưng tôi có thể giải thích tại sao tôi lại đưa ra - vì tôi nghĩ nó đơn giản hơn. Không phải "đúng" hay "đúng". Tôi không muốn nó phát ra âm thanh đó. Nó đơn giản hơn đối với tôi bởi vì nếu tôi thấy mình giải quyết các lớp có quá nhiều trách nhiệm, thì việc phân tách sẽ nhanh hơn việc buộc mã hiện tại có thể đọc được. Tôi thích mã trong các đoạn mà tôi có thể hiểu và tái cấu trúc để phản ứng với các vấn đề như cái mà @futlib đang gặp phải.
Liosan

12
@ Snake5 Nói thêm nhiều lớp thêm chi phí cho lập trình viên thường là hoàn toàn sai trong kinh nghiệm của tôi. Theo ý kiến ​​của tôi, các lớp dòng 10x100 với tên thông tin và trách nhiệm được xác định rõ ràng sẽ dễ đọc hơnít chi phí hơn cho lập trình viên so với một lớp thần 1000 dòng duy nhất.
Martin

7
Như một lưu ý về những gì rút ra cái gì, một Rendererloại nào đó là cần thiết, nhưng điều đó không có nghĩa là logic cho cách mỗi thứ được hiển thị được xử lý bởi Renderer, mỗi thứ cần được rút ra có lẽ nên kế thừa từ một giao diện chung như IDrawablehoặc IRenderable(hoặc giao diện tương đương với bất kỳ ngôn ngữ nào bạn đang sử dụng). Thế giới có thể là Renderer, tôi cho rằng, nhưng dường như nó sẽ vượt qua trách nhiệm của mình, đặc biệt nếu nó đã là một IRenderablechính nó.
zzzzBov

35

Đây là cách một công cụ kết xuất điển hình xử lý những điều này:

Có một sự khác biệt cơ bản giữa nơi một vật thể ở trong không gian và cách vật thể được vẽ.

  1. Vẽ một đối tượng

    Bạn thường có một lớp Renderer thực hiện điều này. Nó chỉ đơn giản là lấy một đối tượng (Model) và vẽ trên màn hình. Nó có thể có các phương thức như drawSprite (Sprite), drawLine (..), drawModel (Model), bất cứ điều gì bạn cảm thấy cần. Đó là một Trình kết xuất để nó phải làm tất cả những điều này. Nó cũng sử dụng bất kỳ API nào bạn có bên dưới để bạn có thể có một trình kết xuất sử dụng OpenGL và một API sử dụng DirectX. Nếu bạn muốn chuyển trò chơi của mình sang nền tảng khác, bạn chỉ cần viết một trình kết xuất mới và sử dụng trình kết xuất đó. Nó là dễ dàng.

  2. Di chuyển một đối tượng

    Mỗi đối tượng được gắn vào một cái gì đó mà chúng tôi muốn gọi là một SceneNode . Bạn đạt được điều này thông qua thành phần. Một SceneNode chứa một đối tượng. Đó là nó. CảnhNode là gì? Đó là một lớp đơn giản bao gồm tất cả các phép biến đổi (vị trí, góc quay, tỷ lệ) của một đối tượng (thường liên quan đến một SceneNode khác) cùng với đối tượng thực tế.

  3. Quản lý các đối tượng

    Cảnh được quản lý như thế nào? Thông qua một Trình quản lý cảnh . Lớp này tạo và theo dõi mọi SceneNode trong cảnh của bạn. Bạn có thể yêu cầu nó cho một SceneNode cụ thể (thường được xác định bởi một tên chuỗi như "Trình phát" hoặc "Bảng") hoặc danh sách tất cả các nút.

  4. Vẽ thế giới

    Điều này là khá rõ ràng bây giờ. Đơn giản chỉ cần đi bộ qua mọi SceneNode trong cảnh và Trình kết xuất vẽ nó ở đúng nơi. Bạn có thể vẽ nó ở đúng nơi bằng cách trình kết xuất lưu trữ các biến đổi của một đối tượng trước khi kết xuất nó.

  5. Phát hiện va chạm

    Điều này không phải lúc nào cũng tầm thường. Thông thường bạn có thể truy vấn cảnh về vật thể nào ở một điểm nhất định trong không gian hoặc vật thể nào sẽ chiếu tia. Bằng cách này, bạn có thể tạo một tia từ trình phát của mình theo hướng chuyển động và hỏi người quản lý cảnh xem đối tượng đầu tiên mà tia đó giao nhau là gì. Sau đó, bạn có thể chọn di chuyển người chơi đến vị trí mới, di chuyển anh ta bằng một lượng nhỏ hơn (để đưa anh ta bên cạnh đối tượng va chạm) hoặc không di chuyển anh ta chút nào. Hãy chắc chắn để có các truy vấn này được xử lý bởi các lớp riêng biệt. Họ nên hỏi Trình quản lý cảnh để biết danh sách các Cảnh, nhưng đó là một nhiệm vụ khác để xác định xem Cảnh đó có bao phủ một điểm trong không gian hay giao nhau với một tia không. Hãy nhớ rằng Trình quản lý cảnh chỉ tạo và lưu trữ các nút.

Vậy, người chơi là gì, và thế giới là gì?

Trình phát có thể là một lớp có chứa CảnhNode, trong đó lần lượt chứa mô hình được hiển thị. Bạn di chuyển trình phát bằng cách thay đổi vị trí của nút cảnh. Thế giới chỉ đơn giản là một ví dụ của Trình quản lý cảnh. Nó chứa tất cả các đối tượng (thông qua SceneNodes). Bạn xử lý phát hiện va chạm bằng cách thực hiện các truy vấn về trạng thái hiện tại của cảnh.

Đây không phải là một mô tả đầy đủ hoặc chính xác về những gì xảy ra trong hầu hết các động cơ, nhưng nó sẽ giúp bạn hiểu các nguyên tắc cơ bản và tại sao điều quan trọng là phải tôn trọng các nguyên tắc OOP được gạch dưới bởi RẮN . Đừng từ bỏ ý tưởng rằng quá khó để cơ cấu lại mã của bạn hoặc nó sẽ không thực sự giúp bạn. Bạn sẽ giành được nhiều hơn nữa trong tương lai bằng cách cẩn thận thiết kế mã của bạn.


+1 - Tôi đã thấy mình xây dựng các hệ thống trò chơi của mình giống như thế này và thấy nó khá linh hoạt.
Cypher

+1, câu trả lời tuyệt vời. Cụ thể hơn và đến điểm hơn của riêng tôi.
jcora

+1, tôi đã học được rất nhiều từ câu trả lời này và nó thậm chí còn có một kết thúc đầy cảm hứng. Cảm ơn @rootl Focus
joslinm

16

Tại sao bạn muốn tránh điều đó? Phụ thuộc tròn nên tránh nếu bạn muốn tạo một lớp có thể tái sử dụng. Nhưng Người chơi không có lớp nào cần phải sử dụng lại cả. Bạn có bao giờ muốn sử dụng Trình phát mà không có thế giới không? Chắc là không.

Hãy nhớ rằng các lớp không có gì nhiều hơn các bộ sưu tập chức năng. Câu hỏi chỉ là làm thế nào để phân chia chức năng. Làm bất cứ điều gì bạn cần làm. Nếu bạn cần một suy đồi tròn, thì hãy là nó. (Nhân tiện cũng vậy đối với bất kỳ tính năng OOP nào. Mã hóa mọi thứ theo cách nó phục vụ mục đích, đừng chỉ theo mô hình một cách mù quáng.)

Chỉnh sửa
Được rồi, để trả lời câu hỏi: bạn có thể tránh rằng Người chơi cần biết Thế giới để kiểm tra va chạm bằng cách sử dụng các cuộc gọi lại:

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 ...]
}

Loại vật lý mà bạn đã mô tả trong câu hỏi có thể được xử lý bởi thế giới nếu bạn phơi bày vận tốc của các thực thể:

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);
    }
}

Tuy nhiên lưu ý rằng bạn có thể sẽ cần một sự phụ thuộc vào thế giới sớm hay muộn, đó là bất cứ khi nào bạn cần chức năng của Thế giới: bạn muốn biết kẻ thù gần nhất ở đâu? Bạn muốn biết các gờ tiếp theo là bao xa? Đó là phụ thuộc.


4
+1 Phụ thuộc thông tư không thực sự là một vấn đề ở đây. Ở giai đoạn này, không có lý do để lo lắng về điều đó. Nếu trò chơi phát triển và mã đáo hạn, có lẽ bạn nên cấu trúc lại các lớp Người chơi và Thế giới đó trong các lớp con, có một hệ thống dựa trên thành phần thích hợp, các lớp để xử lý đầu vào, có thể là Kết xuất, v.v. một khởi đầu, không vấn đề gì
Laurent Couvidou

4
-1, đó chắc chắn không phải là lý do duy nhất để không đưa ra các phụ thuộc vòng tròn. Bằng cách không giới thiệu chúng, bạn làm cho hệ thống của bạn dễ dàng mở rộng và thay đổi hơn.
jcora

4
@Bane Bạn không thể mã bất cứ điều gì mà không có keo đó. Sự khác biệt chỉ là số lượng bạn thêm vào. Nếu bạn có các lớp Trò chơi -> Thế giới -> Thực thể hoặc nếu bạn có các lớp Trò chơi -> Thế giới, SoundManager, InputManager, Vật lý học, Thành phần quản lý. Nó làm cho mọi thứ ít đọc hơn bởi vì tất cả các chi phí (cú pháp) và bởi sự phức tạp ngụ ý đó. Và tại một thời điểm, bạn sẽ cần các thành phần để tương tác với nhau. Và đó là điểm mà một lớp keo làm cho mọi thứ dễ dàng hơn mọi thứ được phân chia giữa nhiều lớp.
API-Beast

3
Không, bạn đang di chuyển các cột gôn. Tất nhiên một cái gì đó phải gọi render(World). Cuộc tranh luận xoay quanh việc liệu tất cả các mã nên được nhồi nhét bên trong một lớp hay liệu mã có nên được chia thành các đơn vị logic và chức năng, sau đó dễ dàng hơn để duy trì, mở rộng và quản lý. BTW, chúc may mắn sử dụng lại các trình quản lý thành phần, động cơ vật lý và trình quản lý đầu vào, tất cả đều không phân biệt và kết hợp hoàn toàn.
jcora

1
@Bane Có nhiều cách khác để chia mọi thứ thành các phần logic hơn là giới thiệu các lớp mới, btw. Bạn cũng có thể thêm các chức năng mới hoặc chia các tệp của mình thành nhiều phần được phân tách bằng các khối nhận xét. Chỉ cần giữ nó đơn giản không có nghĩa là mã sẽ là một mớ hỗn độn.
API-Beast

13

Thiết kế hiện tại của bạn dường như đi ngược lại nguyên tắc đầu tiên của thiết kế RẮN .

Nguyên tắc đầu tiên này, được gọi là "nguyên tắc trách nhiệm duy nhất", nói chung là một hướng dẫn tốt để tuân theo để không tạo ra các đối tượng nguyên khối, làm mọi thứ sẽ luôn làm tổn thương thiết kế của bạn.

Để cụ thể hóa, Worldđối tượng của bạn chịu trách nhiệm cả về cập nhật và giữ trạng thái trò chơi và vẽ mọi thứ.

Điều gì nếu mã kết xuất của bạn thay đổi / phải thay đổi? Tại sao bạn phải cập nhật cả hai lớp thực sự không liên quan gì đến kết xuất? Như Liosan đã nói, bạn nên có một Renderer.


Bây giờ, để trả lời câu hỏi thực tế của bạn ...

Có nhiều cách để làm điều này, và đây chỉ là một cách để tách rời:

  1. Thế giới không biết người chơi là gì.
    • ObjectTuy nhiên, nó có một danh sách các vị trí mà người chơi được đặt, nhưng nó không phụ thuộc vào lớp người chơi (sử dụng tính kế thừa để đạt được điều này).
  2. Người chơi được cập nhật bởi một số InputManager.
  3. Thế giới xử lý phát hiện chuyển động và va chạm, áp dụng các thay đổi vật lý phù hợp và gửi cập nhật cho các đối tượng.
    • Ví dụ, nếu đối tượng A và đối tượng B va chạm, thế giới sẽ thông báo cho họ và sau đó họ có thể tự xử lý nó.
    • Thế giới vẫn sẽ xử lý vật lý (nếu thiết kế của bạn là như vậy).
    • Sau đó, cả hai đối tượng có thể thấy liệu vụ va chạm có làm họ quan tâm hay không. Ví dụ, nếu đối tượng A là người chơi và đối tượng B là một đột biến, thì người chơi có thể tự gây sát thương.
    • Điều này có thể được giải quyết theo những cách khác, mặc dù.
  4. Các Rendererthu hút tất cả các đối tượng.

Bạn nói rằng thế giới không biết người chơi là gì, nhưng nó xử lý phát hiện va chạm có thể cần biết thuộc tính của người chơi, nếu đó là một trong những vật thể va chạm.
Markus von Broady

Kế thừa, thế giới phải nhận thức được một số loại đối tượng, có thể được mô tả một cách tổng quát. Vấn đề không nằm ở chỗ thế giới chỉ có một tài liệu tham khảo cho người chơi, mà nó có thể phụ thuộc vào nó như một lớp (tức là sử dụng các trường healthmà chỉ có trường hợp Playernày mới có).
jcora

À, ý bạn là thế giới không có tham chiếu đến người chơi, nó chỉ có một loạt các đối tượng thực hiện giao diện ICollidable, cùng với người chơi nếu cần.
Markus von Broady

2
+1 Câu trả lời hay. Nhưng: "xin vui lòng bỏ qua tất cả những người nói rằng thiết kế phần mềm tốt không quan trọng". Chung. Không ai nói vậy.
Laurent Couvidou

2
Đã chỉnh sửa! Dường như nó không cần thiết ...
jcora

1

Người chơi nên hỏi Thế giới về những thứ như phát hiện va chạm. Cách để tránh sự phụ thuộc vòng tròn là không để Thế giới có sự phụ thuộc vào Người chơi. Thế giới cần biết chính nó đang vẽ ở đâu: bạn có thể muốn nó được trừu tượng hóa hơn nữa, có lẽ với một tham chiếu đến một đối tượng Máy ảnh có thể lần lượt giữ một tham chiếu đến một Thực thể nào đó để theo dõi.

Những gì bạn muốn tránh về các tham chiếu vòng tròn không phải là quá nhiều giữ các tham chiếu cho nhau, mà là đề cập đến nhau một cách rõ ràng trong mã.


1

Bất cứ khi nào hai loại đối tượng khác nhau có thể hỏi nhau. Chúng sẽ phụ thuộc vào nhau vì chúng cần giữ một tham chiếu đến cái khác để gọi các phương thức của nó.

Bạn có thể tránh sự phụ thuộc vòng tròn bằng cách yêu cầu Thế giới hỏi Người chơi, nhưng Người chơi không thể hỏi Thế giới hoặc ngược lại. Theo cách này, Thế giới có các tham chiếu đến Người chơi nhưng người chơi không cần tham khảo Thế giới. Hoặc ngược lại. Nhưng điều này sẽ không giải quyết được vấn đề, bởi vì Thế giới sẽ cần hỏi người chơi xem họ có gì muốn hỏi không, và nói với họ trong cuộc gọi tiếp theo ...

Vì vậy, bạn không thể thực sự giải quyết "vấn đề" này và tôi nghĩ không cần phải lo lắng về điều đó. Giữ thiết kế ngu ngốc đơn giản miễn là bạn có thể.


0

Tước ra các chi tiết về người chơi và thế giới, bạn có một trường hợp đơn giản là không muốn đưa ra sự phụ thuộc vòng tròn giữa hai đối tượng (tùy thuộc vào ngôn ngữ của bạn có thể không quan trọng, hãy xem liên kết trong nhận xét của Fuhrmanator). Có ít nhất hai giải pháp cấu trúc rất đơn giản sẽ áp dụng cho vấn đề này và các vấn đề tương tự:

1) Giới thiệu mẫu singleton vào đẳng cấp thế giới của bạn . Điều này sẽ cho phép người chơi (và mọi đối tượng khác) dễ dàng tìm thấy đối tượng thế giới mà không cần tìm kiếm đắt tiền hoặc liên kết được giữ vĩnh viễn. Ý chính của mẫu này chỉ là lớp có một tham chiếu tĩnh đến thể hiện duy nhất của lớp đó, được đặt theo khởi tạo của đối tượng và bị xóa khi xóa.

Tùy thuộc vào ngôn ngữ phát triển của bạn và mức độ phức tạp mà bạn muốn, bạn có thể dễ dàng thực hiện điều này như một siêu lớp hoặc giao diện và sử dụng lại nó cho nhiều lớp chính mà bạn không mong muốn có nhiều hơn một trong dự án của mình.

2) Nếu ngôn ngữ bạn đang phát triển hỗ trợ ngôn ngữ đó (nhiều người làm), hãy sử dụng Tham chiếu yếu . Đây là một tài liệu tham khảo không ảnh hưởng đến những thứ như bộ sưu tập rác. Nó rất hữu ích trong chính xác những trường hợp này, chỉ cần đảm bảo không đưa ra bất kỳ giả định nào về việc liệu đối tượng bạn tham chiếu yếu có còn tồn tại hay không.

Trong trường hợp cụ thể của bạn, (các) Người chơi của bạn có thể có một tài liệu tham khảo yếu về thế giới. Lợi ích của việc này (như với singleton) là bạn không cần phải tìm kiếm đối tượng thế giới bằng cách nào đó từng khung hoặc có một tham chiếu vĩnh viễn sẽ cản trở các quá trình bị ảnh hưởng bởi các tham chiếu vòng tròn như bộ sưu tập rác.


0

Khi những người khác đã nói, tôi nghĩ rằng bạn Worldđang làm một điều quá nhiều: nó đang cố gắng để cả hai có chứa các trò chơi Map(mà phải là một thực thể riêng biệt) là một Renderercùng một lúc.

Vì vậy, tạo một đối tượng mới (được gọi là GameMapcó thể) và lưu trữ dữ liệu mức bản đồ trong đó. Viết các hàm trong đó tương tác với bản đồ hiện tại.

Sau đó, bạn cũng cần một Rendererđối tượng. Bạn có thể làm cho Rendererđối tượng này là thứ chứa cả GameMapPlayer(cũng như Enemies), và cũng vẽ chúng.


-6

Bạn có thể tránh phụ thuộc vòng tròn bằng cách không thêm các biến làm thành viên. Sử dụng hàm CurrentWorld () tĩnh cho trình phát hoặc một cái gì đó tương tự. Mặc dù vậy, không phát minh ra một giao diện khác với giao diện được triển khai trên Thế giới, điều này hoàn toàn không cần thiết.

Cũng có thể hủy tham chiếu trước / trong khi phá hủy đối tượng người chơi để ngăn chặn hiệu quả các sự cố do tham chiếu vòng tròn gây ra.


1
Tôi với bạn OOP được đánh giá quá cao. Hướng dẫn và giáo dục nhanh chóng nhảy vào OO sau khi học các công cụ điều khiển cơ bản. Các chương trình OO thường chậm hơn mã thủ tục, bởi vì có sự quan liêu giữa các đối tượng của bạn, bạn có rất nhiều truy cập con trỏ, điều này gây ra lỗi shitload của bộ nhớ cache. Trò chơi của bạn hoạt động nhưng rất chậm. Các trò chơi thực sự, rất nhanh và có tính năng phong phú sử dụng các mảng toàn cầu đơn giản và các chức năng được điều chỉnh tốt, được tối ưu hóa cho mọi thứ để tránh bỏ lỡ bộ nhớ cache. Mà có thể dẫn đến tăng gấp mười lần hiệu suất.
Calmarius
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.