AI: Steering and Pathfinding
We are now going to start the AI labs. The first one will focus on movement and the decision making around movement. This will involve defining steering behaviours and pathfinding. This lab will use the engine as it stands at the end of practical 6. That is, we will be using scenes and components to build our solution. You will need the standard set up for a new lab. Our addition to the CMakeLists.txt is as follows.
file(GLOB_RECURSE SOURCES lab_7_steering/*.cpp lab_7_steering/*.h)
file(GLOB_RECURSE CMPNTS lab_7_steering/components/*.cpp lab_7_steering/components/*.h)
file(GLOB_RECURSE SCENES lab_7_steering/scenes/*.cpp lab_7_steering/scenes/*.h)
add_executable(LAB_7_STEERING ${SOURCES} ${CMPNTS} ${SCENES} ${RESOURCE_FILES})
target_link_libraries(LAB_7_STEERING lib_engine)
set(EXECUTABLES ${EXECUTABLES} LAB_7_STEERING)
You will also need the components from practical 6 copied across.
Steering Behaviours
We are going to split the lab into two parts. First we will focus on steering behaviours. We will need to do some setup first to get a menu system up and running and using that to drive our high-level game logic.
Setting Up
The first item we will add is a MenuSystem
class. First we will add a scene_menu.h
file to our scenes
folder of our project. This is below. The definition is just a standard scene.
//"scene_menu.h"
#pragma once
#include "engine.h"
class MenuScene : public Scene {
public:
MenuScene() = default;
~MenuScene() = default;
void Load() override;
void Update(const double& dt) override;
};
Our implementation (scene_menu.cpp
in the scene
folder) is below.
//"scene_menu.cpp"
#include "scene_menu.h"
#include "../components/cmp_text.h"
#include "../game.h"
#include <SFML/Window/Keyboard.hpp>
using namespace std;
using namespace sf;
void MenuScene::Load() {
auto txt = makeEntity();
auto t = txt->addComponent<TextComponent>(
"Movement Demos\nPress 1 for Steering\nPress 2 for Pathfinding");
setLoaded(true);
}
void MenuScene::Update(const double& dt) {
if (sf::Keyboard::isKeyPressed(Keyboard::Num1)) {
...
} else if (sf::Keyboard::isKeyPressed(Keyboard::Num2)) {
...
}
Scene::Update(dt);
}
The MenuScene
will display a TextComponent
which will give two options:
- Steering Behaviour Demo
- Pathfinding Demo
The Update
checks which button is pressed and has the outline for the code to handle this change. Once we have our other scenes in place we will add the new lines of code.
We will use a game.h
file to allow easy access to the scenes. This is given below.
//"game.h"
#pragma once
#include "scenes/scene_menu.h"
extern MenuScene menu;
And finally we need our main.h
file. This is below.
//"main.h"
#include "engine.h"
#include "game.h"
using namespace std;
MenuScene menu;
SteeringScene steeringScene;
PathfindingScene pathfindingScene;
int main() {
Engine::Start(1280, 720, "Steering", &menu);
return 0;
}
You should be able to run this now and get the output shown
SteeringScene
Before moving onto steering behaviours properly let us set up our SteeringScene
. We are going to have an object on the screen (represented by a red dot) that the player will move around using the arrow keys. To do this we will create a basic BasicMovementComponent
. You can replace this with the physics movement component from last practical if you wish. The cmp_basic_movement.h
and cmp_basic_movement.cpp
files are provided below. You should be able to understand the basic premise by now.
//"cmp_basic_movement.h"
#pragma once
#include <ecm.h>
// A component to allow basic movement behaviour
class BasicMovementComponent : public Component {
protected:
// Speed we can travel
float _speed;
// Checks if the move is valid.
bool validMove(const sf::Vector2f&);
public:
// Will check the keyboard and move the component's parent.
void update(double) override;
// Moves the component's parent.
void move(const sf::Vector2f&);
// Moves the component's parent.
void move(float x, float y);
// Component does not need rendered.
void render() override {}
// Used to create the component for an entity
explicit BasicMovementComponent(Entity* p);
BasicMovementComponent() = delete;
};
//"cmp_basic_movement.cpp"
#include "cmp_basic_movement.h"
#include <SFML/Window/Keyboard.hpp>
#include <engine.h>
using namespace sf;
using namespace std;
// Checks keyboard and moves the player.
void BasicMovementComponent::update(double dt) {
Vector2f direction(0.0f, 0.0f);
if (Keyboard::isKeyPressed(Keyboard::Left)) {
direction.x -= 1.0f;
}
if (Keyboard::isKeyPressed(Keyboard::Right)) {
direction.x += 1.0f;
}
if (Keyboard::isKeyPressed(Keyboard::Up)) {
direction.y -= 1.0f;
}
if (Keyboard::isKeyPressed(Keyboard::Down)) {
direction.y += 1.0f;
}
move(normalize(direction) * _speed * (float)dt);
}
// Initialises the component.
BasicMovementComponent::BasicMovementComponent(Entity* p)
: _speed(100.0f), Component(p) {}
// Checks if the proposed move is valid.
bool BasicMovementComponent::validMove(const sf::Vector2f& pos) {
if (pos.x < 0.0f || pos.x > Engine::GetWindow().getSize().x ||
pos.y < 0.0f || pos.y > Engine::GetWindow().getSize().y) {
return false;
}
return true;
}
// Moves the component's parent.
void BasicMovementComponent::move(const sf::Vector2f& p) {
auto new_pos = _parent->getPosition() + p;
if (validMove(new_pos)) {
_parent->setPosition(new_pos);
}
}
// Moves the component's parent.
void BasicMovementComponent::move(float x, float y) {
move(Vector2f(x, y));
}
Our SteeringScene
will add an Entity
with this component attached. We first need to define the scene_steering.h
file which is given below.
//"scene_steering.h"
#pragma once
#include "engine.h"
class SteeringScene : public Scene {
public:
void Load() override;
void UnLoad() override;
void Update(const double& dt) override;
void Render() override;
};
The key behaviour we can add at the moment will be defined in the Load
method. Here we will create an Entity
and add a BasicMovementComponent
. scene_steering.cpp
is given below.
//"scene_steering.cpp"
#include "scene_steering.h"
#include "../components/cmp_sprite.h"
#include "../components/cmp_basic_movement.h"
#include <LevelSystem.h>
#include <random>
#include <chrono>
using namespace std;
using namespace std::chrono;
using namespace sf;
void SteeringScene::Load(){
auto player = makeEntity();
player->setPosition(Vector2f(Engine::GetWindow().getSize().x / 2,
Engine::GetWindow().getSize().y / 2));
auto s = player->addComponent<ShapeComponent>();
s->setShape<CircleShape>(10.0f);
s->getShape().setFillColor(Color::Red);
player->addComponent<BasicMovementComponent>();
}
void SteeringScene::UnLoad() { Scene::UnLoad(); }
void SteeringScene::Update(const double& dt) { Scene::Update(dt); }
void SteeringScene::Render() { Scene::Render(); }
All we have to do now is add to game.h
as below.
``` {caption=”Updated game.h”} extern SteeringScene steeringScene;
Update `MenuScene::Update` as below.
``` {caption="Updated MenuScene::Update method"}
void MenuScene::Update(const double &dt){
if (sf::Keyboard::isKeyPressed(Keyboard::Num1)){
// New line
Engine::ChangeScene(&steeringScene);
}
else if (sf::Keyboard::isKeyPressed(Keyboard::Num2)){
}
Scene::Update(dt);
}
And then add a line SteeringScene steeringScene;
to main.cpp
. You should now be able to run the game, select option 1 and control the red dot
SteeringOutput
As discussed in the lecture, the first part we need is the SteeringOutput
type. We will define this in steering.h
. This is given below.
//"steering.h"
#pragma once
#include <engine.h>
// Output from a steering behaviour.
struct SteeringOutput
{
// Direction of travel.
sf::Vector2f direction;
// Rotation of travel.
float rotation;
};
We defined these values in the lecture. They allow us to move or rotate on the basis of a steering behaviour.
SteeringBehaviour
SteeringBehaviour
we also defined in the lecture. This is a virtual
class. The important part is the getSteering
method. Add the below to the steering.h
file.
//"steering.h"
// Base class for steering behaviour
class SteeringBehaviour{
public:
virtual ~SteeringBehaviour() = default;
// Gets the output from a steering behaviour.
virtual SteeringOutput getSteering() const noexcept = 0;
};
We will build two steering behaviours – seek and flee. They are fairly easy as the following sections will show.
Seek
Seek
is the behaviour to move an object to a target. We described the general idea in the lecture. The following is the definition to add to steering.h
.
//"steering.h"
// Seek steering behaviour
class Seek : public SteeringBehaviour{
private:
Entity* _character;
Entity* _target;
float _maxSpeed;
public:
Seek() = delete;
Seek(Entity *character, Entity *target, float maxSpeed)
: _character(character), _target(target), _maxSpeed(maxSpeed) { }
SteeringOutput getSteering() const noexcept;
};
The only part we need to define is the Seek::getSteering
method. We will do this in a new file – steering.cpp
. This is given below.
//"steering.cpp"
#include "steering.h"
using namespace sf;
SteeringOutput Seek::getSteering() const noexcept{
SteeringOutput steering;
steering.direction = _target->getPosition() - _character->getPosition();
steering.direction = normalize(steering.direction);
steering.direction *= _maxSpeed;
steering.rotation = 0.0f;
return steering;
}
Flee
Flee is likewise simple to implement. We add the declaration to steering.h
and then the definition to steering.cpp
. These are given below.
//"steering.h"
class Flee : public SteeringBehaviour{
private:
Entity* _character;
Entity* _target;
float _maxSpeed;
public:
Flee() = delete;
Flee(Entity *character, Entity *target, float maxSpeed)
: _character(character), _target(target), _maxSpeed(maxSpeed) { }
SteeringOutput getSteering() const noexcept;
};
//"steering.cpp"
SteeringOutput Flee::getSteering() const noexcept{
SteeringOutput steering;
steering.direction = _character->getPosition() - _target->getPosition();
steering.direction = normalize(steering.direction);
steering.direction *= _maxSpeed;
steering.rotation = 0.0f;
return steering;
}
SteeringComponent
We will now add a new component that will control the AI movement. This we will call SteeringComponent
and we will define it in cmp_ai_steering.h
as below.
//"cmp_ai_steering.h"
#pragma once
#include <ecm.h>
#include "../steering.h"
class SteeringComponent : public Component{
protected:
Seek _seek;
Flee _flee;
Entity *_player;
bool validMove(const sf::Vector2f&) const;
public:
void update(double) override;
void move(const sf::Vector2f&);
void move(float x, float y);
void render() override { }
explicit SteeringComponent(Entity *p, Entity *player);
SteeringComponent() = delete;
};
The component is similar to the other movement ones we have defined and you could reuse those as well if you wanted. The key behaviour is really in update
. This is defined in cmp_ai_steering.cpp
(as well as other methods). See this below.
//"cmp_ai_steering.cpp"
#include "cmp_ai_steering.h"
using namespace sf;
void SteeringComponent::update(double dt) {
// If target (player) is more than 100 pixels away seek
if (length(_parent->getPosition() - _player->getPosition()) > 100.0f) {
auto output = _seek.getSteering();
move(output.direction * (float)dt);
}
// If target (player) is less than 50 pixels away flee
else if (length(_parent->getPosition() - _player->getPosition()) <
50.0f) {
auto output = _flee.getSteering();
move(output.direction * (float)dt);
}
}
SteeringComponent::SteeringComponent(Entity* p, Entity* player)
: _player(player), _seek(Seek(p, player, 100.0f)),
_flee(Flee(p, player, 100.0f)), Component(p) {}
bool SteeringComponent::validMove(const sf::Vector2f& pos) const {
if (pos.x < 0.0f || pos.x > Engine::GetWindow().getSize().x ||
pos.y < 0.0f || pos.y > Engine::GetWindow().getSize().y) {
return false;
}
return true;
}
void SteeringComponent::move(const sf::Vector2f &p){
auto new_pos = _parent->getPosition() + p;
if (validMove(new_pos)){
_parent->setPosition(new_pos);
}
}
void SteeringComponent::move(float x, float y){ move(Vector2f(x, y));}
The key behaviour in update
is a check on how far the entity
(_parent
) is from the player. If it is more than 100 pixels the component will use seek. If it is less than 50 it will flee. Now all we need to do is add some enemies with this behaviour. We do this back in the SteeringScene::Load
method as below.
//"Updated SteeringScene::Load"
void SteeringScene::Load()
{
auto player...
...
// New lines for here.
// Setup C++ random number generation
random_device dev;
default_random_engine engine(dev());
uniform_real_distribution<float> x_dist(0.0f,
Engine::GetWindow().getSize().x);
uniform_real_distribution<float> y_dist(0.0f,
Engine::GetWindow().getSize().y);
for (size_t n = 0; n < 100; ++n) {
auto enemy = makeEntity();
enemy->setPosition(Vector2f(x_dist(engine), y_dist(engine)));
auto s = enemy->addComponent<ShapeComponent>();
s->setShape<RectangleShape>(Vector2f(10.0f, 10.0f));
s->getShape().setFillColor(Color::Blue);
enemy->addComponent<SteeringComponent>(player.get());
}
}
You can now run the application and you will have the enemies chase and run away from you accordingly. It should like the video at the top.