DEV Community

Cover image for The 20 Game Challenge - Game 2
Brittany Blair
Brittany Blair

Posted on

The 20 Game Challenge - Game 2

This is a continuation of my last blog post found Here. If this post has caught your interest the first one goes over all the basics of the challenge, why I'm doing it, etc.

This took me longer than expected. Making your own game engine is hard, and structuring the engine to be scalable & modular is even harder. Most of my time spent on game #2 was dedicated to improving the game engine to support a ton of new features. Even though the game itself is fairly simple, the additions and changes made to the engine itself were quite an undertaking.

Game 2 Requirements

So our first game was a flappy bird style game, and the challenge helps you out by building upon the previous challenges. So if you chose pong last time, you could do Breakout this time are re-use a lot of your previously written code.

I have the option of choosing either Breakout or Jetpack Joyride for game number 2. I am going to do the jetpack joyride, this time around which adds a few more stretch goals that we can use to expand upon our MonoGame Engine logic, but more on that later.

The requirements for this game are a half step more difficult than the last game. Here are the main goals:

  1. Create a game world with a floor. The world will scroll from right to left endlessly.

  2. Add a player character that falls when no input is held, but rises when the input is held.

  3. Add obstacles that move from right to left. Feel free to make more than one type of obstacle.

  4. Obstacles can be placed in the world using a script so the level can be truly endless.

  5. Obstacles should either be deleted or recycled when they leave the screen.

  6. The score increases with distance. The goal is to beat your previous score, so the high score should be displayed alongside the current score.

Optional Goals:

  1. Save the high score between play sessions.

  2. The jetpack is a machine gun! Add bullet objects that spew from your character when the input is held.

  3. Particle effects are a fun way to add game juice. Mess around with some here, making explosions or sparks when things get destroyed!

So that's quite a bit more than the first game, and of course same as the last time. I will be doing my best to reach every goal, including the stretch goals.

The Plan

Same as before, I decided to make my game plan on paper as seen above. I couldn't tell you exactly why, but when I first read the requirements and the description of Jetpack Joyride the first thing I thought of was a squid. So I ran with it, and in my game, you play as a little Squid running endlessly to escape a fish market.

The more I wrote down in my plan the more I loved the idea so This time around we are going to put more effort into the art & animation of the game to make something befitting our little squid.

Right out of the gate, I knew that I needed to do some refactors on the game engine systems I designed during game 1. So I also wrote down a note about what systems I don't have, that I will need to reach all the goals.

Engine Systems Update

The very first thing I did was work on refactoring what I already had. The worst thing I could do to myself between games was to trust the code I had written previously to be bulletproof. So I reworked a lot.

I worked on Improving collisions as a large part of this project. Before I was using the MonoGame Rectangle class and just calling _collierRect.IsIntersecting(otherRectangle) and that worked fine and all. But this time we are going to have bullets that will move at various speeds and we could risk bullets not colliding if the framerate and speed aren't correct.

So I decided to implement both AABB collision detection ( similar to the rectangle class ) and the Segment AABB collision detection which is a more accurate collision detection ( at a performance cost. ).

I made some other miscellaneous improvements to UI buttons, object classes, etc. Check out my GitHub repository if you're interested in seeing what's new.

Animation and VFX, & Window Scaling Systems for the Engine

Listed in the requirements of this game there is a stretch goal of having some visual effects for particles and destruction. So that means working on creating a system to handle particles. But also, I want to have an animated character so we needed to implement an Animation state system.

Starting with particles, I made a few new classes: Particle, Emitter, EmitterParticleState, an Interface for Emitter types, and a ConeEmitterType. The Emitter class utilizes Object Pooling, using a linked list of active particles and a linked list of inactive particles. Here's how it works

  • The emitter has a maximum number of particles and when it updates, it will spawn particles if the linked list length has not reached the maximum.
  • When a particle gets to the end of its lifespan it no longer gets drawn to the screen. Instead, it is moved from the active list over to the inactive list where it will wait to be re-spawned.
  • When the Emitter reaches its maximum number of spawned particles, I will stop making new particles and instead re-spawn the existing particle objects from the inactive list, moving them over to the active list once more.

For the animation system, I created a custom importer for sprite sheet animations. A full post about that importer is here. This was super helpful in the process of making the game. The importer takes in a PNG and a bunch of import settings for the sprite sheet and then creates custom animation assets that I can load and unload from the game.

The last major update I made the the engine files was the addition of a Viewport scaler. I wanted to add the ability for players to scale up or down the game's viewport and maintain my desired aspect ratio.

This was pretty difficult because of how mouse input is captured. If the window itself tracks the location of the mouse and when you get the mouse position it returns the pixel position of the mouse.

