Creating a game with Godot Engine - Ep.7 - The ball
In this seventh article we will talk about the center of the gameplay, the ball. We will talk about how the ball is made but also how do we emit and receive it. We will also take a look at a way to create an explosion. Finally we will see how the player interact with the ball using a box. (source code)
Disclaimer: the following article represents one way to do it and, it may not be the best way. Please adapt this technique using your best judgement.
Creating interactive elements is how you create your gameplay, therefore it is very important to pay attention to details, otherwise your game will be frustrating for the player. We can distinct two types of interactive elements, direct and indirect. The direct elements are the ones the player can interact with the character. The indirect elements are the ones the player can only interact through other elements. The only direct element in this tutorial is the box that the player can move around by pushing it. I do not consider the ball a direct element, even if it explodes on player contact, because the player has to use the box to change its direction. The emitter is indirect because in order to stop it the player has to put a ball in the receiver. The receiver is indirect because in order to trigger it the player has to put a ball in it.
# Contains the ball logic extends RigidBody2D var exploded = false var ball var emitter func set_color(color): ball.set_modulate(color) func explode(): exploded = true var explosion = ResourceLoader.load("res://objects/ball_explosion/object_ball_explosion.tscn") var exp_inst = explosion.instance() exp_inst.set_pos(get_pos()) get_parent().add_child(exp_inst) queue_free() func received(): queue_free() func _ready(): ball = find_node("ball") func _on_root_body_enter_shape(body_id, body, body_shape, local_shape): if(!exploded && "explode_ball" in body.get_groups()): explode()
The ball is quite simple and contains just few tricks in the attached script. It starts with a RigidBody2D root node and it contains two children nodes, a Sprite2D and a CollisionShape2D. The set_color function is called by the emitter so the ball has the color of the receiver it has to go into. The received function is called by the receiver in the case the ball end up in it, it simply delete the ball. We call queue_free to delete the node at the next idle of the game loop. The reason you want to queue the deletion is that it is never a good idea to let a node delete itself, you will get game crash for sure.
The explode function is interesting because it shows how to easily replace a node with another, in this case we replace the ball with the explosion. If we go through the function, in the first line we just set the ball in the exploded state so it does no get exploded twice and possibly crash the game. Then we load the explosion scene. Then we create an instance of the explosion scene. Then we place it at the same position as the ball. Then we add it into the same parent as the ball. Finally, we delete the ball.
The _on_root_body_enter_shape function is connected to the body_enter_shape signal from RigidBody2D. It is the function which decide if the ball has to explode or not. Thanks to Godot it is quite an easy job. First, make sure that Contact Monitor of RigidBody2D is On, otherwise the signal will not be triggered. The condition first checks if the ball is not already in the exploded state, to avoid game crash. Then the trick is to make use of the group feature of Godot. Instead of checking all the different types of object which make the ball explode, we just have to add such objects in the explode_ball group. Therefore, by only checking if the colliding body has the explode_ball group assigned we know if the ball has to explode.
# Contains the ball emitter logic extends StaticBody2D export var emit_vector = Vector2() export var emit_interval = 2.0 signal stopped() var timer = Timer.new() var ball = preload("res://objects/basic_ball/object_basic_ball.tscn") var emitter var is_stopped = false func get_color(): return emitter.get_modulate() func stop(): timer.stop() is_stopped = true emit_signal("stopped") func _ready(): add_child(timer) timer.connect("timeout", self, "_shoot") timer.set_wait_time(emit_interval) timer.set_one_shot(false) timer.start() emitter = find_node("emitter") func _shoot(): var ball_instance = ball.instance() ball_instance.set_linear_velocity(emit_vector) add_child(ball_instance) ball_instance.set_color(emitter.get_modulate()) ball_instance.emitter = self
The setup of the scene for the emitter is very similar to the ball scene, except the root is a StaticBody2D, because we don't want it to move. As for the ball all the "magic" happens in the script, which we will explore now.
But first, I want to talk about the collision layers and mask. Those properties appear in the Godot UI as arrays of checkboxes. I had to use them to avoid the ball to collide with the emitter while being able to block the character to go on the emitter. To not complicate things at this point we can just consider putting the same values for the layers and the mask and just talk about the layers. The idea behind is that you want to put in the same layer all the objects you want to collide together. In the tutorial, the ball is in layer 1 and therefore is able to collide with all objects in layer 1 like walls and the character but not emitters and receivers which are in the layer 2. The character is in both layer 1 and 2 making it able to collide with walls and balls but also emitters and receivers.
If we look at the var declaration at the beginning of the script we can observe something special, some of the var have the export keyword in front. This is a way to make a value from the script accessible through the Godot UI, making easy to modify the value especially when instancing the scene in another scene.
The get_color function is just a shortcut to get the color of the emitter to adjust the corresponding receiver color accordingly. The stop function is called when the receiver is triggered to make the emitter stop throwing balls. The _ready function contains the initialization of the timer, set_wait_time is called to make sure the emitter does not throw a ball immediately when the level start, set_one_shot is set to false so the emitter continuously throw balls until stopped. The _shoot function is called by the timer on each timeout signal and throw a ball, the important part is the call to set_linear_velocity to throw the ball in a given direction with a given force.
# Contains the ball receiver logic extends StaticBody2D var receiver var light var emitter func set_emitter(object): emitter = object receiver.set_modulate(emitter.get_color()) light.set_color(emitter.get_color()) func _ready(): receiver = find_node("receiver") light = find_node("light") func _on_trigger_area_body_enter( body ): if("ball" in body.get_groups()): if(body.emitter == emitter): body.received() emitter.stop() else: body.explode()
The setup of the scene has additional nodes compared to the emitter. The first additional node is an Area2D. It is used to detect the balls. The child CollisionShape2D allows us to tweak the hit-box which trigger the receiver. The second additional node is a Light2D used to make clearer where the player has to put the ball.
The set_emitter function is called when creating a level to link an emitter to a receiver, it also set the colors so the player knows which receiver receives which ball. The _on_trigger_area_body_enter function is connected to the Area2D body_enter signal. We use it to detect if the correct ball triggered the receiver. The function make use of the groups to first check if the triggering object is a ball. Then we check if the ball is linked to same emitter in which case the ball is removed and the emitter stopped, otherwise the ball just explode. This is not the best way to check it but for the purpose of this tutorial it is sufficient.
# Contains the ball explosion logic extends StaticBody2D func _ready(): var space_state = get_world_2d().get_direct_space_state() var query = Physics2DShapeQueryParameters.new() var shape = find_node("trigger").get_shape() query.set_shape(shape) query.set_transform(Matrix32(Vector2(1,0), Vector2(0,1), get_global_pos())) var hits = space_state.intersect_shape(query) for hit in hits: if("player" in hit.collider.get_groups()): hit.collider.killed() func _on_explosion_anim_finished(): queue_free()
In theory we could have incorporated the explosion in the ball scene, but it makes thing much more cleaner and easier by doing it in a separate scene. The root of the scene is a StaticBody2D. The trigger node is CollisionShape2D set with a circle shape, it is use to detect other objects possibly affected by the explosion. The explosion node is a Particle2D used to create the explosion effect. The explosion_light node is a Light2D used to add light to the explosion. The explosion_anim is an AnimationPlayer used to fire just a burst of particles and also to control the lighting.
The script is quite short but still contains an interesting part, how to detect objects within a given area. The detection code is in the _ready function. The first thing we want to do is to get the space state of the world which will allows us to query arbitrary collisions. Then we want also to create a query object. We also want the shape of the hit-box which we assign to the query. The last step before executing the query is to set the transformation matrix, the two first parameters are set to the default value because we want to the default orientation, the third parameter is set to the position of the explosion in world coordinates. The query is executed by the intersect_shape function on the space state, in return we get a list of colliding objects. From there we go through the list and check if the player is in by using again a group check. If the list contains the player we kill it. The _on_explosion_anim_finished is connected to the anim_finished signal of our explosion_anim to simply delete the explosion when done as it is a one time event.
The box, despite being an important part of the gameplay, is very simple to implement thanks to Godot, there is no code in the script. By using a KinematicBody2D the player character can push it around using the Godot physic engine. And by using a ConvexPolygonShape2D in a shape of triangle for the collision handling, we are able to change the direction of the ball.
What is next?
In the next article we will speak about the creation of a level and how to load it.
Written by Olivier on Monday January 16, 2017