09.creating-a-bullet-and-spell

Creating the ice spell and bullet

In this part, you will create the ice punch spell and bullet.

We will not be going through all the steps together. Instead, you will have to look at the basic fire spell and bullet and use them as a reference to create the ice spell.

We’ll start by giving you a list of requirements for how the spell should work, and we invite you to use those to try and code the spell yourself.

Requirements are the starting point for coding any game mechanic. It’s a precise list of everything that code should do.

Below, you will find more tips and steps to help you complete the task. We’ll gradually share more details up to the complete solution. Feel free to peek ahead anytime if you get stuck.

The ice punch spell’s requirements

Your task is to add the ice punch spell to the game.

The spell should work like this:

Note that we gave names to this project’s collision layers and masks. You can see the names in the Inspector by clicking to the right of collision layers or masks.

You will see that mobs are on layer 2 and walls on layer 5.

Here is what the finished spell scene should look like.

Your bullet scene should look like this.

The blue rectangle is the bullet’s hitbox. It will cause it to collide with walls and damage enemies, while the thin blue square that encloses the sprite is due to the presence of the particle node.

You can find all the sprites and particles to use for a given mechanic in the corresponding folder. For example, you will find the ice fists in the spells/ice_punch/ directory.

We recommend trying to create the spell and bullet on your own by looking at the provided SpellBasicFire.tscn and Fireball.tscn scenes.

Below, you will find some information to start creating a new spell and bullet. If you continue down the guide, you will find a growing amount of information, up to the complete code explanations. However, it will still not be a linear, step-by-step solution.

If you ever need the complete solution, you may open the godot-final-game-completed/ project, where you will find the finished code with detailed comments.

How to test your spell

While coding your spell, you will want to test it with the player character. Open the Robot.tscn scene, select the SpellHolder node, and assign your spell scene to its Spell Scene property in the Inspector.

Getting started

To get started, we recommend that you create the spell scene before the bullet because you can always test a spell with any existing bullet in the project.

You can create the ice spell and make it fire the provided fireball first. That will allow you to ensure that the spell works as intended. Then, you can create the ice bullet scene and assign it to the ice spell to complete the task.

Creating a new spell

To create a new spell, you need to inherit the Spell scene. You can right-click on the Spell.tscn file and select New Inherited Scene.

You will then need to give your node a new script that inherits the Spell class.

Finally, you will have to assign a bullet to the spell in the Inspector.

Creating a new bullet

For the bullet, it’s a bit different because we did not provide a base scene to inherit.

You need to create the bullet scene from scratch with an Area2D node as the root. You then need to assign it a new script that inherits the Bullet class.

We then invite you to read the code in the Bullet.gd and the Spell.gd scripts to figure out which functions you need to override in your inherited scripts.

Please give it a try on your own and come back if and when you get completely stuck. Note that there is no shame and coming back to get more information. What’s most important is that you first try your own because it will train your code reading and problem-solving skills.


Making the spell shoot only upon clicking

The provided basic fires spell fires automatically on a cooldown. As long as the player keeps to shoot action pressed. to achieve that, we use the _physics_process() function, although the _process() function would also work.

How can we make the bullet shoot only on the frame the player presses down the shoot action?

You have two options:

  1. You can still use one of the two process functions and call Input.is_action_just_pressed() like you did for the jump in the side-scroller series.
  2. You can define the _unhandled_input() function and call event.is_action_pressed().

Either approach is completely fine.

Solution: shooting ice fists when pressing down the shoot action

Here’s one solution to make the spell fire only upon pressing the shoot action.

We still want a cooldown: we don’t want the player to be able to frantically click to shoot many high-damage ice fists.

Otherwise, the spell would be a little too imbalanced.

# You can either use the unhandled input function or a process function with
# Input.is_action_just_pressed() to achieve this. Either is fine.
func _unhandled_input(event: InputEvent) -> void:
    if event.is_action_pressed("shoot") and _cooldown_timer.is_stopped():
        _cooldown_timer.start()
        shoot()

Solution: making the ice fist bullet emit particles before disappearing

For the ice fist bullet, you want to create a scene with an Area2D node, a CollisionShape2D node, the ice.png sprite, an AudioStreamPlayer2D, and an instance of the IceSplashParticles2D.tscn scene.

Code-wise, you need to emit the particles using the Particles2D.emitting property. You need to do that when the bullet collides with something.

You could override the Bullet._hit_body() function, but it is intended to deal damage to entities with a function named take_damage().

Instead, we want to override the _destroy() function as it gets called whenever the bullet hits something.

Our version emits the particles but also hides the ice fist and disables the bullet so that it stops moving and damaging enemies.

func _destroy() -> void:
    _disable()
    _particles.emitting = true
    _sprite.hide()
    _audio.play()

Lastly, we need to call queue_free() on the bullet so it does not linger invisibly in the game.

Otherwise, if the game session gets long, the invisible bullets will accumulate and eventually start to hurt performance.

We want to give the particles enough time to finish emitting before calling queue_free().

The trick we found was to wait for the audio to finish playing. We connected the AudioStreamPlayer2D’s finished signal to the queue_free() function.

func _ready() -> void:
    _audio.connect("finished", self, "queue_free")

A good alternative would be to use a timer with its timeout signal and start the timer in the _destroy() function.

This covers all the problems involved in making this bullet work and spell work.

Bullet and spell challenges

In this series, you will find many challenges for additional practice.

We invite you to do all the ones that you find cool. You can do them now or at the end of the series, where you will find them all listed again.

The more challenges you take, the more practice you get, and the more you improve your skills, the more unique your final project will be.

Also, feel free to go further and come up with entirely new mechanics or variations!

Do you want to make a bullet move in a spiral? Go for it! Don’t hesitate to look for code snippets online or to ask us.

Challenge: Can you make an ice fist bullet that starts very slowly and accelerates (hint: you need to change its speed)?

Challenge: Can you make a fire spell that shoots bullets rapidly (hint: it’s a variation of the provided SpellBasicFire)?

Challenge: Can you make an ice spell that shoots three ice fists in a cone instead of just one (hint: you need to override the shoot() function and use a for loop)?

In the next guide, we will work on a pickup for the player to loot and equip the ice spell.