Saturday, April 6, 2024

Godot dynamicFont

 This is a short entry, but it fixes the problem I had with the localization files. It was due to the font not properly found, after "hardcoding it" into the project for each node, it worked. Here's the change and the proof, in case someone has the same problem in the future (or even just a reminder for me)



Also, here's the PS Vita with the multilanguage working:



Extra: The Lifebar Node working:
- Root: Control Node (Lifebar), with children nodes: Tween, HBoxContainer (Bars), and a Label (Number)
- Besides, the Bars node also has children Nodes: MarginContainer (Count), and TextureProgress.
And the code for the attached script,
```
extends Control

var hearts = 4 setget set_hearts
var max_hearts = 4

onready var number_label = $Number
onready var bar = $Bars/TextureProgress
onready var tween = $Tween

var animated_health = 0

func _ready():
    var PlayerStats = get_parent().get_node("Stats")
    var player_max_health = PlayerStats.max_health
    bar.max_value = player_max_health
    update_health(player_max_health)


    self.max_hearts = PlayerStats.max_health
    self.hearts = PlayerStats.health
    # warning-ignore:return_value_discarded
    PlayerStats.connect("health_changed", self, "set_hearts")
    # warning-ignore:return_value_discarded
    PlayerStats.connect("max_health_changed", self, "set_max_hearts")



func set_hearts(value):
    hearts = clamp(value, 0, max_hearts)
    update_health(value)


func update_health(new_value):
    tween.interpolate_property(self, "animated_health", animated_health, new_value, 0.2, Tween.TRANS_LINEAR, Tween.EASE_IN)
    if not tween.is_active():
        tween.start()
    number_label.text = str(new_value)


func _process(delta):
    var round_value = round(animated_health)
    bar.value = round_value
```

Thursday, April 4, 2024

Sprite Overlap

 Today, we are going to create a new scene(node) to handle enemy sprites overlapping on screen:
- Area2D root node
- and a collisionShape

Then attach a script to your Area2D. In this script, we're going to check if there are two sprites in the same position (overlapping), and if they are, one of them is going to receive a little push to separate them.

```
extends Area2D

func is_colliding():
    var areas = get_overlapping_areas()
    return areas.size() > 0

func get_push_vector():
    var areas = get_overlapping_areas()
    var push_vector = Vector2.ZERO
    if is_colliding():
        var area = areas[0]
        push_vector = area.global_position.direction_to(global_position)
        push_vector = push_vector.normalized()
    return push_vector
```


For this to work, we also need to create a layer for this collision.

And then set it up to be used

Once the new Area2D scene is set up, instance it to your enemy nodes, and add their collision shapes

> I bet you think that's all, but you need to use those functions in your bat script for them to work

`onready var spriteOverlap = $SpriteOverlap`

and at the end of your physic_process:
```
    if spriteOverlap.is_colliding():
        velocity += spriteOverlap.get_push_vector() * delta * 400
    velocity = move_and_slide(velocity)
```


In other words, your enemy script should be eventually something like this:
```
extends KinematicBody2D

const EnemyDeathEffect = preload("res://Effects/EnemyDeathEffect.tscn")

export var ACCELERATION = 300
export var MAX_SPEED = 50
export var FRICTION = 200
export var WANDER_TARGET_RANGE = 4

enum {
    IDLE,
    WANDER,
    CHASE
}

var velocity = Vector2.ZERO
var knockback = Vector2.ZERO

var state = CHASE

onready var stats = $Stats
onready var playerDetectionZone = $PlayerDetectionZone
onready var spriteOverlap = $SpriteOverlap

func _physics_process(delta):
    knockback = knockback.move_toward(Vector2.ZERO, FRICTION * delta)
    knockback = move_and_slide(knockback)
    
    match state:
        IDLE:
            velocity = velocity.move_toward(Vector2.ZERO, FRICTION * delta)
            seek_player()
        WANDER:
            pass
        CHASE:
            var player = playerDetectionZone.player
            if player != null:
                var direction = (player.global_position - global_position).normalized()
                velocity = velocity.move_toward(direction * MAX_SPEED, ACCELERATION * delta)
            else:
                state = IDLE
                
    if spriteOverlap.is_colliding():
        velocity += spriteOverlap.get_push_vector() * delta * 400
    velocity = move_and_slide(velocity)

func seek_player():
    if playerDetectionZone.can_see_player():
        state = CHASE

func _on_Hurtbox_area_entered(area):
    stats.health -= area.damage
    knockback = area.knockback_vector * 150

func _on_Stats_no_health():
    queue_free()
    var enemyDeathEffect = EnemyDeathEffect.instance()
    get_parent().add_child(enemyDeathEffect)
    enemyDeathEffect.global_position = global_position

```

