Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Godot 2D Platformer #334

Merged
merged 1 commit into from
Oct 13, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Binary file added src/assets/godot/2DPlatformer/screenshot1.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added src/assets/godot/2DPlatformer/screenshot2.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added src/assets/godot/2DPlatformer/screenshot3.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added src/assets/godot/2DPlatformer/screenshot4.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added src/assets/godot/2DPlatformer/screenshot5.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added src/assets/godot/2DPlatformer/screenshot6.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added src/assets/godot/2DPlatformer/screenshot7.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added src/assets/godot/2DPlatformer/screenshot8.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added src/assets/godot/2DPlatformer/screenshot9.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
231 changes: 231 additions & 0 deletions src/content/docs/game-design/godot/2dPlatformerGame.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,231 @@
---
title: 2D Platformer
description: This page is a step by step guide to making a 2D platformer
sidebar:
order: 2
---

This is a tutorial for a basic 2D platformer in Godot. We will be starting with a new project.

Please download the asset pack linked [here](https://brackeysgames.itch.io/brackeys-platformer-bundle) (it is free so you don't need to pay).

## Making the Player

Firstly, click "other node" and add a CharacterBody2D. Rename this to Player. In the inspector, click on "collision" in the CollisionObject2D section and change the layer to only have 2 selected. Now, give your CharacterBody2D 2 child nodes, an AnimatedSprite2D and a CollisionShape2D. In the project menu at the top left, open the project settings, go to the rendering heading in the general tab, click on texture, and change the default texture filter to Nearest.
Darkflame72 marked this conversation as resolved.
Show resolved Hide resolved

![Screenshot showing the settings menu](/src/assets/godot/2DPlatformer/screenshot1.png)

Now click on AnimatedSprite2D and click on "animation" in the inspector. Click on Sprite Frames and New SpriteFrames. Now click on the SpriteFrames you just made, this should open a SpriteFrames dock at the bottom of the screen. Here we are going to add the sprite animations for our character. Click on the new animation button at the top left of the new dock and create 4 new animations so you have 5 total.
Darkflame72 marked this conversation as resolved.
Show resolved Hide resolved

![Screenshot showing new animation button](/src/assets/godot/2DPlatformer/screenshot2.png)

These will be the Idle, Run, Roll, Hit, and Death animations. Starting with Idle, click the grid icon to the right of the play/pause buttons.

![Screenshot showing add sprite frames button](/src/assets/godot/2DPlatformer/screenshot3.png)

Navigate to the knight image in the sprites and open it. On the right, you will need to change both Vertical and Horizontal to 8, as the images are in an 8x8 grid. Now select the first 4 squares, these are our idle animation frames. Add these frames and you can now watch your knight idle if you click the play button. Make sure to have this animation auto play by clicking the icon to the right of the trash can above the "filter animations" bar.

![Screenshot showing auto play on load button](/src/assets/godot/2DPlatformer/screenshot4.png)

Now repeat these steps (not including the auto play) for each of the animations.

Click on CollisionShape2D in the Node Tree and make the Shape in the inspector a New CapsuleShape2D. Modify it so that it roughly covers the knight.

![Screenshot showing CollisionShape covering player](/src/assets/godot/2DPlatformer/screenshot5.png)

:::tip
Making this shape smaller makes the game easier for the player. At this stage of the design, making the shape slightly smaller than the sprite is recommended. You can experiment with this to find a size you like for your design.
:::

You can save this scene as we are done with the knight for right now.

## Creating a Level

Create a new scene by clicking the + button in the scene tabs. Add a 2D Scene and rename it Level 1. Click and drag your player scene from the FileSystem into the Level 1 scene node tree as a child of Level 1. Add a Camera2D node as a child of the Player and set the Zoom for it in the inspector to 4 for both x and y. If you now run the game, you should see your knight floating in a gray void.

Add a TileMapLayer node as a child of Level 1. Rename this to Foreground and create a new Tile Set using the inspector. This should open 2 new menus at the bottom of the screen, a TileSet menu and a TileMap menu.

![Screenshot showing TileSet and Tilemap menus](/src/assets/godot/2DPlatformer/screenshot6.png)

Inside the TileSet menu, click the + button and select "Atlas". Navigate to the world_tileset image in sprites and load that. You will be asked if Godot can automatically create tiles, select yes. Now, click the "TileSet" button in the inspector, go to the "Physics Layers" drop down and click the "Add Element" button.

![Screenshot showing physics layer add element](/src/assets/godot/2DPlatformer/screenshot7.png)

Now back to the TileSet menu, click the Paint button, click the drop down that appears and select "Physics Layer 0". Now click on some of the square platforms and they should be given a blue collision box. It will look highlighted on the Base Tiles sections.

![Screenshot showing TileMap collision boxes](/src/assets/godot/2DPlatformer/screenshot8.png)

When you click on a sprite, you should see it appear in the paint window. You can then drag the white diamonds so that the blue collision box roughly covers the sprite. For things like the ground sprites, the default square is ok, but you may need to play around for other sprites, such as the bridge sprites.

Now open the TileMap menu at the bottom of the screen and click on a tile to enable drawing in the editor. You can click to place individual tiles, or click and drag to paint them following the cursor. Play around with the tile paint tools to create a fun level to play. Some cool things to check out are the Rect tool and the Place Random Tile options.

Now add a new TileMapLayer as a child of Level 1 and place it above Foreground in the scene tree.

![Screenshot showing scene tree](/src/assets/godot/2DPlatformer/screenshot9.png)

Repeat the steps used for the Foreground (ignoring the physics steps) to create your Background. If you placed the Background TileMapLayer above the Foreground, Background tiles should appear behind the Foreground tiles. Unless your player is below the TileMapLayers, they will disappear behind the background, so make sure to move them below the Foreground in the scene tree.

## Adding Movement

Now we are going to add gravity and movement to the player. Go back to your player scene and attach a script by right clicking the player node and clicking "attach script". Click "create" as the defaults are what we want. Now if you run your game, you should be able to run around. You may want to reduce your speed and jump height, depending on how you want the game to feel (I recommend a speed of 150 and a jump velocity of -300). You may notice that your player doesn't flip when going to the left, nor does the animation change from idle to run when you're moving. To solve this, add the below code above `move_and_slide()` as well as adding `@onready var sprite_node = $AnimatedSprite2D` below `extends CharacterBody2D`. Note that the animation names are case sensitive, so if your player disappears that may be why.

```gdscript
Darkflame72 marked this conversation as resolved.
Show resolved Hide resolved
if velocity.x > 0:
sprite_node.flip_h = false
elif velocity.x < 0:
sprite_node.flip_h = true

if abs(velocity.x) > 0:
sprite_node.animation = "Run"
else:
sprite_node.animation = "Idle"
```

## Adding Killzones

Next let's make a killzone so that when you fall off the map or hit an enemy, you'll restart the level. Create a new scene with an Area2D node and rename it Killzone. Now go to Collision in the inspector and change the mask to only have 2 selected. Add a Timer node as a child of Killzone and change the wait time in the inspector to 0 (this will auto change to 0.001). Now let's attach a script to Killzone. Default options are ok. Click on Timer and in the node menu on the right connect the "timeout" signal to the Killzone script. Now click on Killzone and in the node menu, connect the "body entered" signal to the Killzone script. Replace the all of the code in the script with this.

```gdscript
Darkflame72 marked this conversation as resolved.
Show resolved Hide resolved
extends Area2D

@onready var timer = $Timer

func _on_body_entered(body:Node2D):
timer.start()


func _on_timer_timeout():
get_tree().reload_current_scene()
```

After saving, you can now drag this scene into your Level 1 scene. Give the Killzone a CollisionShape2D as a child node and make the shape a New WorldBoundaryShape2D. Drag this below your platforms so the player can fall into it. You don't need to stretch it out as it will automatically cover the bottom of the scene.

![Screenshot showing WorldBoundary](/src/assets/godot/2DPlatformer/screenshot10.png)


## Making Enemies

Now let's make an enemy. Create a new scene with a CharacterBody2D and rename this to Slime. We'll need to give it 3 child nodes: an AnimatedSprite2D, a CollisionShape2D, and a Killzone. The Killzone will also need a CollisionShape2D as a child. To add the sprite, simply repeat the steps we used for the player, but this time picking a different sprite (e.g. slime_green.png). For the Killzone, add a New RectangleShape2D to the CollisionShape2D and make it roughly cover the slime. Make the other CollisionShape2D the same as the Killzone's one. We need to give it some way to recognise walls and turn around. Add a RayCast2D as a child of Slime and move it to the center of the sprite and rotate the arrow so it points forward and stays close to the edge of the sprite. Now duplicate this node and flip the arrow the other way. At this point I'd suggest renaming them RayCastLeft and RayCastRight (or something similar) so it's not confusing which is which when we put them in code.

![Screenshot showing slime scene](/src/assets/godot/2DPlatformer/screenshot11.png)

Now attach a script to Slime and change all the code to this.

```gdscript
extends CharacterBody2D


const SPEED = 30.0
var direction = 1
@onready var sprite_node = $AnimatedSprite2D
@onready var raycast_left = $RayCastLeft
@onready var raycast_right = $RayCastRight

func _physics_process(delta: float) -> void:
# Add the gravity.
if not is_on_floor():
velocity += get_gravity() * delta


if raycast_left.is_colliding():
direction = 1
elif raycast_right.is_colliding():
direction = -1

if velocity.x > 0:
sprite_node.flip_h = false
elif velocity.x < 0:
sprite_node.flip_h = true

velocity.x = direction * SPEED

move_and_slide()
```

Now you can add your Slime to your level and if you run into it, you will reset the level.

## Adding Items

Now let's add a coin pickup. Create a new scene with an Area2D node and rename it Coin. Change the collision mask to only have 2 selected. Give this node a AnimatedSprite2D and a CollisionShape2D as children. Add the coin sprite and animation to the node as before. Give the CollisionShape2D a New CircleShape2D and have it cover the coin. Attach a script to the Area2D node and connect the "body entered" signal to it. Inside the `_on_body_entered` function make it print something so we can test that it's working. If you put the coin in your level, it should print out your message when you run over it. To make the coin disappear when you run over it, add `queue_free()` after the print.

## Connecting Levels

Now let's create a trigger to load the next level. In Level 1 add a new Area2D node and give is a CollisionShape2D as a child. On the Area2D node, change the collision mask to only have 2 selected. Give the CollisionShape2D a shape and place it where you'd like the end of the level to be. Now attach a script to the Level 1 node, then click on the new Area2D node you just made and connect the "body entered" signal to Level 1. Change the code to this:

```gdscript
extends Node2D

signal level1_finished


func _on_area_2d_body_entered(body:Node2D) -> void:
level1_finished.emit()
```

Now create a new Scene with a Node2D called Game and attach a script to it. Change the code to below and make sure the preload filepath goes to your Level 1 scene.

```gdscript
extends Node2D

var level1 = preload("res://Scenes/level1.tscn")

func _ready() -> void:
var l = level1.instantiate()
l.connect("level1_finished", _on_level1_finished)
add_child(l)

func _on_level1_finished():
for n in get_children():
remove_child(n)
n.queue_free()
```

Now go into Project -> Project Settings -> Application -> Run and change the main scene to your new Game scene.

![Screenshot showing changing the main scene](/src/assets/godot/2DPlatformer/screenshot12.png)

If you now run the project, you should be met with a gray void upon reaching the end of the level. You can make another scene called Level 2 and once you've done that, change the code in the Game script to the below.

```gdscript
extends Node2D

var level1 = preload("res://Scenes/level1.tscn")
var level2 = preload("res://Scenes/level2.tscn")
var levels = [level1, level2]
var current_level = 0

func _ready() -> void:
var l = levels[current_level].instantiate()
l.connect("level1_finished", _on_level1_finished)
call_deferred("add_child", l)

func reload_current_level():
for n in get_children():
remove_child(n)
n.queue_free()

var l = levels[current_level].instantiate()
if current_level == 0:
l.connect("level1_finished", _on_level1_finished)
call_deferred("add_child", l)

func _on_level1_finished():
for n in get_children():
remove_child(n)
n.queue_free()

current_level += 1
call_deferred("add_child", levels[current_level].instantiate())
```

You will also need to change the code in the `_on_timer_timeout()` function in the Killzone's script to `get_tree().get_root().get_node("Game").reload_current_level()` keeping in mind that `"Game"` is the name of the node in your game scene and is case sensitive. Now when you reach the end of Level 1, it will load Level 2 and you can play it. If you want to add further levels, don't forget to emit a finished signal from the level and load in and connect the new level and signal in the Game script.

## Next Steps

Congratulations, you now have a working 2D platformer. Here is a list of possible extensions you might want to add to your game:
1. Investigate adding moving platforms using AnimationPlayers
2. Killing enemies if you jump on them or some form of combat
3. On screen health or life system
4. On screen timer or score
5. Game over screen
6. Messing with tile collisions to create secret areas