Làm thế nào tôi có thể có các đối tượng tương tác và giao tiếp với nhau mà không buộc một hệ thống phân cấp?


9

Tôi hy vọng những lời huyên thuyên này sẽ làm cho câu hỏi của tôi rõ ràng - tuy nhiên tôi hoàn toàn hiểu nếu họ sẽ không, vì vậy, hãy cho tôi biết nếu đó là trường hợp, và tôi sẽ cố gắng làm cho mình rõ ràng hơn.

Gặp gỡ BoxPong , một trò chơi rất đơn giản mà tôi đã thực hiện để làm quen với việc phát triển trò chơi hướng đối tượng. Kéo hộp để kiểm soát bóng và thu thập những thứ màu vàng.
Làm BoxPong giúp tôi hình thành, trong số những điều khác, một câu hỏi cơ bản: làm thế nào tôi có thể có các đối tượng tương tác với nhau mà không phải "thuộc về" nhau? Nói cách khác, có cách nào để các đối tượng không được phân cấp, mà thay vào đó cùng tồn tại? (Tôi sẽ đi sâu vào chi tiết bên dưới.)

Tôi nghi ngờ vấn đề của các đối tượng cùng tồn tại là một vấn đề phổ biến, vì vậy tôi hy vọng có một cách được thiết lập để giải quyết nó. Tôi không muốn phát minh lại bánh xe vuông, vì vậy tôi đoán câu trả lời lý tưởng mà tôi đang tìm kiếm là "đây là một mẫu thiết kế thường được sử dụng để giải quyết vấn đề của bạn."

Đặc biệt là trong các trò chơi đơn giản như BoxPong, rõ ràng là có hoặc nên có một số đối tượng cùng tồn tại ở cùng cấp độ. Có một cái hộp, có một quả bóng, có một cái sưu tập. Mặc dù vậy, tất cả những gì tôi có thể diễn đạt bằng các ngôn ngữ hướng đối tượng - hoặc có vẻ như vậy - là các mối quan hệ HAS-A nghiêm ngặt . Điều đó được thực hiện thông qua các biến thành viên. Tôi không thể bắt đầu ballvà để nó làm việc của mình, tôi cần nó vĩnh viễn thuộc về một đối tượng khác. Tôi đã thiết lập nó để đối tượng trò chơi chính một hộp và hộp lần lượt một quả bóng và một bộ đếm điểm. Mỗi đối tượng cũng có mộtupdate()Phương thức tính toán vị trí, hướng, v.v. và tôi đi theo một cách tương tự ở đó: Tôi gọi phương thức cập nhật của đối tượng trò chơi chính, gọi phương thức cập nhật của tất cả các con của nó và chúng lần lượt gọi các phương thức cập nhật của tất cả các con của chúng . Đây là cách duy nhất tôi có thể thấy để tạo ra một trò chơi hướng đối tượng, nhưng tôi cảm thấy đó không phải là cách lý tưởng. Rốt cuộc, tôi sẽ không nghĩ chính xác quả bóng thuộc về chiếc hộp, mà là ở cùng cấp độ và tương tác với nó. Tôi cho rằng điều đó có thể đạt được bằng cách biến tất cả các đối tượng trò chơi thành các biến thành viên của đối tượng trò chơi chính, nhưng tôi không thấy việc giải quyết bất cứ điều gì. Ý tôi là ... bỏ qua sự lộn xộn rõ ràng, làm thế nào để bóng và hộp có thể biết nhau , nghĩa là tương tác?