Here was my problem If your game is designed for 1920 x 1080 but your window size was changed by the user to be larger or smaller than that, the mouse position wouldn't account for this new scale. So, if I have a button placed at x = 400, y = 400, and my window is scaled down by half the designed size. when I click on the image of the button the mouse's position is x = 200, y = 200. Meaning that it looks like you clicked the button but mathematically you did not click it.

Unfortunately, the only solution to this is to use a matrix to calculate scale and invert the matrix to find the mouse position. I never learned any matrix math so I went into this blind. Here is my solution to this problem

namespace Engine.Viewports
{
    public class ScalingViewport
    {
        private readonly GameWindow _Window;
        public GraphicsDeviceManager _Graphics { get; }
        public Viewport _Viewport => _Graphics.GraphicsDevice.Viewport;

        int _virtualWidth;
        int _virtualHeight;

        bool useBlackBars = true;
        int _barWidth;
        int _barHeight;

        bool isResizing;

        int DESIGNED_RESOLUTION_WIDTH;
        int DESIGNED_RESOLUTION_HEIGHT;
        float DESIGNED_RESOLUTION_ASPECT_RATIO;

        public ScalingViewport(GameWindow window, GraphicsDeviceManager graphics, int DesignedWidth, int DesignedHeight, float DesignedRatio)
        {
            _Window = window;
            _Graphics = graphics;

            DESIGNED_RESOLUTION_WIDTH = DesignedWidth;
            DESIGNED_RESOLUTION_HEIGHT = DesignedHeight;
            DESIGNED_RESOLUTION_ASPECT_RATIO = DesignedRatio;

            isResizing = false;
            window.ClientSizeChanged += OnClientSizeChanged;

            _Graphics.HardwareModeSwitch = true;
            _Graphics.IsFullScreen = false;
            _Graphics.ApplyChanges();
        }

        public Rectangle GetDestinationRectangle()
        {
            return new Rectangle(0, 0, _virtualWidth, _virtualHeight);
        }

        private void OnClientSizeChanged(object sender, EventArgs e)
        {
            if (!isResizing && _Window.ClientBounds.Width > 0 && _Window.ClientBounds.Height > 0)
            {
                isResizing = true;
                RefreshViewport();
                isResizing = false;
            }
        }

        public virtual void RefreshViewport()
        {
            _Graphics.GraphicsDevice.Viewport = GetViewportScale();

        }

        protected virtual Viewport GetViewportScale()
        {
            var variance = 0.5;
            int windowWidth = _Graphics.GraphicsDevice.PresentationParameters.BackBufferWidth;
            int windowHeight = _Graphics.GraphicsDevice.PresentationParameters.BackBufferHeight;
            var actualAspectRatio = (float)windowWidth / windowHeight;
            _barHeight = 0;
            _barWidth = 0;

            if (actualAspectRatio <= DESIGNED_RESOLUTION_ASPECT_RATIO)
            {
                var presentHeight = (int)(windowWidth / DESIGNED_RESOLUTION_ASPECT_RATIO + variance);
                _barHeight = (int)(windowHeight - presentHeight) / 2;

                _virtualWidth = windowWidth;
                _virtualHeight = presentHeight;

            }
            else
            {
                var presentWidth = (int)(windowHeight * DESIGNED_RESOLUTION_ASPECT_RATIO + variance);
                _barWidth = (int)(windowWidth - presentWidth) / 2;

                _virtualWidth = presentWidth;
                _virtualHeight = windowHeight;

            }


            int x = _barWidth;
            int y = _barHeight;

            if(!useBlackBars)
            {
                _Graphics.PreferredBackBufferWidth = _virtualWidth;
                _Graphics.PreferredBackBufferHeight = _virtualHeight;
                _Graphics.ApplyChanges();

                x = 0;
                y = 0;
            }

            return new Viewport
            {
                X = x ,
                Y = y ,
                Width = _virtualWidth,
                Height = _virtualHeight,
                MinDepth = 0,
                MaxDepth = 1,
            };

        }

        public virtual Matrix GetScaleMatrix()
        {
            float Scale = (float)_virtualWidth / (float)DESIGNED_RESOLUTION_WIDTH;
            return Matrix.CreateScale(Scale);
        }

        public Point PointToScreen(Point point)
        {
            return PointToScreen(point.X, point.Y);
        }

        public virtual Point PointToScreen(int x, int y)
        {
            Matrix matrix = Matrix.Invert(GetScaleMatrix());


            return Vector2.Transform(new Vector2(x - _barWidth, y - _barHeight), matrix).ToPoint();
        }

        public Point GetScaledMousePosition()
        {
            return PointToScreen(Mouse.GetState().Position);
        }

    }
}
Enter fullscreen mode Exit fullscreen mode

this solution worked for me, and while I don't like the black bars on the side of the screen. I tried to adjust this so that the window would automatically adjust the scale after a user made a change to fill the space without black bars. However, even though it worked and there were no bars, I found a bug with the mono game framework where if you directly set the width or height of the window it locks the game window to your main monitor. I have a dual monitor setup so only being able to test it on my main monitor and not being able to move it outside of that would not be ideal

