Du bist nicht angemeldet.

Stilllegung des Forums
Das Forum wurde am 05.06.2023 nach über 20 Jahren stillgelegt (weitere Informationen und ein kleiner Rückblick).
Registrierungen, Anmeldungen und Postings sind nicht mehr möglich. Öffentliche Inhalte sind weiterhin zugänglich.
Das Team von spieleprogrammierer.de bedankt sich bei der Community für die vielen schönen Jahre.
Wenn du eine deutschsprachige Spieleentwickler-Community suchst, schau doch mal im Discord und auf ZFX vorbei!

Werbeanzeige

buggypixels

Treue Seele

Beiträge: 125

Wohnort: Meerbusch

Beruf: Programmierer

  • Private Nachricht senden

11

15.06.2015, 11:37

Also grundsätzlichen gibt es keinen Grund "private" Methoden zu testen, denn das sollte ja über die Tests der "public" Methoden geschehen sein.
Sollte das aber nicht so einfach gehen, dann ist das eventuell ein Hinweis, dass das Design nicht so gut ist. Ich finde immer, dass man gerade beim Testen
doch noch einige Schwachstellen findet. Also wenn es zu kompliziert ist zu testen, dann stimmt vielleicht etwas nicht.

Aber das ist anhand von Deinem ersten Beispiel auch etwas schwierig alles zu beurteilen. Vor allem weil Deine Beschreibung deiner Systeme und Data Oriented Design
nicht so ganz zusammen passen. Vielleicht ist Dein Beispiel aber auch einfach schlecht gewählt. Jedenfalls hat Dein Beispiel eher weniger mit DOD zu tun.

Aber die Frage war, wie testet man "private" Methoden und die Antwortet ist "nicht direkt sondern über die public".
Denk bitte nicht mal in Ansatz an die Idee von Koschi mit den "ifdef" Kram. Da schüttelt es einen ja.

12

15.06.2015, 13:55

Vor allem weil Deine Beschreibung deiner Systeme und Data Oriented Design
nicht so ganz zusammen passen. [...] Jedenfalls hat Dein Beispiel eher weniger mit DOD zu tun.

Warum?

buggypixels

Treue Seele

Beiträge: 125

Wohnort: Meerbusch

Beruf: Programmierer

  • Private Nachricht senden

13

15.06.2015, 15:54

Wie gesagt, anhand der dürftigen Beschreibung kann man es nur vermuten. Aber wenn ich schon pushEvent lese, dann kann das nicht passen.
DOD bedeutet doch im Grunde, dass man genau die Daten direkt im Speicher beieinander hat, die man gerade genau für die Berechnung braucht.
Alleine die Signatur Deiner Methode läßt da Zweifel aufkommen. Wenn das noch Events in Spiel kommen und du eventuell dann wahllos dadurch
im Code umher springst, dann ist eh alles verloren.
Aber wie gesagt, man kann es nur vermuten. Oder Deine Beispiele sind einfach unglücklich gewählt.

14

15.06.2015, 16:38

Wie gesagt, anhand der dürftigen Beschreibung kann man es nur vermuten. Aber wenn ich schon pushEvent lese, dann kann das nicht passen.
DOD bedeutet doch im Grunde, dass man genau die Daten direkt im Speicher beieinander hat, die man gerade genau für die Berechnung braucht.
Alleine die Signatur Deiner Methode läßt da Zweifel aufkommen. Wenn das noch Events in Spiel kommen und du eventuell dann wahllos dadurch
im Code umher springst, dann ist eh alles verloren.
Aber wie gesagt, man kann es nur vermuten. Oder Deine Beispiele sind einfach unglücklich gewählt.


oO Also entweder kann man nur vermuten oder man sagt "das ist nicht DOD" :D Die Daten sind in der Tat cache-freundlich angeordnet und die internen/privaten Methoden enthalten die Logik die auf den Daten ausgeführt wird... Und die übergeordnete Logik (z.B. vor einer Bewegung wird ein Kollisionstest ausgeführt) steckt in der update()-Methode.

