Big Game Project – Camera movement script

The camera in our game is meant to feel familiar to people who have played games in the Real Time Strategy genre before. It can be moved around by the player in three different ways:

  • With the keyboard: WASD and arrow keys
  • By moving the cursor to the screen edges
  • By holding the middle mouse button and dragging

There is also a zoom function that lets the player zoom in and out using the mouse wheel. This works by simply moving the camera forward and backward, i.e. along its local Z-axis. (Local means that it takes into consideration the rotation of the transform. The camera is always pointing down towards the ground at 60 degrees.) The zoom is smoothed so as not to snap to the choppy rotation of the mouse wheel.

Below is the full Unity C# script:

using UnityEngine;
using System.Collections;

public class CameraMovement : MonoBehaviour 
{
    enum Directions
    {
        LEFT,
        RIGHT,
        FORWARD,
        BACKWARD,
    }

    public float moveSpeed = 10f;
    public float dragSpeed = 0.01f;
    public float zoomSpeed = 7f;
    public float maxZoom = 4f;
    public float borderMargin = 50f;
    public bool mouseMovementActive = true;

    Transform cameraTransform;
    Vector2 mousePrevPos = Vector2.zero;
    private float currentZoom = 0f;
    private float targetZoom = 0f;

    void Awake()
    {
        cameraTransform = Camera.main.transform;
    }

    void Update() 
    {
        UpdateKeysMovement();
        if (mouseMovementActive && !Input.GetMouseButton(2))
            UpdateMouseToBorderMovement();
        UpdateMousewheelMovement();
        UpdateZoom();
    }

    void MoveCamera(Directions direction)
    {
        switch (direction)
        {
            case Directions.LEFT:
                {
                    MoveCameraFreely(Vector3.right * -1 * moveSpeed * Time.deltaTime);
                    break;
                }
            case Directions.RIGHT:
                {
                    MoveCameraFreely(Vector3.right * moveSpeed * Time.deltaTime);
                    break;
                }
            case Directions.FORWARD:
                {
                    MoveCameraFreely(Vector3.forward * moveSpeed * Time.deltaTime);
                    break;
                }
            case Directions.BACKWARD:
                {
                    MoveCameraFreely(Vector3.forward * -1 * moveSpeed * Time.deltaTime);
                    break;
                }
        }
    }

    void MoveCamera(Vector2 movement)
    {
        Vector3 movement3D = new Vector3(-movement.x * dragSpeed, 0, -movement.y * dragSpeed);
        MoveCameraFreely(movement3D);
    }

    void MoveCameraFreely(Vector3 movement)
    {
        cameraTransform.Translate(movement, Space.World);
    }

    void Zoom(float value)
    {
        cameraTransform.Translate(Vector3.forward * value, Space.Self);
    }

    void UpdateMouseToBorderMovement()
    {
        Vector3 mousePos = Input.mousePosition;
        if (mousePos.x < borderMargin)
            MoveCamera(Directions.LEFT);
        if (mousePos.x > (Screen.width - borderMargin))
            MoveCamera(Directions.RIGHT);
        if (mousePos.y > (Screen.height - borderMargin))
            MoveCamera(Directions.FORWARD);
        if (mousePos.y < borderMargin)
            MoveCamera(Directions.BACKWARD);
    }

    void UpdateKeysMovement()
    {
        if (Input.GetKey(KeyCode.A) || Input.GetKey(KeyCode.LeftArrow))
            MoveCamera(Directions.LEFT);
        if (Input.GetKey(KeyCode.D) || Input.GetKey(KeyCode.RightArrow))
            MoveCamera(Directions.RIGHT);
        if (Input.GetKey(KeyCode.W) || Input.GetKey(KeyCode.UpArrow))
            MoveCamera(Directions.FORWARD);
        if (Input.GetKey(KeyCode.S) || Input.GetKey(KeyCode.DownArrow))
            MoveCamera(Directions.BACKWARD);
    }

    void UpdateMousewheelMovement()
    {
        if (Input.GetMouseButtonDown(2))
            mousePrevPos = Input.mousePosition;
        if (Input.GetMouseButton(2))
        {
            Vector2 mouseDelta = (Vector2)Input.mousePosition - mousePrevPos;
            Vector3 movement = new Vector3(-mouseDelta.x * dragSpeed, 0, -mouseDelta.y * dragSpeed);
            MoveCamera(mouseDelta);
            mousePrevPos = Input.mousePosition;
        }
    }