Ngoài ra còn có vấn đề về các đối tượng cần truyền thông tin lẫn nhau. Tôi có khá nhiều kinh nghiệm viết mã cho SNES, nơi bạn có quyền truy cập vào thực tế toàn bộ RAM mọi lúc. Giả sử bạn đang tạo một kẻ thù tùy chỉnh cho Super Mario World và bạn muốn nó xóa tất cả các đồng tiền của Mario, sau đó chỉ cần lưu trữ số 0 để giải quyết $ 0DBF, không vấn đề gì. Không có giới hạn cho biết kẻ thù không thể truy cập trạng thái của người chơi. Tôi đoán rằng tôi đã bị phá hỏng bởi sự tự do này, bởi vì với C ++ và tương tự tôi thường thấy mình tự hỏi làm thế nào để tạo ra một giá trị có thể truy cập được đối với một số đối tượng khác (hoặc thậm chí toàn cầu).
Sử dụng ví dụ về BoxPong, nếu tôi muốn bóng bật ra khỏi các cạnh của màn hình thì sao? widthheightlà các thuộc tính của Gamelớp,ballđể có quyền truy cập vào chúng. Tôi có thể chuyển các loại giá trị này (thông qua các nhà xây dựng hoặc các phương thức cần thiết), nhưng điều đó chỉ làm tôi khó chịu.

Tôi đoán vấn đề chính của tôi là tôi cần các đối tượng để biết nhau, nhưng cách duy nhất tôi có thể thấy để làm điều đó là hệ thống phân cấp chặt chẽ, xấu xí và không thực tế.

Tôi đã nghe nói về "các lớp bạn bè" trên C ++ và có thể biết chúng hoạt động như thế nào, nhưng nếu chúng là giải pháp cuối cùng, thì tại sao tôi không thấy friendcác từ khóa đổ ra khắp mọi dự án C ++ và làm thế nào khái niệm không tồn tại trong mọi ngôn ngữ OOP? (Điều tương tự cũng xảy ra với các con trỏ hàm, mà tôi mới biết gần đây.)

Cảm ơn trước câu trả lời dưới bất kỳ hình thức nào - và một lần nữa, nếu có một phần không có ý nghĩa với bạn, hãy cho tôi biết.


2
Phần lớn ngành công nghiệp trò chơi đã chuyển sang kiến ​​trúc Entity-Element-System và các biến thể của nó. Đó là một suy nghĩ khác với các cách tiếp cận OO truyền thống nhưng nó hoạt động tốt và có ý nghĩa một khi khái niệm này chìm vào. Unity sử dụng nó. Trên thực tế, Unity chỉ sử dụng phần Thực thể-Thành phần nhưng dựa trên ECS.
Dunk

Vấn đề cho phép các lớp cộng tác với nhau mà không có kiến ​​thức về nhau được giải quyết bằng mẫu thiết kế của Người hòa giải. Bạn đã nhìn nó chưa?
Fuhrmanator

Câu trả lời:


13

Nói chung, nó trở nên rất tệ nếu các đối tượng cùng cấp biết về nhau. Một khi các đối tượng biết về nhau, chúng bị trói hoặc ghép với nhau. Điều này khiến chúng khó thay đổi, khó kiểm tra, khó bảo trì.

Nó hoạt động tốt hơn nhiều nếu có một số đối tượng "ở trên" biết về hai người và có thể thiết lập các tương tác giữa chúng. Đối tượng biết về hai đồng nghiệp có thể gắn kết chúng lại với nhau thông qua việc tiêm phụ thuộc hoặc thông qua các sự kiện hoặc thông qua tin nhắn (hoặc bất kỳ cơ chế tách rời nào khác). Vâng, điều đó dẫn đến một chút thứ bậc nhân tạo, nhưng nó tốt hơn nhiều so với mớ hỗn độn spaghetti mà bạn nhận được khi mọi thứ chỉ tương tác với nhau. Điều đó chỉ quan trọng hơn trong C ++, vì bạn cũng cần một cái gì đó để sở hữu vòng đời của các đối tượng.

