Advance Techniques for Resolving Circular Dependancies?

The Partridge Family were neither partridges nor a family. Discuss.
colorlessquark
Posts: 6
Joined: August 13th, 2020, 11:55 pm

Advance Techniques for Resolving Circular Dependancies?

Post by colorlessquark » August 14th, 2020, 1:33 am

I'm trying to plot out the mechanics for a Zelda clone (mostly, just messing with seeing if I can make the mechanics right now) and am banging my head against a wall probably too hard) to resolve an issue involving some circular dependencies. I'm considering a structure like this (warning, I have no idea if this is a good enough way to get my design across simply):
ShittySchematic.png
(11.76 KiB) Not downloaded yet
Room class objects are meant to handle coordination between objects (e.g. hit detection mostly)

Entities classes will spawn Attack classes and probably contain them in their own list (which Room will have to lookup), and hit detection will happen in the room, easy peasy. Except, if I use Link to the Past as inspiration, a "hit" on an enemy can also have a rebound on it's source (sword hitting an electric blob causes the attacker to take damage, but only the target of the attack will be able to identify this case). The obvious fix is that an Attack keeps a reference to its source (As an "Entity", regardless of it being a Character or an Enemy), which also allows hit detection to do some IFF (ie. enemy attacks mostly do not hurt other enemies).

That means I have Entity.h aware of Attack.h, which needs access to the Entity class as well. The thing I probably ought to do is therefore just forward declare the Entity class in Attack.h. But I reviewed the video about circular dependence and there was a vague reference to "other Advanced techniques", which I'm not sure what those are. Well, except for maybe a better design, which I'd like to know as well.

So looking for ideas from you guys. Should I 1) Shut up and forward declare; 2) Learn some new technology; 3) Reconfigure the logic design?

Bonus: I will probably want certain objects to have the ability to spawn attacks in the future. This means attack holding a reference to an Entity will eventually fail me, and I'm not at all sure how to handle that, so I'd accept ideas on that as well.

albinopapa
Posts: 4373
Joined: February 28th, 2013, 3:23 am
Location: Oklahoma, United States

Re: Advance Techniques for Resolving Circular Dependancies?

Post by albinopapa » August 14th, 2020, 3:13 am

Use modules LOL.

Nah, forward declarations work fine, as long as there are no misspellings between the forward declared name and the concrete name.

An option that I like to use a lot is having a bridge class that works between Entity and Action or Room and Entity or Room and Action. So for instance, Room holds a collection of Objects ( decorations, moveable and immovable blocks, and enemies ) and you cannot pass through these blocks. Leave the responsibility of keeping track of those objects to the Room, but collision handling and resolution could be a separate class that knows about the Room class and all the other collidable objects, but they won't have to know about the bridge class that tests for collision. This means that Entity doesn't need to know about Room at all.

As far as Action, forward declarations may be the best option. If you have to pass an Entity to Action::PerformActionOn( Entity& entity ), and Entity stores a std::vector<std::unique_ptr<Action>>, then you will have to #include one or the other in the header, and forward declare the other and #include in source file ( cpp ).

Unless you do the same thing,

Code: Select all

#include "Action.h"
#include <memory>
#include <vector>
class Entity{
public:

private:
    std::vector<std::unique_ptr<Action>> actions;
};

#include "Action.h"
#include "Entity.h" 
#include "Collider.h"
// Some people really hate this method, but it works
struct EntityManager{
public:
    void Update( Collider& collider, float dt ){
        for( auto j = std::size_t{}; j < entities.size(); ++j ){
            for( auto i = j + 1; i < entities.size(); ++i ){
                if( collider.collides( (*entities[ j ] ), ( *entities[i] ) ) ){
                    PerformAction( ( *entities[j] ), ( *entities[i] ) );
                }
            }
        }
    }
    void PerformAction( Entity& lhs, Entity& rhs );

private:
    std::vector<std::unique_ptr<Entity>> entities;
};

#include "EntityManager.h"
#include "Collider.h"

class Room{
public:
    void Update( float dt ){
        entities.Update( collider, dt );
    }
private:
    EntityManager entities;
    Collider collider;
};
Another thing you can look into is the visitor pattern, which is pretty much designed to deal with the situation you're in.
If you think paging some data from disk into RAM is slow, try paging it into a simian cerebrum over a pair of optical nerves. - gameprogrammingpatterns.com

