MiniPlatformGame/addons/Gamepad/GamepadStick.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

546 lines
No EOL
22 KiB
GDScript

###[ INFO ]######################################################################################################
# Component: GamepadStick
# 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 stick virtuali, analogici o digitali.
# Possono essere aggiunti nel contenitore quanti stick si desideri (generalmente 1 o 2)
# Esistono diversi tipi di stick:
# - analogici:
# restituiscono un vettore 2D forza contenente valori che vanno da 0 a 1 (positivi per le direzioni
# destra e basso, e negativi per le direzioni sinistra e alto) dipendenti dalla distanza dello stick
# dal suo centro.
# Stick analogici sono:
# - ANALOG (consentono qualsiasi direzione)
# - LEFT/RIGHT (consentono lo spostamento solo in orizzontale - l'asse y avrà sempre valore 0)
# - UP/DOWN (consentono lo spostamento solo in verticale - l'asse x avrà sempre valore 0)
#
# - digitali:
# restituiscono un vettore 2D digitale, ovvero i valori di x e y possono valere solo 0, 1, e -1
# a seconda della direzione (positivi per le direzioni destra e basso, e negativi per le direzioni
# sinistra e alto).
# Stick digitali sono:
# - DIGITAL 8 (consente lo spostamento dello stick nelle 8 direzioni digitali (su, giù, sinistra,
# destra, e relative diagonali)
# - DIGITAL 4 PLUS (consente lo spostamento nelle sole 4 direzioni principali disposte cardinalmente
# (su, giù, destra, e sinistra)
# - DIGITAL 4 X (consente lo spostamento nelle 4 direzioni diagonali (alto-sinistra, alto-destra,
# basso-sinistra, e basso destra)
# - DIGITAL 4 ISO (è come il DIGITAL 4 X ma consente di modificare alcuni parametri particolari per
# visualizzare lo stick in maniera isometrica)
#
# La direzione corrente, nel caso di stick digitali, viene restituita anche in una lista di nome direction in cui
# è possibile verificare se una data direzione è presente (es.: if sender.UP in sender.direction: )
#
# Nota: è possibile tramutare gli analogici LEFT/RIGHT e UP/DOWN in digitali impostando la proprietà step = 1
# Requirements:
# - il parent di questo nodo deve essere di tipo GamepadArea se si desidera utilizzare la proprietà show_dinamically
# per far apparire lo stick dinamicamente alla posizione della pressione del dito sullo schermo. Il suo parent
# può essere di tipo GamepadContainer se lo stick è 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 dello stick deve essere comunque quadrata (width == height), anche nel caso di LEFT/RIGHT,
# UD/DOWN, o DIGITAL 4 ISO, altrimenti si verificheranno problemi di visualizzazione a runtime (vedere le
# immagini di esempio nella cartella assets/Gamepad)
# - se lo stick deve essere sempre visibile in una posizione fissa, è necessario valorizzare questa posizione nella
# proprietà static_position.
# To do:
# - inserire un flag per invertire l'asse y
# - provare a gestire l'analogico da tastiera come nella paddle
# Changelog:
#
#
###[ BEGIN ]#####################################################################################################
tool
extends Control
###[ CONSTS ]####################################################################################################
# contiene un valore per definire un angolo non valido
const INVALID_ANGLE = -99
###[ ENUMS ]#####################################################################################################
# consente di specificare che tipo di stick si intende gestire
enum STICK_TYPE { _ANALOG, _DIGITAL_8, _DIGITAL_4_PLUS, _DIGITAL_4_X, _DIGITAL_4_ISO, _LEFT_RIGHT, _UP_DOWN }
# contiene le quattro direzioni fondamentali, utilizzato per valorizzare la lista delle direzioni digitali direction
enum DIGITAL_DIRECTIONS { UP, LEFT, DOWN, RIGHT }
###[ INTERNAL OBJECTS ]##########################################################################################
# texture di sfondo dello stick
onready var bg = $StickBackground
# texture del centro dello stick
onready var stick = $Stick
# animazione di visualizzazione/nascondimento dello stick
onready var fader = $ShowHideAnimation
###[ EXPORTED VARIABLES ]########################################################################################
# indica se lo stick deve essere disabilitato (se true, lo stick non riceverà i tocchi dell'utente)
export var disabled = false
# indica se lo stick deve essere staticamente sempre visualizzato (false) o se questo deve apparire nascosto e
# mostrarsi (true) quando l'utente tocca la sua area (in questo caso deve essere contenuto 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 = "STICK 0"
# indica il tipo dello stick (fare riferimento alla documentazione in alto e all'enum STICK_TYPE)
export(STICK_TYPE) var stick_type = STICK_TYPE._ANALOG
# texture di sfondo dello stick
export(Texture) var background_texture setget _set_bg_texture, _get_bg_texture
# texture del centro dello stick
export(Texture) var stick_texture setget _set_texture, _get_texture
# scala della texture del centro dello stick (la dimensione dello sfondo è data dal rect_size dell'oggetto,
# quindi per impostare la dimensione del centro dello stick si usa questa proprietà)
export(Vector2) var stick_scale setget _set_scale, _get_scale
# contiene la reale posizione dello stick
export var static_position = Vector2(0, 0)
# indica se il centro dello stick deve essere nascosto (true) o meno se esso si trova al centro dell'oggetto
# (ovvero se la sua forza = 0)
export var hide_stick_on_stop = false
# questa proprietà è da utilizzarsi solo se lo stick è di tipo DIGITAL 4 ISO e serve ad indicare di quanti
# pixel deve essere spostato il centro dello stick se si trova nelle posizioni diagonali alte
export var adjust_iso = 0
# questa proprietà indica la forza minima da imporre al centro dello stick per iniziare a considerare validi
# i valori (es. con un valore = 0.5, il centro dello stick non si sposterà fino a quando non sarà raggiunto
# almeno la metà della distanza tra il centro dello stick ed il bordo)
export var valid_threshold = 0.2
# consente di restituire i valori analogici arrotondati per step
# (es.: con un valore = 0.25, lo stick restituirà come forze i soli valori 0, 0.25, 0.5, 0.75, 1)
export var step = 0.0
# per utilizzare uniformemente gli oggetti anche in presenza di tastiera, consento di associare
# direttamente degli input map alle direzioni
# Attenzione: la simulazione non funzionerà correttamente con stick analogici
export var simulate_up = "ui_up"
export var simulate_left = "ui_left"
export var simulate_down = "ui_down"
export var simulate_right = "ui_right"
###[ PRIVATE AND PUBLIC VARIABLES ]##############################################################################
# centro dello stick (ovvero dello sfondo dello stick)
var center_point = Vector2(0,0)
# ultima forza calcolata (serve per emettere i segnali solo se la forza corrente è diversa da quella precedente)
var last_force = Vector2(0,0)
# forza calcolata dal centro dello stick (oppure i valori digitali nel caso di stick digitali)
var current_force = Vector2(0,0)
# metà della dimensione dello sfondo dello stick
var half_size = Vector2()
# metà della dimensione del centro dello stick
var half_stick = Vector2()
# posizione del centro dello stick
var stick_pos = Vector2()
# area del rettangolo costituito da metà delle dimensioni dello sfondo
var squared_half_size_length = 0
# i dati del tocco (in modo che possano essere recuperati negli eventi)
var finger_data = null
# angolo tra la posizione del centro dello stick e l'asse x
var angle = -1
# lista delle direzioni digitali in cui si trova lo stick
var direction = []
# indica se sto simulando lo stick con i tasti della tastiera oppure no
var simulation = false
# mantiene lo stato della visualizzazione dinamica
var shown = true
###[ SIGNALS ]###################################################################################################
# viene emesso quando lo stick si muove, restituendo il vettore della forza (se analogico altrimento valori 0, 1,
# -1 se digitale) e l'oggetto stick stesso (in modo da poter recuperare altre informazioni come l'angolo, le
# direzioni, i dati del tocco, o qualsiasi altra proprietà dell'oggetto)
signal gamepad_force_changed(current_force, sender)
# viene emesso quando l'utente rilascia il dito dallo stick (la forza sarà sempre 0, l'angolo sarà sempre invalido,
# e la lista delle direzioni sarà sempre vuota, pertanto è inutile passare il sender)
signal gamepad_stick_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_stick_template = load("res://addons/Gamepad/GamepadStickTemplate.tscn").instance()
# quindi se ci sono oggetti nel template (ovviamente si)
if gamepad_stick_template.get_child_count() > 0:
# prendo ogni oggetto nel template
for child in gamepad_stick_template.get_children():
# e ne 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_stick()
# 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
stick.position = half_size
half_stick = (stick.texture.get_size() * stick.scale) / 2
squared_half_size_length = half_size.x * half_size.y
# emula lo stick tramite i tasti
func handle_input(event):
if event is InputEventKey:
if !((simulate_up and event.is_action(simulate_up)) or \
(simulate_down and event.is_action(simulate_down)) or \
(simulate_left and event.is_action(simulate_left)) or \
(simulate_right and event.is_action(simulate_right))): return
else:
return
var ev
# verifica quale tasto è stato premuto
var up = simulate_up and Input.is_action_pressed(simulate_up)
var down = simulate_down and Input.is_action_pressed(simulate_down)
var left = simulate_left and Input.is_action_pressed(simulate_left)
var right = simulate_right and Input.is_action_pressed(simulate_right)
simulation = false
# se nessuna delle 4 direzioni è premuta, azzero la forza così che verrà sollevato l'evento di rilascio
if !up and !down and !left and !right:
current_force = Vector2(0, 0)
else:
# se almeno una delle 4 direzioni è premuta, inizializza la posizione del'oggetto
ev = InputEventScreenTouch.new()
ev.position = get_parent().rect_global_position + static_position + half_size
simulation = true
# se lo stick è di qualsiasi tipo tranne un DIGITAL 4 diagonale
if stick_type != STICK_TYPE._DIGITAL_4_X and stick_type != STICK_TYPE._DIGITAL_4_ISO:
# imposto la forza al valore digitale corrispondente ai tasti premuti
current_force.x = -1 if left else 1 if right else 0
current_force.y = -1 if up else 1 if down else 0
else:
# altrimenti, se lo stick è di tipo DIGITAL 4 diagonale, decido io la forza in base al tasto premuto
if up:
down = false; left = false; right = false
current_force = Vector2(-1, -1)
elif left:
down = false; up = false; right = false
current_force = Vector2(-1, 1)
elif down:
up = false; left = false; right = false
current_force = Vector2(1, 1)
elif right:
down = false; left = false; up = false
current_force = Vector2(1, -1)
# se la forza è diversa da 0
if current_force.x != 0 or current_force.y != 0:
# ed è diversa dalla precedente
if last_force.x != current_force.x or last_force.y != current_force.y:
# simulo la pressione del dito sullo stick
handle_down_event(ev, null)
else:
# mentre se la forza è 0 ma la precedente non lo era,
if last_force.x != 0 or last_force.y != 0:
# simulo il rilascio del dito dallo stick
handle_up_event(ev, null)
# l'utente ha toccato lo schermo in corrispondenza dello stick o dell'area che contiene lo stick
func handle_down_event(event, finger):
# se lo stick è disabilitato 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 lo stick deve essere visualizzato dinamicamente vuol dire che in questo momento non è visibile e quindi lo mostro
if show_dynamically:
_show_stick(event)
# se il tocco è avvenuto nella zona dello sfondo dello stick
if simulation or bg.get_global_rect().has_point(event.position):
# calcolo la forza, aggiorno la posizione del centro dello stick 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 lo stick o la sua area
func handle_up_event(event, finger):
# se lo stick è disabilitato 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 lo stick deve essere visualizzato dinamicamente vuol dire che in questo momento è visibile e quindi lo nascondo
if show_dynamically:
_hide_stick()
# resetto i dati interni
reset()
# quindi emetto il segnale per comunicare che lo stick è stato rilasciato
emit_signal("gamepad_stick_released")
# l'utente ha spostato il dito con cui aveva toccato lo stick o la sua area
func handle_move_event(event, finger):
# se lo stick è disabilitato 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, aggiorno la posizione del centro dello stick ed emetto il segnale
calculate(event)
# calcolo la forza, aggiorno la posizione del centro dello stick 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
var pos = event.position - rect_global_position
calculate_force(pos)
update_stick_pos()
emit()
func calculate_force(pos):
# print ("pos: ", pos, " - center_point: ", center_point, " - half_size: ", half_size)
if !simulation:
# calcolo la forza in relazione alla posizione del mouse e il centro dello stick, 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 dello stick)
if (current_force.length() < valid_threshold):
current_force = Vector2(0,0)
# effettuo aggiustamenti vari alla forza in baso a che tipo di stick sto gestendo
select_force()
# aggiorno la posizione del centro dello stick in modo che graficamente sia coerente
func update_stick_pos():
stick_pos.x = center_point.x + half_size.x * current_force.x
stick_pos.y = center_point.y + half_size.y * current_force.y
# questa funzione serve solo se lo stick è di tipo DIGITAL 4 ISO
adjust_stick_pos()
stick.position = Vector2(stick_pos)
# calcolo anche l'angolo tra la posizione dello stick e l'asse x
angle = stick.position.angle_to_point(center_point)
# infine gestisco la visualizzazione o meno del centro dello stick se deve essere
# gestita in base al valore di hide_stick_on_stop (vedi commento)
if hide_stick_on_stop and current_force.x == 0 and current_force.y == 0:
stick.hide()
else:
stick.show()
# effettuo un reset dei dati interni, ovvero faccio si che la forza sia impostata a 0,
# il centro dello stick torni graficamente al centro, e sia impostato un angolo invalido
func reset():
# calculate_force(center_point)
current_force = Vector2(0,0)
last_force = Vector2(0,0)
update_stick_pos()
angle = INVALID_ANGLE
# emit()
# emette il segnale per comunicare il cambiamento della forza dello stick
func emit():
if current_force.x != last_force.x or current_force.y != last_force.y:
# solo se la forza corrente è diversa da quella precedente
last_force = Vector2(current_force.x, current_force.y)
emit_signal("gamepad_force_changed", current_force, self)
# se lo stick è di tipo DIGITAL 4 ISO, e la posizione del centro dello stick capita in una
# diagonale alta, viene aggiustata graficamente la posizione
func adjust_stick_pos():
if stick_type != STICK_TYPE._ANALOG and stick_type != null:
if stick_type == STICK_TYPE._DIGITAL_4_ISO and adjust_iso != 0 and current_force.y == -1:
if stick_pos.x < half_stick.x + adjust_iso:
stick_pos.x = half_stick.x + adjust_iso
elif stick_pos.x > rect_size.x - half_stick.x - adjust_iso:
stick_pos.x = rect_size.x - half_stick.x - adjust_iso
else:
if stick_pos.x < half_stick.x:
stick_pos.x = half_stick.x
elif stick_pos.x > rect_size.x - half_stick.x:
stick_pos.x = rect_size.x - half_stick.x
if stick_pos.y < half_stick.y:
stick_pos.y = half_stick.y
elif stick_pos.y > rect_size.y - half_stick.y:
stick_pos.y = rect_size.y - half_stick.y
# qui la forza viene adattata in base al tipo di stick che sto gestendo
func select_force():
match stick_type:
STICK_TYPE._DIGITAL_8:
# la forza viene semplicemente convertita in digitale
to_digital()
STICK_TYPE._DIGITAL_4_PLUS:
# il minore dei due assi viene azzerato in modo che possano
# essere restituite solo forze cardinali
if abs(current_force.x) > abs(current_force.y):
current_force.y = 0
else:
current_force.x = 0
# quindi la forza viene convertita in digitale
to_digital()
STICK_TYPE._DIGITAL_4_X, STICK_TYPE._DIGITAL_4_ISO:
# salvo la forza analogica prima di convertirla in digitale
# in modo da poter capire effettivamente dove si trova il
# centro dello stick
var curr = Vector2(current_force.x, current_force.y)
# converto la forza in digitale
to_digital()
# determino quindi in quale diagonale mi trovo
if abs(current_force.x) == 1:
if curr.y > 0.35:
current_force.y = 1
else:
current_force.y = -1
else:
if abs(current_force.y) == 1:
if curr.x > 0.35:
current_force.x = 1
else:
current_force.x = -1
STICK_TYPE._LEFT_RIGHT:
# azzero l'asse y
current_force.y = 0
# quindi, essendo un controllo analogico, lo sottopondo allo step
to_steps()
STICK_TYPE._UP_DOWN:
# azzero l'asse x
current_force.x = 0
# quindi, essendo un controllo analogico, lo sottopondo allo step
to_steps()
_:
# ANALOG
# essendo un controllo analogico, lo sottopondo allo step
to_steps()
# popolo la lista delle direzioni in base ai valori digitali ottenuti
direction = []
if current_force.x < 0:
direction.append(DIGITAL_DIRECTIONS.LEFT)
elif current_force.x > 0:
direction.append(DIGITAL_DIRECTIONS.RIGHT)
if current_force.y < 0:
direction.append(DIGITAL_DIRECTIONS.UP)
elif current_force.y > 0:
direction.append(DIGITAL_DIRECTIONS.DOWN)
func to_steps():
# se lo step vale 0 (o meno) non applico lo step ed esco
if step <= 0:
return
# se lo step vale 1 (o più) converto direttamente in digitale ed esco
if step >= 1:
to_digital()
return
# altrimenti applico lo step
var modx = int(current_force.x / step) * step if abs(current_force.x) < 0.99 else 1 * sign(current_force.x)
var mody = int(current_force.y / step) * step if abs(current_force.y) < 0.99 else 1 * sign(current_force.y)
current_force = Vector2(modx, mody)
# digitalizza la forza corrente
func to_digital():
current_force = current_force.normalized()
current_force.x = stepify(current_force.x, 1)
current_force.y = stepify(current_force.y, 1)
# mostra lo stick
func _show_stick(event):
# se event è diverso dal null (nel caso in l'utente tocca lo stick 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 dello stick è quella statica impostata in static_position
rect_position = static_position
# avvio l'animazione di visualizzazione
if fader:
if !simulation: reset()
fader.stop()
fader.play("fade_in", -1, 10)
# nasconde lo stick
func _hide_stick():
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():
# if !has_node("Stick"): return Vector2(1.0, 1.0)
return $Stick.scale
func _set_scale(value):
# if !has_node("Stick"): return
$Stick.scale = value
$Stick.position = $StickBackground.rect_size / 2
func _get_bg_texture():
# if !has_node("StickBackground"): return null
return $StickBackground.texture
func _set_bg_texture(value):
# if !has_node("StickBackground"): return
$StickBackground.texture = value
$Stick.position = $StickBackground.rect_size / 2
func _get_texture():
# if !has_node("Stick"): return null
return $Stick.texture
func _set_texture(value):
# if !has_node("Stick"): return
$Stick.texture = value
$Stick.position = $StickBackground.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_stick()
else:
_show_stick(null)
###[ END ]#######################################################################################################