Deine Pauschalisierung bzgl. Events kann ich überhaupt nicht verstehen. Warum ist alles verloren? Warum spring ich wahllos im Code herum? Wie gesagt: Das Physik-System an sich arbeitet intern in Anlehnung an DOD, die öffentliche Schnittstelle ist objektorientierter und arbeitet (aus Gründen der losen Kopplung verschiedener Systeme) über Events (anstelle von direct dispatch etc.). Ws ist daran wahllos gesprungen oder gar verloren?

Und meine grundlegende Frage ist, wie ich meine DOD-basierten Methoden am besten teste. Da ich sie nur innerhalb des Systems aufrufe sehe ich eigentlich keinen Grund sie prinzipiell öffentlich zu machen. Die Implementierungsdetails durch die update()-API zu testen finde ich etwas zu aufwändig...

Hier mal eine (vereinfachte) Skizze des Systems (um Missverständnisse zu vermeiden):

C-/C++-Quelltext

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
struct PhysicsData {
    unsigned int id;
    unsigned int x, y; // pos
    bool collideable;
    int move_x, move_y; // movement vector
};

struct PhysicsEvent {
    unsigned int id;
    int move_x, move_y;
};

class PhysicsSystem {
    private:
        std::vector<PhysicsData> components;
        std::vector<PhysicsEvent> incomming;

        // liefert true falls objekt auf (x,y) kollidieren würde (mit terrain oder anderem objekt)
        bool doesCollide(PhysicsData const & data, unsigned int x, unsigned int y) const {
            // ...
        }

        // führt Bewegungsinterpolation aus
        void interpolate(PhysicsData& data, int elapsed_time) {
            // ...
        }
    public:
        void push(PhysicsEvent const & event) {
            incomming.push_back(event);
        }

        void update(int elapsed_time) {
            // handle events
            for (auto const & ev: incomming) {
                auto& obj = components.at(ev.id); // Zuordnung via ID in echt anders ^^
                if (!doesCollide(obj, ev.move_x, ev.move_y) {
                    // apply event
                    obj.move_x = ev.move_x;
                    obj.move_y = ev.move_y;
                }
            }
            incomming.clear();

            // interpolate object components
            for (auto& obj: components) {
                if (obj.move_x == 0 && obj.move_y == 0) {
                    continue;
                }
                interpolate(obj, elapsed_time);
            }
        }
};


Und in jedem Frame wird das System aktualisiert: Neue Physik-Events (um Bewegungen auszulösen) werden propagiert und das System aktualisiert (was den Rest auslöst). Daher ist das gesamte Innenleben schön gekapselt und ich kann das System als Blackbox verwenden: Events propagieren + System aktualisieren.
Dazu kommen noch div. andere Unterfunktionen die ich hier natürlich jetzt nicht alle aufliste ^^

Aber z.B. die Interpolation: Die würde ich gerne Testen. Dazu müsste ich sie entweder public machen oder über die push-update-API arbeiten, d.h.

C-/C++-Quelltext

1
2
3
4
5
6
7
8
9
10
PhysicsEvent ev;
ev.id = 13;
ev.move_x = 1;
ev.move_y = -1;
my_physics_system.push(ev);
my_physics_system.update(50); // z.B. 50ms

// und dann über die (hier weggelassene restliche API)
auto& data = my_physics_system.query(13);
// prüfen ob sich data.x, data.y richtig geändert haben etc.

Das bläht mir allerdings die Testfälle ziemlich auf.

Die andere Variante (interpolate() öffentlich) halte ich für nicht richtungsweisend ... aus meiner Sicht ist das Teil der internen Struktur des Systems und sollte nicht öffentlich sein. Zur Komunikation zwischen den Systemen (Beispiel: KI-System will für Objekt X eine Bewegung starten --> erzeugt PhysicsEvent und kennt einen PhysicsEventListener - ohne das ganze PhysicsSystem kennen zu müssen) verwende ich bereits die Events. Jetzt noch die restlichen Teile (die durch die Events aufgerufen werden) öffentlich zu machen wirkt auf mich dabei äußerst fragwürdig.

So, ich hoffe nun ist das ganze weniger unklar :)

LG Glocke