colorlessquark
Posts: 6
Joined: August 13th, 2020, 11:55 pm

Re: Advance Techniques for Resolving Circular Dependancies?

Post by colorlessquark » August 14th, 2020, 12:14 pm

I've been considering a bridge class for attacking after I posted this, and thought something that takes some sort of "attack signature" (includes "type" info, and a bunch of flags that indicate any special effects like stunning) and a "defense signature" (basically immunity flags) inspired by the bitwise operator video, but I'd like to see it be more robust than "yes/no" on effects/damage. What if some attacks are variable on the duration of a status? What if some enemies are "partially" resistant to statuses? Maybe it's better handled elsewhere, speaking of...

I'm not sure I understand your suggestion with the visitor pattern. I looked up some info on it, and it does not seem to address the issue with dependence. It might be good for extending new statuses, though! I don't quite "get" why it should be easier to maintain since it looks like you still have to effectively define each interaction anyway (not to mention, handling how it affects the Entity over a course of time which means some timer needs to exist somewhere).

But bridge classes are definitely a good idea. I was planning on ripping out the behaviors so they could exist separate from Entities and be played around with, but I didn't think of taking the collider logic out of the Room for some reason. But it makes sense- I'll want to collide in other places where I won't have a room.

albinopapa
Posts: 4373
Joined: February 28th, 2013, 3:23 am
Location: Oklahoma, United States

Re: Advance Techniques for Resolving Circular Dependancies?

Post by albinopapa » August 15th, 2020, 6:45 am

I usually respond to statements out of order...so...

Yes, collision detection and resolution should be it's own procedure at the very least.

Visitor pattern

The ideas is that Entity only needs to know about Visitor and Action can know about Entity, but the concrete actions HitEntityAction, StunEntityAction and OpenEntityAction are unknowns to Entity, so no circular dependency.

Those actions need to know about Action's and Entity: HitAction::do_action( Entity& agressor, Entity& victim );
KnockBack::do_action(Entity& aggressor, Entity& victim );
and so on.
If you think paging some data from disk into RAM is slow, try paging it into a simian cerebrum over a pair of optical nerves. - gameprogrammingpatterns.com

albinopapa
Posts: 4373
Joined: February 28th, 2013, 3:23 am
Location: Oklahoma, United States

Re: Advance Techniques for Resolving Circular Dependancies?

Post by albinopapa » August 15th, 2020, 6:46 am

If that doesn't make sense, just say so, i'll reply more coherently when I"m sober.
If you think paging some data from disk into RAM is slow, try paging it into a simian cerebrum over a pair of optical nerves. - gameprogrammingpatterns.com

colorlessquark
Posts: 6
Joined: August 13th, 2020, 11:55 pm

Re: Advance Techniques for Resolving Circular Dependancies?

Post by colorlessquark » August 15th, 2020, 6:37 pm

I think I mostly get it- enough to try it out at least. The things I don't understand is that Visitor Design is billed to extend functionality, but all I see is encapsulation. If I use HitEntity action, it basically can seal away the interface for doing damage (good encapsulation- if you want to change health it MUST go through there). But for StunEntity, the behavior is extended in time, so I can see this design encapsulating an interface that sets a flag or state of an entity to "Stunned", but the behavior of what that means will need to be defined elsewhere- either as function of Entity or some Behavior class that will control the Entity.

So do I have it right that Visitor Design is good for Encapsulation, but added truly new functions is always going to be a lot of work? In terms of writing code, it seems easier to have the Entity class (or maybe Behavior if I separate that out like I should) have a virtual Stun() member that has a default behavior, which special cases override.

albinopapa
Posts: 4373
Joined: February 28th, 2013, 3:23 am
Location: Oklahoma, United States

Re: Advance Techniques for Resolving Circular Dependancies?

Post by albinopapa » August 16th, 2020, 4:56 am

For effects that need to happen over several frames, setting a flag is definitely the way to do.

The next thing to look up is finite state machines. Setting up a simple enum class/struct with the various states an entity can be in should suffice.

So for the concrete visitor

Code: Select all

StunVisitor::DoAction( Stunnable& entity ){ entity.SetStunState( Stunnable:StunState:Start ); }
then in your entity

