Search icon CANCEL
Subscription
0
Cart icon
Your Cart (0 item)
Close icon
You have no products in your basket yet
Arrow left icon
Explore Products
Best Sellers
New Releases
Books
Videos
Audiobooks
Learning Hub
Newsletter Hub
Free Learning
Arrow right icon
timer SALE ENDS IN
0 Days
:
00 Hours
:
00 Minutes
:
00 Seconds
Arrow up icon
GO TO TOP
Godot Engine Game Development Projects

You're reading from   Godot Engine Game Development Projects Build five cross-platform 2D and 3D games with Godot 3.0

Arrow left icon
Product type Paperback
Published in Jun 2018
Publisher Packt
ISBN-13 9781788831505
Length 298 pages
Edition 1st Edition
Languages
Tools
Arrow right icon
Author (1):
Arrow left icon
Chris Bradfield Chris Bradfield
Author Profile Icon Chris Bradfield
Chris Bradfield
Arrow right icon
View More author details
Toc

Part 1 – Player scene

The first scene you'll make defines the Player object. One of the benefits of creating a separate player scene is that you can test it independently, even before you've created the other parts of the game. This separation of game objects will become more and more helpful as your projects grow in size and complexity. Keeping individual game objects separate from each other makes them easier to troubleshoot, modify, and even replace entirely without affecting other parts of the game. It also makes your player reusable—you can drop the player scene into an entirely different game and it will work just the same.

The player scene will display your character and its animations, respond to user input by moving the character accordingly, and detect collisions with other objects in the game.

Creating the scene

Start by clicking the Add/Create a New Node button and selecting an Area2D. Then, click on its name and change it to Player. Click Scene | Save Scene to save the scene. This is the scene's root or top-level node. You'll add more functionality to the Player by adding children to this node:

Before adding any children, it's a good idea to make sure you don't accidentally move or resize them by clicking on them. Select the Player node and click the icon next to the lock:

The tooltip will say Make sure the object's children are not selectable, as shown in the preceding screenshot.

It's a good idea to always do this when creating a new scene. If a body's collision shape or sprite becomes offset or scaled, it can cause unexpected errors and be difficult to fix. With this option, the node and all of its children will always move together.

Sprite animation

With Area2D, you can detect when other objects overlap or run into the player, but Area2D doesn't have an appearance on its own, so click on the Player node and add an AnimatedSprite node as a child. The AnimatedSprite will handle the appearance and animations for your player. Note that there is a warning symbol next to the node. An AnimatedSprite requires a SpriteFrames resource, which contains the animation(s) it can display. To create one, find the Frames property in the Inspector and click <null> | New SpriteFrames:

Next, in the same location, click <SpriteFrames> to open the SpriteFrames panel:

On the left is a list of animations. Click the default one and rename it to run. Then, click the Add button and create a second animation named idle and a third named hurt.

In the FileSystem dock on the left, find the run, idle, and hurt player images and drag them into the corresponding animations:

Each animation has a default speed setting of 5 frames per second. This is a little too slow, so click on each of the animations and set the Speed (FPS) setting to 8. In the Inspector, check On next to the Playing property and choose an Animation to see the animations in action:

Later, you'll write code to select between these animations, depending on what the player is doing. But first, you need to finish setting up the player's nodes.

Collision shape

When using Area2D, or one of the other collision objects in Godot, it needs to have a shape defined, or it can't detect collisions. A collision shape defines the region that the object occupies and is used to detect overlaps and/or collisions. Shapes are defined by Shape2D, and include rectangles, circles, polygons, and other types of shapes.

For convenience, when you need to add a shape to an area or physics body, you can add a CollisionShape2D as a child. You then select the type of shape you want and you can edit its size in the editor.

