Thursday, March 28, 2019

18. OS execute

We can run commands using OS.execute function.


The root node is a Node with sole child, a Label.


This script is attached to the Node.



extends Node

var label

func _ready():
    label= get_node("Label")
    label.align = Label.ALIGN_CENTER
    label.valign = Label.VALIGN_CENTER
    var output = []
    var pid = OS.execute('csound', ["--version"], true, output)
    var result = ""
    for i in range(122):
        result += output[0][i]
    label.set_text(result) 


We assume that csound is installed on the system. This is the first 122 characters on my version:


17. AnimatedSprite

We use a set of images from the website OpenGameArt for different animations mentioned on this site.



We have a Node with child of Label and Node2D, which has its child an AnimatedSprite. The Node2D is there, so we may set its scale of 0.5, 0.5. Thus we do not have to change scale of the AnimatedSprite to reduce image sizes. We have to rename the nodes as 'node2d', 'anim_sprite' and 'label' and position the label and image so they can be viewed.


For the Animated Sprite we add two actions, Idle-Normal and Idle-Blinking after creating the new SpriteFrames. There are many other set of sequences, but they were not used. Below, we show the sequence for Idle-Blinking:



We add this script to Node.



extends Node

var anim_sprite
var label

func _ready():
    anim_sprite = get_node('node2d/anim_sprite')
    label = get_node("label")

func _process(delta):
    if Input.is_action_pressed('ui_left'):
        anim_sprite.play("idle_blinking")
        label.set_text(anim_sprite.animation)
    elif Input.is_action_pressed('ui_right'):
        anim_sprite.play("idle_normal")
        label.set_text(anim_sprite.animation)
 


We have this output for the blinking case (left arrow pressed):


16. RayCast2D

We will have 3 scenes. The first one is a plain Node called 'World.tscn'. The second is a KinematicBody2d ('Player') and third is a StaticBody2d ('Enemy')


Underneath both the 'Player' and 'Enemy' we have a Sprite, and CollsionShape2D. The Sprite for both is the Godot icon, however for the 'Enemy' one, we set the modulate property to red. The collision shape is a rectangle matching the extents of Sprite.


For the 'Player', we have four RayCast2D nodes as children. They are named Left, Right, Down, and Up. We have to check the Enable in the Inspector and change cast to, for Left, Right, and Up. For Down, the default value is ok. For Up, set it to (0,-50). For Right, set it to (50,0), and Left to (-50,0). We should be able to see the arrows pointing in correct direction. We should enable visibility on only 1 of the nodes at a time so we can see the individual rays pointing in the correct direction.


Finally to the 'World', we attach this script:



extends Node

const SPEED = 100.0

onready var Player = preload("res://Player.tscn")
onready var Enemy = preload("res://Enemy.tscn")
var player
var enemy

func _ready():
    setup()

func _process(delta):
    var motion = Vector2(0.0, 0.0)
    if Input.is_action_pressed("ui_right"):
        motion.x = SPEED
    elif Input.is_action_pressed("ui_left"):
        motion.x = -SPEED
    elif Input.is_action_pressed("ui_down"):
        motion.y = SPEED
    elif Input.is_action_pressed("ui_up"):
        motion.y = -SPEED
    player.move_and_slide(motion)
    var left = player.get_node("Left")
    var right = player.get_node("Right")
    var up = player.get_node("Up")
    var down = player.get_node("Down")
    if left.is_colliding():
        print("left")
    elif right.is_colliding():
        print("right")
    elif up.is_colliding():
        print("up")
    elif down.is_colliding():
        print("down")
    else:
        print("Not colliding")
 
func setup():
    player = Player.instance()
    add_child(player)
    player.position = Vector2(100.0, 100.0)
    enemy = Enemy.instance()
    enemy.position = Vector2(200.0, 200.0)
    add_child(enemy)


If we are to left of the 'Enemy' and such that the ray intersects (that is close enough), the Right ray will collide. We set the window as (300,300) in Project Settings. This shows the output including the text:


15. Tween

Any property can be animated by the Tween node. To a Node, we attach a script to animate the scale property of the sprite if either up arrow is pressed or down arrow is pressed.


In the _process function, we see if up or down have just been pressed. If they are, we either start the scale up animation or scale down animation. However we only start if no animations are running.


We create Sprite Node and two Tween nodes using new(). Then this script keeps monitoring for input to start animation:



extends Node

var sprite = Sprite.new()
var tween_up = Tween.new()
var tween_down = Tween.new()

