Forest Defender Devlog 2: Basic Camera and Movement
Written on October 28th, 2022
Table of Contents
- Preface
- Movement
- Gravity and Collisions
- Camera Enhancements
- Finishing Notes
- Resources / Further Reading
This week's results of development. Each piece will be explained below.
Preface
General Accomplishments
Since the previous devlog, Forest Defender: development setup, a lot of groundwork has been completed.
We now have created a simple tileset. After some searching for a nice but small color palette, we settled on using the Wild Jungle Color Scheme as it had the right feel of jungle vibes. It is also very narrow in its color range, really just being brown and green, which is exactly what we were looking for. I am a big fan of games built around minimal color differences since it reminds me of original Nintendo GameBoy games. In the created tileset, you can see we have just a handful of tiles to start with and two entities: a mushroom entity and the player. These will be expanded into animations later on.
We then took that tileset and created a basic testing level for movement by using Tiled. Building the level is easy and there are plenty of tutorials for reading the export from Tiled into a Python project via a custom import function. I will have a couple tutorials and examples linked in the further reading section at the end of this devlog.
Once the level was created, we plopped in our player's character and added in some gravity and controls. Using force based physics, it is easy to tweak movement to feel just right. We also added in a nice camera to follow the player around to get the first steps of this project complete. I have detailed how I did these movement and physics additions in the following sections.
Why movement and camera motion are the most important piece of a game
General motion of the game (camera and player movement) is often overlooked by first time game developers. Many people either put in a simple system early on and work on improving it later in development, or they just use an out of the box solution. In my opinion, general motion is the most important part of a game and should be designed and made as perfect as possible for your end goal at the beginning of development. I am willing to play most games with great motion feel regardless of graphics, animation, etc. but I will usually get frustrated in a game if there are lots of collision problems, clunky movement, etc. even with beautiful graphics.
Let's start with what makes good movement in a game. In general, good movement refers to smooth motion, intuitive controls, and accurate collisions. Since there are various types of games, all with different ideal movements, let's just focus on our Forest Defender game.
Since our game is 2-D, collisions are pretty easy. We really only need to worry about vertical collisions and horizontal collisions. We aren't going to have any tiles with special collision properties (such as steep slopes), so there isn't much more to plan for.
Our character is a 'fairy' type creature, so we designed the sprite with wings. While this won't help the fairy fly, this helps the character not fall too quickly and lets the player change direction mid-air. Therefore, when the character is falling or has jumped we want the player to be able to still move the character left and right just like the player was still on the ground.
Camera movement comes in too many varieties to describe them all. For our game, we are going with a simple camera movement. Essentially, when your character moves right, the camera moves right. When your character moves left, your camera moves left, etc. The biggest quality of life enhancement we are making to the camera is to give it a slight delay in its movement by using forces on the camera. This gives the camera a smoother look instead of jarring motion. This will be described more further down in this devlog.
Movement
The goal of our character movement is to be smooth and intuitive while fitting the character design.
Keeping motion smooth is generally easy. We will be using a force based system. So if the player presses the key to move right, a force will be applied to the character and it's acceleration will raise. The acceleration is then applied to the character's velocity each frame, which will move the character's position. The calculations are below, with dt being the time between last frame and current frame to make sure this process is frame-rate independent:
force = 1 * dt
acceleration = acceleration + (force * dt)
velocity = velocity + (acceleration * dt)
position = position + (velocity * dt)
Gravity and Collisions
Gravity
Gravity as a concept in 2-D physics is relatively straightforward: if down in the window is the ground, that is where the acceleration of gravity will point.
To add gravity to our character (and any future entities), we will include it as an aspect. An aspect is essentially an additional property you can add to an entity in the Python game engine we are creating for this project. To explain it simply, each entity can have 0+ aspects that it runs through each frame of the game. These aspects can be for animation, physics, random checks, etc. We could create each of these as their own function or class, but that would make our entity class contain many properties that need to be maintained and entered individually. Instead, we can have a base aspect class that other classes, like our gravity or physics movement, can inherit from. Then, we can simply add this new aspect into a list of aspects the entity holds onto and runs through each frame. Much easier and cleaner!
Back to gravity. Since in our game gravity will always be pointing downward in the window, we can just give the gravity acceleration a positive y value. This is because in Pygame, downward is a positive value and upward is a negative value. You will notice I mentioned giving gravity a positive acceleration, not a force. This is due to gravity being a constant, and therefore the acceleration never changes.
To add our gravity in, we do a calculation very similar to the calculation for our movement, since it is essentially the same thing:
acceleration.y = acceleration.y + (gravity * dt)
velocity.y = velocity.y + (acceleration * dt)
position.y = position.y + (velocity * dt)
Here we only modify the y value of the vectors since gravity only affects the player's vertical position. The result can be seen in the GIF below:
Collisions
Collisions can become as complicated as you need them to be. For us, collisions will be on the simpler side. We will only be adding vertical collisions and horizontal collisions. Put even simpler, checking if an entity hits an object with the top of its head or the bottom of its body, or if it hits an object with the left or right side of its body.
While it sounds easy and simple, the code for this still looks a bit complicated. In the code snippet below, we are adding the movement to the entity in the x and y directions, then checking if it is within (therefore hitting) another object. For checking if there is a hit, we are using Pygame's built in sprite hit detection function colliderect. In the snippet, this is called with self.get_hits(tiles) which is a wrapper function I wrote and is included below.
If there was a collision detected with the entity, we add that hit to the collision_type dictionary to be referenced later in repositioning the entity so they are just outside the collided object (therefore colliding instead of going through). We also modify the velocity of the entity so the entity loses its inertia upon a collision.
The code we used for now is as follows:
def get_hits(self, tiles) -> list:
hits = []
for tile in tiles:
if self.entity.rect.colliderect(tile):
hits.append(tile)
return hits
def move(self, rect, velocity, tiles, dt):
collision_types = {"top": False, "bottom": False, "right": False, "left": False}
rect.x += velocity.x * dt
hits = self.get_hits(tiles)
for tile in hits:
if velocity.x > 0:
rect.right = tile.rect.left
velocity.x = 0
collision_types["right"] = True
elif velocity.x < 0:
rect.left = tile.rect.right
velocity.x = 0
collision_types["left"] = True
rect.y += velocity.y * dt
hits = self.get_hits(tiles)
for tile in hits:
if velocity.y > 0:
rect.bottom = tile.rect.top
velocity.y = 0
collision_types["bottom"] = True
elif velocity.y < 0:
rect.top = tile.rect.bottom
velocity.y = 0
collision_types["top"] = True
return rect, collision_types, velocity
Other
Besides the above mentioned additions to the game, we mainly added quality of life additions. These mainly include applying maxes to velocity so the player can't move infinitely fast, a slight delay allowed when jumping i.e. you can be a few frames off a platform (like walking off) and still be able to jump, which improves the feeling of the game, and small force tweaks.
The applying of maxes is simple, as shown below:
def apply_maxes(self) -> None:
if self.entity.velocity.x > self.entity.max_speed.x:
self.entity.velocity.x = self.entity.max_speed.x
elif self.entity.velocity.x < -self.entity.max_speed.x:
self.entity.velocity.x = -self.entity.max_speed.x
if self.entity.velocity.y > self.entity.max_speed.y:
self.entity.velocity.y = self.entity.max_speed.y
elif self.entity.velocity.y < -self.entity.max_jump_speed:
self.entity.velocity.y = -self.entity.max_jump_speed
Camera Enhancements
Basic camera movement is easy to implement. Simply place your camera's center position on the player character at the end of each frame when all the updates are done. This leads to harsh behavior of the camera though. The motion of the camera would feel jarring when changing directions.
We resolved this by instead applying forces to the camera for movement. Essentially we take the difference in position between the player's center and the camera's center positions and apply that to the default force applied to the camera. This means the further away the camera is from the player, the faster the camera will move toward the player. This is done easily in the code:
self.position.x += (
player.rect.centerx - (self.position.x + self.width / 2)
) / 5
self.position.y += (
player.rect.centery - (self.position.y + self.height / 2)
) / 5
This gives a sort of 'lag behind' motion of the camera that smooths any jarring movements. You can see that in the GIF below.
You can see that instead of the camera being exactly on the player at all times, the camera sort of 'lags' behind the player. The distance * force feature we have added means the camera will never fall too far behind.
Finishing Notes
That is about all that was completed during this last bit of development. I will try to get these blogs out at least every two weeks, though the next few may be quicker since I have a lot of development worked on and just need to get it into devlogs.
If you have any questions, feel free to leave a comment and I will do my best to answer it!
Resources / Further Reading
Reading from Tiled into Python
Resources for Physics
Other
Top comments (0)