Ludum Dare 43 - Part 5

Turning the toy into a game - February 18, 2019 by Steve

Back Home

title.png

Back in Part 4, we finished giving the Minotaur a brain. So far, we have all the basic mechanics in place: a player that can run around a maze, offerings that can be picked up, an altar to place the offerings on, and a Minotaur to chase the player and chow down on delivery. Now all we need is the concept of scoring and progression in the game.

Right now, when you start the game in the Unity editor it launches right into the maze. We'll want to change that. Also as things stand, you endlessly provide offerings onto the altar until the end of time or Minotaur catches you, which ever comes first. So we'll want to add in levels. Levels should become more challenging as the game progresses otherwise players will lose interest. First things first, lets make a start screen that gets shown when the game first loads up.

I created a Canvas in Unity, and dropped a large Image UI element on it which was the background image above. I created this in Inkscape using Theseus and the Minotaur in the Labyrinth as inspiration. I also added two Text UI elements as children to the Image. One Text element had static help text set, while the other would indicate pressing a key to start the game, or that the game was over with the user's final score.

The Awake() method of the GameController gets called once on startup, and at the end of it I call a method to display the start screen.

private void Awake()
    {
        if (instance == null) {
            instance = this;
        } else if (instance != this) {
            Destroy(gameObject);
        }
        // we want this to persist between levels
        DontDestroyOnLoad(gameObject);
        InitGame();
        AstarPath.active.Scan();

        Image alertImage = GameObject.Find("AlertImage")
                                     .GetComponent<Image>();
        Text alertText = GameObject.Find("AlertText")
                                   .GetComponent<Text>();
        HelpText = GameObject.Find("HelpText")
                             .GetComponent<Text>();

        if (HelpText) {
            HelpText.enabled = false;
        }

        if (alertImage) {
            alertImage.enabled = false;
        }

        if (alertText) {
            alertText.enabled = false;
        }

        ShowIntroTitle();
    }

private void ShowIntroTitle()
    {
        TitleText.text = "Press any key to start";
        TitleImage.enabled = true;
        TitleText.enabled = true;
    }

You'll notice at the start of Awake() we set a variable called instance to the actual instance of the GameController. Then we call DontDestroyOnLoad() which allows the GameController to live between scene loads. Originally I was going to just re-load the scene each new level, but wanted to keep the score between loads (since that is saved in the GameController).

After we set up the instance, we call InitGame() which generates the maze, places the player and gets the game setup to run. Afterwards, at the end, we call ShowIntroTitle() which changes the text on the splash screen, and enables it. When the TitleImage is enabled, it covers the maze and other game elements.

The Update() method then takes over. Since the game isn't running yet there is an if statement which determines if the game is just paused, being started, or if the game has just ended and will reset.

    void Update ()
    {
        bool justUnpaused = false;

        if (isRunning() && initialized && !isWaitingForNewLevel())
        {
            // logic for while the game is running here...
        } 
        else if (!isRunning() && Input.anyKeyDown)
        {
            justUnpaused = true;
            if ((gameOver || !gameStarted))
            {
                // we're starting or restarting the game
                gameStarted = true;
                if (gameOver)
                {
                    gameOver = false;
                    gamePaused = false;
                    GameReset();
                }
                else
                {
                    StartGame();
                }
            }
            else // we're just paused
            {
                gamePaused = false;
                TitleImage.enabled = false;
                HelpText.enabled = false;
                minotaurController.unpause();
            }

        }

        if(Input.GetKeyDown(KeyCode.H) && isRunning()) 
        {
            if (!justUnpaused)
            {
                gamePaused = true;
                minotaurController.pause();
                TitleImage.enabled = true;
                TitleText.enabled = false;
                HelpText.enabled = true;
            }
        } 
    }

The Update() method will look for the h key being pressed if the game is running, pause the game if it is, and then display the help screen. StartGame() shows a title image, which consists of changing the TitleText to indicate the Level and required number of offerings, and then delays a set amount of time.

    private void StartGame()
    {
        waitingForNewLevel = true;
        gameStarted = true;
        ShowTitleImage();
    }

    private void ShowTitleImage()
    {
        TitleText.text = "Level " + level 
                         + "\nRequired Offerings: " 
                         + pointsPerLevel;

        TitleImage.enabled = true;
        TitleText.enabled = true;
        Invoke("HideTitleImage", levelStartDelay);
    }

    private void HideTitleImage()
    {
        TitleImage.enabled = false;
        TitleText.enabled = false;
        waitingForNewLevel = false;
        tillNextSpawn = offeringSpawnTime;
    }