Code: Select all

Link::Update( Mouse& m, Keyboard& kbd, float dt ){
    switch( m_stun_state ){
        case StunState::Start:
            is_transparent = true;
            stun_timer = stun_duration;
            is_invincible = true;
            SetStunState( StunState::Flashing );
            break;
        case StunState::Stop:
            is_transparent = false;
            stun_timer = 0.f;
            is_invincible = false;
            break;
        case StunState::Flashing:
            flash_timer -= dt;
            stun_timer -= dt;
            if( flash_timer <= 0.f ){
                flash_timer = flash_duration;
                is_transparent = !is_transparent;
            }
            if( stun_timer <= 0.f{
                m_stun_state = StunState::Stop;
            }
    }
}
Finite state machines help keep state specific code together, something that I also suggest is for each case in the switch block for each state, put the code in their own functions. I usually do something like

Code: Select all

    switch( m_stun_state ){
        case StunState::Start:
           HandleStunStateStart( dt );
            break;
        case StunState::Stop:
            HandleStunStateStop( dt );
            break;
        case StunState::Flashing:
            HandleStunStateFlashing( dt );
            break;
    }
If you think paging some data from disk into RAM is slow, try paging it into a simian cerebrum over a pair of optical nerves. - gameprogrammingpatterns.com

colorlessquark
Posts: 6
Joined: August 13th, 2020, 11:55 pm

Re: Advance Techniques for Resolving Circular Dependancies?

Post by colorlessquark » August 18th, 2020, 9:05 pm

I tried to implement the Visitor design and I run into circular dependencies again- though I think it's because of my particular implementation. The problem is "Entities create Attacks" and "Attacks are visitors to Entities". I think I could fix it if I use a single header file for Attacks and Entities, but that doesn't seem like good documentation management. Right now Attack.h contains the abstract Attack and two concrete implementations:

Code: Select all

#pragma once

#include "Vec.h"
#include "Rect.h"
#include "Graphics.h"

#include "Character.h"
#include "Enemy.h"

class Attack
{
public:
	//V Concrete Elements to be Visited V
	virtual void Afflict(Character& targ) = 0;
	virtual void Afflict(Enemy& targ) = 0;
	//^ Concrete Elements to be Visited ^

	Attack(const Vec<float> pos, const Vec<float> hBoxSize);
	Attack()
	{
		pos = { 0,0 };
		hitBoxSize = { 0,0 };
	}

	void Update(float dt);
	void Draw(Graphics& gfx, Color col) const;
	Rect<float> GetCollBox() const;

private:
	Vec<float> pos;
	Vec<float> hitBoxSize;
};

class SwordStrike : public Attack
{
public:
	SwordStrike(const Vec<float> pos, const Vec<float> hBoxSize)
		:Attack(pos, hBoxSize)
	{}

	void Afflict(Character& targ)
	{
		targ.TakeDamage(2);
	}
	void Afflict(Enemy& targ)
	{
		targ.TakeDamage(2);
	}
};

class SwordStun : public Attack
{
public:
	SwordStun(const Vec<float> pos, const Vec<float> hBoxSize)
		:Attack(pos, hBoxSize)
	{}

	void Afflict(Character& targ)
	{
		targ.TakeDamage(1);
		targ.Stun();
	}
	void Afflict(Enemy& targ)
	{
		targ.TakeDamage(1);
		targ.Stun();
	}
};
As far as I can tell, this is pretty standard definition of a "Visitor" class- it knows the Elements it will visit, and what to do with them, hence "Character.h" and "Enemy.h" (two concrete "Entities" are included.

But I have a problem managing attacks, because I am concerned with the Character's state interacting with certain attacks (e.g. an attack may end with a character state interruption), so I had Entities managing a vector of "Attacks" in Entity.h:

Code: Select all

#pragma once

#include "Vec.h"
#include<vector>
#include<memory>
#include<cassert>

class Entity
{
public:
	// V Accept Visitors and Break Encapsulation V
	virtual void OnHit(class Attack& attack) = 0;

	void TakeDamage(float hp)
	{
		assert(hp >=0);
		health -= hp;
	}

	void Stun(float duration = 2.0f)
	{
		if (!stun)
		{
			stun = true;
			stuntime = -duration;
		}
	}
	// ^ Accept Visitors and Break Encapsulation ^

protected:
	virtual ~Entity() = default;

	Entity(Vec<float> pos, Vec<float> vel, int health);

	(...data members...)
	std::vector<std::unique_ptr<class Attack>> attack;
};
This requires forward declaration which will result in concrete elements having problems (I think). Right now, only "Character"s have the ability to "make an attack", so in "Character.h":

Code: Select all

#pragma once

#include "Entity.h"

class Character : public Entity
{
public:
	Character(const Vec<float>& pos);
	
	// V Concrete Implementation of Visitors V
	void OnHit(class Attack& attack) override
	{
		attack.Afflict(*this);
	}
	// ^ Concrete Implementation of Visitors ^

private:
	void MakeAttack()
	{
		Vec<float> loc = { 0.0f, 0.0f };
		Vec<float> size = { 25.0f, 5.0f };

		attack.push_back(
			std::make_unique<class SwordStrike>(loc, size)
		);
	}
};
The forward declarations seem to be messing with using the Attack class in this way. The major problem for the Visitor pattern is "attack.Afflict(*this)". Intellisense complains "incomplete type is not allowed" if I try to call Afflict, which I take to mean the compiler will complain that "Yo, if you want to actually do something with this class, I need to know more than it exists". I don't see how I can resolve that, since reversing the implementation such that "Character" is forward declared in "Attack.h", then I won't be able to call "TakeDamage" in Attack.h.

I also get a much wordier error in MakeAttack, which I think is because the compiler may know "there is a class called SwordStrike", but it can't handle the construction without knowing more about the class.

albinopapa
Posts: 4373
Joined: February 28th, 2013, 3:23 am
Location: Oklahoma, United States

Re: Advance Techniques for Resolving Circular Dependancies?

Post by albinopapa » August 19th, 2020, 2:11 am

I think the problem is the concrete classes in Attack.h.

If they weren't there, then Character and Enemy could be forward declared in Attack.h and since the implementations to them aren't used in Attack::Afflict() a forward declaration is all you'd need.

Then in SwordStrike.h and SwardStun.h, you'd include
#include "Attack.h"
#include "Character.h"
#include "Enemy.h"

Character.h would include Entity which would include Attack.h
Enemy.h would include Entity which would include Attack.h
Attack.h would only have the concrete types forward declared forward declared
SwordStrike.h would include Attack.h, Character.h and Enemy.h
SwoardStun.h would include Attack.h, Character.h and Enemy.h
If you think paging some data from disk into RAM is slow, try paging it into a simian cerebrum over a pair of optical nerves. - gameprogrammingpatterns.com

albinopapa
Posts: 4373
Joined: February 28th, 2013, 3:23 am
Location: Oklahoma, United States

Re: Advance Techniques for Resolving Circular Dependancies?

Post by albinopapa » August 19th, 2020, 2:18 am

And yeah, if you are trying to use only a header file, you have to get creative with the order of things. If you split your code into header and source files, it makes things easier because you can forward declare in header, then #include in source file since cpp files are normally included anywhere.

One thing I've done which is not suggested, but it works is in a single header:
forward declare class A.
forward declare class B.
declare class A.
declare class B
define class A
define class B
This way, class A knows there's going to be a class B, so you can use references and pointers to B in A's declaration as well as return types from functions.
B knows about A's declarations as if it were from an #include
Defining A after declaring B means now you can actually create and use objects of B since it now knows about the interface of B ( as if B is included in A.cpp ).
And B get's to use A as normal.

Example:

Code: Select all

// SomeStuff.h
class A;
class B;

class A{
public:
    B make_b( int );
    void take_b( B&& b_donor );
private:
    int value = 0;
};

class B{
public:
    B( const A& a_ );
    A give_a();
private:
    A a;
};

B A::make_b( int val ){
    value = val;
    return B( *this )
}

void A::take_b( B&& donor ){
    *this = donor.give_a();
}

B::B( const A& a_ )
    :
    a(a_)
    {}
    
A B::give_a(){
    return a;
}
If you think paging some data from disk into RAM is slow, try paging it into a simian cerebrum over a pair of optical nerves. - gameprogrammingpatterns.com

Post Reply