Implementing Movement using Vectors

Learn some more vector math, and see how we can use it to implement gameplay movement.
This lesson is part of the course:

Game Dev with SDL2

Learn C++ and SDL development by creating hands on, practical projects inspired by classic retro games

Free, Unlimited Access
3D art of a teacher in a classroom
Ryan McCombe
Ryan McCombe
Posted

Note: if following along with this chapter, it will be useful to have a relatively thorough implementation of the Vector3 struct. An example is available here:

#pragma once

struct Vector3 {
  float x;
  float y;
  float z;
  
  float GetLength() const {
    return std::sqrt(x * x + y * y + z * z);
  }
  
  float GetDistance(const Vector3& Other) const {
    using namespace std;
    return sqrt(
      pow(x - Other.x, 2) +
      pow(y - Other.y, 2) +
      pow(z - Other.z, 2)
    );
  }
  
  Vector3 operator+ (const Vector3& Other) const {
    return Vector3 {
      x + Other.x,
      y + Other.y,
      z + Other.z
    };
  }
  
  Vector3 operator- (const Vector3& Other) const {
    return Vector3 {
      x - Other.x,
      y - Other.y,
      z - Other.z
    };
  }
  
  Vector3 operator* (float Multiplier) const {
    return Vector3 {
      x * Multiplier,
      y * Multiplier,
      z * Multiplier
    };
  }
  
  Vector3 operator/ (float Divisor) const {
    return Vector3 {
      x / Divisor,
      y / Divisor,
      z / Divisor
    };
  }
  
  Vector3 operator+= (const Vector3& Other) {
    x += Other.x;
    y += Other.y;
    z += Other.z;
    return *this;
  };
  
  Vector3 operator-= (const Vector3& Other) {
    x -= Other.x;
    y -= Other.y;
    z -= Other.z;
    return *this;
  };
  
  Vector3 operator*= (float Multiplier) {
    x *= Multiplier;
    y *= Multiplier;
    z *= Multiplier;
    return *this;
  };
  
  Vector3 operator/= (float Divisor) {
    x /= Divisor;
    y /= Divisor;
    z /= Divisor;
    return *this;
  };
  
  Vector3 operator- () const {
    return Vector3 {-x, -y, -z };
  };
};

This struct is similar to what we've been creating in previous chapters. The only difference is that we've made sure it is const correct. All function arguments that are references are marked as const, and class functions that do not modify the underlying object are also marked as const.

Why some operators are marked as const but others are not

A common point of confusion here is why an operator like -- cannot be marked as const but the unary - can (and should) be marked as const

This is simply a question of what the operators actually do to the object they are called on. The -- operator modifies the object it is called on; the - operator does not.

An expression like --MyVector modifies MyVector. This operator changes the values of x, y and z variables of MyVector, therefore it cannot be marked as const.

An expression like -MyVector does not modify MyVector. The operator creates and returns a new vector object, with different x, y and z values. Because it does not modify the original vector on which it is used, this operator should be marked as const.

Movement Vectors

Vectors have more uses than storing where our objects currently are - they can also be used to calculate and store movements.

By storing a movement as vector, we can determine and visualise both the direction and distance of that movement.

A diagram showing how movement can be represented by a vector

To calculate an objects position after applying this movement, we add the movement vector to its current position. For example, imagine our character was initially at position { 5, 1 } and a movement vector of { 1, 3 } was applied.

The character's new position will be the result of adding those two vectors: { 5+1, 1+3 } which is { 6, 4 }

A diagram showing how position can be determined by vector addition

We can add a function to our class to make that happen:

#include "Vector3.h"

class Character {
public:
  Vector3 WorldPosition { 3.0, 1.0, 0.0 };
  void Move(const Vector3& Movement) {
    WorldPosition += Movement;
  }
};

Character MyCharacter;
Vector3 MovementVector { 3.0, 2.0, 0.0 };
MyCharacter.Move(MovementVector);

Limiting Movement Speed

Typically, we don't want our objects to be able to immediately move to a position. Rather than teleporting, we often want characters to move towards a location over time. For example, it might take a character 3 turns, or 100 frames to reach their destination.

One way we could implement this is through adding a MovementSpeed variable to our Character class. Then, we can implement a movement function such that it respects these limits.

To set this up, we need to take three steps:

  1. Create a normalised vector from the movement vector
  2. Multiply the normalised vector by the character's MovementSpeed to create a new vector
  3. Add this new vector to the character's WorldPosition

1. Normalising Vectors

We previously saw how we could calculate the length (or magnitude) of a vector using the Pythagorean theorem.

x2+y2+z2\sqrt{x^2 + y^2 + z^2}

We made this available as the GetLength function within our struct.

Normalising a vector means finding another vector that has the same direction, but a length of 1.

A diagram illustrating normalised vectors

To do this, we calculate the length of the movement vector, and then divide the vector by this number.

Lets imagine we have the vector { 234.0, 105.0, 279.0 }. Applying the pythagorean theorem, we'd find the length of this vector turns out to be 379.0.

So, to normalise this vector, we divide it by 379.0. This yields approximately { 0.617, 0.277, 0.736 }. If we apply the pythagorean theorem to calculate the length of this new vector, we'd find it to be approximately 1.0, as planned.

That means { 0.617, 0.277, 0.736 } is the normalised form of { 234.0, 105.0, 279.0}

Lets give our Vector3 struct the ability to normalise Vector3 objects:

struct Vector3 {
  float x;
  float y;
  float z;
  
  Vector3 Normalise() const {
    return *this / GetLength();
  }
  
  // Rest of struct
}

Unit Vectors

If a vector has a length of 1, it is sometimes called a unit vector.

For example, { 1.0, 0.0, 0.0 } and { 0.0, 1.0, 0.0 } are unit vectors. The vector we calculated in this section - { 0.617, 0.277, 0.736 } - is also a unit vector.

2. Limiting Movement Speed

Lets update our Character objects with a new MovementSpeed variable. We can then calculate our final movement vector by multiplying the normalised input vector with the character's maxiumum MovementSpeed.

This ensures the character moves in the same direction requested by the input vector, but doesn't move further in that direction than their movement speed allows:

class Character {
public:
  Vector3 WorldPosition { 2.0, 6.0, 0.0 };
  float MovementSpeed { 10.0 };

  void Move(const Vector3& Movement) {
    Vector3 MovementVector =
      Movement.Normalise() * MovementSpeed;
  }
};

3. Updating World Position

Finally, our last step is to add this new movement vector to the character's WorldPosition, thereby making the character move:

class Character {
public:
  Vector3 WorldPosition { 2.0, 6.0, 0.0 };
  float MovementSpeed { 10.0 };

  void Move(const Vector3& Movement) {
    Vector3 MovementVector =
      Movement.Normalise() * MovementSpeed;
    WorldPosition += MovementVector;
  }
};

In this code, the character will always move the maximum distance permitted by their MovementSpeed. This means there is a bug when the character is told to move a distance shorter than their movement speed: they will overshoot the target.

We can check for this scenario, and fix this with an if statement. If the length of the input vector is less than or equal to the character's maximum MovementSpeed, we can just move the full length of that vector immediately:

void Move(const Vector3& Movement) {
  // Can the character reach their destination this frame?
  if (Movement.GetLength() <= MovementSpeed) {
    WorldPosition += Movement;
    return;
  }
  Vector3 MovementVector =
    Movement.Normalise() * MovementSpeed;
  WorldPosition += MovementVector;
}

Moving Towards a Target

Having our Move function require a movement vector be provided is not especially friendly to anyone using our code.

Supporting an instruction like "move towards this other object" is going to be more useful that "move along this vector".

If we have two points in space, represented by the vectors A and B. The vector that moves from A to B is the result of subtracting A from B, which is B - A

A diagram showing how a vector can be used to represent movement towards a target

Lets use this to add the more useful MoveTowards function to our Character:

class Character {
public:
  Vector3 WorldPosition { 2.0, 6.0, 0.0 };
  float MovementSpeed { 10.0 };

  void Move(const Vector3& Movement) {
    if (Movement.GetLength() < MovementSpeed) {
      WorldPosition += Movement;
      return;
    }
    Vector3 MovementVector =
      Movement.Normalise() * MovementSpeed;
    WorldPosition += MovementVector;
  }
  
  void MoveTowards(const Character& Target) {
    Move(Target.WorldPosition - WorldPosition);
  }
};

Moving Away From a Target

We might want to make an evasive archer character that prefers to fight at range, moving away from her targets.

To make a character move away from a target, we can simply invert the order of operations:

void MoveAway(const Character& Target) {
  Move(WorldPosition - Target.WorldPosition);
}

This works because of a useful property of vectors: SomeVector and -SomeVector have the same length, but point in opposite directions.

With movement mechanics now available to use, we can create much more dynamic combat simulations.

Currently, every time we run our program, the results will be the same. In the next section, we will introduce a way to create randomness.

Was this lesson useful?

Part 2 - Available Now

Professional C++

Advance straight from the beginner course, dive deeper into C++, and learn expert workflows

Screenshot from Warhammer: Total War
Screenshot from Tomb Raider
Screenshot from Jedi: Fallen Order
Ryan McCombe
Ryan McCombe
Posted
sdl2-promo.jpg
This lesson is part of the course:

Game Dev with SDL2

Learn C++ and SDL development by creating hands on, practical projects inspired by classic retro games

Free, Unlimited Access
sdl2-promo.jpg
This lesson is part of the course:

Game Dev with SDL2

Learn C++ and SDL development by creating hands on, practical projects inspired by classic retro games

Free, unlimited access

This course includes:

  • 27 Lessons
  • 100+ Code Samples
  • 91% Positive Reviews
  • Regularly Updated
  • Help and FAQ
Part 2 - Available Now

Professional C++

Advance straight from the beginner course, dive deeper into C++, and learn expert workflows

Screenshot from Warhammer: Total War
Screenshot from Tomb Raider
Screenshot from Jedi: Fallen Order
Contact|Privacy Policy|Terms of Use
Copyright © 2024 - All Rights Reserved