The Art

All art assets and source art files

Just as I did for the first game I am making my art for this game as well. But this time I am also animating the character I haven't animated something in like 7 years and I was a 3D animator so this step was kind of scary for me. I wanted to take more time on the other assets In the level but time was not on my side so I wasn't able to achieve a cohesive look. But it's okay, I can always go back and revisit this in the future.

So, I started with images. The cover image for this blog post is the start menu for the game. Going with our game's theme I pulled up some references to fish markets and saw that there are tons of blue bins with ice and fish usually stacked in front of or around various stalls. Then I looked up some common types of squid you can purchase at a fish market and chose these red squids to be the type of squid our character will be. Then I worked on making some unique UI buttons & a logo. I decided to make the UI look like price ticket signs and I made the logo in a graphic style.

After a few sketches and trying to figure out how I wanted the characters to move when they were on the ground, I came up with the perfect design. Squids have 10 tentacles and to keep the character's silhouette clear I decided to do 8 tentacles, and let's just say they lost 2 of them at some point.

Here is a look into my animation process.
Initial animation sketch. Rough version of what I wanted the animation to look Like. Motion passes to better define the movements of each part of the character. Base color pass. animating the various spots and shading of just the base color of the character. Shading pass, adding light and shadow to the animation to give the character a more 3-dimensional look.

This is the first animation I made for this character, a simple walking animation to better understand who the character is and how they move around the world. I love how they use their tiny tentacles to run and their big arms are being dragged behind them it's so cute. All the animated elements of the game were made in Marmoset Hexels 3, this program is game art friendly and will let me export my animation frames as a sprite sheet.

by the time I got done with the player animation, I had to speed up the other assets so I used a voxel art program called Magica Voxel to model out some obstacles for the game and render them as PNG images. Here is a list of the assets I made as obstacles

  • A blue bin filled with ice and fish filets
  • A simple wooden crate
  • hanging ceiling fan that is animated
  • hanging ceiling light
  • Air ducts
  • Tiling brick wall background
  • Tiling Floor Background

The Game

Ok now that all the systems are designed, Art is drawn, and animations are exported. I moved on to programming the game itself.

This game is pretty similar to the Flappy Duck game, but there are a few key differences when it comes to the obstacles you're avoiding. I had to make a spawning system that would spawn obstacles on the ground and stack them if an obstacle already exists rather than pile them on top of each other.

Collisions Visible Screenshot

for there are lots of collision checks and items being spawned so I had to be very careful about memory leaks. I had to make some automated functionality that would garbage-collect the spawned obstacles and items once they exited the main viewport.

Player Hovering

I had to take a bit of time working out how I wanted animations to change / transition by making a kind of state machine for the player. The player's animation state could be walking, jumping, falling, or hovering in the air. each of these had its transition requirements and actions that would be performed once the transition was complete.

For example, Hovering until you run out of ink and running out of ink transitions you into falling. Walking over the edge of an obstacle also lets you fall, or getting Hit by an overhead obstacle would force you to fall. So there were lots of conditions to check for when switching animation states and that was time-consuming but gave the best visual outcome for the game.

The game has 3 scenes: the Start Menu, the Main Game, and the End Menu. The loop for the game has the player playing until the player either dies or quits the game. When the player destroys obstacles by hovering over them and shooting them with their ink, they get points which adds up to a high score. When the game ends, the player's session score and their Highest Score are listed in the End Menu. This is the same as how we did scores in the Flappy Duck game.

Conclusion

This second game took far longer than anticipated but I learned a TON about game engine architecture. Even though this is game 2 of a 20-game challenge I already feel way more confident in my coding abilities than when I started.

There was a lot I wanted to do that I just couldn't spare the time to do for this game. It was hard to just get the game done because It was really easy to fall into a rabbit hole just working to add new things to the engine or improve existing features. Toward the end here, I just had to draw a line for myself and say "Just finish the game". At times It's hard for me to truly accept that This 20-game project is not about making perfect games. But rather it's about getting a game over the finish line, Something that I frequently struggle to do while working on personal projects.

I am going to be taking a break from the 20-game challenge for a bit as I need to do some research and figure out if there is a way to publish a Mono Game project for the Web. I've seen others talk about doing it but have no clue how they achieved it and I would like to have Web builds for games I make with the Engine so I can post them on Itch.io. Also, many game jams only accept web platform builds because of people handing over viruses in build projects and I want to use my game engine to do some game jams as a part of this 20-game challenge.

There is a possibility that I won't be able to figure out web publishing for Mono Game, and if that happens then Mono Game might not be the framework for me. If that is the case I'll have to find a new Framework and continue the challenge using whatever new framework I find.

Top comments (0)