Vì vậy, trong ngắn hạn, bạn có thể làm điều đó bằng cách chỉ có các đối tượng cạnh nhau ở mọi nơi gắn liền với nhau bằng cách truy cập ad hoc, nhưng đó là một ý tưởng tồi. Hệ thống phân cấp cung cấp trật tự và quyền sở hữu rõ ràng. Điều chính cần nhớ là các đối tượng trong mã không nhất thiết phải là đối tượng trong cuộc sống thực (hoặc thậm chí là trò chơi). Nếu các đối tượng trong trò chơi không tạo ra một hệ thống phân cấp tốt, một sự trừu tượng khác nhau có thể tốt hơn.


2

Sử dụng ví dụ về BoxPong, nếu tôi muốn bóng bật ra khỏi các cạnh của màn hình thì sao? chiều rộng và chiều cao là thuộc tính của lớp Trò chơi và tôi sẽ cần bóng để có quyền truy cập vào chúng.

Không!

Tôi nghĩ vấn đề chính mà bạn đang gặp phải là bạn đang sử dụng "Lập trình hướng đối tượng" theo đúng nghĩa đen. Trong OOP, một đối tượng không đại diện cho "vật" mà là "ý tưởng" có nghĩa là "Quả bóng", "Trò chơi", "Vật lý", "Toán học", "Ngày", v.v. Tất cả đều là các đối tượng hợp lệ. Cũng không có yêu cầu cho các đối tượng "biết" về bất cứ điều gì. Ví dụ: Date.Now().getTommorrow()Sẽ hỏi máy tính hôm nay là ngày gì, áp dụng quy tắc ngày phức tạp để tìm ra ngày mai và trả lại cho người gọi. Đối Datetượng không biết về bất cứ điều gì khác, nó chỉ cần yêu cầu thông tin khi cần thiết từ hệ thống. Ngoài ra, Math.SquareRoot(number)không cần biết gì ngoài logic về cách tính căn bậc hai.

Vì vậy, trong ví dụ của bạn tôi đã trích dẫn, "Quả bóng" không nên biết gì về "Chiếc hộp". Hộp và bóng là những ý tưởng hoàn toàn khác nhau, và không có quyền nói chuyện với nhau. Nhưng một cỗ máy Vật lý biết Hộp và Bóng là gì (hoặc ít nhất là ThreeDShape), và nó biết chúng ở đâu, và điều gì sẽ xảy ra với chúng. Vì vậy, nếu quả bóng co lại vì trời lạnh, Công cụ Vật lý sẽ nói với trường hợp quả bóng đó rằng nó nhỏ hơn bây giờ.

Nó giống như chế tạo một chiếc xe hơi. Một con chip máy tính không biết gì về động cơ xe hơi, nhưng một chiếc xe hơi có thể sử dụng chip máy tính để điều khiển động cơ. Ý tưởng đơn giản là sử dụng những thứ nhỏ, đơn giản lại với nhau để tạo ra một thứ lớn hơn, phức tạp hơn một chút, bản thân nó có thể tái sử dụng như một thành phần của các bộ phận phức tạp khác.

Và trong ví dụ Mario của bạn, điều gì sẽ xảy ra nếu bạn ở trong phòng thử thách khi chạm vào kẻ thù không rút cạn tiền của Marios, mà chỉ đẩy anh ta ra khỏi phòng đó? Chính bên ngoài không gian ý tưởng của Mario hoặc kẻ thù, Mario nên mất xu khi chạm vào kẻ thù (thực tế, nếu Mario có một ngôi sao bất khả xâm phạm, thay vào đó, anh ta sẽ giết kẻ thù). Vì vậy, bất kỳ đối tượng nào (miền / ý tưởng) chịu trách nhiệm cho những gì xảy ra khi mario chạm vào kẻ thù là đối tượng duy nhất cần biết về một trong hai và nên làm bất cứ điều gì với chúng (trong phạm vi mà đối tượng đó cho phép thay đổi do bên ngoài điều khiển ).