Add a CollisionShape2D as a child of Player (make sure you don't add it as a child of the AnimatedSprite). This will allow you to determine the player's hitbox, or the bounds of its collision area. In the Inspector, next to Shape, click <null> and choose New RectangleShape2D. Adjust the shape's size to cover the sprite:

Be careful not to scale the shape's outline! Only use the size handles (red) to adjust the shape! Collisions will not work properly with a scaled collision shape.

You may have noticed that the collision shape is not centered on the sprite. That is because the sprites themselves are not centered vertically. We can fix this by adding a small offset to the AnimatedSprite. Click on the node and look for the Offset property in the Inspector. Set it to (0, -5).

When you're finished, your Player scene should look like this:

Scripting the Player

Now, you're ready to add a script. Scripts allow you to add additional functionality that isn't provided by the built-in nodes. Click the Player node and click the Add Script button:

In the Script Settings window, you can leave the default settings as they are. If you've remembered to save the scene (see the preceding screenshot), the script will automatically be named to match the scene's name. Click Create and you'll be taken to the script window. Your script will contain some default comments and hints. You can remove the comments (lines starting with #). Refer to the following code snippet:

extends Area2D

# class member variables go here, for example:
# var a = 2
# var b = "textvar"

func _ready():
# Called every time the node is added to the scene.
# Initialization here
pass

#func _process(delta):
# # Called every frame. Delta is time since last frame.
# # Update game logic here.
# pass

The first line of every script will describe what type of node it is attached to. Next, you'll define your class variables:

extends Area2D

export (int) var speed
var velocity = Vector2()
var screensize = Vector2(480, 720)

Using the export keyword on the speed variable allows you to set its value in the Inspector, as well as letting the Inspector know what type of data the variable should contain. This can be very handy for values that you want to be able to adjust, just like you adjust a node's built-in properties. Click on the Player node and set the Speed property to 350, as shown in the following screenshot:

velocity will contain the character's current movement speed and direction, and screensize will be used to set the limits of the player's movement. Later, the game's main scene will set this variable, but for now you will set it manually so you can test.

Moving the Player

Next, you'll use the _process() function to define what the player will do. The _process() function is called on every frame, so you'll use it to update elements of your game that you expect to be changing often. You need the player to do three things:

  • Check for keyboard input
  • Move in the given direction
  • Play the appropriate animation

First, you need to check the inputs. For this game, you have four directional inputs to check (the four arrow keys). Input actions are defined in the project settings under the Input Map tab. In this tab, you can define custom events and assign different keys, mouse actions, or other inputs to them. By default, Godot has events assigned to the keyboard arrows, so you can use them for this project.

You can detect whether an input is pressed using Input.is_action_pressed(), which returns true if the key is held down and false if it is not. Combining the states of all four buttons will give you the resultant direction of movement. For example, if you hold right and down at the same time, the resulting velocity vector will be (1, 1). In this case, since we’re adding a horizontal and a vertical movement together, the player would move faster than if they just moved horizontally.

You can prevent that by normalizing the velocity, which means setting its length to 1, then multiplying it by the desired speed:

func get_input():
velocity = Vector2()
if Input.is_action_pressed("ui_left"):
velocity.x -= 1
if Input.is_action_pressed("ui_right"):
velocity.x += 1
if Input.is_action_pressed("ui_up"):
velocity.y -= 1
if Input.is_action_pressed("ui_down"):
velocity.y += 1
if velocity.length() > 0:
velocity = velocity.normalized() * speed

By grouping all of this code together in a get_input() function, you make it easier to change things later. For example, you could decide to change to an analog joystick or other type of controller. Call this function from _process() and then change the player's position by the resulting velocity. To prevent the player from leaving the screen, you can use the clamp() function to limit the position to a minimum and maximum value:

func _process(delta):
get_input()

position += velocity * delta
position.x = clamp(position.x, 0, screensize.x)
position.y = clamp(position.y, 0, screensize.y)

Click Play the Edited Scene (F6) and confirm that you can move the player around the screen in all directions.

About delta

The _process() function includes a parameter called delta that is then multiplied by the velocity. What is delta?

The game engine attempts to run at a consistent 60 frames per second. However, this can change due to computer slowdowns, either in Godot or from the computer itself. If the frame rate is not consistent, then it will affect the movement of your game objects. For example, consider an object set to move 10 pixels every frame. If everything is running smoothly, this will translate to moving 600 pixels in one second. However, if some of those frames take longer, then there may only have been 50 frames in that second, so the object only moved 500 pixels.

Godot, like most game engines and frameworks, solves this by passing you delta, which is the elapsed time since the previous frame. Most of the time, this will be around 0.016 s (or around 16 milliseconds). If you then take your desired speed (600 px/s) and multiply by delta, you will get a movement of exactly 10. If, however, the delta increased to 0.3, then the object will be moved 18 pixels. Overall, the movement speed remains consistent and independent of the frame rate.

As a side benefit, you can express your movement in units of px/s rather than px/frame, which is easier to visualize.

Choosing animations

Now that the player can move, you need to change which animation the AnimatedSprite is playing based on whether it is moving or standing still. The art for the run animation faces to the right, which means it should be flipped horizontally (using the Flip H property) for movement to the left. Add this to the end of your _process() function:

    if velocity.length() > 0:
$AnimatedSprite.animation = "run"
$AnimatedSprite.flip_h = velocity.x < 0
else:
$AnimatedSprite.animation = "idle"

Note that this code takes a little shortcut. flip_h is a Boolean property, which means it can be true or false. A Boolean value is also the result of a comparison like <. Because of this, we can set the property equal to the result of the comparison. This one line is equivalent to writing it out like this:

if velocity.x < 0:
$AnimatedSprite.flip_h = true
else:
$AnimatedSprite.flip_h = false

Play the scene again and check that the animations are correct in each case. Make sure Playing is set to On in the AnimatedSprite so that the animations will play.

Starting and Ending the Player's Movement

When the game starts, the main scene will need to inform the player that the game has begun. Add the start() function as follows, which the main scene will use to set the player's starting animation and position:

func start(pos):
set_process(true)
position = pos
$AnimatedSprite.animation = "idle"

The die() function will be called when the player hits an obstacle or runs out of time:

func die():
$AnimatedSprite.animation = "hurt"
set_process(false)

Setting set_process(false) causes the _process() function to no longer be called for this node. That way, when the player has died, they can't still be moved by key input.

Preparing for collisions

The player should detect when it hits a coin or an obstacle, but you haven't made them do so yet. That's OK, because you can use Godot's signal functionality to make it work. Signals are a way for nodes to send out messages that other nodes can detect and react to. Many nodes have built-in signals to alert you when a body collides, for example, or when a button is pressed. You can also define custom signals for your own purposes.

Signals are used by connecting them to the node(s) that you want to listen and respond to. This connection can be made in the Inspector or in the code. Later in the project, you'll learn how to connect signals in both ways.

Add the following to the top of the script (after extends Area2D):

signal pickup
signal hurt

These define custom signals that your player will emit (send out) when they touch a coin or an obstacle. The touches will be detected by the Area2D itself. Select the Player node and click the Node tab next to the Inspector to see the list of signals the player can emit:

Note your custom signals are there as well. Since the other objects will also be Area2D nodes, you want the area_entered() signal. Select it and click Connect. Click Connect on the Connecting Signal window—you don't need to change any of those settings. Godot will automatically create a new function called _on_Player_area_entered() in your script.

When connecting a signal, instead of having Godot create a function for you, you can also give the name of an existing function that you want to link the signal to. Toggle the Make Function switch to Off if you don't want Godot to create the function for you.

Add the following code to this new function:

func _on_Player_area_entered( area ):
if area.is_in_group("coins"):
area.pickup()
emit_signal("pickup")
if area.is_in_group("obstacles"):
emit_signal("hurt")
die()

When another Area2D is detected, it will be passed in to the function (using the area variable). The coin object will have a pickup() function that defines the coin's behavior when picked up (playing an animation or sound, for example). When you create the coins and obstacles, you'll assign them to the appropriate group so they can be detected.

To summarize, here is the complete player script so far:

extends Area2D

signal pickup
signal hurt

export (int) var speed
var velocity = Vector2()
var screensize = Vector2(480, 720)

func get_input():
velocity = Vector2()
if Input.is_action_pressed("ui_left"):
velocity.x -= 1
if Input.is_action_pressed("ui_right"):
velocity.x += 1
if Input.is_action_pressed("ui_up"):
velocity.y -= 1
if Input.is_action_pressed("ui_down"):
velocity.y += 1
if velocity.length() > 0:
velocity = velocity.normalized() * speed

func _process(delta):
get_input()
position += velocity * delta
position.x = clamp(position.x, 0, screensize.x)
position.y = clamp(position.y, 0, screensize.y)

if velocity.length() > 0:
$AnimatedSprite.animation = "run"
$AnimatedSprite.flip_h = velocity.x < 0
else:
$AnimatedSprite.animation = "idle"

func start(pos):
set_process(true)
position = pos
$AnimatedSprite.animation = "idle"

func die():
$AnimatedSprite.animation = "hurt"
set_process(false)

func _on_Player_area_entered( area ):
if area.is_in_group("coins"):
area.pickup()
emit_signal("pickup")
if area.is_in_group("obstacles"):
emit_signal("hurt")
die()
lock icon The rest of the chapter is locked
Register for a free Packt account to unlock a world of extra content!
A free Packt account unlocks extra newsletters, articles, discounted offers, and much more. Start advancing your knowledge today.
Unlock this book and the full library FREE for 7 days
Get unlimited access to 7000+ expert-authored eBooks and videos courses covering every tech area you can think of
Renews at $19.99/month. Cancel anytime