Come posso avere oggetti che interagiscono e comunicano tra loro senza forzare una gerarchia?


9

Spero che queste chiacchiere chiariscano la mia domanda - capirò totalmente se non lo faranno, quindi fammi sapere se è così, e proverò a chiarirmi.

Ecco BoxPong , un gioco molto semplice che ho realizzato per conoscere lo sviluppo di giochi orientati agli oggetti. Trascina la casella per controllare la palla e colleziona oggetti gialli.
Fare BoxPong mi ha aiutato a formulare, tra le altre cose, una domanda fondamentale: come posso avere oggetti che interagiscono tra loro senza "appartenere" gli uni agli altri? In altre parole, esiste un modo per gli oggetti di non essere gerarchici, ma invece coesistere? (Andrò in ulteriori dettagli di seguito.)

Ho il sospetto che il problema degli oggetti che coesistono sia comune, quindi spero che ci sia un modo stabilito per risolverlo. Non voglio reinventare la ruota quadrata, quindi immagino che la risposta ideale che sto cercando sia "ecco un modello di progettazione che viene comunemente utilizzato per risolvere il tuo tipo di problema".

Soprattutto in giochi semplici come BoxPong, è chiaro che ci sono, o dovrebbero esserci, una manciata di oggetti che coesistono allo stesso livello. C'è una scatola, c'è una palla, c'è un oggetto da collezione. Tutto ciò che posso esprimere in linguaggi orientati agli oggetti, sebbene - o almeno così sembra - sono strette relazioni HAS-A . Questo viene fatto attraverso le variabili membro. Non posso semplicemente iniziare balle lasciarlo fare, ho bisogno che appartenga permanentemente a un altro oggetto. L'ho impostato in modo che l'oggetto di gioco principale abbia una scatola e la scatola a sua volta abbia una palla e abbia un segnalino punteggio. Ogni oggetto ha anche unupdate()metodo, che calcola posizione, direzione ecc., e vado in modo simile lì: chiamo il metodo di aggiornamento dell'oggetto di gioco principale, che chiama i metodi di aggiornamento di tutti i suoi figli, e questi a loro volta chiamano i metodi di aggiornamento di tutti i loro figli. Questo è l'unico modo in cui riesco a vedere un gioco orientato agli oggetti, ma penso che non sia il modo ideale. Dopotutto, non penserei esattamente alla palla come appartenente alla scatola, ma piuttosto allo stesso livello e all'interazione con essa. Suppongo che ciò possa essere ottenuto trasformando tutti gli oggetti di gioco in variabili membro dell'oggetto di gioco principale, ma non vedo che risolvere nulla. Voglio dire ... lasciando da parte l'evidente disordine, come potrebbe esserci un modo per la palla e la scatola di conoscersi , cioè di interagire?

C'è anche il problema degli oggetti che hanno bisogno di scambiarsi informazioni. Ho un po 'di esperienza nella scrittura di codice per SNES, dove puoi accedere praticamente all'intera RAM per tutto il tempo. Supponiamo che tu stia creando un nemico personalizzato per Super Mario World e desideri che rimuova tutte le monete di Mario, quindi memorizzi zero per indirizzare $ 0DBF, nessun problema. Non ci sono limiti nel dire che i nemici non possono accedere allo stato del giocatore. Immagino di essere stato viziato da questa libertà, perché con C ++ e simili mi trovo spesso a chiedermi come rendere un valore accessibile ad altri oggetti (o persino globali).
Usando l'esempio di BoxPong, cosa accadrebbe se volessi che la palla rimbalzasse sui bordi dello schermo? widthe heightsono proprietà della Gameclasse,ballper avervi accesso. Potrei trasmettere questo tipo di valori (attraverso i costruttori o i metodi in cui sono necessari), ma questo mi urla solo cattive pratiche.

Immagino che il mio problema principale sia che ho bisogno di oggetti per conoscerci, ma l'unico modo in cui posso vedere è la rigida gerarchia, che è brutta e poco pratica.

Ho sentito parlare di "classi di amici" su C ++ e so come funzionano, ma se sono la soluzione definitiva, come mai non vedo friendparole chiave riversate su ogni singolo progetto C ++ e come mai il concetto non esiste in tutte le lingue OOP? (Lo stesso vale per i puntatori a funzione, di cui ho appena appreso.)

Grazie in anticipo per le risposte di qualsiasi tipo - e ancora, se c'è una parte che non ha senso per te, fammelo sapere.


2
Gran parte del settore dei giochi si è spostato verso l'architettura Entity-Component-System e le sue varianti. È una mentalità diversa rispetto ai tradizionali approcci OO ma funziona bene e ha senso una volta che il concetto affonda. Unity lo usa. In realtà, Unity utilizza solo la parte Entity-Component ma si basa su ECS.
Dunk,

Il problema di consentire alle classi di collaborare tra loro senza conoscersi è risolto dal modello di progettazione del mediatore. L'hai visto?
Fuhrmanator,

Risposte:


13