Ngoài ra, với tuyên bố của bạn về các đối tượng gọi trẻ em Update(), điều đó cực kỳ dễ bị lỗi như nếu Updateđược gọi nhiều lần trên mỗi khung từ các cha mẹ khác nhau thì sao? (ngay cả khi bạn nắm bắt được điều này, việc lãng phí thời gian CPU có thể làm chậm trò chơi của bạn) Mọi người chỉ nên chạm vào những gì họ cần, khi họ cần. Nếu bạn đang sử dụng Update (), bạn nên sử dụng một số dạng mẫu đăng ký để đảm bảo tất cả Cập nhật được gọi một lần trên mỗi khung (nếu điều này không được xử lý cho bạn như trong Unity)

Học cách xác định các ý tưởng miền của bạn thành các khối rõ ràng, tách biệt, được xác định rõ và dễ sử dụng sẽ là yếu tố lớn nhất trong việc bạn có thể sử dụng OOP tốt như thế nào.


1

Gặp gỡ BoxPong, một trò chơi rất đơn giản mà tôi đã thực hiện để làm quen với việc phát triển trò chơi hướng đối tượng.

Làm BoxPong giúp tôi hình thành, trong số những điều khác, một câu hỏi cơ bản: làm thế nào tôi có thể có các đối tượng tương tác với nhau mà không phải "thuộc về" nhau?

Tôi có khá nhiều kinh nghiệm viết mã cho SNES, nơi bạn có quyền truy cập vào thực tế toàn bộ RAM mọi lúc. Giả sử bạn đang tạo một kẻ thù tùy chỉnh cho Super Mario World và bạn muốn nó xóa tất cả các đồng tiền của Mario, sau đó chỉ cần lưu trữ số 0 để giải quyết $ 0DBF, không vấn đề gì.

Bạn dường như đang thiếu điểm lập trình hướng đối tượng.

Lập trình hướng đối tượng là về việc quản lý các phụ thuộc bằng cách đảo ngược có chọn lọc một số phụ thuộc chính nhất định trong kiến ​​trúc của bạn để bạn có thể ngăn chặn sự cứng nhắc, dễ vỡ và không thể sử dụng lại.

Phụ thuộc là gì? Sự phụ thuộc là phụ thuộc vào một cái gì đó khác. Khi bạn lưu trữ số 0 để giải quyết $ 0DBF, bạn đang dựa vào thực tế rằng địa chỉ đó là nơi đặt các đồng tiền của Mario và các đồng tiền được thể hiện dưới dạng một số nguyên. Mã kẻ thù tùy chỉnh của bạn phụ thuộc vào mã triển khai Mario và tiền của anh ấy. Nếu bạn thực hiện thay đổi nơi Mario lưu trữ tiền của mình trong bộ nhớ, bạn phải cập nhật thủ công tất cả mã tham chiếu vị trí bộ nhớ.

Mã hướng đối tượng là tất cả về việc làm cho mã của bạn phụ thuộc vào trừu tượng và không phụ thuộc vào chi tiết. Vì vậy, thay vì

class Mario
{
    public:
        int coins;
}

bạn sẽ viết

class Mario
{
    public:
        void LoseCoins();

    private:
        int coins;
}

Bây giờ, nếu bạn muốn thay đổi cách Mario lưu trữ tiền của mình từ int thành dài hoặc gấp đôi hoặc lưu trữ trên mạng hoặc lưu trữ trong cơ sở dữ liệu hoặc khởi động một số quy trình dài khác, bạn thực hiện thay đổi ở một nơi: Lớp Mario và tất cả các mã khác của bạn tiếp tục hoạt động mà không có thay đổi.

Do đó, khi bạn hỏi

Làm thế nào tôi có thể có các đối tượng tương tác với nhau mà không phải "thuộc về" nhau?

bạn đang thực sự hỏi:

Làm thế nào tôi có thể có mã phụ thuộc trực tiếp vào nhau mà không có bất kỳ trừu tượng nào?

mà không phải là lập trình hướng đối tượng.

Tôi khuyên bạn nên bắt đầu bằng cách đọc mọi thứ ở đây: http://objectmentor.com/omSolutions/oops_what.html và sau đó tìm kiếm youtube cho mọi thứ của Robert Martin và xem tất cả.

