Creating flexible behaviour without scripting?

Started by
4 comments, last by jHaskell 10 years, 9 months ago

I'm creating a simple shmup from scratch. This is how my base Enemy class looks like:


class Enemy
{
    public:
        Enemy(int ntype, double nx, double ny, double nvx, double nvy):
            type(ntype), x(nx), y(ny), vx(nvx), vy(nvy) {};
        virtual ~Enemy() {};

        void Move(){x+=vx;y+=vy;};
        virtual void Logic()=0;
        virtual void Draw();

        bool IsDead(){return health<0;}
        void ReduceHealth(int damage){health-=damage;}
        SDL_Rect GetLoc(){return loc;}
    protected:
        int type;
        int health;
        double x, y;
        double vx, vy;
        SDL_Rect loc, source;

        void GetVectorToPlayer(double &x, double &y);
        double GetAngleToPlayer();
};

The Logic() function is the main function that defines the enemies' behaviours. It can influence both its movements and shooting patterns. For example, it may look like this:


    double angle=GetAngleToPlayer();
    if(globalFrameCounter%120==0)  //will shoot every two seconds
        for(int i=-3;i<=3;i+=1)  //will shoot seven bullets in the player's direction
            bulletManager->AddEnemyBullet(B_BULLET1, x, y, 5, angle+i); //bullet sprite, location, velocity and direction

Finally, all the enemies are created on the vector via the manager, like this (I have only one BasicEnemy subclass for now, so the function will be expanded int the future):


void EnemyManager::AddEnemy(int type, double x, double y, double vx, double vy)
{
    enemies.push_back(std::unique_ptr<BasicEnemy>(new BasicEnemy(type, x, y, vx, vy)));
}

Now, my problem is, what if I want to have a lot of different types of enemy behaviours. In the current state, it would force me to write a separate subclass with a new Logic() function for every possible behaviour. That's obviously the wrong approach, since it would force me to create tens and tens of subclasses. I also don't want to use any scripting language like Lua, because I have no knowledge of them and it feels like using a sledgehammer to crack a nut.

So... is there an easier way that I'm not seeing?

Advertisement

You could separate it out to an "EnemyAI" sort of class, that chooses the right behavior based on the AI (it can have states for example and change state depending on surroundings), which is then applied to the enemy. Then you would write these different states and corresponding behaviours, and the logic for choosing between a state.

o3o

For the shmup I created, I implemented an Enemy Action framework. Each enemy had a list of these actions it executed in order. Each individual action was a simple, specific action. Like an action to move to a specified location, an action to start firing weapons, an action to chase the player, an action to hold position for a period of time. Each ship had it's own list of actions to perform, so it wasn't difficult to give each and every ship in the game a completely different set of actions. The base action class looked something like this:


class ComputerAction
{
public:
    ComputerAction(void) { }

    //Methods to set up the hooks for this action
    virtual void SetShip(Ship* s) { }
    virtual void SetWeapon(WeaponSystem* g) { }
    virtual void SetTargetFunc(shipContainer *t) { }

    //common update method that has to be implemented by each action
    virtual bool Update(int DeltaTime) = 0;
};


