4
\$\begingroup\$

I am finishing up an actor system (like those used in 90s games, like Doom, Unreal, etc.), which currently deals with damage and event handling.

actor.cpp

/*
 * Actor System
 * (with Event System included)
 *
 * Author: Gustavo Ramos "Gustavo6046" Rehermann.
 * (c)2018. The MIT License.
 *
 * This system basically allows the creation of
 * Actors (entities), in a game world. This does
 * not handle 3D coordinates nor collision. I
 * have neither ideas nor time to implement those,
 * so I hope someone will implement those on top
 * of this code, either extending it or modifying it.
 *
 * For the sake of extensibility, actors aren't removed
 * when they die. That task is here delegated to the 
 * upper levels.
 *
 * For an example of a possible extension:
 *
 *      struct Vec3
 *      {
 *          int XYZ[3];
 *      }
 *
 *      struct G_PhysicalActor
 *      {
 *          G_Actor* baseActor;
 *          Vec3 pos;
 *          int colCylinderRadius;
 *          int colCylinderHeight;
 *          // ...
 *      }
 */
#include <iostream>
#include <cmath>
#include <algorithm>
#include <cstring>
#include <cstdlib>
#include "actor.h" // > structs

E_ActorEvent*   E_BaseEvent = NULL;    // > Master actor event linked list.
G_Actor*        G_Actors = NULL;       // > Master actor linked list.
unsigned int    G_NumActors = 0;

// > This function shall append actor events
//   to the master event linked list (I call
//   this event notification).
void G_NotifyActorEvent(E_ActorEvent* evt)
{   
    evt->nextEvent = NULL;

    if ( E_BaseEvent == NULL )
        E_BaseEvent = evt;

    else
    {
        E_ActorEvent* e = NULL;
        for ( e = E_BaseEvent; e->nextEvent != NULL; e = e->nextEvent );    // > quick way to get
                                                                            //   the last event on
                                                                            //   the linked list :)

        e->nextEvent = evt;
    }
}

// > Returns the next actor event.
//   
E_ActorEvent* G_PollActorEvent()
{
    // > For the sake of while loop polling, we want
    //   to return NULL (0) when there is no event.
    if ( !E_BaseEvent ) return NULL;

    // > Since our linked list is like a queue, we want
    //   to take only the 1st event (FIFO), and the 2nd
    //   will become the 1st. Fortunately, this is very
    //   easy to do.
    E_ActorEvent* res = E_BaseEvent;
    E_BaseEvent = res->nextEvent;


    return res;
}

// > Handle actor death (health being
//   smaller than or equal to 0).
void G_Die(G_Actor* actor)
{
    // > At the moment, all it does is notify an event.
    //   This may change.

    E_ActorEvent* evt = new E_ActorEvent;
    evt->eventType = AEV_DEATH;
    evt->actor = actor;
    G_NotifyActorEvent(evt);
}

// > Create an actor and append it to
//   the master Actor linked list.
G_Actor* G_CreateActor(int health)
{
    // > Initializes the actor.
    G_Actor* a = new G_Actor;

    _G_DamageTrack* dmg = new _G_DamageTrack;
    dmg->health = health;
    dmg->totalDamage = 0;
    dmg->totalHeal = 0;
    a->damage = *dmg;
    a->index = G_NumActors;

    char* estr = new char[1]; // allocate an empty string.
    *estr = 0; // strings terminate with a null byte.
    std::fill_n(a->damageFactor_K, 256, estr);
    std::fill_n(a->damageFactor_V, 256, 1000);

    // > Adds the actor to the list.
    if ( G_Actors == NULL )
        G_Actors =  a;

    else
    {
        // > Find last element on actor list, then append
        //   to it.
        G_Actor* prev;
        for ( prev = G_Actors; prev->nextActor != NULL; prev = prev->nextActor );
        prev->nextActor = a;
    }

    // > Notify the actor's creation.
    G_NumActors++;

    E_ActorEvent* evt = new E_ActorEvent;
    evt->eventType = AEV_CREATED;
    evt->actor = a;
    G_NotifyActorEvent(evt);
    a->nextActor = NULL;

    return a;
}

