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

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

Frogger project going forward

Of course I forgot to update when I meant to. The project is going forward. We now have correct background colors and the purple sidewalks are in place. Since we have a sprite sheet, we figured the easiest way to draw the sidewalks was to make a class for the sidewalk tile, and make several instances of that class. The frog can be moved using the arrow keys, but there is no animation. We decided to postpone implementation of all features that aren’t absolutely necessary.

We also have a moving car, but we need to develop the system that will handle the spawning of those cars. There was a discussion on this because problems arose: since every car is an instance of the same class, there is no simple way for the manager class to tell them apart, so the easiest way to handle the cars is to make each car move and get destroyed when it disappears outside the screen. It can send a message to the GameState class so it can keep track of how many number of cars are on the screen. The reason we were hesitant to use this method is that we used to draw objects in the same order they were created, and if the game destroys and creates new cars, the frog will no longer be drawn on top like we wanted to. So I changed our draw function to draw on different depths. Three different is all we need: bottom for the background tiles, top for the frog, and middle for all other objects. This was achieved with a public method in each entity that returns the depth of that particular object: one of three enum values. Then I changed the drawing loop to what can be seen below.


void GameState::Draw()
{
    m_systems.draw_manager->DrawBackground(m_systems.width, m_systems.height);

    // Only for clarity. Datatype enum 'EDepth' could cast automatically to 'unsigned int'.
    unsigned int bottom = static_cast<unsigned int>(DEPTH_BOTTOM);
    unsigned int middle = static_cast<unsigned int>(DEPTH_MIDDLE);
    unsigned int top = static_cast<unsigned int>(DEPTH_TOP);

    // Inner loop draws all entities of a certain depth. Outer loop repeats the process for each depth.
    for (unsigned int drawDepth = bottom; drawDepth <= top; drawDepth++)
    {
        for (unsigned int i = 0; i < m_entities.size(); i++)
        {
            if (static_cast<unsigned int>(m_entities[i]->GetDepth()) == drawDepth) // Cast would also here happen automatically.
            {
                Sprite* sprite = m_entities[i]->GetSprite();
                if (sprite)
                    m_systems.draw_manager->Draw(sprite, m_entities[i]->GetX(), m_entities[i]->GetY());
            }
        }
    }
}

Welcome to my blog

My name is Johan Öhman and I am a first year student of game design at Uppsala University, Campus Gotland. The program is called “Game Design & Programming” and so obviously includes programming too, something I hope I will come to enjoy as much as design. Games is not my only main interest; I am an amateur composer as well. The music I compose is in the genre of film music, which kind of means that it doesn’t belong in only one genre but at least can be said to always aim to build upon a source of inspiration from another medium; like a film, picture or… that’s right: a GAME! I hope that my interest in music and audio in general will be beneficial in the game projects I will do during my time here.

So this is my first blog post ever, but I will try and get used to blogging and update as often as I can. Updates will probably often be a recap of things I’ve learned to help remember or to deepen my understanding. Or maybe just random thoughts about games. I have those sometimes.