In generale, risulta molto male se gli oggetti dello stesso livello si conoscono. Una volta che gli oggetti si conoscono, vengono legati o accoppiati tra loro. Questo li rende difficili da cambiare, difficili da testare, difficili da mantenere.

Funziona molto meglio se c'è un oggetto "sopra" che conosce i due e può impostare le interazioni tra loro. L'oggetto che conosce i due peer può collegarli insieme tramite l'iniezione di dipendenza o tramite eventi o tramite il passaggio di messaggi (o qualsiasi altro meccanismo di disaccoppiamento). Sì, questo porta a un po 'di una gerarchia artificiale, ma è molto meglio del pasticcio di spaghetti che si ottiene quando le cose interagiscono semplicemente volenti o nolenti. Questo è solo più importante in C ++, dal momento che hai bisogno di qualcosa per possedere anche la durata degli oggetti.

Quindi, in breve, puoi farlo semplicemente affiancando oggetti ovunque collegati da un accesso ad hoc, ma è una cattiva idea. La gerarchia fornisce ordine e chiara proprietà. La cosa principale da ricordare è che gli oggetti nel codice non sono necessariamente oggetti nella vita reale (o persino nel gioco). Se gli oggetti nel gioco non formano una buona gerarchia, una diversa astrazione potrebbe essere migliore.


2

Usando l'esempio di BoxPong, cosa accadrebbe se volessi che la palla rimbalzasse sui bordi dello schermo? larghezza e altezza sono proprietà della classe di gioco e avrei bisogno della palla per accedervi.

No!

Penso che il problema principale che stai riscontrando sia che stai prendendo "Programmazione orientata agli oggetti" un po 'troppo alla lettera. In OOP, un oggetto non rappresenta una "cosa" ma una "idea" che significa che una "Palla", "Gioco", "Fisica", "Matematica", "Data", ecc. Sono tutti oggetti validi. Inoltre, non è necessario che gli oggetti "sappiano" nulla. Ad esempio, Date.Now().getTommorrow()chiederei al computer che giorno è oggi, applicare regole di data arcana per capire la data di domani e restituirla al chiamante. L' Dateoggetto non è a conoscenza di nient'altro, deve solo richiedere informazioni secondo necessità dal sistema. Inoltre, Math.SquareRoot(number)non è necessario conoscere altro oltre alla logica di come calcolare una radice quadrata.

Quindi nel tuo esempio che ho citato, la "palla" non dovrebbe sapere nulla della "scatola". Scatole e palle sono idee completamente diverse e non hanno il diritto di parlarsi. Ma un motore di fisica sa cos'è una Box and Ball (o almeno ThreeDShape), e sa dove si trovano e cosa dovrebbe succedere loro. Quindi, se la palla si restringe perché fa freddo, il motore fisico direbbe all'istanza della palla che ora è più piccola.

È un po 'come costruire un'auto. Un chip di computer non sa nulla di un motore di un'auto, ma un'auto può usare un chip di computer per controllare un motore. La semplice idea di usare insieme piccole cose semplici per creare qualcosa di leggermente più grande e più complesso, che è di per sé riutilizzabile come componente di altre parti più complesse.

E nel tuo esempio di Mario, cosa succede se ti trovi in ​​una stanza di sfida in cui toccare un nemico non prosciuga le monete di Marios, ma lo espelle da quella stanza? È fuori dallo spazio dell'idea di Mario o del nemico che Mario dovrebbe perdere monete quando tocca un nemico (in effetti, se Mario ha una stella di invulnerabilità, invece uccide il nemico). Quindi qualsiasi oggetto (dominio / idea) che è responsabile di ciò che accade quando Mario tocca un nemico è l'unico che deve sapere su uno dei due, e dovrebbe fare qualunque cosa a uno di essi (nella misura in cui quell'oggetto consente cambiamenti guidati dall'esterno ).

Inoltre, con le tue dichiarazioni su oggetti che chiamano bambini Update(), è estremamente soggetto a bug come se Updatefosse chiamato più volte per frame da genitori diversi? (anche se lo capisci, è uno spreco di tempo della CPU che può rallentare il tuo gioco) Tutti dovrebbero toccare ciò di cui hanno bisogno, quando ne hanno bisogno. Se stai usando Update (), dovresti usare una qualche forma di modello di abbonamento per assicurarti che tutti gli Update vengano chiamati una volta per frame (se questo non è gestito per te come in Unity)

Imparare a definire le idee del tuo dominio in blocchi chiari, isolati, ben definiti e facili da usare sarà il fattore principale nel modo in cui puoi utilizzare OOP.


1

Incontra BoxPong, un gioco molto semplice che ho realizzato per conoscere lo sviluppo di giochi orientati agli oggetti.

Fare BoxPong mi ha aiutato a formulare, tra le altre cose, una domanda fondamentale: come posso avere oggetti che interagiscono tra loro senza "appartenere" gli uni agli altri?

Ho un po 'di esperienza nella scrittura di codice per SNES, dove puoi accedere praticamente all'intera RAM per tutto il tempo. Supponiamo che tu stia creando un nemico personalizzato per Super Mario World e desideri che rimuova tutte le monete di Mario, quindi memorizzi zero per indirizzare $ 0DBF, nessun problema.

Sembra che manchi il punto della programmazione orientata agli oggetti.

La programmazione orientata agli oggetti riguarda la gestione delle dipendenze invertendo selettivamente determinate dipendenze chiave nell'architettura in modo da poter prevenire rigidità, fragilità e non riusabilità.

Che cos'è la dipendenza? La dipendenza è dipendenza da qualcos'altro. Quando memorizzi zero per indirizzare $ 0DBF, fai affidamento sul fatto che quell'indirizzo è dove si trovano le monete di Mario e che le monete sono rappresentate come un numero intero. Il tuo codice del nemico personalizzato dipende dal codice che implementa Mario e le sue monete. Se apporti una modifica alla posizione in cui Mario conserva le sue monete in memoria, devi aggiornare manualmente tutto il codice che fa riferimento alla posizione della memoria.

Il codice orientato agli oggetti consiste nel far dipendere il tuo codice dalle astrazioni e non dai dettagli. Quindi invece di

class Mario
{
    public:
        int coins;
}

tu scriveresti

class Mario
{
    public:
        void LoseCoins();

    private:
        int coins;
}

Ora, se vuoi cambiare il modo in cui Mario conserva le sue monete da un int a un lungo o un doppio o memorizzale sulla rete o memorizzale in un database o avvia un altro lungo processo, fai la modifica in un unico posto: il Classe Mario e tutti gli altri codici continuano a funzionare senza modifiche.

Pertanto, quando lo chiedi

come posso avere oggetti che interagiscono tra loro senza "appartenere" gli uni agli altri?

stai davvero chiedendo:

come posso avere un codice che dipende direttamente l'uno dall'altro senza astrazioni?

che non è una programmazione orientata agli oggetti.

Ti suggerisco di iniziare a leggere tutto qui: http://objectmentor.com/omSolutions/oops_what.html e quindi cercare su YouTube tutto da Robert Martin e guardarlo tutto.

Le mie risposte derivano da lui e alcune di esse sono direttamente citate da lui.


Grazie per la risposta (e la pagina a cui ti sei collegato; sembra interessante). In realtà conosco l'astrazione e la riusabilità, ma credo di non averlo espresso molto bene nella mia risposta. Tuttavia, dal codice di esempio che hai fornito, posso illustrare meglio il mio punto ora! In pratica stai dicendo che l'oggetto nemico non dovrebbe fare mario.coins = 0;, ma mario.loseCoins();, il che è bello e vero - ma il mio punto è, come può comunque il nemico avere accesso marioall'oggetto? marioessere una variabile membro di enemynon mi sembra giusto.
vvye,

Bene, la semplice risposta è passare Mario come argomento a una funzione in Enemy. Potresti avere una funzione come marioNearby () o attackMario () che prenderebbe un Mario come argomento. Quindi, ogni volta che la logica dietro quando un nemico e un Mario dovrebbero interagire si innesca, si chiamerebbe nemici.marioNearby (mario) che chiamerebbe mario.loseCoins (); Più avanti lungo la strada potresti decidere che esiste una classe di nemici che fa perdere a Mario solo una moneta o addirittura guadagnare monete. Ora hai un posto dove effettuare quella modifica che non causa cambiamenti di effetti collaterali ad altro codice.
Mark Murfin,

Passando Mario a un nemico, li hai appena accoppiati. Mario e Enemy non dovrebbero sapere che l'altro è persino una cosa. Questo è il motivo per cui creiamo oggetti di ordine superiore per gestire come accoppiare gli oggetti semplici.
Tezra,

@Tezra Ma allora, questi oggetti di ordine superiore non sono riutilizzabili? Sembra che questi oggetti agiscano come funzioni, esistono solo per essere la procedura che esibiscono.
Steve Chamaillard,

@SteveChamaillard Ogni programma avrà almeno un po 'di logica specifica che non ha senso in nessun altro programma, ma l'idea è quella di mantenere questa logica isolata ad alcune classi di alto ordine. Se hai una classe mario, nemico e livello, puoi riutilizzare mario e nemico in altri giochi. Se leghi nemico e mario direttamente l'uno all'altro, qualsiasi gioco che ha bisogno di uno deve tirare anche l'altro.
Tezra,

0

È possibile abilitare l'accoppiamento lento applicando il modello mediatore. L'implementazione richiede di avere un riferimento a un componente mediatore che conosce tutti i componenti riceventi.

Si rende conto del tipo di pensiero "game master, per favore, lascia che questo e quello accada".

Un modello generalizzato è il modello di pubblicazione-sottoscrizione. È appropriato se il mediatore non deve contenere molta logica. Altrimenti usa un mediatore fatto a mano che sa dove indirizzare tutte le chiamate e forse persino modificarle.

Esistono varianti sincrone e asincrone di solito denominate bus eventi, bus messaggi o coda messaggi. Cercali per determinare se sono appropriati nel tuo caso particolare.

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.