    void UpdateZoom()
    {
        float mousewheelDelta = Input.mouseScrollDelta.y / 10f;
        targetZoom += mousewheelDelta * zoomSpeed;
        targetZoom = Mathf.Clamp(targetZoom, 0, maxZoom);
        if (targetZoom == currentZoom)
            return;
        float delta = targetZoom - currentZoom;
        delta *= 0.5f;
        currentZoom += delta;
        float zoomValue = delta;
        Zoom(zoomValue);
    }
}
Advertisements

Big Game Project – First post

So it seems I never take time to update this blog except when mandatory; but when I do update, I actually really kinda like it.

Anyway, the course I’m taking now is called “Big Game Project” and the eponymous big game is a digital game with a production time of approximately 8 weeks. I’m in a group of six students and our game concept (which we started defining way before the course start) is a survival game inspired by the likes of Klei Entertainment’s “Don’t Starve” but with several playable settlers, which brings an RTS element to it. The settlers live on an island floating above a planet covered in dangerous jungle. The island is a home where buildings can be built and settlers are relatively safe but they need to be taken down into the hazardous jungle to gather resources for survival.

Two weeks have passed now, and we have a prototype that includes a few features we wanted to test first. I am very content with how the game looks and feels after two weeks of production, but I feel far from safe considering all the work we still have to do. My roles are programmer and sound designer, but so far I have barely started on the sound design. This is some of the programming I’ve done so far:

  • Camera movement
  • Camera switch between island and jungle
  • Implementing pathfinding into our settler and enemy AI
  • Construction mode, which lets the player place new buildings on the island
  • Pathfinding fix
  • Resource item spawning with a simple coded animation
  • Another big pathfinding fix
  • Enemy spawn systems
  • Even more pathfinding fixes
  • Placeholder healthbar and damage-taking effect
  • Did I mention pathfinding?

And the next thing on my task list is to bin all of our current pathfinding to replace it with Unity’s built-in system. The reason we hadn’t used this from the beginning is poor research regarding what Unity NavMeshes are capable of. So this is a rather grim end of the second week for me.

Anyway, the coming weeks I will try and make each blog post focus on a specific task I’ve worked on and hopefully that will make it more interesting.

Coding a main menu

This week has been a bit hectic. Lots to do and little time to do it. We’ve had some big features that feel absolutely necessary to implement for the final version of the game. One of those was a main menu from where the player can start the game and look at highscores, credits, and the control scheme. (And quit the application of course. This was a bit of a hassle to achieve, and the solution isn’t even very pretty. More about that later. Actually, probably not.) The different buttons in the menu are instances of the same class, GUIButton, separated by an Enum parameter. Their features include:

  • Clicked using the mouse, which triggers different functions depending on the kind of button
  • Changing the image when the cursor is on top of the button
  • Hiding the button and making it non-clickable if the player is currently in a sub menu where the button is not supposed to be shown

Since there are only two layers in the menu: the main screen, and the other screens, all that’s needed to navigate back to the main screen is a back button which always sends the player back to the main screen, regardless of which sub menu it’s in.

The system seems simple enough, but the getting all the logic right took me some time. The worst part is the clicking, because since we haven’t used a clever way to manage inputs, all we know is whether the mouse button is pressed or not. Only getting the code to perform only one menu button click when the mouse button is held means we need another member variable to store a boolean whether the button has been let go of or not between ticks. The system as it turned out is not perfect, with the main issue being that a button is clicked if the cursor is moved to the GUI button while the mouse button is held. But I figured it’s not big enough a problem to spend more time on the code. Included below is the complete Update() method from the MenuState class. I think it’s rather self-explanatory, and I included some comments to possibly clarify some things. The Draw() function is not very interesting, it basically just checks what sub menu the player is in, and draws different backgrounds, and then it checks which buttons are visible and draws those. I was going to add a screenshot of the menu, but for some reason my screenshots have started to capture Visual Studio and the Console window instead of the game screen; so here’s a picture of a fish instead. He is not amused.

Since made by one of our artists as a placeholder for the highscore screen, it’s actually perfectly related to the rest of the post.

 