Câu trả lời của tôi xuất phát từ anh ấy, và một số trong đó được trích dẫn trực tiếp từ anh ấy.


Cảm ơn câu trả lời (và trang bạn liên kết đến; trông thú vị). Tôi thực sự biết về sự trừu tượng và khả năng sử dụng lại, nhưng tôi đoán rằng tôi đã không nói điều đó rất tốt trong câu trả lời của mình. Tuy nhiên, từ mã ví dụ bạn cung cấp, tôi có thể minh họa rõ hơn quan điểm của mình bây giờ! Về cơ bản, bạn đang nói rằng đối tượng kẻ thù không nên làm mario.coins = 0;, nhưng mario.loseCoins();, điều đó tốt và đúng - nhưng quan điểm của tôi là, làm thế nào kẻ thù có thể truy cập vào mariođối tượng? mariolà một biến thành viên enemydường như không đúng với tôi.
vvye

Câu trả lời đơn giản là chuyển Mario thành một đối số cho một chức năng trong Enemy. Bạn có thể có một hàm như marioNearby () hoặc AttackMario () sẽ lấy Mario làm đối số. Vì vậy, bất cứ khi nào logic phía sau khi Kẻ thù và Mario tương tác sẽ được kích hoạt, bạn sẽ gọi kẻ thù.marioNearby (mario) sẽ gọi mario.loseCoins (); Sau này, bạn có thể quyết định rằng có một nhóm kẻ thù khiến mario chỉ mất một đồng xu hoặc thậm chí kiếm được tiền. Bây giờ bạn có một nơi để thực hiện thay đổi đó mà không gây ra thay đổi tác dụng phụ cho mã khác.
Đánh dấu Murfin

Bằng cách chuyển Mario cho kẻ thù, bạn vừa ghép chúng lại. Mario và Enemy không nên biết rằng người kia thậm chí là một thứ. Đây là lý do tại sao chúng ta tạo các đối tượng bậc cao hơn để thay đổi cách ghép các đối tượng đơn giản lại với nhau.
Tezra

@Tezra Nhưng sau đó, những đối tượng bậc cao này không thể tái sử dụng chút nào? Cảm giác như những vật thể này hoạt động như các chức năng, chúng chỉ tồn tại để trở thành thủ tục mà chúng thể hiện.
Steve Chamaillard

@SteveChamaillard Mỗi chương trình sẽ có ít nhất một chút logic cụ thể không có ý nghĩa trong bất kỳ chương trình nào khác, nhưng ý tưởng là giữ logic này tách biệt với một vài lớp bậc cao. Nếu bạn có mario, kẻ thù và lớp cấp, bạn có thể tái sử dụng mario và kẻ thù trong các trò chơi khác. Nếu bạn buộc kẻ thù và mario trực tiếp với nhau, hơn bất kỳ trò chơi nào cần một người khác cũng phải kéo vào nhau.
Tezra

0

Bạn có thể kích hoạt khớp nối lỏng bằng cách áp dụng mô hình hòa giải. Việc thực hiện nó đòi hỏi bạn phải có một tham chiếu đến một thành phần hòa giải, biết tất cả các thành phần nhận.

Nó nhận ra kiểu "chủ trò chơi, làm ơn hãy để điều này và điều đó xảy ra".

Một mẫu tổng quát là mẫu đăng ký xuất bản. Nó phù hợp nếu hòa giải viên không nên chứa nhiều logic. Mặt khác, sử dụng một bộ trung gian được chế tạo thủ công để biết nơi định tuyến tất cả các cuộc gọi và thậm chí có thể sửa đổi chúng.

Có các biến thể đồng bộ và không đồng bộ thường được đặt tên là bus sự kiện, bus tin nhắn hoặc hàng đợi tin nhắn. Nhìn chúng để xác định thời tiết chúng phù hợp trong trường hợp cụ thể của bạn.

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.