Godot Lesson 5: Item Setup

Summary

This lesson creates a sword item that is setup to be used by both entities and players. This sets the framework for adding other weapons and items as well.

Prerequisites

  • An entity class with one enemy (Lessons 1, 2, 3)

  • Everything will make more sense with Lesson 4 (knockback)

Video

Code

Repository: https://github.com/los-alamos-steam-lab/godot-tutorial/tree/5-item-setup

Item Code

extends Node2D

var TYPE = null
var DAMAGE = 1

#number of the item that can be owned by a single entity on the screen
var maxamount = 1

# Called when the node enters the scene tree for the first time.
func _ready():
    # find out who is holding the sword
    TYPE = get_parent().TYPE

    # when the animation finsishes, call the function "destroy"
    $anim.connect("animation_finished", self, "destroy")

    #animate the sword
    $anim.play(str("swing", get_parent().spritedir))

    # if the parent has a state definted for swing, then set it
    if get_parent().has_method("state_swing"):
        get_parent().state = "swing"

func destroy(animation):
    # if the parent has a state definted for swing, then unset it
    if get_parent().has_method("state_swing"):
        get_parent().state = "default"

    #delete the item
    queue_free()

Entity Code

class_name entity
extends KinematicBody2D

# we put this here instead of autoloading it
# nothing wrong with autoload, but I prefer things in the code
var dir = directions.new()

# "CONSTANTS"
var SPEED = 0
var TYPE = "ENEMY"
# have to declare damage here so we can set it in the child scripts
var DAMAGE = null

# MOVEMENT
var movedir = Vector2.ZERO
var knockdir = Vector2.ZERO
var spritedir = "down"

var hitstun = 0
var health = 1


# Putting this here so that we can setup future calls from the
# child scripts and not have them fail
func _ready():
    return

func movement_loop():
    var motion

    # if you aren't in hitstun then move normally
    # otherwise get knocked back
    if hitstun == 0:
        motion = movedir.normalized() * SPEED
    else:
        motion = knockdir.normalized() * SPEED * 1.5


    # move_and_slide takes care of collisions and has you slide
    # along walls that are blocking your path
    move_and_slide(motion, Vector2.ZERO)

func spritedir_loop():
    match movedir:
        Vector2.LEFT:
            spritedir = "left"
        Vector2.RIGHT:
            spritedir = "right"
        Vector2.UP:
            spritedir = "up"
        Vector2.DOWN:
            spritedir = "down"

# This changes our player animation.  "animation" is a string
# of the sort "idle", "push", or "walk"
func anim_switch(animation):
    var newanim = str(animation, spritedir)
    if $anim.current_animation != newanim:
        $anim.play(newanim)

func damage_loop():
    # If you're in hitstun countdown the timer
    if hitstun > 0:
        hitstun -= 1

    # for any area that is overlapping the entity's hitbox
    for area in $hitbox.get_overlapping_areas():
        # Body is the area's parent - a weapon or an entity
        var body = area.get_parent()
        # if the entity isn't already hit, and the body gives damage,
        # and the body is a different type that the entity
        if hitstun == 0 and body.get("DAMAGE") != null and body.get("TYPE") != TYPE:
            # decrease health by the body's damage
            health -= body.get("DAMAGE")
            # Set the hitstun timer
            hitstun = 10
            # set knockdir to the opposite of the entity approached
            # the body from
            knockdir = global_transform.origin - body.global_transform.origin

# Accepts an actual item scene not the name of the scene
func use_item(item):
    # create an instance of the item
    var newitem = item.instance()

    # add it to the group with item name and the id of its parent
    newitem.add_to_group(str(newitem.get_name(), self))

    # make it a child of the entity
    add_child(newitem)

    # if there are already too many items of that type on the screen, delete it
    if get_tree().get_nodes_in_group(str(newitem.get_name(), self)).size() > newitem.maxamount:
        newitem.queue_free()

Player Code

extends entity

var state = "default"

# ready function lets us set "constants" when the file loads
func _ready():
    SPEED = 70
    TYPE = "PLAYER"

# _physics_process is called by the game engine
func _physics_process(delta):
    # making things neater with a state engine
    # this lets us break each state out in to its own function
    match state:
        "default":
            state_default()
        "swing":
            state_swing()

func state_default():
    controls_loop()
    movement_loop()
    spritedir_loop()
    damage_loop()

    if movedir == Vector2.ZERO:
        anim_switch("idle")
    elif is_on_wall():
        if (spritedir == "left" and test_move(transform, Vector2.LEFT))\
        or (spritedir == "right" and test_move(transform, Vector2.RIGHT))\
        or (spritedir == "up" and test_move(transform, Vector2.UP))\
        or (spritedir == "down" and test_move(transform, Vector2.DOWN)):
            anim_switch("push")
    else:
        anim_switch("walk")

    # action keys get put into project settings
    # if the key assigned to 'a' is pressed then use the sword
    if Input.is_action_just_pressed("a"):
        use_item(preload("res://items/sword.tscn"))


# we want to keep the player still but allow them to take damage
func state_swing():
    anim_switch("idle")
    damage_loop()

# controls_loop looks for player input
func controls_loop():
    var LEFT                = Input.is_action_pressed("ui_left")
    var RIGHT       = Input.is_action_pressed("ui_right")
    var UP          = Input.is_action_pressed("ui_up")
    var DOWN                = Input.is_action_pressed("ui_down")

    # By adding our values together, we make it so that one key
    # stroke does not take precidence over another, i.e. pushing
    # left and right keys at the same time
    movedir.x = -int(LEFT) + int(RIGHT)
    movedir.y = -int(UP) + int(DOWN)