bool MenuState::Update(float deltatime)
<pre>{
    // This stuff is to make the mouse button perform only one click when held
    if (!sf::Mouse::isButtonPressed(sf::Mouse::Left))
        m_mousePressed = false;

    // Set button border to NOT show
    for (int i = 0; i < m_buttons.size(); i++)
    {
        m_buttons[i]->SetHover(false);
    }

    for (int i = 0; i < m_buttons.size(); i++)
    {
        int buttonX = m_buttons[i]->GetPosition().x;
        int buttonY = m_buttons[i]->GetPosition().y;
        int buttonWidth = m_buttons[i]->GetWidth();
        int buttonHeight = m_buttons[i]->GetHeight();
        int mouseX = sf::Mouse::getPosition().x;
        int mouseY = sf::Mouse::getPosition().y;
        ETYPE buttonType = m_buttons[i]->GetType();

        if (!m_buttons[i]->GetHidden())
        {
            
            if (mouseX >= buttonX && mouseX <= (buttonX + buttonWidth) && mouseY >= buttonY && mouseY <= (buttonY + buttonHeight)) // Horizontal and vertical
            {
                m_buttons[i]->SetHover(true);
                if (sf::Mouse::isButtonPressed(sf::Mouse::Left) == true && m_mousePressed == false)
                {
                    if (buttonType == START)
                    {
                        m_mousePressed = true;
                        return false; // Go to next State, which is the GameState
                    }

                    else if (buttonType == HIGHSCORE)
                    {
                        m_subMenu = HIGHSCORE_SCREEN;
                        for (int i = 0; i < 4; i++)
                        {
                            m_buttons[i]->SetHidden(true);
                        }
                        m_buttons[4]->SetHidden(false);
                        m_mousePressed = true;
                        return true;
                    }

                    else if (buttonType == CREDITS)
                    {
                        m_subMenu = CREDITS_SCREEN;
                        for (int i = 0; i < 4; i++)
                        {
                            m_buttons[i]->SetHidden(true);
                        }
                        m_buttons[4]->SetHidden(false);
                        m_mousePressed = true;
                        return true;
                    }

                    else if (buttonType == QUIT)
                    {
                        m_shutdown = true; // Will break the game loop in Engine
                    }

                    else if (buttonType == BACK)
                    {
                        m_subMenu = MAIN_SCREEN;
                        for (int i = 0; i < 4; i++)
                        {
                            m_buttons[i]->SetHidden(false);
                        }
                        m_buttons[4]->SetHidden(true);
                        m_mousePressed = true;
                        return true;
                    }
                }
                return true;

            }
        }
    }

    return true;
}

Creating Colliders by Magic

So we realized that our wall colliders were too many to be efficient. Each wall segment the size of one tile had its own collider which was created by the tile itself at the time of its instantiation. Every wall instance knew what kind of wall segment it was (information that was also used for determining which texture to load), and shaped the collider accordingly. (We had some struggles here because the shape of certain wall segments called for a more complex collider than a rectangle. It could have been achieved with two rectangular colliders, but this would have caused further problems with looping through all wall colliders when using them for collision checking; so we stuck with only one for each segment but adjusted the size of some colliders until there were no gaps.) In the picture below, the colliders can be seen visualized in yellow. Note that the angled perspective entails the colliders being placed at the bottom of the walls.

old_colliders_EDIT2

The point is that many colliders in a row just as well could be merged into one big collider to greatly reduce the number of collision calculations to be done. I coded a function that looks at the tile map and creates the colliders separately from the walls, storing pointers to them in a separate std::vector for later access. The result looks like below.

What it basically does is finding the end segments and creating colliders between them. It does so by looping through the walls looking at one at a time, starting at the upper left corner. The tile map can be seen below.

First of all, we copy the numbers in the tile map to an std::vector for easier access, like below. (The numbers in the tile map are actually separated by commas and it doesn’t contain any blank spaces like above. Those just make it align more prettily for a blog post.)

std::string number;
    for (int i = 0; i <= height - 1; i++)
    {
        for (int j = 0; j <= width - 1; j++)
        {
            std::getline(dataStream, number, ',');
            wallData[j + i * width] = stoi(number);
        }
    }

We do horizontal and vertical walls separately. The first round we do horizontal, and start by looking for any segment that works as a left end. (There are four different: 1, 3, 8 & 14.) After finding one, we step to the right until finding any segment that works as a right end (2, 4, 7 & 14). We then create a collider between these two tiles (the height being the constant value of a wall’s thickness). After this we continue from the rightmost tile and repeat the process of finding a left end segment and then a right end segment. When having reached the end of the row, we go to the row below, and so on. The code for this can be seen below:

int index = 0;
while (index < size)
{
    if (wallData[index] == 1 || wallData[index] == 3 || wallData[index] == 8 || wallData[index] == 14) // Search for left end segments
    {
        for (int search = index + 1; (search % width) != 0; search++) // Search until on the first position in a row, which means search while on the current row.
        {
            if (wallData[search] == 2 || wallData[search] == 4 || wallData[search] == 13 || wallData[search] == 7) // Search for right end segments
            {
                int colWidth, x, y, addLeft = 0, addRight = 0; // addLeft and addRight are to increase the width for the horizontal straight end pieces which are somewhat special
                if (wallData[index] == 14)
                    addLeft = 48;
                else
                    addLeft = 0;
                if (wallData[search] == 13)
                    addRight = 48;
                else
                    addRight = 0;
                colWidth = (search - index) * 128 + 32 + addLeft + addRight;

                x = (index % width) * 128 - 16 - addLeft;
                y = (floor(index / width) + 1) * 128 - 16; // floor() is just for clarity. Division between integers doesn't produce a remainder

                Collider* collider = new Collider(x, y); // Create the new collider
                collider->SetWidthHeight(colWidth, 32); // The height of horizontal walls is 32, the walls' thickness
                m_wall_colliders.push_back(collider); // Put the new collider in an std::vector for easy access
                index = search; // Set the position for the next search
                break;
            }
        }
    }
    index++; // Step one index to the right
}

 

Creating the vertical walls is very similar, except counting with adjacent positions vertically requires a bit more thinking than horizontally. For example, going one position down means adding the map’s width. I simplified the iteration so some unnecessary positions are checked, but this is insignificant in terms of optimization since it is still quite light and, most importantly, only performed at the start of each level.

index = 0;
<pre>while (index < size)
{
    if (wallData[index] == 1 || wallData[index] == 6 || wallData[index] == 2 || wallData[index] == 10)
    {
        for (int search = index; search < size; search += width)
        {
            if (wallData[search] == 3 || wallData[search] == 4 || wallData[search] == 11 || wallData[search] == 9)
            {
                int colHeight = (floor(search / width) - floor(index / width)) * 128 + 32;

                int x = (index % width) * 128 - 16;
                int y = floor((index / width) + 1) * 128 - 16;

                Collider* collider = new Collider(x, y);
                collider->SetWidthHeight(32, colHeight);
                m_wall_colliders.push_back(collider);
                index++;
                break;
            }
        }
    }
    index++;
}

Parsing text files exported from ‘Tiled’ software

This week was the week we decided to use the Tiled map editor software after all. Tiled is a level creator that exports data that can be read by the program to place the correct tiles at the correct places. The main advantage of this is that levels can be edited easily by someone unfamiliar with the code, and coding in general. Our first means of editing levels was to just write numbers manually in a text file, which kind of sucks, but is easy to read with code. We had previously decided against using Tiled because writing code for parsing the exported xml files seemed like more trouble than writing the level data text files manually; but then a wise man taught us how Tiled could also export normal text files.

These text files contain more data than we need and it also seemed like a good idea to split the data and save all we needed in different variables. It was up to me to write the code. The code we had simply read one text file (in which every tile was represented by a single character) and created the level immediately, which means the level was never copied from the text file which would have to be read again if the game was to revert to that level. The new system would also mean that three layers of tiles would be acquired, one for floors, one for walls, and one for all other objects like furniture and pickups. (We will possibly add a layer for patrol pattern nodes.) The text file structure can be seen in the picture below:

level textfile_for blog2

I cropped it for obvious aesthetic reasons; the missing part only contains two more “[layer]”s with different numbers in the map, and type “WALLS” and “Objects”, respectively.

The method LoadLevel() does the following:

  1. Finds the “width=30” line and saves the value to a variable.
  2. Does the same for the height.
  3. Finds the line “data=” and saves the tile map data to a string.
  4. Removes the line breaks from this string.
  5. Saves the string to a variable.
  6. Repeats steps 3 through 5 for the remaining two layers, storing them in different variables.
  7. Creates a new instance of class Level, sending all the acquired level data into its constructor.
  8. Returns a pointer to that instance of Level.

The complete method LoadLevel() with detailed comments can be found below. The main difficulty with this code was to make it look organized, which I think I partially failed. It would have been better to put the text file management in a new class with some operations having their own methods, to be able to reuse them and get a better structure.

 

{
    // All variables we need as parameters for Level
    int index = 1; // "Level 1"
    int width; // Level width, in number of tiles
    int height; // Level height, in number of tiles
    std::string floorData, wallData, objectData; // These will contain the tile map for each level

    // Read the map from the text file and put everything in a string
    std::ifstream levelFile(filepath);

    std::string str, levelData;
    while (std::getline(levelFile, str))
    {
        levelData += str;
        levelData.push_back('\n');
    }

    // Variables that will be needed
    std::size_t foundAt; // Data type "size_t" is a position in an std::string. It's used pretty much like an integer
    std::size_t position; // Will be the main position, only increasing in value since we start from the beginning and never go back
    std::size_t tempPos;
    std::string target;
    std::string tempString;

    //Start parsing levelData
    position = 0; // Beginning of string
    target = "[header]"; // Why don't I just search for "width=" ? I presumably planned a different, shinier approach for the parsing, that would work with slightly different looking text files.
    foundAt = levelData.find(target, position); // Search for the string "[header]" starting at the beginning of the string.
    position = foundAt + target.size() + 1; // Set position to the beginning of the next line. (foundAt is where the target string was found. target.size() is the length of target. + 1 is for going to the next line
    tempPos = levelData.find_first_of("\n", position); // Search for next line break
    tempString = levelData.substr(position, tempPos - position); // Create a temporary substring from the current line

    // More variables needed
    std::string strMain;
    std::string strResult;
    std::size_t pos;

    // Parse the substring for the value
    strMain = tempString; // Copy substring to new string. (Probably because I first planned to do this in a separate method.)
    pos = strMain.find_first_of("0123456789"); // Find the first digit
    while (pos != std::string::npos) // While not at end of string
    {
        strResult += strMain[pos]; // Add the digit
        pos = strMain.find_first_of("0123456789", pos + 1); // Find the next digit
    }

    if (!strResult.empty()) // If there are digits in the string
        width = stoi(strResult); // Save the string as an integer
    strResult.clear(); //Clear strResult for next use

    position = levelData.find("height", position); //Go to the line with "height" in it

    tempPos = levelData.find_first_of("\n", position); //Search for next line break
    tempString = levelData.substr(position, tempPos - position); //Create a temporary substring from the line with "height" in it

    // Here is the same parsing stuff as for width
    strMain = tempString;
    strResult.clear();
    pos = strMain.find_first_of("0123456789");
    while (pos != std::string::npos)
    {
        strResult += strMain[pos];
        pos = strMain.find_first_of("0123456789", pos + 1);
    }

    if (!strResult.empty())
        height = stoi(strResult);

    for (int i = 1; i <= 3; i++) // Once for each layer. This will be increased if we add more layers to the level
    {
        target = "data=";
        foundAt = levelData.find(target, position);
        position = (foundAt + target.size() + 1); // Go to beginning of the tile map

        tempPos = position;
        for (int i = 1; i <= height; i++) // Set tempPos to position, plus height number of lines down. This will encompass the whole map.
        {
            tempPos = levelData.find_first_of("\n", tempPos + 1);
        }

        tempPos++; // Get the last line break, too. Can't remember why...

        tempString = levelData.substr(position, tempPos - position); // Create a temporary string from the map

        // Loop through the map and delete all line breaks, as these will be an annoyance when reading this string and creating tiles based on the numbers
        for (std::size_t found = tempString.find("\n"); found != std::string::npos; found = tempString.find("\n"))
        {
            tempString.erase(found, 1);
        }

        if (i == 1) // If at first layer...
            floorData = tempString; // ...save in floor layer data container
        else if (i == 2) // If at second data layer...
            wallData = tempString; // ...save in wall layer data container
        else if (i == 3) // If at third data layer...
            objectData = tempString; // ... save in object layer data container

        position = tempPos; // Jump to the end of map, so the loop will search for the next "data="
    }

    Level* level = new Level(1, width, height, floorData, wallData, objectData); // Save level data in an instance of the Level class

    return level; // Return the pointer to the newly created Level instance
}

Work going forward. Slowly.

