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

1

24.01.2015, 11:04

[C++] ObjectType über Bitmasks definieren

Hi, ich bin vor kurzem über Bitmasks gestolpert und habe die ziemlich lieb gewonnen :love:

In meinem RPG habe ich verschiedene Objekteigenschaften, die ich (eigentlich) über eigenständige Bool'sche Variablen realiseren will... nun überlege ich dazu Bitmasks zu verwenden. Hier ein Beispiel:

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
#include <cstdint>
#include <iostream>

using ObjectType = std::uint8_t;

namespace types {

ObjectType const Focusable      = 0x01;
ObjectType const Collideable    = 0x02;
ObjectType const Lighted        = 0x04;
ObjectType const Damageable     = 0x08;
ObjectType const Lootable       = 0x10;
ObjectType const Interactable   = 0x20;

}

int main() {
    ObjectType corpse, chest, player, torch, switch_;
    
    corpse  = types::Focusable | types::Lootable;
    chest   = types::Focusable | types::Collideable | types::Lootable;
    player  = types::Focusable | types::Collideable | types::Lighted | types::Damageable;
    torch   = types::Lighted;
    switch_ = types::Focusable | types::Interactable;

    if (corpse & types::Lootable) {
        std::cout << "Corpses can be looted" << std::endl;
    }
    if ((chest & types::Collideable) && !(chest & types::Lighted)) {
        std::cout << "You can collide with chests but their are not lighted" << std::endl;
    }
    if (player & types::Damageable) {
        std::cout << "Players can be harmed" << std::endl;
    }
    if (!(torch & types::Focusable) && (torch & types::Lighted)) {
        std::cout << "Torches cannot be focused but their are lighted" << std::endl;
    }
    if (switch_ & types::Interactable) {
        std::cout << "You can interact with switches" << std::endl;
    }
}


Haltet ihr das für angemessen oder ist das schon wieder "Overuse" eines Sprachfeatures? :D
Bei sehr vielen Objekten spare ich damit etwas Speicher; und ich kann die Objekterzeugung besser parametrisieren, indem ich ObjectType (statt zig bool's) als Parameter übergebe. Was denkt ihr?

Btw werde ich vermutlich std::uint16_t zu Grunde legen, weil 8 verschiedene Eigenschaften auf Dauer zu knapp sein könnten :)

LG Glocke

TrommlBomml

Community-Fossil

Beiträge: 2 117

Wohnort: Berlin

Beruf: Software-Entwickler

  • Private Nachricht senden

2

24.01.2015, 11:13

Naja früher wo der Speicher knapp war macht das vielleicht was aus. Doof finde ich, dass beim Debuggen nicht gerade gut ersichtlich ist, was der Objekttypen ist. Ich würde eine enum class oder strings vorziehen. Strings würde ich vor allem für frameworks benutzen, weil man dann einfach Erweiterungen implementieren kann. Die jedoch sauberste Lösung ist vermutlich eine eigene ObjectType Klasse mit eigenen Vergleichsoperatoren.

Architekt

Community-Fossil

Beiträge: 2 481

Wohnort: Hamburg

Beruf: Student

  • Private Nachricht senden

3

24.01.2015, 11:21

Was spricht gegen ein normales enum? BitMasken sind eine sehr schöne Lösung (bin auch ein großer Fan davon :P), funktionieren aber (eig.) nicht mit enum classes bzw. man muss sich die zugehörigen Operatoren selbst schreiben. Daher würde ich wirklich einfach ein normales C enum nehmen. Ansonsten kann ich gegen Bitmasken nichts schlechtes sagen, benutze sie oft genug selbst. Bei strings ist es immer so eine Sache: War das jetzt groß? Oder alles klein? Bei einem enum oder einer const Variable sagt dir der Compiler zumindest "Ey das kenn ich nicht". Bei einem string ist das dein Problem. Somit: Pro Bitmaske. :)
Der einfachste Weg eine Kopie zu entfernen ist sie zu löschen.
- Stephan Schmidt -

eXpl0it3r

Treue Seele

Beiträge: 386

Wohnort: Schweiz

Beruf: Professional Software Engineer

  • Private Nachricht senden

4

24.01.2015, 11:25

Wie praktikabel das Ganze ist, wird sich wohl nur durch ausprobieren heraus stellen. Für dein Component Based System solltest du ja die meisten Typen ja schon schön getrennt halt, womit keine Type Deduction während der Laufzeit nötig sein sollte. Aber für spezielle Szenarien könnte eine Bitmask relative simple den Typ heraus finden.
Ein Problem das ich sehen könnte ist, dass du zwar die "virtuellen" Typen heraus finden kannst, aber wenn du dann damit etwas anfangen willst müsstest du dies alles mit riesigen if/switch Strukturen abfangen...
Blog: https://dev.my-gate.net/
—————————————————————————
SFML: https://www.sfml-dev.org/
Thor: http://www.bromeon.ch/libraries/thor/
SFGUI: https://github.com/TankOs/SFGUI/

5

24.01.2015, 11:43

Doof finde ich, dass beim Debuggen nicht gerade gut ersichtlich ist, was der Objekttypen ist.

Interessanter Punkt!

Strings würde ich vor allem für frameworks benutzen, weil man dann einfach Erweiterungen implementieren kann.

Erweiterbar ist der Bitmask-Ansatz auch (sofern der ObjectType groß genug ist um neue Bitmasken unterzubringen). Den String-Ansatz finde ich nicht so toll: Wenn ich prüfen woll ob ein Objekt kollidieren kann müsste ich den String nach den entsprechenden Zeichen durchsuchen. Beispielsweise bei der Suche nach Kollisions-Gegnern wäre mir das ggf. zu langatmig.

Was spricht gegen ein normales enum?

Ein Enum würde mir helfen wenn ich eine kleine Anzahl verschiedener Typen habe. Ich möchte aber viele verschiedene Objekttypen aus einer Hand voll (oder zwei :D ) Eigenschaften erzeugen.

Für dein Component Based System solltest du ja die meisten Typen ja schon schön getrennt halt, womit keine Type Deduction während der Laufzeit nötig sein sollte.

Im Moment habe ich z.B. im Physics-System ein struct PhysicsData (= Daten der Komponente) die alle möglichen Fälle abgedeckt. Der Vorteil ist, dass ich über ein std::vector<PhysicsData> iterieren kann ohne (durch z.B. verschiedene abgeleitete Typen) eine zusätzliche Indirection zu haben. Daher beeinhalten alle Physik-Komponenten alle Daten (z.B. auch Blickrichtung, obwohl das von einer gewöhnlichen Truhe nicht verwendet wird). Zusätzlich will ich den Objekten eben einen Typ mitgeben der verschiedene Eigenschaften (siehe Bitmask-Bezeichner) hat.

Aber für spezielle Szenarien könnte eine Bitmask relative simple den Typ heraus finden.

Genau das ist die Idee :) z.B. beim Kollisionstest werden alle benachbarten Objekte die gar nicht kollidieren können (z.B. Gegnerleichen) nicht berücksichtigt.

Thema Gegnerleichen: Das ist z.B. einer der Gründe warum ich nicht verschiedene PhysicsData-Strukturen nehmen möchte: So kann ich einem Gegner der gestorben ist einen anderen ObjektTyp zuweisen und z.B. Collideable entfernen und Lootable hinzufügen.

/EDIT: Fassen wir mal zusammen:
+ kompakter Typ (übersichtlicher, auch für Parameterübergaben)
- nicht Debugfreundlich

Architekt

Community-Fossil

Beiträge: 2 481

Wohnort: Hamburg

Beruf: Student

  • Private Nachricht senden

6

24.01.2015, 12:26

Zitat

Ein Enum würde mir helfen wenn ich eine kleine Anzahl verschiedener Typen habe. Ich möchte aber viele verschiedene Objekttypen aus einer Hand voll (oder zwei :D ) Eigenschaften erzeugen.

Das verstehe ich nicht so ganz bzw. sehe nicht, inwiefern ein enum da nicht passt. Kannst du das nochmal deutlicher machen?
Der einfachste Weg eine Kopie zu entfernen ist sie zu löschen.
- Stephan Schmidt -

Beiträge: 1 223

Wohnort: Deutschland Bayern

Beruf: Schüler

  • Private Nachricht senden

7

24.01.2015, 13:20

Mein Hauptkritikpunkt daran ist eher, die mangelnde Erweiterbarkeit.
Die (niedrige) Anzahl an maximal möglichen Typen wird zu Compile Zeit festgelegt und ist nicht mehr änderbar bzw. erweiterbar.
Ich würde den Ansatz nur wählen, wenn die Anzahl Typen nicht erweitert werden muss.

Deine + und - Punkt finde ich übrigens beide sehr schwach.
Das "+" Argument ist mit jedem Typ gegeben. Du kannst alles in eine Klasse packen und hast damit diese Eigenschaft gegeben. (Sollte man zur Typsicherheit evt. sowieso machen.)
Das "-" Argument ist aber auch eher schwach. Wenn die Anzeige im Debugger dir nicht gefällt, musst du die Daten nur anders darstellen aber nicht anders speichern. Speziell für Visual Studio zum Beispiel so.

Das wirkliche + Argument für den Ansatz wäre für mich Speicherplatz und Effizienz und das wirkliche - Argument die fehlende Flexibilität zur Laufzeit.

