A card game without physical-feeling cards is a spreadsheet. This lesson covers Godot's drag-drop system and Tween animation โ the two pieces you need for cards that fly, snap, and snap back with convincing weight.
A Tween is an object that interpolates a property between values over time. Create one, chain calls, done.
var t := create_tween()
t.tween_property(card, "position", target_pos, 0.3) # over 0.3s
t.tween_property(card, "rotation", 0.0, 0.1)
t.parallel().tween_property(card, "scale", Vector2.ONE, 0.15)
t.tween_callback(func(): print("done"))
Key moves:
tween_property(node, path, to_value, duration) โ the core call.set_trans(Tween.TRANS_CUBIC) โ easing curve.set_ease(Tween.EASE_OUT) โ in, out, in-out.parallel() โ next call runs alongside previous instead of after.set_delay(0.2) โ wait before startingtween_callback(...) โ run a function at this point in the timeline# A card "wobble" on hover โ use this everywhere
func wobble() -> void:
var t := create_tween()
t.set_trans(Tween.TRANS_ELASTIC).set_ease(Tween.EASE_OUT)
t.tween_property(self, "scale", Vector2(1.1, 1.1), 0.15)
t.tween_property(self, "scale", Vector2.ONE, 0.3)
Godot's Control nodes have built-in drag-drop routing. Three virtual methods:
# In card.gd (extends Control)
func _get_drag_data(_pos: Vector2) -> Variant:
# Called when drag starts. Return data or null to cancel.
set_drag_preview(_make_preview())
return self # pass self; other nodes receive this in _can_drop_data
func _make_preview() -> Control:
var ghost := Control.new()
var rect := ColorRect.new()
rect.color = Color("fca311")
rect.size = size
rect.modulate.a = 0.6
ghost.add_child(rect)
return ghost
And on the target (e.g. a "word slot" container):
# In word_slot.gd (extends Control)
func _can_drop_data(_pos: Vector2, data: Variant) -> bool:
return data is Card
func _drop_data(_pos: Vector2, data: Variant) -> void:
var card: Card = data
accept_card(card)
When you hold and drag a Card, Godot:
_get_drag_data on the card (dragged control)._can_drop_data. If true, the preview shows "ok"._drop_data on the target.Handles mouse and touch uniformly, courtesy of the emulation settings from Module 2.
Lexicon Duel can work two ways:
For a mobile-first game with 5 hours/week, ship tap-to-select first. Add drag as a polish pass (or a setting) if you still have steam after the core loop is fun.
When a drag is dropped somewhere invalid, the card should smoothly animate back. Godot's drag system doesn't do this automatically โ but it's four lines:
# In card.gd
var _drag_origin: Vector2
func _get_drag_data(_pos: Vector2) -> Variant:
_drag_origin = global_position
set_drag_preview(_make_preview())
return self
# Called by Godot when the drag completes, whether accepted or not.
func _notification(what: int) -> void:
if what == NOTIFICATION_DRAG_END:
if not get_viewport().gui_is_drag_successful():
_snap_back()
func _snap_back() -> void:
var t := create_tween()
t.tween_property(self, "global_position", _drag_origin, 0.2).set_trans(Tween.TRANS_BACK)
TRANS_BACK has that delightful overshoot-and-settle curve. Use it here.
Cards should visually respond to hover on desktop (and long-press preview on mobile). Common pattern:
# card.gd
const HOVER_LIFT := Vector2(0, -20)
var _base_pos: Vector2
func _ready() -> void:
_base_pos = position
mouse_entered.connect(_on_hover_in)
mouse_exited.connect(_on_hover_out)
func _on_hover_in() -> void:
var t := create_tween()
t.tween_property(self, "position", _base_pos + HOVER_LIFT, 0.12).set_trans(Tween.TRANS_CUBIC)
func _on_hover_out() -> void:
var t := create_tween()
t.tween_property(self, "position", _base_pos, 0.12).set_trans(Tween.TRANS_CUBIC)
Subtle but players will feel it. On mobile, replace mouse_entered with a long-press detection โ but honestly, don't bother on the first pass. Phones get tap-to-select.
If you trigger a hover tween while another is running, both animate and the card jitters. Always kill prior tweens:
var _current_tween: Tween
func _on_hover_in() -> void:
if _current_tween: _current_tween.kill()
_current_tween = create_tween()
_current_tween.tween_property(self, "position", _base_pos + HOVER_LIFT, 0.12)
This is the #1 cause of "my animations are glitchy" bugs. Just kill the old one.