MiniPlatformGame/addons/Gamepad/GamepadPaddle.gd
Martin Dimitrov 8689a4732a Initial commit
In this commit I add a human character, a traveler that can move around
a tiny world made of a single screen so far. :)

Materials used so far:
 * 'HeartBeast' video lesons: https://goo.gl/3DtqPn
 * The very informative and rich documentation of Godot itself!

For the Gamepad I used wonderful addon by fiaful:
https://github.com/fiaful/Gamepad

The beautiful arts are from "Open Pixel Project":
https://openpixelproject.itch.io
2018-10-26 16:20:12 +03:00

408 lines
No EOL
15 KiB
GDScript

###[ INFO ]######################################################################################################
# Component: GamepadPaddle
# Author: Francesco Iafulli (fiaful)
# E-mail: fiaful@hotmail.com
# Version: 1.0
# Last modify: 2018-07-20
# What is this:
# E' l'oggetto che consente di gestire paddle virtuali analogici (la paddle è un controller che ruota su se stesso,
# come ad esempio il volante di una automobile).
# Possono essere aggiunti nel contenitore quanti paddle si desideri (generalmente 1 o 2)
# Requirements:
# - il parent di questo nodo deve essere di tipo GamepadArea se si desidera utilizzare la proprietà show_dinamically
# per far apparire la paddle dinamicamente alla posizione della pressione del dito sullo schermo. Il suo parent
# può essere di tipo GamepadContainer se la paddle è sempre visibile sullo schermo ad una posizione fissa.
# - deve comunque essere contenuto (direttamente o indirettamente) in un nodo di tipo GamepadContainer, altrimenti
# non funzionerà
# - la texture di sfondo della paddle deve essere quadrata altrimenti si verificheranno problemi di visualizzazione
# a runtime (vedere le immagini di esempio nella cartella assets/Gamepad)
# - se la paddle deve essere sempre visibile in una posizione fissa, è necessario valorizzare questa posizione nella
# proprietà static_position.
# Changelog:
#
#
###[ BEGIN ]#####################################################################################################
tool
extends Control
###[ CONSTS ]####################################################################################################
# contiene un valore per definire un angolo non valido
const INVALID_ANGLE = -99
###[ INTERNAL OBJECTS ]##########################################################################################
# texture di sfondo della paddle
onready var bg = $PaddleBackground
# texture del centro della paddle
onready var paddle = $Paddle
# animazione di visualizzazione/nascondimento della paddle
onready var fader = $ShowHideAnimation
onready var timer = $Timer
###[ EXPORTED VARIABLES ]########################################################################################
# indica se la paddle deve essere disabilitata (se true, la paddle non riceverà i tocchi dell'utente)
export var disabled = false
# indica se la paddle deve essere staticamente sempre visualizzata (false) o se questa deve apparire nascosta e
# mostrarsi (true) quando l'utente tocca la sua area (in questo caso deve essere contenuta in un oggetto di tipo
# GamepadArea)
export var show_dynamically = false setget _set_show_dynamically
# questa proprietà contiene il nome dell'oggetto (che viene restituito nell'oggetto finger)
export var gamepad_type = "PADDLE 0"
# texture di sfondo della paddle
export(Texture) var background_texture setget _set_bg_texture, _get_bg_texture
# texture del centro della paddle
export(Texture) var paddle_texture setget _set_texture, _get_texture
# scala della texture del centro della paddle (la dimensione dello sfondo è data dal rect_size dell'oggetto,
# quindi per impostare la dimensione del centro della paddle si usa questa proprietà)
export(Vector2) var paddle_scale setget _set_scale, _get_scale
# contiene la reale posizione della paddle
export var static_position = Vector2(0, 0)
# questa proprietà indica la forza minima da imporre alla paddle per iniziare a considerare validi
# i valori (es. con un valore = 0.5, la paddle inizierà a ruotare solo se l'utente toccherà l'oggetto
# su oltre la metà della distanza tra il centro della paddle ed il bordo)
export var valid_threshold = 0.2
# se impostato a true, il rilascio della paddle resetterà i valori e la posizione della paddle, mentre
# se impostato a false, al rilascio i valori e la posizione resteranno gli ultimi validi
export var reset_on_release = true
# impone un limite inferiore alla rotazione della paddle (limite inferiore e superiore possono essere invertiti)
export var low_limit = 0
# impone un limite alto alla rotazione della paddle (limite inferiore e superiore possono essere invertiti)
export var high_limit = 0
# per utilizzare uniformemente gli oggetti anche in presenza di tastiera, consento di associare
# direttamente degli input map per ruotare la paddle in senso antiorario e orario
export var simulate_counter_clockwise = "ui_left"
export var simulate_clockwise = "ui_right"
# in caso di simulazione con la tastiera, indica lo step di incremento/decremento dell'angolo
export var simulation_increment = 0.05
# in caso di simulazione con la tastiera, indica la velocità di incremento/decremento dell'angolo
export var simulation_delay = 0.01
###[ PRIVATE AND PUBLIC VARIABLES ]##############################################################################
# centro della paddle (ovvero dello sfondo della paddle)
var center_point = Vector2(0,0)
# forza calcolata dal centro della paddle (serve per calcolare l'angolo)
var current_force = Vector2(0,0)
# metà della dimensione dello sfondo della paddle
var half_size = Vector2()
# area del rettangolo costituito da metà delle dimensioni dello sfondo
var squared_half_size_length = 0
# indica se l'angolo di rotazione della paddle si trova all'interno o all'esterno dei limiti imposti
var into_limits = false
# i dati del tocco (in modo che possano essere recuperati negli eventi)
var finger_data = null
# angolo di rotazione della paddle
var angle = -1
# indica se sto ruotando la paddle con i tasti della tastiera oppure no
var simulation = false
# ultimo angolo calcolato (serve per emettere i segnali solo se l'angolo corrente è diverso da quello precedente)
var last_angle = INVALID_ANGLE
# mantiene lo stato della visualizzazione dinamica
var shown = true
# indica la direzione di rotazione nel caso di simulazione con la tastiera
var direction = 0
###[ SIGNALS ]###################################################################################################
# viene emesso quando la paddle ruota, restituendo l'angolo di rotazione e l'oggetto paddle stesso (in modo da
# poter recuperare altre informazioni (qualsiasi proprietà dell'oggetto)
signal angle_changed(current_angle, sender)
# viene emesso quando l'utente rilascia il dito dalla paddle (l'angolo sarà sempre invalido, pertanto è inutile
# passare il sender)
signal paddle_released
###[ METHODS ]###################################################################################################
# costruisce l'albero dei nodi necessari all'oggetto prendendoli dal template
func _init():
# se non sono già stati caricati
if get_child_count() > 0: return
# carico e istanzio il template
var gamepad_paddle_template = load("res://addons/Gamepad/GamepadPaddleTemplate.tscn").instance()
# quindi se ci sono oggetti nel template (ovviamente si)
if gamepad_paddle_template.get_child_count() > 0:
# prendo ogni oggetto nel template
for child in gamepad_paddle_template.get_children():
# se l'oggetto è il timer
if child is Timer:
# ne creo il duplicato
var tmr = child.duplicate()
# lo aggiungo al mio nodo
add_child(tmr)
tmr.wait_time = simulation_delay
# connetto il suo segnale timeout allo script
tmr.connect("timeout", self, "_on_timer_timeout")
else:
# aggiungo un duplicato al mio nodo
add_child(child.duplicate())
func _ready():
# se l'oggetto deve essere visualizzato dinamicamente (ovvero solo quando l'utente tocca lo schermo) lo nascondo
if show_dynamically:
_hide_paddle()
# imposto la sua posizione statica (non ha senso se visualizzato dinamicamente in quanto la sua posizione
# varierà in base al tocco dell'utente)
rect_position = static_position
# ricavo i restanti valori che mi serviranno più avanti per fare i calcoli
half_size = bg.rect_size / 2
center_point = half_size
paddle.position = half_size
squared_half_size_length = half_size.x * half_size.y
# emula la paddle tramite i tasti
func handle_input(event):
if event is InputEventKey:
if !((simulate_counter_clockwise and event.is_action(simulate_counter_clockwise)) or \
(simulate_clockwise and event.is_action(simulate_clockwise))): return
else:
return
# verifica quale tasto è stato premuto
var cnt = simulate_counter_clockwise and Input.is_action_pressed(simulate_counter_clockwise)
var clk = simulate_clockwise and Input.is_action_pressed(simulate_clockwise)
simulation = false
# se nessuna delle 2 direzioni è premuta, azzero l'angolo e sollevo l'evento di rilascio
if !cnt and !clk:
# fermo il timer che si occupa di far ruotare la paddle
timer.stop()
handle_up_event(null, null)
else:
# imposto la direzione di rotazione
if cnt:
clk = false
direction = -simulation_increment
elif clk:
cnt = false
direction = simulation_increment
# ed avvio il timer che si occuperà di far ruotare la paddle
timer.start()
func _on_timer_timeout():
# inizializza la posizione del'oggetto
var ev = InputEventScreenTouch.new()
ev.position = get_parent().rect_global_position + static_position + half_size
simulation = true
# incrementa/decrementa l'angolo
angle += direction
# se l'angolo è diverso dal precedente
if angle != last_angle:
last_angle = angle
# simulo la rotazione della paddle
handle_down_event(ev, null)
# l'utente ha toccato lo schermo in corrispondenza della paddle o dell'area che contiene la paddle
func handle_down_event(event, finger):
# se la paddle è disabilitata esco senza fare nulla (prima però resetto i dati interni)
if disabled:
reset()
return
# altrimenti imposto i dati del tocco in modo che possano essere recuperati da fuori
finger_data = finger
# se la paddle deve essere visualizzata dinamicamente vuol dire che in questo momento non è visibile e quindi la mostro
if show_dynamically:
_show_paddle(event)
# se il tocco è avvenuto nella zona dello sfondo della paddle
if simulation or bg.get_global_rect().has_point(event.position):
# calcolo la forza e l'angolo, aggiorno la rotazione della paddle ed emetto il segnale
calculate(event)
else:
# altrimenti resetto tutti i dati e esco
reset()
# l'utente ha sollevato il dito con cui aveva toccato la paddle o la sua area
func handle_up_event(event, finger):
# se la paddle è disabilitata esco senza fare nulla (prima però resetto i dati interni)
if disabled:
reset()
return
# altrimenti imposto i dati del tocco in modo che possano essere recuperati da fuori
finger_data = finger
# se la paddle deve essere visualizzata dinamicamente vuol dire che in questo momento è visibile e quindi la nascondo
if show_dynamically:
_hide_paddle()
# resetto i dati interni
reset()
# quindi emetto il segnale per comunicare che la paddle è stata rilasciata
emit_signal("paddle_released")
# l'utente ha spostato il dito con cui aveva toccato la paddle o la sua area
func handle_move_event(event, finger):
# se la paddle è disabilitata esco senza fare nulla (prima però resetto i dati interni)
if disabled:
reset()
return
# altrimenti imposto i dati del tocco in modo che possano essere recuperati da fuori
finger_data = finger
# calcolo la forza, l'angolo, aggiorno la rotazione della paddle ed emetto il segnale
calculate(event)
# calcolo la forza, l'angolo, aggiorno la rotazione della paddle ed emetto il segnale
func calculate(event):
# ricalcolo la posizione dell'evento in modo che lo 0,0 coincida con lo 0,0 dell'oggetto
if !simulation:
var pos = event.position - rect_global_position
calculate_force(pos)
update_paddle_pos()
emit()
func calculate_force(pos):
# print ("pos: ", pos, " - center_point: ", center_point, " - half_size: ", half_size)
# calcolo la forza in relazione alla posizione del mouse e il centro della paddle, e la normalizzo
current_force.x = (pos.x - center_point.x) / half_size.x
current_force.y = (pos.y - center_point.y) / half_size.y
if current_force.length_squared() > 1:
current_force = current_force / current_force.length()
# quindi se la forza è minore della soglia di validità, resituisco 0,0 (per comunicare che
# non è stato effettuato uno spostamento valido del centro della paddle)
if (current_force.length() < valid_threshold):
current_force = Vector2(0,0)
# aggiorno la rotazione della paddle in modo che graficamente sia coerente
func update_paddle_pos():
var new_angle
if !simulation:
var x = center_point.x + half_size.x * current_force.x
var y = center_point.y + half_size.y * current_force.y
new_angle = Vector2(x, y).angle_to_point(center_point)
else:
new_angle = angle
# quindi verifico che il nuovo angolo sia nei limiti
into_limits = false
var deg_angle = rad2deg(new_angle) + 180
# print ([deg_angle, low_limit, high_limit])
# se low_limit e high_limit sono uguali non devo imporre limiti, ovvero sono sempre nei limiti
if low_limit != high_limit:
if low_limit > high_limit:
if deg_angle <= high_limit:
into_limits = true
if deg_angle >= low_limit:
if deg_angle >= high_limit:
into_limits = true
else:
if deg_angle <= high_limit and deg_angle >= low_limit:
into_limits = true
else:
into_limits = true
# se sono nei limiti, imposto il nuovo angolo ed aggiorno la rotazione della paddle
if into_limits:
angle = new_angle
paddle.rotation = angle
# solo se reset_on_release = true (vedi commento) effettuo un reset dei dati interni,
# imposto l'angolo della paddle ad un valore invalido e ne aggiorno graficamente la rotazione
func reset():
if !reset_on_release: return
calculate_force(center_point)
update_paddle_pos()
angle = INVALID_ANGLE
last_angle = angle
# emit()
# emette il segnale per comunicare il cambiamento dell'angolo della paddle
func emit():
if into_limits:
emit_signal("angle_changed", angle, self)
# print (angle / PI * 180)
# print (rad2deg(angle) + 180)
# mostra la paddle
func _show_paddle(event):
# se event è diverso dal null (nel caso in l'utente tocca la paddle o la sua area) calcolo la posizione
# in base a quella passata nell'evento
if shown: return
shown = true
if event:
rect_global_position = event.position - center_point
else:
# altrimenti la posizione della paddle è quella statica impostata in static_position
rect_position = static_position
# avvio l'animazione di visualizzazione
if fader:
reset()
fader.stop()
fader.play("fade_in", -1, 10)
# nasconde la paddle
func _hide_paddle():
if !shown: return
shown = false
# avvia l'animazione di nascondimento
if fader:
fader.stop()
fader.play("fade_out", -1, 10)
###[ SETTER/GETTER ]#############################################################################################
func _get_scale():
return $Paddle.scale
func _set_scale(value):
# if !has_node("Paddle"): return
$Paddle.scale = value
$Paddle.position = $PaddleBackground.rect_size / 2
func _get_bg_texture():
return $PaddleBackground.texture
func _set_bg_texture(value):
# if !has_node("PaddleBackground"): return
$PaddleBackground.texture = value
$Paddle.position = $PaddleBackground.rect_size / 2
func _get_texture():
return $Paddle.texture
func _set_texture(value):
# if !has_node("PaddleBackground"): return
$Paddle.texture = value
$Paddle.position = $PaddleBackground.rect_size / 2
func _set_show_dynamically(value):
show_dynamically = value
# se sono nell'editor non faccio nulla (altrimenti mi verrebbe nascosto l'oggetto anche dall'editor)
if Engine.editor_hint: return
if value:
_hide_paddle()
else:
_show_paddle(null)
###[ END ]#######################################################################################################