And that's all for this entry

Wednesday, April 3, 2024

Enemy AI part 2

For the enemy to attack, the player needs to have a hurtbox and some stats. That's the first thing we are going to do.

- Add a hurtbox and a stats child node to your player

- As we did with the enemy, we need to connect the no_health signal from the player stats too

- Yes, we also need another signal connected to the player for the hurtbox Area entered too

* After connecting this signal, you should have two, one for the hitbox to the area (for the animation effect), and one for the hitbox to the player (for the damage).


- After that, add the variable and the function for the player to update their stats
`onready var stats = $Stats` and

```
func _on_Hurtbox_area_entered(area):
    stats.health -= area.damage
```


- So, the player can be hurt, but the enemy doesn't have a hitbox, it's time to change that. Add a hitbox child node to your enemy

* Don't forget its shape, for now we will just attach a circle shape (will update this according to enemy's attacks later)


* It also needs the mask set up properly, 3 = player hurtbox:


With all of that, our enemy can kill the player now. However, the player never exits the area hitbox and the enemy can't repeat the attack until that.


>>> Things to fix: The enemy attack animation and hitbox, and the enemy exiting the area. Both should be fixable at the same time when we do the attack sprite animation part.

>>> Things to fix 2: enemy Sprite Overlap. If there is more than one enemy following you they overlap and you only see one sprite which also affect to colliders.

Tuesday, April 2, 2024

Enemy AI part 1

This entry has quite a few similarities with the ones on "animating sprites". However, it has key extra functions and creating some new entries for the explanations is worth it. Mind you, we are still mostly following https://youtu.be/R0XvL3_t840 if you want to watch it in video.

Let's start with the base script for the NPC to find and chase the player. It won't deal damage, and it will use some already created nodes from the animated sprites entries.

As you can see below:
- We already have created the export variables that manage our enemy movement stats.
- A basic state machine diagram for the enemy's possible actions is defined through an enum.
- The physics_process deals with the engine's physics and has a knockback function that moves the enemy when it receives a hit.
- And we have functions for the enemy dying and receiving damage

```
extends KinematicBody2D

const EnemyDeathEffect = preload("res://Effects/EnemyDeathEffect.tscn")

export var ACCELERATION = 300
export var MAX_SPEED = 50
export var FRICTION = 200
export var WANDER_TARGET_RANGE = 4

enum {
    IDLE,
    WANDER,
    CHASE
}

var velocity = Vector2.ZERO
var knockback = Vector2.ZERO

onready var stats = $Stats

func _physics_process(delta):
    knockback = knockback.move_toward(Vector2.ZERO, FRICTION * delta)
    knockback = move_and_slide(knockback)


func _on_Hurtbox_area_entered(area):
    stats.health -= area.damage
    knockback = area.knockback_vector * 100

func _on_Stats_no_health():
    queue_free()
    var enemyDeathEffect = EnemyDeathEffect.instance()
    get_parent().add_child(enemyDeathEffect)
    enemyDeathEffect.global_position = global_position

```


For our enemy scene, we currently have:
- The KinematicBody2D's scene root node, so it can move around.
- A sprite node, to display the enemy on screen.
- A CollisionShape2D to collide with the world environment.
- A hurtbox composed node, for the enemy to be able to receive hits
- And a stats composed node, to deal with enemy stats updates.


First, we need to add the code for the states diagram to be active: 

A new var `var state = IDLE` and the match state in the physics_process:

```
func _physics_process(delta):
    knockback = knockback.move_toward(Vector2.ZERO, FRICTION * delta)
    knockback = move_and_slide(knockback)
    
    match state:
        IDLE:
            pass
        WANDER:
            pass
        CHASE:
            pass
```


Now let's work with our states. The easiest one is to let the enemy be idle until the player enters in its vision range:

```
        IDLE:
            velocity = velocity.move_toward(Vector2.ZERO, FRICTION * delta)
            seek_player()
```


To check if the player is in its vision range, we will use a function:

```
func seek_player():
    if playerDetectionZone.can_see_player():
        state = CHASE
```


And a correlated composed node (scene), connected to your enemy script by a new var:
`onready var playerDetectionZone = $PlayerDetectionZone`
in the enemy's script.

- This composed scene will be an Area2D with a CollisionShape2D

- Attach a script to your CollisionShape2D, so it can manage the colliders
```
extends Area2D

var player = null

func can_see_player():
    return player != null

func _on_PlayerDetectionZone_body_entered(body):
    player = body

func _on_PlayerDetectionZone_body_exited(_body):
    player = null
```


And make sure your `body_entered` and `body_exited` signals are connected:

* Don't forget to set your collision mask properly, so the Area is only activated when the player enters and not any other object:

- Once you have your custom node, add it to your enemy and check its editable children property

- Now you can add the collision detection shape:

After that, it's time to update our CHASE state (in the enemy's script) so the enemy can chase the player
```
        CHASE:
            var player = playerDetectionZone.player
            if player != null:
                var direction = (player.global_position - global_position).normalized()
                velocity = velocity.move_toward(direction * MAX_SPEED, ACCELERATION * delta)
            else:
                state = IDLE
```


* Don't forget to update your velocity in the script right after, outside the 'match':
`velocity = move_and_slide(velocity)`

>>> Since the enemy's script code updates are scattered around the entry to be properly explained, I'll copy the current full script here, so you can double check:

```
extends KinematicBody2D

const EnemyDeathEffect = preload("res://Effects/EnemyDeathEffect.tscn")

export var ACCELERATION = 300
export var MAX_SPEED = 50
export var FRICTION = 200
export var WANDER_TARGET_RANGE = 4

enum {
    IDLE,
    WANDER,
    CHASE
}

var velocity = Vector2.ZERO
var knockback = Vector2.ZERO

var state = CHASE

onready var stats = $Stats
onready var playerDetectionZone = $PlayerDetectionZone

func _physics_process(delta):
    knockback = knockback.move_toward(Vector2.ZERO, FRICTION * delta)
    knockback = move_and_slide(knockback)
    
    match state:
        IDLE:
            velocity = velocity.move_toward(Vector2.ZERO, FRICTION * delta)
            seek_player()
        WANDER:
            pass
        CHASE:
            var player = playerDetectionZone.player
            if player != null:
                var direction = (player.global_position - global_position).normalized()
                velocity = velocity.move_toward(direction * MAX_SPEED, ACCELERATION * delta)
            else:
                state = IDLE
                
    velocity = move_and_slide(velocity)

func seek_player():
    if playerDetectionZone.can_see_player():
        state = CHASE

func _on_Hurtbox_area_entered(area):
    stats.health -= area.damage
    knockback = area.knockback_vector * 100

func _on_Stats_no_health():
    queue_free()
    var enemyDeathEffect = EnemyDeathEffect.instance()
    get_parent().add_child(enemyDeathEffect)
    enemyDeathEffect.global_position = global_position
```


Extra: For the player or the enemy to be in front properly, when they are one over the other, you need to use ysort ( https://docs.godotengine.org/en/3.5/classes/class_ysort.html )

And that's all for this entry, will update this enemy so at least it can attack in a future one.

Monday, April 1, 2024

Godot Game Localization

In this entry, we will learn about how to make a multilanguage text in Godot.
The Godot's documentation can be seen here: https://docs.godotengine.org/en/3.6/tutorials/assets_pipeline/importing_translations.html and https://docs.godotengine.org/en/3.6/tutorials/i18n/internationalizing_games.html


First, we're going to make one simple example to see how it works.
According to the documentation, we need to have a CSV file formatted as follows:
CSV files must be formatted as follows: keys    <lang1>        <lang2>        <langN>

* The "Lang" tags must represent a language, which must be one of the valid locales supported by the engine.
* The "KEY" tags must be unique and represent a string universally. These keys will be replaced at runtime by the matching translated string.
* Note that the case is important, "KEY1" and "Key1" will be different keys.
* In addition, you have to take into account that the first raw will be ignored by the engine, and that, can have empty rows (which we can use to keep our sections separated)

With all that info, we are going to create a CSV file using LibreOffice in this entry. Open your calc file and fill it. Example below:

Once you have your cells filled, the next step is saving the file properly. Go to "Save as" and select CSV file:

Click on OK, and when for the next prompt, remember what the documentation says:
`The CSV files must be saved with UTF-8 encoding without a byte order mark`

* Be sure to click Reimport after any change to these options.

After saving your CSV file, go to your Godot's project's folder and copy-paste it or drag and drop it inside.

* Once you do this, Godot will automatically parse it and create the translation files for the project, as you can see below:

With this done, go to "Project > Project Settings"

And here, go to the "Localization" tab, it will be empty. We are going to add our recently created translation files:

One by one, select and open all your translation files

When you finish, you can close this window:

Now, if you write a key in one text string (no extra spaces or symbols, or it won't work), Godot will try to find the key in your system set language (This can only be seen during execution, so don't be afraid if you only see the key in your developer screen).

    godot07.png

* Font used: https://www.dafont.com/essays1743.font

You have made it!! However, we want the user to be able to change it so...

Let's write the text so we don't have to restart the system to change the language:
Attach this code to your label:
```
func _unhandled_input(event):
  if event.is_action_pressed("translate"):
    TranslationServer.set_locale("ja")
    text = tr("GREETINGS00")
```


This will take user input through the "translated" key (defined in your Input Map), you can use whatever key and name here.

If you run your game now, you will see the window empty (or in some cases just lack characters):

Don't worry, this only means that the default font doesn't have characters for the string, and you have to add one that has. To do this, go to your "Inspector" on the right side of the window, and in "Theme Overrides > Fonts" add your font and fallbacks fonts for those cases that the characters don't exist. VoilĂ !!

* Japanese font used: https://www.freekanjifonts.com/japanesefont-rii-handwriting-download/

If we use this code with a lot of ifs, we can make the text display in any language now. But that wouldn't be clean at all, in fact it would be long and dirty. So, for the second part of this entry, we will write and share the bullet points and the code to create a simpler and short modular scene (tscn) with a menu, that we can copy and reuse for any other project. [We can do it editing the previous tscn or creating a new one. We will make a new scene, so the steps are clearer]

Open your new and clean scene and add the following root and children nodes:

- CenterContainer: We will use this as root, so we can hide it from display when we use it as a child for other nodes.
- VBoxContainer: We will sort our screen text pattern with this one
- Label: This will contain the "language" string in the different languages, to give a nice visual touch
- Button: This will be in charge to show the language in use and the signals for it to change, among the other possible options.

Now attach this script to your `CenterContainer` node ("Locale" in the image above), so it knows what to do to manage the scene.
```
extends Node

var langKey = 00

func _ready():
    # use and display english as default locale
    # to save resources finding the language in the first run
    TranslationServer.set_locale("en")
    _set_Locale()

func _on_Button_pressed():
    langKey += 1
    if langKey > 2 : # Keep the key value in language bounds
        langKey = 0
    _set_Locale()

func _set_Locale():
    match langKey:
        00:
            TranslationServer.set_locale("en")
        01:
            TranslationServer.set_locale("es")
        02:
            TranslationServer.set_locale("ja")

    var langCode = "LANGUAGE0" + str(langKey)
    get_node("VBoxContainer/Label").text = tr("LANGUAGEID")
    get_node("VBoxContainer/Button").text = tr(langCode)
```


Don't forget to go into your Button node, and connect the "pressed" signal (to your CenterContainer node), or it won't work.

If you followed the guide carefully, you should be able to run your project and get a result like this one.
The first image shows your default window (English), if you click on the language button (the gray one), you should be able to step from one to the next and loop among them.


With that, we have accomplished our goal for today!!


Warning: at the release of this entry, the display in the PS Vita does not work, https://github.com/SonicMastr/godot-vita/issues/47 . However, since it will probably work in the future, the same way it does in the PC, I will leave it here as is. [Edit: The error was related to the font, solved here,  https://homebrew-psvita.blogspot.com/2024/04/godot-dynamicfont.html]

Extra: I added that sample project to godot's asset library if you want to tinker with it, https://godotengine.org/asset-library/asset/2855

Thursday, March 28, 2024

Animating the sprite part4

 More entries on animated sprites, now it's turn for hitbox and hurtbox where it depends on the animated frame. Again, we'll be getting help from: https://www.youtube.com/watch?v=vDbEfmPcv-Q

We are going to start renaming a few layers and masks so we can deal with them more easily in the long run.
Key part here is understanding and differentiating what is the use of each ( https://docs.godotengine.org/en/stable/tutorials/physics/physics_introduction.html ):

Layer: describes the layers that the object appears in.
Mask: describes what layers the body will scan for collisions


Go to Project > "Project Settings" and, in the general category, scroll down until you see "Layer names". Select the "2d physics" option for this.

We'll name it in this order: World, Player, PlayerHurtbox, EnemyHurtbox


That will make our life easier when we want to know what collides with.

- Now we are going to create one scene for hurtbox and another for hitbox.


- Add a collisionShape2d child note to each of them


Note: there's a warning telling us that the node needs a shape, but we're not going to create one here. The explanations for it is inheritance, we want them as a base to attach to other nodes like the player or the enemies. That's where we are going to work with that.

- Attach the hitbox scene to the player


- Right-click on the hitbox scene you've attached to the player and select "Editable Children"


- Now we are going to create the shapes, in the child of the hitbox node from the player.


- Before working with the shape, we want a Position2D node to use a visual indicator for the position of the item that will contain the hitbox, we are going to call it "HitboxPivot"


- Time to go back to our animation to add the hitbox. Select your animatedTree


- And then select your AnimationPlayer


You want to use the frame that better displays the range of the collision shape you want to create. Let's do an example with the attackDown here.

- Select a frame to display and go to your Hitbox to edit its shape


After editing your shape, you should have something like this


- You have to key the rotation of that hitboxPivot at the first frame of the animation


- Save and repeat the process for all the attack positions (left, up, down and up)


- But the hitbox is now set to active all the time, and we want it to only be active during the attack input. To do that, select your collisionShape in the Hitbox and disable it.


- Now go to your animation frames and key the disable property, so it sets off the collision during the attack frames and disables the collision after the animation ends finishes.


This works properly now, but before finishing, remember the layers from the beginning of the entry? It's time to set them (the layers were: 1 = World, 2 = Player, 3 = PlayerHurtbox, 4 = EnemyHurtbox).

- Put your player in his layer, 2. And select where he will scan for collisions, 1 and 4.



That's all for this animated sprites section.