And the ComputerControl class had the following Update method (Actions is a vector of ComputerAction pointers:


void ComputerControl::Update(int DeltaTime)
{
	if(Actions.size() == 0 || ActionIndex >= Actions.size()) return;

	if(Actions[ActionIndex]->Update(DeltaTime))
		ActionIndex++;

	//When Looping is enabled, check to reset our Index
	//If Looping is disabled, when we reach the end of our Action list
	//the ship is dead
	if(ActionIndex == Actions.size() )
	{
		if(Looping)
			ActionIndex = 0;
		else
			source->SetHealth(0);
	}

	//Call Weapon Update
	Gun->Update(DeltaTime);
}

And a method to add actions to it's list. This method was also responsible for setting up all the hooks the various actions needed.


void ComputerControl::AddAction(ComputerAction* a)
{
	//Add hooks to our Sprite and weapon so the Action can work with them
	a->SetShip(source);
	a->SetWeapon(Gun);
	a->SetTargetFunc(Game::GetPlayerList);
	//Add the action to our list
	Actions.push_back(a);
}

I then had derived action classes like ComputerActionMove, ComputerActionEnableFire, etc. that implemented the actual functionality. Each actions Update method returned false until it's Action was complete. ComputerActionMove only returned true when it had finally moved to it's target location (as specified by param1 and param2). ComputerController just stepped to the next action when the current one returned true.

An example derived class, ComputerActionMove:


class ComputerActionMove : public ComputerAction
{
	//Params and handles to the source and weapon associated with this action
	int Param1, Param2;
	Ship* source;
public:
	ComputerActionMove(void) : ComputerAction() {}
	ComputerActionMove(int p1, int p2) : ComputerAction(p1, p2) {}

	//Methods to set up the hooks for this action
	virtual void SetShip(Ship* s) { source = s; }

	virtual bool Update(int DeltaTime) override;
};

This implementation allowed for relatively easily setting up some fairly intricate patterns, and no two ships ever had identical patterns in my game, but it does have it's limitations. For me, the biggest one was no easy way for the enemy ships to react to any external triggers.

You could separate it out to an "EnemyAI" sort of class, that chooses the right behavior based on the AI (it can have states for example and change state depending on surroundings), which is then applied to the enemy. Then you would write these different states and corresponding behaviours, and the logic for choosing between a state.

agreed.

generally, it makes sense to separate most of the AI logic from the enemies themselves, but allow each the chance to do whatever they need, or resort to default behavior.

typically each AI state and behavior will be given its own specific methods, which will be supplied by the Entity class or similar. these methods will generally either implement entity-specific behavior or forward things to the EntityAI class or similar (which will typically implement core behaviors).

example states would be, say:

standing idly;

moving idly (say, following a path or wandering);

moving aggressively (hostile towards an enemy);

performing an attack;

...

various types of actions would be handled mostly via state transitions:

while standing or moving idly, if an enemy is seen, set the enemy as the current enemy and switch into aggressive mode;

if aggressive and the enemy is dead or hasn't been seen recently, return to being idle;

...

in some cases though, typically for smaller detail things (which don't need their own methods, but which may apply to a range of entities), flags may also sometimes make sense. these would be for things like if the AI is naturally hostile (attacks player on sight) or neutral (will attack if provoked), or if they are player-aligned (will not attack players, but may be neutral or hostile towards enemies), or if they will only attack the player vs any player-aligned entities, ... the specific entity class would then be responsible for setting up these flags.

while it is also possible to create a different type of AI class or similar for different types of AIs (rather than flags), this may be less flexible, and doesn't really as-easily allow for entities to change between different sets of behaviors (say: neutral vs hostile based on the players' current reputation, ...).

For the shmup I created, I implemented an Enemy Action framework. Each enemy had a list of these actions it executed in order. Each individual action was a simple, specific action. Like an action to move to a specified location, an action to start firing weapons, an action to chase the player, an action to hold position for a period of time.

Actually, I have implemented something similar - for bullets with complex movements. Every complex bullet has its own vector of instructions which are executed at specified time frames. It's not perfect, since I would also love to make a bullet spawn other bullets with an instruction. Aside from that, I'm very happy with this.

That's how it looks like:


//BasicEnemy::Logic()    

        for(int i=0;i<360;i+=15)
        {
            bulletManager->AddEnemyComplexBullet(B_BULLET1, x, y, basev, i-a, 0, 0);
            bulletManager->AddEnemyBulletData(45, BULLET_ANGULAR_VELOCITY, SET, 5);
            bulletManager->AddEnemyBulletData(45, BULLET_ACCEL, SET, -0.7);
            bulletManager->AddEnemyBulletData(55, BULLET_ANGULAR_VELOCITY, SET, 0);
            bulletManager->AddEnemyBulletData(55, BULLET_ACCEL, SET, 0);
        }

Anyway, that was the first thing I thought about when thinking about enemy behaviors, but unfortunately it won't work. That's simply because, no matter how complex the instruction is, it won't allow looping (...but I hope I'm wrong about that). If I want to shoot 100 bullets in a circle, writing 100 instructions for each shot makes no sense. That's exactly why I couldn't make the bullet spawn other bullets.

You could separate it out to an "EnemyAI" sort of class, that chooses the right behavior based on the AI (it can have states for example and change state depending on surroundings), which is then applied to the enemy. Then you would write these different states and corresponding behaviours, and the logic for choosing between a state.

I don't really need to use states and respond to any behavior, because I don't need the enemies to respond to anything whatsoever. I just need a clean way to set a behavior once, when creating the enemy or declare it when the program begins and then assign it to enemy during its creation.

(and as "behavior" I just mean either "at a given frame since creation, set/change velocity and direction" or "shoot a bullet/a lot of bullets in a given direction" OR "...with a random offset" OR "...in the player's direction" OR "in the player's direction with a random offset" OR ... )


If I want to shoot 100 bullets in a circle, writing 100 instructions for each shot makes no sense.

Do you really need it to be that specific. I can get that sort of effect with my various actions. First, I enable fire (and I can specify the rate of fire for this action, or if no rate is specified it'll use the default fire rate), which will just fire bullets at the rate specified until I disable fire. Then I enable spin, which can be enabled either for a specific duration (say 3 seconds), or a specific angular displacement (say 360 degrees). Once the enable spin action has completed, I disable fire. Based on how long it takes to complete the spin, I could set a fire rate that would give me close to 100 shots, but it may end up being 99, or 101. It's unlikely it'd be off by more than 1 shot, but even if it was off by 2 or 3, would that really make that much of a difference? The code to create this affect would look like this:


controller->AddAction(new ComputerActionEnableFire(250)); //Add a new enable fire action with a firing rate of 250msec
controller->AddAction(new ComputerActionEnableSpin(0, 360)); //first param is duration, second param is angular displacement
controller->AddAction(new ComputerActionDisableFire());

I would put a move action to a particular location before that secquence. I could also repeat that sequence at one or more other locations, though that's another minor downside of this particular framework. I'd have to repeatedly add those actions in between move actions to new locations. On the other hand, I could add entirely different sequences of actions before or after those ones to give a single ship a fairly complex pattern of actions as well.

This topic is closed to new replies.

Advertisement