8

24.01.2015, 13:31

Das verstehe ich nicht so ganz bzw. sehe nicht, inwiefern ein enum da nicht passt. Kannst du das nochmal deutlicher machen?

Nehmen wir folgendes Beispiel mit Bitmasken:

C-/C++-Quelltext

1
2
3
4
5
6
using ObjectType = std::uint8_t;
ObjectType const Focusable = 0x01; // ob es ein anderes Objekt fokussieren kann
ObjectType const Collideable = 0x02; // ob es mit einem anderen Objekt kollidieren kann
ObjectType const Lighted = 0x04; // ob es beleuchtet ist (einfache Variante: Sichtradius dargestellt)
ObjectType const Lootable = 0x08; // ob es gelootet werden kann
ObjectType const Damageable = 0x10; // ob es beschädigt werden kann


Dann kann ich so verschiedene Objekttypen konstruieren:

C-/C++-Quelltext

1
2
3
myChest.type = Focusable | Collideable | Lootable;
myTorch.type = Lighted
myEnemy.type = Focusable | Collideable | Damageable


Und kann im Code einzelne Eigenschaften prüfen ohne konkret wissen zu müssen, ob das Objekt vom Typ XY ist:

C-/C++-Quelltext

1
2
3
if (object.type & Lighted) {
    // draw lighted area around object
}

C-/C++-Quelltext

1
2
3
if (object.type & Collideable) {
    // check for collision with object
}


Würde ich nun ein Enum nehmen, müsste ich alle Objekttyp-Konstellationen einzeln reinbringen - und dafür Namen überlegen.

C-/C++-Quelltext

1
2
3
4
5
enum ObjectType {
    Chest, // Focus + Collide + Loot
    Torch, // Lighted
    Enemy // Focus + Collide + Damage
}


Das ist mir vom Namen her zu einengend - abgesehen davon, dass ich jeden Sonderfall (z.B. eine Feuerstelle mit z.B. Collide + Lighted) extra schreiben müsste. Im späteren Verlauf möchte ich die Objekteigenschaften aus Konfigurationsdateien parsen lassen (da dann "collideable=true, lighted=false" etc.), so dass im Code dann die Bitmasks ge-or'ed werden können.

C-/C++-Quelltext

1
2
3
myChest.type = ObjectType::Chest;
// usw.
myFireplace.type = ObjectType::Fireplace; // <-- enum nachträglich erweitern


Dazu kommt, dass ich aus dem konkreten Typ nicht mehr ablesen kann, ob collideable etc; d.h. ich müsse dann immer auf die konkreten Typen (die z.B. beleuchtet sind oder kollidieren können) prüfen.

C-/C++-Quelltext

1
2
3
if (object.type == Chest || object.type == Enemy) { // <-- wird u.U. lang
    // check for collision with object
}


Ich hoffe meine Absicht ist etwas klarer geworden :) (und ich habe den Enum-Ansatz richtig interpretiert ^^ )

LG Glocke

9

24.01.2015, 15:04

C-/C++-Quelltext

1
2
3
4
5
6
7
enum ObjectTypes 
{
    Focusable  = 1 << 0, 
    Collideable  = 1 << 1,
    Lighted = 1 << 2, 
    Lootable  = 1 << 3, 
};

Dann muss aber:

C-/C++-Quelltext

1
int type = ObjectTypes::Focusable | ObjectTypes::Collideable;

auf einen int angewendet werden, weil Focusable | Collideable == 0x03 und das kein Element des Enums ist. clang bemängelt das beispielsweise:

Zitat

fatal error: assigning to 'ObjectTypes' from incompatible type 'int'


Ansonsten ist das Konzept (wie bereits von Bambi erwähnt) das gleiche - nur die Realisierung leicht modifiziert (enum statt Namespace).

Architekt

Community-Fossil

Beiträge: 2 481

Wohnort: Hamburg

Beruf: Student

  • Private Nachricht senden

10

24.01.2015, 15:19

Ich denke das mit den Enums war eher so gemeint, dass du deine ObjectTypes zu nem Enum machst.

C-/C++-Quelltext

1
2
3
4
5
6
7
enum ObjectTypes 
{
    Focusable  = 1 << 0, 
    Collideable  = 1 << 1,
    Lighted = 1 << 2, 
    Lootable  = 1 << 3, 
};


Ich finde aber auch, dass das sehr Eingeschränkt ist, was Erweiterbarkeit angeht. (Bits)
-> std::bitset

Genau so war es gemeint. ;)
Der einfachste Weg eine Kopie zu entfernen ist sie zu löschen.
- Stephan Schmidt -

Werbeanzeige