Select/hover tetramino with Area3D signal
This commit is contained in:
@@ -1,19 +1,22 @@
|
|||||||
# Tetromino Selector System
|
# Tetromino Selector System
|
||||||
|
|
||||||
## Overview
|
## Overview
|
||||||
Adds interactive tetromino selection and repositioning during gameplay. Players can select already-placed tetrominoes, move them, rotate them, and place them in new positions.
|
Adds interactive tetromino selection and repositioning during gameplay. Players can select already-
|
||||||
|
placed tetrominoes, move them, rotate them, and place them in new positions.
|
||||||
|
|
||||||
## Components
|
## Components
|
||||||
|
|
||||||
### 1. TetrominoSelector (scripts/tetromino_selector.gd)
|
### 1. TetrominoSelector (scripts/tetromino_selector.gd)
|
||||||
|
|
||||||
Main controller for tetromino selection logic.
|
Main controller for tetromino selection logic.
|
||||||
|
|
||||||
**Features:**
|
**Features:**
|
||||||
- Raycast-based tetromino detection on left-click
|
- Raycast-based tetromino detection on left-click from camera to world
|
||||||
- Ghost mode visualization (semi-transparent)
|
|
||||||
- Real-time position tracking with mouse movement
|
- Real-time position tracking with mouse movement
|
||||||
- Rotation with spacebar (45° increments)
|
|
||||||
|
- Ghost mode visualization (semi-transparent)
|
||||||
- Placement validation using board manager
|
- Placement validation using board manager
|
||||||
|
- Rotation with spacebar (45° increments)
|
||||||
- Cancellation with right-click or ESC key
|
- Cancellation with right-click or ESC key
|
||||||
|
|
||||||
**Signals:**
|
**Signals:**
|
||||||
@@ -78,12 +81,12 @@ Added collision detection for raycast selection.
|
|||||||
- Player left-clicks to place
|
- Player left-clicks to place
|
||||||
- Selector validates placement using BoardManager3D._can_place_tetromino()
|
- Selector validates placement using BoardManager3D._can_place_tetromino()
|
||||||
- If valid:
|
- If valid:
|
||||||
- Updates board manager occupied cells
|
- Updates board manager occupied cells
|
||||||
- Tetromino exits ghost mode
|
- Tetromino exits ghost mode
|
||||||
- Signals emit `selection_placed`
|
- Signals emit `selection_placed`
|
||||||
- If invalid:
|
- If invalid:
|
||||||
- Tetromino snaps back to original position
|
- Tetromino snaps back to original position
|
||||||
- Reverts to original rotation
|
- Reverts to original rotation
|
||||||
|
|
||||||
5. **Cancellation**
|
5. **Cancellation**
|
||||||
- Player right-clicks or presses ESC
|
- Player right-clicks or presses ESC
|
||||||
|
|||||||
Binary file not shown.
BIN
assets/models/MSH_T_Corta.res
Normal file
BIN
assets/models/MSH_T_Corta.res
Normal file
Binary file not shown.
@@ -1,7 +1,9 @@
|
|||||||
[gd_resource type="Resource" script_class="TetrominoDefinition" load_steps=2 format=3 uid="uid://curn7voye0ewx"]
|
[gd_resource type="Resource" script_class="TetrominoDefinition" load_steps=3 format=3 uid="uid://curn7voye0ewx"]
|
||||||
|
|
||||||
[ext_resource type="Script" uid="uid://p168a1urs4im" path="res://scripts/tetromino_definition.gd" id="1_suu2q"]
|
[ext_resource type="Script" uid="uid://p168a1urs4im" path="res://scripts/tetromino_definition.gd" id="1_suu2q"]
|
||||||
|
[ext_resource type="ArrayMesh" uid="uid://dev1jigsm4i4e" path="res://assets/models/MSH_1x2_Orizzontale.res" id="1_tot0g"]
|
||||||
|
|
||||||
[resource]
|
[resource]
|
||||||
script = ExtResource("1_suu2q")
|
script = ExtResource("1_suu2q")
|
||||||
|
mesh = ExtResource("1_tot0g")
|
||||||
metadata/_custom_type_script = "uid://p168a1urs4im"
|
metadata/_custom_type_script = "uid://p168a1urs4im"
|
||||||
|
|||||||
9
resources/data/tetrominos/t_piece.tres
Normal file
9
resources/data/tetrominos/t_piece.tres
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
[gd_resource type="Resource" script_class="TetrominoDefinition" load_steps=3 format=3 uid="uid://cqgsjnof4dly0"]
|
||||||
|
|
||||||
|
[ext_resource type="Script" uid="uid://p168a1urs4im" path="res://scripts/tetromino_definition.gd" id="1_0geyk"]
|
||||||
|
[ext_resource type="ArrayMesh" uid="uid://5jb2cluw5746" path="res://assets/models/MSH_T_Corta.res" id="1_il264"]
|
||||||
|
|
||||||
|
[resource]
|
||||||
|
script = ExtResource("1_0geyk")
|
||||||
|
mesh = ExtResource("1_il264")
|
||||||
|
metadata/_custom_type_script = "uid://p168a1urs4im"
|
||||||
@@ -1,8 +1,9 @@
|
|||||||
[gd_scene load_steps=5 format=3 uid="uid://j3vihw63lw7q"]
|
[gd_scene load_steps=6 format=3 uid="uid://j3vihw63lw7q"]
|
||||||
|
|
||||||
[ext_resource type="Script" uid="uid://bvxt8mmibr1uj" path="res://scripts/board_manager.gd" id="1_jnyf0"]
|
[ext_resource type="Script" uid="uid://bvxt8mmibr1uj" path="res://scripts/board_manager.gd" id="1_jnyf0"]
|
||||||
[ext_resource type="PackedScene" uid="uid://btdnshtrnejt" path="res://scenes/board/tetromino.tscn" id="2_jnyf0"]
|
[ext_resource type="PackedScene" uid="uid://btdnshtrnejt" path="res://scenes/board/tetromino.tscn" id="2_jnyf0"]
|
||||||
[ext_resource type="Script" uid="uid://bv7xi75mklk7d" path="res://scripts/tetromino_selector.gd" id="3_jnyf0"]
|
[ext_resource type="Script" uid="uid://bv7xi75mklk7d" path="res://scripts/tetromino_selector.gd" id="3_jnyf0"]
|
||||||
|
[ext_resource type="Resource" uid="uid://cqgsjnof4dly0" path="res://resources/data/tetrominos/t_piece.tres" id="3_q07he"]
|
||||||
|
|
||||||
[sub_resource type="PlaneMesh" id="PlaneMesh_s78fa"]
|
[sub_resource type="PlaneMesh" id="PlaneMesh_s78fa"]
|
||||||
size = Vector2(1, 1)
|
size = Vector2(1, 1)
|
||||||
@@ -25,6 +26,7 @@ mesh = SubResource("PlaneMesh_s78fa")
|
|||||||
[node name="TetrominoContainer" type="Node3D" parent="."]
|
[node name="TetrominoContainer" type="Node3D" parent="."]
|
||||||
|
|
||||||
[node name="Tetromino" parent="TetrominoContainer" instance=ExtResource("2_jnyf0")]
|
[node name="Tetromino" parent="TetrominoContainer" instance=ExtResource("2_jnyf0")]
|
||||||
|
resource = ExtResource("3_q07he")
|
||||||
|
|
||||||
[node name="TetrominoSelector" type="Node3D" parent="."]
|
[node name="TetrominoSelector" type="Node3D" parent="."]
|
||||||
script = ExtResource("3_jnyf0")
|
script = ExtResource("3_jnyf0")
|
||||||
|
|||||||
@@ -1,14 +1,10 @@
|
|||||||
[gd_scene load_steps=6 format=3 uid="uid://btdnshtrnejt"]
|
[gd_scene load_steps=5 format=3 uid="uid://btdnshtrnejt"]
|
||||||
|
|
||||||
[ext_resource type="Script" uid="uid://c8041v2usigk4" path="res://scripts/tetromino.gd" id="1_hprdj"]
|
[ext_resource type="Script" uid="uid://c8041v2usigk4" path="res://scripts/tetromino.gd" id="1_hprdj"]
|
||||||
[ext_resource type="Resource" uid="uid://curn7voye0ewx" path="res://resources/data/tetrominos/i_piece.tres" id="2_f3wyc"]
|
[ext_resource type="Resource" uid="uid://curn7voye0ewx" path="res://resources/data/tetrominos/i_piece.tres" id="2_f3wyc"]
|
||||||
[ext_resource type="ArrayMesh" uid="uid://e1oj3t6audj3" path="res://assets/models/MSH_1x3.res" id="3_f3wyc"]
|
[ext_resource type="ArrayMesh" uid="uid://5jb2cluw5746" path="res://assets/models/MSH_T_Corta.res" id="3_f3wyc"]
|
||||||
|
|
||||||
[sub_resource type="StandardMaterial3D" id="StandardMaterial3D_f3wyc"]
|
|
||||||
albedo_color = Color(1, 0, 0, 1)
|
|
||||||
|
|
||||||
[sub_resource type="BoxShape3D" id="BoxShape3D_f3wyc"]
|
[sub_resource type="BoxShape3D" id="BoxShape3D_f3wyc"]
|
||||||
size = Vector3(3.0031738, 1, 1)
|
|
||||||
|
|
||||||
[node name="Tetromino" type="Node3D"]
|
[node name="Tetromino" type="Node3D"]
|
||||||
script = ExtResource("1_hprdj")
|
script = ExtResource("1_hprdj")
|
||||||
@@ -19,7 +15,6 @@ mesh_color = Color(1, 0, 0, 1)
|
|||||||
|
|
||||||
[node name="MeshInstance3D" type="MeshInstance3D" parent="Visuals"]
|
[node name="MeshInstance3D" type="MeshInstance3D" parent="Visuals"]
|
||||||
layers = 2
|
layers = 2
|
||||||
material_override = SubResource("StandardMaterial3D_f3wyc")
|
|
||||||
mesh = ExtResource("3_f3wyc")
|
mesh = ExtResource("3_f3wyc")
|
||||||
skeleton = NodePath("../..")
|
skeleton = NodePath("../..")
|
||||||
|
|
||||||
@@ -28,5 +23,8 @@ collision_layer = 16
|
|||||||
collision_mask = 0
|
collision_mask = 0
|
||||||
|
|
||||||
[node name="CollisionShape3D" type="CollisionShape3D" parent="SelectionArea"]
|
[node name="CollisionShape3D" type="CollisionShape3D" parent="SelectionArea"]
|
||||||
transform = Transform3D(-4.371139e-08, 0, 1, 0, 1, 0, -1, 0, -4.371139e-08, -1.6541168e-10, 0.49949223, -0.0037841797)
|
|
||||||
shape = SubResource("BoxShape3D_f3wyc")
|
shape = SubResource("BoxShape3D_f3wyc")
|
||||||
|
|
||||||
|
[connection signal="input_event" from="SelectionArea" to="." method="_on_selection_area_input_event"]
|
||||||
|
[connection signal="mouse_entered" from="SelectionArea" to="." method="_on_selection_area_mouse_entered"]
|
||||||
|
[connection signal="mouse_exited" from="SelectionArea" to="." method="_on_selection_area_mouse_exited"]
|
||||||
|
|||||||
@@ -5,6 +5,11 @@
|
|||||||
class_name Tetromino
|
class_name Tetromino
|
||||||
extends Node3D
|
extends Node3D
|
||||||
|
|
||||||
|
signal hovered(tetromino)
|
||||||
|
signal unhovered(tetromino)
|
||||||
|
signal selected(tetromino, grid_pos)
|
||||||
|
signal deselected(tetromino)
|
||||||
|
|
||||||
# Data
|
# Data
|
||||||
@export var resource: TetrominoDefinition
|
@export var resource: TetrominoDefinition
|
||||||
|
|
||||||
@@ -28,15 +33,15 @@ var _grid_cells: PackedVector3Array = [] # Relative cell coordinates
|
|||||||
|
|
||||||
|
|
||||||
func _ready() -> void:
|
func _ready() -> void:
|
||||||
#_initialize_material()
|
_initialize_material()
|
||||||
#_set_mesh_representation()
|
_set_mesh_representation()
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
## Initializes the material for visual representation.
|
## Initializes the material for visual representation.
|
||||||
func _initialize_material() -> void:
|
func _initialize_material() -> void:
|
||||||
_material = StandardMaterial3D.new()
|
_material = StandardMaterial3D.new()
|
||||||
_material.albedo_color = mesh_color
|
_material.albedo_color = Color.RED
|
||||||
_material.metallic = 0.3
|
_material.metallic = 0.3
|
||||||
_material.roughness = 0.7
|
_material.roughness = 0.7
|
||||||
|
|
||||||
@@ -44,17 +49,11 @@ func _initialize_material() -> void:
|
|||||||
## Creates a simple mesh representation based on shape type.
|
## Creates a simple mesh representation based on shape type.
|
||||||
func _set_mesh_representation() -> void:
|
func _set_mesh_representation() -> void:
|
||||||
_mesh_instance.mesh = resource.mesh
|
_mesh_instance.mesh = resource.mesh
|
||||||
#_mesh_instance.set_surface_override_material(0, _material)
|
_mesh_instance.set_surface_override_material(0, _material)
|
||||||
|
$SelectionArea/CollisionShape3D.shape = _mesh_instance.mesh.create_trimesh_shape()
|
||||||
_grid_cells = resource.grid_cells
|
_grid_cells = resource.grid_cells
|
||||||
|
|
||||||
|
|
||||||
## Creates a simple box mesh for tetromino visualization.
|
|
||||||
func _create_box_mesh(width: float, height: float, depth: float) -> BoxMesh:
|
|
||||||
var box = BoxMesh.new()
|
|
||||||
box.size = Vector3(width, height, depth)
|
|
||||||
return box
|
|
||||||
|
|
||||||
|
|
||||||
## Gets the grid cells occupied by this tetromino at its current position.
|
## Gets the grid cells occupied by this tetromino at its current position.
|
||||||
func get_grid_cells(at_position: Vector3i = grid_position) -> PackedVector3Array:
|
func get_grid_cells(at_position: Vector3i = grid_position) -> PackedVector3Array:
|
||||||
var result = PackedVector3Array()
|
var result = PackedVector3Array()
|
||||||
@@ -139,3 +138,26 @@ func _to_string() -> String:
|
|||||||
grid_position,
|
grid_position,
|
||||||
_grid_cells.size()
|
_grid_cells.size()
|
||||||
]
|
]
|
||||||
|
|
||||||
|
## Callback used to handle tetromino selection and deselection
|
||||||
|
func _on_selection_area_input_event(camera: Node, event: InputEvent, event_position: Vector3, normal: Vector3, shape_idx: int) -> void:
|
||||||
|
if event is InputEventMouseButton:
|
||||||
|
if event.pressed and event.button_index == MOUSE_BUTTON_LEFT:
|
||||||
|
print("Tetromino selected")
|
||||||
|
selected.emit(self, grid_position)
|
||||||
|
elif event.pressed and event.button_index == MOUSE_BUTTON_RIGHT:
|
||||||
|
print("Tetromino released")
|
||||||
|
deselected.emit(self)
|
||||||
|
|
||||||
|
|
||||||
|
## Callback used to handle tetromino hovering
|
||||||
|
func _on_selection_area_mouse_entered() -> void:
|
||||||
|
print("Tetromino hovered")
|
||||||
|
set_highlighted(true)
|
||||||
|
hovered.emit(self)
|
||||||
|
|
||||||
|
## Callback used to handle tetromino unhovering
|
||||||
|
func _on_selection_area_mouse_exited() -> void:
|
||||||
|
print("Tetromino unhovered")
|
||||||
|
set_highlighted(false)
|
||||||
|
unhovered.emit(self)
|
||||||
|
|||||||
@@ -33,34 +33,34 @@ func _ready() -> void:
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
func _input(event: InputEvent) -> void:
|
#func _input(event: InputEvent) -> void:
|
||||||
if event is InputEventMouseButton:
|
#if event is InputEventMouseButton:
|
||||||
if event.pressed and event.button_index == MOUSE_BUTTON_LEFT:
|
#if event.pressed and event.button_index == MOUSE_BUTTON_LEFT:
|
||||||
_handle_left_click()
|
#_handle_left_click()
|
||||||
elif event.pressed and event.button_index == MOUSE_BUTTON_RIGHT:
|
#elif event.pressed and event.button_index == MOUSE_BUTTON_RIGHT:
|
||||||
_handle_right_click()
|
#_handle_right_click()
|
||||||
|
#
|
||||||
if _selected_tetromino and event is InputEventMouseMotion:
|
#if _selected_tetromino and event is InputEventMouseMotion:
|
||||||
_update_ghost_position()
|
#_update_ghost_position()
|
||||||
|
#
|
||||||
if _selected_tetromino:
|
#if _selected_tetromino:
|
||||||
if event.is_action_pressed("ui_select"): # Spacebar
|
#if event.is_action_pressed("ui_select"): # Spacebar
|
||||||
_rotate_ghost()
|
#_rotate_ghost()
|
||||||
elif event.is_action_pressed("ui_cancel"): # ESC
|
#elif event.is_action_pressed("ui_cancel"): # ESC
|
||||||
_cancel_selection()
|
#_cancel_selection()
|
||||||
|
|
||||||
|
|
||||||
## Handles left mouse click for selection or placement.
|
### Handles left mouse click for selection or placement.
|
||||||
func _handle_left_click() -> void:
|
#func _handle_left_click() -> void:
|
||||||
if _selected_tetromino:
|
#if _selected_tetromino:
|
||||||
# Try to place the ghost tetromino
|
## Try to place the ghost tetromino
|
||||||
_place_selection()
|
#_place_selection()
|
||||||
return
|
#return
|
||||||
|
#
|
||||||
# Try to select a tetromino
|
## Try to select a tetromino
|
||||||
var tetromino = _raycast_tetromino()
|
#var tetromino = _raycast_tetromino()
|
||||||
if tetromino:
|
#if tetromino:
|
||||||
_select_tetromino(tetromino)
|
#_select_tetromino(tetromino)
|
||||||
|
|
||||||
|
|
||||||
## Handles right mouse click to deselect.
|
## Handles right mouse click to deselect.
|
||||||
@@ -69,34 +69,34 @@ func _handle_right_click() -> void:
|
|||||||
_cancel_selection()
|
_cancel_selection()
|
||||||
|
|
||||||
|
|
||||||
## Raycasts from mouse position to find a tetromino.
|
### Raycasts from mouse position to find a tetromino.
|
||||||
func _raycast_tetromino() -> Tetromino:
|
#func _raycast_tetromino() -> Tetromino:
|
||||||
if not camera:
|
#if not camera:
|
||||||
return null
|
#return null
|
||||||
|
#
|
||||||
var mouse_pos = get_viewport().get_mouse_position()
|
#var mouse_pos = get_viewport().get_mouse_position()
|
||||||
var from = camera.project_ray_origin(mouse_pos)
|
#var from = camera.project_ray_origin(mouse_pos)
|
||||||
var to = from + camera.project_ray_normal(mouse_pos) * 10000.0
|
#var to = from + camera.project_ray_normal(mouse_pos) * 10000.0
|
||||||
|
#
|
||||||
var space_state = get_world_3d().direct_space_state
|
#var space_state = get_world_3d().direct_space_state
|
||||||
var collision_mask = 0xFFFFFFFF # Hit all layers
|
#var collision_mask = 0xFFFFFFFF # Hit all layers
|
||||||
var query = PhysicsRayQueryParameters3D.create(from, to, collision_mask)
|
#var query = PhysicsRayQueryParameters3D.create(from, to, collision_mask)
|
||||||
query.collide_with_areas = true
|
#query.collide_with_areas = true;
|
||||||
|
#
|
||||||
var result = space_state.intersect_ray(query)
|
#var result = space_state.intersect_ray(query)
|
||||||
|
#
|
||||||
if result and result.get("collider"):
|
#if result and result.get("collider"):
|
||||||
var collider = result["collider"]
|
#var collider = result["collider"]
|
||||||
# Walk up the node tree to find a Tetromino
|
## Walk up the node tree to find a Tetromino
|
||||||
var node = collider
|
#var node = collider
|
||||||
while node:
|
#while node:
|
||||||
if node is Tetromino:
|
#if node is Tetromino:
|
||||||
return node
|
#return node
|
||||||
node = node.get_parent()
|
#node = node.get_parent()
|
||||||
else:
|
#else:
|
||||||
print_debug("Raycast hit nothing")
|
#print_debug("Raycast hit nothing")
|
||||||
|
#
|
||||||
return null
|
#return null
|
||||||
|
|
||||||
|
|
||||||
## Selects a tetromino and converts it to ghost mode.
|
## Selects a tetromino and converts it to ghost mode.
|
||||||
|
|||||||
Reference in New Issue
Block a user