// > Removes this actor from the list, by address.
bool G_RemoveActor(G_Actor* actor)
{
    G_Actor* a = NULL;

    if ( G_Actors == actor )
    {
        G_Actors = NULL;
        return true;
    }

    if ( G_Actors == NULL )
        return false;

    for ( a = G_Actors; a->nextActor != NULL && a->nextActor != actor; a = a->nextActor )

    if ( a == NULL || a->nextActor == NULL )
        return false;

    if ( a->nextActor != NULL )
    {
        a->nextActor = a->nextActor->nextActor;

        if ( a->nextActor != NULL )
            for ( G_Actor* cur = a->nextActor; cur != NULL; cur = cur->nextActor )
                cur->index--;
    }

    else
        a->nextActor = NULL;

    E_ActorEvent* evt = new E_ActorEvent;
    evt->eventType = AEV_REMOVED;
    evt->actor = actor;
    G_NotifyActorEvent(evt); // > Notify the actor's removal.

    return true;

}

// > Perform damage checks on the actor pointed. If
//   the actor's health is smaller than 1 (it's not
//   a floating point number, so we can safely assume
//   that i < 1 is the same as i <= 0), we call the die
//   routine on it.
//
// > For the sake of modifiability, this function exists;
//   however, it is only an if statement with a call to
//   G_Die.
void G_DamageCheck(G_Actor* actor)
{
    if ( actor->damage.health < 1 )
        G_Die(actor);
}

// > Deal damage to an actor, and handle damage factors
//   (unless specified otherwise). Automatically calls
//   the damage check function (kills actor if health is
//   null or negative).
void G_TakeDamage(G_Actor* actor, char* damageType, int damage, bool checkFactors)
{   
    // > Apply damage factors.
    if ( checkFactors && actor->damageFactor_K != NULL && actor->damageFactor_V != NULL )
        for ( unsigned int i = 0; i < 256 && actor->damageFactor_K[i] != ""; i++ )
            if ( std::strcmp(actor->damageFactor_K[i], damageType) == 0 )
                damage = damage * actor->damageFactor_V[i] / 1000;


    actor->damage.health -= damage;

    // > Damage shall be tracked by other _G_DamageTrack
    //   members as well!
    if ( damage < 0 )
        actor->damage.totalHeal -= damage;

    else
        actor->damage.totalDamage += damage;    

    if ( damage > 0 )
    {
        E_ActorEvent* evt1 = new E_ActorEvent;
        evt1->eventType = AEV_DAMAGE;
        evt1->eventData = damage;
        evt1->actor = actor;
        G_NotifyActorEvent(evt1); // > Notify the actor's damage.
    }

    else
    {
        E_ActorEvent* evt1 = new E_ActorEvent;
        evt1->eventType = AEV_HEAL;
        evt1->eventData = -damage;
        evt1->actor = actor;
        G_NotifyActorEvent(evt1); // > Notify the actor's heal.
    }

    E_ActorEvent* evt1 = new E_ActorEvent;
    evt1->eventType = AEV_MODHEALTH;
    evt1->eventData = -damage;
    evt1->actor = actor;
    G_NotifyActorEvent(evt1);   // > Notify any modification to the
                                //   actor's physical integrity.        

    G_DamageCheck(actor);
}

actor.h

#ifndef GUSTAVO_ACTORSYS_HEADER
#define GUSTAVO_ACTORSYS_HEADER

/*
 * Actor System (header)
 * (with Event System included)
 *
 * Author: Gustavo Ramos "Gustavo6046" Rehermann.
 * (c)2018. The MIT License.
 *
 * This system basically allows the creation of
 * Actors (entities), in a game world. This does
 * not handle 3D coordinates nor collision. I
 * have neither ideas nor time to implement those,
 * so I hope someone will implement those on top
 * of this code, either extending it or modifying it.
 *
 * For the sake of extensibility, actors aren't removed
 * when they die. That task is here delegated to the 
 * upper levels.
 *
 * For an example of a possible extension:
 *
 *      struct Vec3
 *      {
 *          int XYZ[3];
 *      }
 *
 *      struct G_PhysicalActor
 *      {
 *          G_Actor* baseActor;
 *          Vec3 pos;
 *          int colCylinderRadius;
 *          int colCylinderHeight;
 *          // ...
 *      }
 */