func _ready():
    sprite.texture = load("res://icon.png")
    sprite.position = Vector2(100, 100)
    add_child(sprite)
    add_child(tween_up)
    add_child(tween_down)
 
func _process(delta):
    var up = Input.is_action_just_pressed("ui_up")
    var down = Input.is_action_just_pressed("ui_down")
    if (up or down) and (tween_up.is_active() or tween_down.is_active()):
        print('Please wait ...')
    elif up:
        print('up')
        tween_up.interpolate_property(sprite, 'scale',
            Vector2(1.0, 1.0), Vector2(2.0, 2.0), 2,
            Tween.TRANS_CIRC, Tween.EASE_OUT)
        tween_up.interpolate_property(sprite, 'scale', 
            Vector2(2.0, 2.0), Vector2(1.0, 1.0), 2,
            Tween.TRANS_QUAD, Tween.EASE_IN, 2)
        tween_up.start()
    elif down:
        print('down')
        tween_down.interpolate_property(sprite, 'scale',
            Vector2(1.0, 1.0), Vector2(0.5, 0.5), 2,
            Tween.TRANS_CUBIC, Tween.EASE_OUT)
        tween_down.interpolate_property(sprite, 'scale',
            Vector2(0.5, 0.5), Vector2(1.0, 1.0), 2,
            Tween.TRANS_QUAD, Tween.EASE_IN, 2)
        tween_down.start()


We can refactor a lot of code into separate functions for ease of reading and developing bug-free code. It will not be a good idea to put everything in the main loop, _process, but we should put them in separate functions.


The output during a scale up animation, with the project window size of 200 by 200:


Wednesday, March 27, 2019

14. Tilemap

We use this tileset from the website OpenGameArt.



This is the png image where each tile is 16 by 16 with spacing of 1 between them.



We have a scene with Node and a Sprite child. Set the texture of Sprite with the above image. Make sure to load image with 2D pixel preset using the Import tab next to Scene. Then we attach a script to Node which will generate our tile resource (tres) file. We can think of this as the atlas of all tiles that Godot can understand.



extends Node

var tile_size = Vector2(16, 16)
onready var texture = $Sprite.texture

func _ready():
    var col = 7
    var row = 5
    var ts = TileSet.new()
    for i in range(col):
        for j in range(row):
            var region = Rect2(i * tile_size.x + i, j * tile_size.y + j,
                               tile_size.x, tile_size.y)
            var id = i + j * 7
            ts.create_tile(id)
            ts.tile_set_texture(id, texture)
            ts.tile_set_region(id, region)
        ResourceSaver.save("res://tiles.tres", ts)



Even though we get 35 tiles, 10 are empty. After creating the tiles.tres file by running this scene, we can now test it. In a new scene, will will have the node of TileMap.


Set the cell size to 16 by 16, and resource to the tiles.tres file. Then we can paint the tiles. It is best to have a grid of 16 by 16 with snapping.


13. look_at

We have a Sprite with attached script for controlling position and rotation.


The position is controlled with left and right arrows, and rotation is set by the look_at() function. For the look_at function, we can imagine a vector going from sprite center (assuming no offset) to look at point. Here we use a look at point at top of screen. (In project setting we get game width and height both as 400). This is the attached script:



extends Sprite

const TOP = Vector2(200,0)
var old_rot

func _ready():
 position = Vector2(200,200)
 old_rot = int(rotation_degrees)

func _process(delta):
 if Input.is_action_pressed('ui_left'):
  position.x -= 1
 elif Input.is_action_pressed('ui_right'):
  position.x += 1
 look_at(TOP)
 if int(rotation_degrees) != old_rot:
  print("Rotation = ", int(rotation_degrees))
  old_rot = int(rotation_degrees)



The output, when we have moved the icon to left of screen so it keeps pointing to middle-top:


Thursday, March 21, 2019

12. Event-Driven Input

Under a Node, we have a Label child. Instead of polling in _process for particular input actions, we can use _input to handle input events.


We attach a script to Node, which outputs a string to the label if key is pressed. Had we attached the script to Label, we would either have referred to label.rect_position as self.rect_position or just rect_position. However I believe it more clear to write label.rect_position and attach script to parent.



extends Node

var label
var string

func _ready():
    label = get_node("Label")
    label.rect_position = Vector2(50, 50)

func _input(event):
    if event is InputEventKey:
        string = "Is key pressed: " + str(event.pressed) + "\n"
        string = string + "key: " + str(event.scancode)
        label.set_text(string)

The output, with window size of 200,200, when the spacebar is pressed and held down: