-
Notifications
You must be signed in to change notification settings - Fork 7
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Showing
13 changed files
with
231 additions
and
0 deletions.
There are no files selected for viewing
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.
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.
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.
231 changes: 231 additions & 0 deletions
231
src/content/docs/game-design/godot/2dPlatformerGame.mdx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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. | ||
|
||
![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. | ||
|
||
![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 | ||
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 | ||
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 |