#define AEV_DEATH 0     // > Surprise! This actor died!
#define AEV_DAMAGE 1    // > This actor took positive damage.
#define AEV_HEAL 2      // > This actor took negative damage (heal).
#define AEV_MODHEALTH 3 // > This actor's health was modified (by
                        //   the G_TakeDamage function, most
                        //   likely).
#define AEV_CREATED 4   // > This actor was newly created.
#define AEV_REMOVED 5   // > This actor was removed from the master linked list..

// > This structure will track the physical integrity
//   of an actor. It is supported by standard functions,
//   like G_TakeDamage.
struct _G_DamageTrack
{
    int health;                 // > tracked actor's health

    unsigned int totalDamage;   // > tracked actor's total damage (scaled
                                //   by damage factors)
    unsigned int totalHeal;     // > tracked actor's total heal (negative damage)
};

struct G_Actor
{
    // char* name;              // > actor's human name
    // char* id;                // > actor's unique identifier
    unsigned int index;         // > actor's G_Actors index + 1.
                                //   If this is equal to 0, this actor
                                //   should be assumed not to be in 
                                //   G_Actors.

    _G_DamageTrack damage;      // > keeps track of 'health' (physical
                                //   integrity) and damage
    char* damageFactor_K[256];  // > this actor's damage factor map's keys
    int damageFactor_V[256];    // > this actor's damage factor map's values

    G_Actor* nextActor;         // > actors follow a linked list.
};

struct E_ActorEvent
{
    unsigned char   eventType;  // > event's type (as defined in AEV_* constants)
    int             eventData;  // > extra information about the event
    G_Actor*        actor;      // > pointer to event's reference actor

    E_ActorEvent*   nextEvent;  // > events, like actors, follow a linked list.
};

extern  E_ActorEvent*   E_BaseEvent;    // > Master actor event linked list.
extern  G_Actor*        G_Actors;       // > Master actor linked list.
extern  unsigned int    G_NumActors;

// > This function shall append actor events
//   to the master event linked list (I call
//   this event notification).
extern void G_NotifyActorEvent(E_ActorEvent* evt);

// > Returns the next actor event.
//   
extern E_ActorEvent* G_PollActorEvent();

// > Handle actor death (health being
//   smaller than or equal to 0).
extern void G_Die(G_Actor* actor);

// > Create an actor and append it to
//   the master Actor linked list.
extern G_Actor* G_CreateActor(int health);

// > Perform damage checks on the actor pointed. If
//   the actor's health is smaller than 1 (it's not
//   a floating point number, so we can safely assume
//   that i < 1 is the same as i <= 0), we call the die
//   routine on it.
extern void G_DamageCheck(G_Actor* actor);

// > Deal damage to an actor, and handle damage factors
//   (unless specified otherwise). Automatically calls
//   the damage check function (kills actor if health is
//   null or negative).
extern void G_TakeDamage(G_Actor* actor, char* damageType, int damage, bool checkFactors = true);

// > Removes this actor from the list, by address.
extern bool G_RemoveActor(G_Actor* actor);

#endif /* GUSTAVO_ACTORSYS_HEADER */

Below is a usage example/demo:

actormain.cpp

/*
 * Actor system usage example.
 *
 * Author: Gustavo Ramos "Gustavo6046" Rehermann.
 * (c)2018. The MIT License.
 */

#include <iostream>
#include "actor.h"