/EDIT: Im Übrigen wäre vllt. denkbar in entfernter Anlehnung bzw. Inspiration an das Pimpl-Idiom zu arbeiten (oder eher: Die Implementierungsdetails auslagern). Konkret: Die Unterroutinen (Bewegungsinterpolation etc.) von den Systemen loslösen und in einem internen Namespace als alleinstehende Funktionen ansammeln, im Stile:

C-/C++-Quelltext

1
2
3
4
5
6
7
namespace impl {

void interpolate(PhysicsData& data, Scene& scene, int elapsed_time) {
    // ...
}

}

Das ganze würde natürlich bedeuten, dass zusätzliche Parameter nötig sind. Beispiel: In meiner Spielszene habe ich zusätzlich ein Kollisionsgrid in dem steht, ob sich auf Position (x,y) eine Wand, ein Objekt etc. befindet. Diese Szene müsste ich beim interpolieren natürlich mit übergeben, wenn ich die Interpolation aus dem System rauslöse, da das PhysikSystem die zugehörige Szene kennt - die alleinstehende Funktion aber nicht (mehr). Das Objekt besitzt weiter die ID der zugehörigen Scene, d.h. nicht die Scene selbst.

Das ganze würde die Komplexität beim Aufruf innerhalb des Physik-Systems nicht wirklich steigern - es müssen nur die zusätzlichen Parameter übergeben werden. Und beim Testen kann ich mir eine Scene und ein Objekt zurecht legen und die Interpolation extrem einfach testen... Was denkt ihr?

/EDIT2: Oder noch "einfacher":

C-/C++-Quelltext

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
namespace priv {

struct PhysicsImpl {
    PhysicsImpl(/* ... */) {
        // ähnliche Konstruktion wie PhysicsSystem, so dass es die Scene(n) bereits kennt
    }   

    void interpolate(PhysicsData& data, int elapsed_time) {
        // ...
    }
};

} // ::priv

class PhysicsSystem: private priv::PhysicsImpl {
    // ähnlich oben, nur ohne die Unterroutinen
};


Dann lassen sich PhysicsImpl mittels einfachem UnitTest

C-/C++-Quelltext

1
2
3
4
5
PhysicsData data;
// setup object
priv::PhysicsImpl impl(/* setup up */);
impl.interpolate(data, 50);
// check data


und PhysicsSystem mittels Integrationstest (in Zusammenarbeit mit anderen Systemen testen - push&update).

Dieser Beitrag wurde bereits 5 mal editiert, zuletzt von »Glocke« (15.06.2015, 17:37)


buggypixels

Treue Seele

Beiträge: 125

Wohnort: Meerbusch

Beruf: Programmierer

  • Private Nachricht senden

15

16.06.2015, 09:13

So wird es etwas klarer. Dann können wir mal ein konkretes Beispiel betrachten. Nehmen wir die interpolate Methode.
Also DOD bedeutet, dass man hier am besten über alle Objekte auf einmal iteriert. Idealerweise liegen die Daten
noch direkt hintereinander im Speicher. Außerdem verwendet man auch nur die Daten, die man auch braucht. Also in
Deinem Fall wäre PhysicsData. Brauchst Du alle Attribute von dem Struct an dieser Stelle? Falls nicht, dann verschwendest
Du Daten auf dem Bus. Das führt dann auch direkt zur nächsten Frage, ob denn diese Methode überhaupt Teil der Klasse
sein muss. Ich denke mal eher nicht, da Du alle Daten ja reinreichst. Also könnte das auch einfach eine "C-Methode" sein.
Die könntest Du dann auch direkt separat testen. Was die eigentlich Ausgangsfrage beantwortet.
Also mal als Beispiel:

C-/C++-Quelltext

1
2
3
4
5
6
7
8
9
struct PhysicsData {
...
};
class PhysicsSystem {
...
}
namespace physics {
  void interpolate(std::vector<PhysicsData>& data,int elapsed);
}

Ich persönlich finde es wichtig, diese Methoden in einen passenden Namespace zu setzen. Aber das muss nicht.

Grundsätzlich wäre es aber besser, statt einem AoS (Array of Struct) hier einen Struct of Array Ansatz zu nehmen

C-/C++-Quelltext

1
2
3
4
5
6
7
8
struct PhysicsData {
  std::vector<unsigned> id;
  std::vector<int> x;
  std::vector<int> y;
  std::vector<int> moveX;
  std::vector<int> moveY;
  std::vector<bool> collidable;
}

Dann könntest Du nämlich die einzelnen Arrays an die Interpolate Methode übergeben, und er liest dann wirklich nur die Daten, die er auch braucht. Also id und collidable wird er ja nicht brauchen für das interpolate, oder?

16

16.06.2015, 09:34

C-/C++-Quelltext

1
2
3
namespace physics {
  void interpolate(std::vector<PhysicsData>& data,int elapsed);
}

Ich persönlich finde es wichtig, diese Methoden in einen passenden Namespace zu setzen. Aber das muss nicht.

Hübsch, gefällt mir!

Brauchst Du alle Attribute von dem Struct an dieser Stelle?

Nein, und ich denke darüber nach das PhysicsSystem aufzuteilen: CollisionSystem, MovementSystem, FocusingSystem und diese über Events miteinander reden zu lassen (CollisionSystem leitet InputEvent (Bewegung) an MovementSystem weiter - ohne propagiert ein CollisionEvent; MovementSystem interpoliert und leitet an Input FocusingSystem weiter; letzteres aktualisiert den Fokus jedes Objekts).

(Zum Fokus: "Wer wen anguckt" (Spieler hat Truhe im Fokus, Goblin hat Spieler im Fokus) .. ist interessant für's Aiming (Kampf, Interaktion).)

Dieser Beitrag wurde bereits 2 mal editiert, zuletzt von »Glocke« (16.06.2015, 09:42)


17

02.09.2015, 12:48

ich denke darüber nach das PhysicsSystem aufzuteilen: CollisionSystem, MovementSystem, FocusingSystem und diese über Events miteinander reden zu lassen (CollisionSystem leitet InputEvent (Bewegung) an MovementSystem weiter - ohne propagiert ein CollisionEvent; MovementSystem interpoliert und leitet an Input FocusingSystem weiter; letzteres aktualisiert den Fokus jedes Objekts).


Inzwischen habe ich es so implementiert und konnte es sehr gut testen :)

DeKugelschieber

Community-Fossil

Beiträge: 2 641

Wohnort: Rheda-Wiedenbrück

Beruf: Software-Entwickler

  • Private Nachricht senden

18

02.09.2015, 14:59

Private/Protected Methoden und Member kannst du im Grunde nur BlackBox testen.
Sowas wie #ifdef UNITTEST ... ist echt unschön. Man sollte (imo) die Tests nicht im Produktivcode sehen.
Zum testen kannst du dafür evt. Mocks verwenden, um z.B. protected Member herausreichen zu können (ableitende Klasse bekommt getter für protected member der Elternklasse).

19

02.09.2015, 15:17

Private/Protected Methoden und Member kannst du im Grunde nur BlackBox testen.
Sowas wie #ifdef UNITTEST ... ist echt unschön. Man sollte (imo) die Tests nicht im Produktivcode sehen.
Zum testen kannst du dafür evt. Mocks verwenden, um z.B. protected Member herausreichen zu können (ableitende Klasse bekommt getter für protected member der Elternklasse).


Deswegen habe ich bei diesen Systemen auf "normale" Klassen verzichtet (auch weil ich an dieser Stelle ohnehin nicht sehr objektorientiert arbeite). Ich habe einen reingeschachtelten Namespace angelegt, in dem sich ein "Context" (struct welches Referenzen auf die Abhängigkeiten enthält) und diverse "interne" Funktionen befinden, die ich testen möchte. Unter'm Strich habe ich als System (Movement System, Animation System) dann eine sehr einfache Klasse um "nach außen" eine übersichtliche API zu haben: eine update() und mehrere handle() Methoden (eine pro Eventtyp). Das ganze wirkt auf den ersten Blick wie "altmodisches C-Programmieren" (durch structs + functions), verwendet aber durchaus einige nützliche C++-Elemente (namespace, references, STL etc.). Die Namespaces habe ich entsprechend benannt, so muss ich aus meinem Testcode heraus agieren via core::collision_impl::checkObjectCollision(...).

Werbeanzeige