This week I have no interesting solutions for problems to write about. The planning says I am supposed to work on sound effects that will give the player feedback upon picking up an object. Each kind of pickup object will have its own sound: coin, armor, light, painting, bear pelt and vase. Unfortunately I have not had time to start on these sounds, because I’ve been thinking about implementing pathfinding and dynamic lighting, as well as working on a task from earlier that has been creeping forward into this week because we feel the result was unsatisfying. This task I’m talking about is the designing of the carpet footstep sounds, which I have problems making distinguishable enough among the other footstep sounds while still keeping them convincing. I was keeping in mind that “it doesn’t have to sound right, it only has to sound good”, but I have problems anyway because it seems that the sounds have to sound realistic to sound right. The samples I use are recorded from grass because they have more character than the ones actually recorded from carpet. Since the wood and stone footsteps sound realistic, the ears becomes confused when the carpet footsteps don’t. So I’m basically still experimenting with different effects on the carpet footsteps and bringing in the opinions of my groupmates in the process.

As mentioned, I have also put time into pathfinding and dynamic lighting. The lighting is implemented using code from another student. We have two main problems with this as implemented now. First of all, it is very slow. The framerate is visibly reduced a lot, which is obviously no good. Our first plan to amend this is to treat several straight wall segments as one long, hence reducing the number of calculations that have to be done. The second problem we have is that the lighting code is designed for strict 2D games, and our game has a slightly angled perspective. We have some ideas how to fix this, but we haven’t actually started implementing them yet. I reckon this will be one of those things that take way more time than anticipated.

The pathfinding is also a bit problematic because of the way our level looks. We don’t strictly divide it into tiles, which if we use the pathfinding method we know, as is, our enemy will not be able to walk everywhere he should, for example close to a wall. We have to consider how much this actually matters and how much work there will be to solve it.

Below is the waveform of one of the carpet footstep sounds; because pictures are very informative, I’ve been told:

Skärmklipp 2015-02-26 23.39.05

Implementing footstep sound effects

Last week I designed some footstep sound effects. One of the tasks I had this week was to implement them into the playable alpha version of our game. I started by cutting up the footstep sound file into 6 short files, each with a separate footstep. I wanted a system that plays sounds with a set interval that depends on the speed of the enemy, and I also wanted it to randomly select one of the 6 different files for each footstep. I solved this by adding variables to the enemy class that holds the position where the enemy was when it lasted played a footstep sound. Each tick, there is a test whether the distance between this position and the enemy’s current position is greater than a constant value which is the footstep length. If true, one of the 6 sounds is picked randomly and played, and the “previous position” coordinates are updated. The code in the update function looks like this:


    if (sqrtf(pow(m_x - m_prevPos.x, 2) + pow(m_y - m_prevPos.y, 2)) > footStepLength)
        {
            int random = static_cast<int>(round(rand() % m_stepSounds.size() + 1) - 1);
            playStepSound(FLOOR_WOOD_BASEMENT, random);
            m_prevPos.x = m_x;
            m_prevPos.y = m_y;
        }

 

The “playStepSound()” function takes two parameters, the first one is the type of floor, because this will ultimately affect how the footsteps sound although I haven’t implemented it yet. The second parameter is a randomly generated integer deciding which index of footsteps is to be played.

The next step was to make the sounds change depending on the positions of the enemy and the avatar. This was achieved with the built-in SFML effects for “spatialization”. There is one “Listener” object. This is basically like a pair of virtual ears for which the position can be set at any time. Every sound source is also given a position, and then every sound coming out of the speakers is automatically adjusted based on the relation between the positions of the sound source and the Listener respectively, as well as a few other factors. The “other factors” I actually used where the “attenuation” and “minimum distance” which basically decide how much, and by what factor, the sound is affected by distance. An example of a function I didn’t use is the Listener’s direction, because we do not want to hear the sounds from the avatar’s actual perspective as we would in a first-person perspective game.

So what I do in the code is updating the Listener’s position to the avatar’s. The attenuation and minimum distance of the sound sources only have to be set once for each sound. This is done directly after each sound is loaded into memory. What has to be updated for each sound source is its position. It would be unnecessary to update all the sounds’ positions each tick, so instead I made the position update for only the sound that’s about to be played, in the function where it’s played, like so:


void Enemy::playStepSound(EFloorType floorType, int index)
{
    switch (floorType)
    {
    case FLOOR_WOOD_NICE:
        m_stepSounds[index].setPosition(m_x, 0.0f, m_y);
            m_stepSounds[index].play();
            break;
    }
}


 

Note that the position is in 3D coordinates. This is just how the spatialization functions work. Since our game is in 2D I leave the y coordinate at zero. (The 3D coordinate system is defined so the 2D “y” becomes the 3D “z”.)

The biggest problem I had with the spatialization was that I realized only after like 2 hours that it doesn’t work at all with stereo sounds. The fix was as simple as to convert my sound files to mono.