int main()
{
    G_Actor* myActor = G_CreateActor(80);
    G_Actor* myActor2 = G_CreateActor(200);

    myActor2->damageFactor_K[0] = "fire"; // Let's just say it's a dragon!
    myActor2->damageFactor_V[0] = 100;
    myActor2->damageFactor_K[1] = "default"; // And it has hard scales.
    myActor2->damageFactor_V[1] = 575;
    myActor2->damageFactor_K[2] = "water"; // But it hates water!
    myActor2->damageFactor_V[2] = 2420;

    std::cout << "Actor 0 address: " << myActor << "\n";
    std::cout << "Actor 1 address: " << myActor2 << "\n";
    std::cout << "Actor 0's initial health: " << myActor->damage.health << "\n";
    std::cout << "Actor 1's initial health: " << myActor2->damage.health << "\n";
    G_TakeDamage(myActor, "default", 30);
    G_TakeDamage(myActor, "default", 40);
    G_TakeDamage(myActor, "default", -30);
    G_TakeDamage(myActor, "default", 100);
    G_TakeDamage(myActor2, "fire", 314);
    G_TakeDamage(myActor2, "default", 120);
    G_TakeDamage(myActor2, "water", 90);
    std::cout << "Actor 0's final health: " << myActor->damage.health << "\n";
    std::cout << "Actor 1's final health: " << myActor2->damage.health << "\n";
    G_RemoveActor(myActor);
    std::cout << "Actor removed.\n";

    E_ActorEvent* evt;

    std::cout << "\n== EVENT LOG ==\n\n";

    // Print every damage/death event notified in the meanwhile.
    while ( evt = G_PollActorEvent() )
    {
        if ( evt->eventType == AEV_CREATED )
            std::cout << "Actor " << evt->actor->index << " was created!\n";

        if ( evt->eventType == AEV_DAMAGE )
            std::cout << "Actor " << evt->actor->index << " took " << evt->eventData << "hp damage!\n";

        else if ( evt->eventType == AEV_DEATH )
            std::cout << "Actor " << evt->actor->index << " has died!\n";

        else if ( evt->eventType == AEV_HEAL )
            std::cout << "Actor " << evt->actor->index << " healed " << evt->eventData << "hp!\n";

        else if ( evt->eventType == AEV_REMOVED )
            std::cout << "Actor " << evt->actor->index << " was removed from the actor list.\n";
    }

    return 0;
}

NOTE: This works when compiled with Open Watcom C++. Tell me if it doesn't work with your compiler!

\$\endgroup\$
  • \$\begingroup\$ Any reason for not using std::list? \$\endgroup\$ – vnp Aug 26 '18 at 22:52
  • \$\begingroup\$ I don't even know why I made this in C++ instead of C, but I wanted to make a program/library that could be run under DOS. \$\endgroup\$ – Gustavo6046 Aug 27 '18 at 3:57
  • 1
    \$\begingroup\$ I see quite a few uses of new. Still looking for the first delete... \$\endgroup\$ – Mike Borkland Aug 27 '18 at 9:48
  • \$\begingroup\$ @MikeBorkland my issue is that, if I don't use new, the Open Watcom compiler will simply allocate only one variable per function, not per function call, so G_Actor newActor; inside a function would not allocate a new actor, but instead allocate only one, and reuse it everytime the function is called. I don' t have much of a clue about deinitialization right now, but I'll try it. \$\endgroup\$ – Gustavo6046 Aug 28 '18 at 1:11
  • \$\begingroup\$ I think this works: gist.github.com/Gustavo6046/2bead16a3c1e4da8f281affc6ded2d07 \$\endgroup\$ – Gustavo6046 Aug 28 '18 at 1:31
1
\$\begingroup\$

Defining constants with #define is only ok in c. In C++ there are better alternatives. Consider using instead const int or an enum for the states instead of #define in C++98.

If C++11 would be available you could even change the #define to constexpr.

Is there a reason to use unsigned int? it is usually not worth using it. The extra precision you gain is not worth it since it can lead to obscure bugs. https://stackoverflow.com/questions/22587451/c-c-use-of-int-or-unsigned-int

return 0 is not necessary to be added in the main function in c++. By definition it is auto generated by the compiler. This only applys for main.

Consider making a Actor class were you create the Actor take damage etc.

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.