If you are pressing a key after the game is over, GameReset() is called which just resets various values to zero, then restarts.

    public void GameReset()
    {
        level = 0;
        totalPoints = 0;
        pointsPerLevel = basePointsPerLevel;
        minotaurController.updateSpeed(0);
        minotaurController.reset();
        playerController.reset();
        _altarController.reset();
        updateRequiredPointsText();
        PointsText.text = "Points: " + totalPoints;
        Restart();
    }

    private void Restart ()
    {
        level++;
        // update the speed of the minotaur etc.
        if (level > 1)
        {
            minotaurController.updateSpeed(Mathf.Log(level, 2f)*.5f);
            pointsPerLevel += (int)Mathf.Log(level, 2f);
            pointsThisLevel = 0;
            updateRequiredPointsText();
        }

        refreshMap();
        //Load the last scene loaded, 
        //  in this case Main, the only scene in the game.
        //SceneManager.LoadScene (0);
    }

Here is where the difficulty goes up as levels get higher. I chose a logarithmic function to quickly ramp up the difficulty fast in the early levels, but not so much as levels increase. For example by adding on the Log for the level to the pointsPerLevel ( number of points required to finish the level), you get the following point values with 1 point required for level 1:

level 1 -> 1 points
level 2 -> 2 points
level 3 -> 3 points
level 4 -> 5 points
level 5 -> 7 points
level 6 -> 9 points
level 7 -> 11 points
level 8 -> 14 points
level 9 -> 17 points

There was some debate on the speed of this curve in the comments on the finished game, and I fully admit I did not spend a huge amount of time tuning this (as I was approaching the end of the 72 hours at this point). You can also see in that code where I attempted to use the SceneManager.LoadScene(0) to reload the level and restart things. This ended up making things a little bit more messy for a simple game, so I scrapped that idea and just added the ability to do everything in the one scene.

Now whenever the Minotaur visits the altar to eat, the altar ends up calling the GameController's updatePoints() method.

    public void updatePoints(int pointsToAdd)
    {
        totalPoints += pointsToAdd;
        pointsThisLevel += pointsToAdd;
        PointsText.text = "Points: " + totalPoints;
        updateRequiredPointsText();
        CheckEndLevel();
    }

    private void CheckEndLevel()
    {
        if (pointsThisLevel >= pointsPerLevel)
        {
            waitingForNewLevel = true;
            minotaurController.reset();
            Restart();
        }
    }

updatePoints() in turn calls CheckEndLevel() and if the total points for the level are more than the required amount, we basically pause everything and call Restart() which then ups the level and starts again.

The final bit is ending the game. When the Minotaur collides with the player, we end the game. The MinotaurController has an OnCollision2D() method that sees if it collided with the player, and then calls the GameController's game over.

    public void GameOver()
    {
        waitingForNewLevel = true;
        TitleText.text = "The Minotaur got you at"
                         + "\nLevel " + level + " with " 
                         + totalPoints + " points" 
                         + "\n\nPress any key to try again";
        TitleImage.enabled = true;
        TitleText.enabled = true;
        gameOver = true;
    }

And that's all there is to it. I had what I need to submit to Ludum Dare 43. I feel like I did pretty good with getting in the 800-900 range with just myself, and not killing myself to get it done. (There were 1750 Jam entries published, with over 3000 unpublished)

You can play Minotaur Maitre D as submitted here

I have some ideas on where to take the game. I did not get a chance to really update the graphics from the Atari 2600 Adventure aesthetic. Even that game had dragon graphics in it. I also want to move onto mobile with the game, and perhaps add some more challenges such as not being able to see the whole maze at once and not knowing where the Minotaur is.

I'll post updates as I have time to work on the game. Ultimately I'd like to release a polished, fun, and quick game on multiple platforms. Thanks for following along. You can get to the full series below.

The full series: