├── .gitignore ├── .gitattributes ├── Sprites ├── crate.png ├── player.png ├── rock.png ├── switch.png ├── tile.png ├── tree.png ├── rock.png.import ├── tile.png.import ├── tree.png.import ├── crate.png.import ├── player.png.import └── switch.png.import ├── Objects ├── crate.gd ├── tree.tscn ├── rock.tscn ├── switch.tscn ├── switch.gd ├── crate.tscn └── grid_object.gd ├── Tiles └── ground_tileset.tres ├── Player ├── player.tscn └── player.gd ├── Scenes └── Levels │ ├── level.tscn │ ├── level.gd │ └── test_level.tscn ├── icon.svg ├── Components ├── interactable_component.gd └── pushable_component.gd ├── icon.svg.import ├── README.md └── project.godot /.gitignore: -------------------------------------------------------------------------------- 1 | # Godot 4+ specific ignores 2 | .godot/ 3 | addons/ 4 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Normalize EOL for all files that Git considers text files. 2 | * text=auto eol=lf 3 | -------------------------------------------------------------------------------- /Sprites/crate.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WhoStoleMyCoffee/ComponentsDemo/HEAD/Sprites/crate.png -------------------------------------------------------------------------------- /Sprites/player.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WhoStoleMyCoffee/ComponentsDemo/HEAD/Sprites/player.png -------------------------------------------------------------------------------- /Sprites/rock.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WhoStoleMyCoffee/ComponentsDemo/HEAD/Sprites/rock.png -------------------------------------------------------------------------------- /Sprites/switch.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WhoStoleMyCoffee/ComponentsDemo/HEAD/Sprites/switch.png -------------------------------------------------------------------------------- /Sprites/tile.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WhoStoleMyCoffee/ComponentsDemo/HEAD/Sprites/tile.png -------------------------------------------------------------------------------- /Sprites/tree.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WhoStoleMyCoffee/ComponentsDemo/HEAD/Sprites/tree.png -------------------------------------------------------------------------------- /Objects/crate.gd: -------------------------------------------------------------------------------- 1 | extends GridObject 2 | 3 | ## Called from InteractableComponent 4 | func _on_just_interacted(_interacted_by: GridObject): 5 | # Break animation 6 | # Although it's more of a "pop" than a "break" 7 | var t: Tween = create_tween()\ 8 | .set_trans(Tween.TRANS_EXPO)\ 9 | .set_ease(Tween.EASE_OUT) 10 | t.tween_property($Sprite2D, ^"scale", Vector2(0.5, 2.0), 0.1) 11 | t.tween_callback(queue_free) 12 | 13 | 14 | -------------------------------------------------------------------------------- /Objects/tree.tscn: -------------------------------------------------------------------------------- 1 | [gd_scene load_steps=3 format=3 uid="uid://cptsprxqn5b86"] 2 | 3 | [ext_resource type="Script" path="res://Objects/grid_object.gd" id="1_1rhj6"] 4 | [ext_resource type="Texture2D" uid="uid://c46pmxvqqrdkr" path="res://Sprites/tree.png" id="2_eey1t"] 5 | 6 | [node name="Tree" type="Node2D"] 7 | script = ExtResource("1_1rhj6") 8 | 9 | [node name="Tree" type="Sprite2D" parent="."] 10 | position = Vector2(16, 16) 11 | texture = ExtResource("2_eey1t") 12 | -------------------------------------------------------------------------------- /Tiles/ground_tileset.tres: -------------------------------------------------------------------------------- 1 | [gd_resource type="TileSet" load_steps=3 format=3 uid="uid://bpwhvukksk6w4"] 2 | 3 | [ext_resource type="Texture2D" uid="uid://b2ccjc1xfhx8q" path="res://Sprites/tile.png" id="1_ji4lx"] 4 | 5 | [sub_resource type="TileSetAtlasSource" id="TileSetAtlasSource_ate4j"] 6 | texture = ExtResource("1_ji4lx") 7 | texture_region_size = Vector2i(32, 32) 8 | 0:0/0 = 0 9 | 10 | [resource] 11 | tile_size = Vector2i(32, 32) 12 | sources/1 = SubResource("TileSetAtlasSource_ate4j") 13 | -------------------------------------------------------------------------------- /Objects/rock.tscn: -------------------------------------------------------------------------------- 1 | [gd_scene load_steps=4 format=3 uid="uid://cpiancq5mgv4j"] 2 | 3 | [ext_resource type="Script" path="res://Objects/grid_object.gd" id="1_44ibv"] 4 | [ext_resource type="Script" path="res://Components/pushable_component.gd" id="3_f86s1"] 5 | [ext_resource type="Texture2D" uid="uid://gnj2art7tanr" path="res://Sprites/rock.png" id="3_vn1fn"] 6 | 7 | [node name="Rock" type="Node2D"] 8 | script = ExtResource("1_44ibv") 9 | 10 | [node name="Rock" type="Sprite2D" parent="."] 11 | position = Vector2(16, 16) 12 | texture = ExtResource("3_vn1fn") 13 | 14 | [node name="PushableComponent" type="Node" parent="."] 15 | script = ExtResource("3_f86s1") 16 | -------------------------------------------------------------------------------- /Player/player.tscn: -------------------------------------------------------------------------------- 1 | [gd_scene load_steps=4 format=3 uid="uid://d13c0p5fwo72p"] 2 | 3 | [ext_resource type="Script" path="res://Player/player.gd" id="1_f0xqn"] 4 | [ext_resource type="Texture2D" uid="uid://bmsdvr3iur6ma" path="res://Sprites/player.png" id="2_v13y5"] 5 | [ext_resource type="Script" path="res://Components/pushable_component.gd" id="3_08es3"] 6 | 7 | [node name="Player" type="Node2D"] 8 | script = ExtResource("1_f0xqn") 9 | 10 | [node name="Sprite2D" type="Sprite2D" parent="."] 11 | position = Vector2(16, 16) 12 | texture = ExtResource("2_v13y5") 13 | 14 | [node name="PushableComponent" type="Node" parent="."] 15 | script = ExtResource("3_08es3") 16 | -------------------------------------------------------------------------------- /Objects/switch.tscn: -------------------------------------------------------------------------------- 1 | [gd_scene load_steps=4 format=3 uid="uid://dxvrtbh5gku7w"] 2 | 3 | [ext_resource type="Script" path="res://Objects/switch.gd" id="1_icfyb"] 4 | [ext_resource type="Texture2D" uid="uid://dpsipxb366pba" path="res://Sprites/switch.png" id="3_lsst7"] 5 | [ext_resource type="Script" path="res://Components/interactable_component.gd" id="3_tdmsj"] 6 | 7 | [node name="Switch" type="Node2D"] 8 | script = ExtResource("1_icfyb") 9 | 10 | [node name="Sprite" type="Sprite2D" parent="."] 11 | position = Vector2(16, 16) 12 | texture = ExtResource("3_lsst7") 13 | hframes = 2 14 | 15 | [node name="InteractableComponent" type="Node" parent="."] 16 | script = ExtResource("3_tdmsj") 17 | 18 | [connection signal="just_interacted" from="InteractableComponent" to="." method="_on_just_interacted"] 19 | -------------------------------------------------------------------------------- /Scenes/Levels/level.tscn: -------------------------------------------------------------------------------- 1 | [gd_scene load_steps=3 format=3 uid="uid://br40ys4g11vno"] 2 | 3 | [ext_resource type="TileSet" uid="uid://bpwhvukksk6w4" path="res://Tiles/ground_tileset.tres" id="1_ckqtp"] 4 | [ext_resource type="Script" path="res://Scenes/Levels/level.gd" id="1_hqm33"] 5 | 6 | [node name="Level" type="Node2D"] 7 | script = ExtResource("1_hqm33") 8 | 9 | [node name="BGLayer" type="CanvasLayer" parent="."] 10 | layer = -99 11 | 12 | [node name="BG" type="ColorRect" parent="BGLayer"] 13 | anchors_preset = 15 14 | anchor_right = 1.0 15 | anchor_bottom = 1.0 16 | grow_horizontal = 2 17 | grow_vertical = 2 18 | color = Color(0.0745098, 0.0627451, 0.133333, 1) 19 | metadata/_edit_lock_ = true 20 | 21 | [node name="TileMap" type="TileMap" parent="."] 22 | tile_set = ExtResource("1_ckqtp") 23 | format = 2 24 | 25 | [node name="Objects" type="Node" parent="."] 26 | -------------------------------------------------------------------------------- /Objects/switch.gd: -------------------------------------------------------------------------------- 1 | extends GridObject 2 | 3 | @export var is_activated: bool = false: 4 | # This setter method makes sure the sprite frame is always synced with `is_activated` 5 | set(v): 6 | is_activated = v 7 | sprite.frame = int(is_activated) 8 | 9 | @onready var sprite = $Sprite 10 | 11 | 12 | ## Called from InteractableComponent 13 | func _on_just_interacted(_interacted_by: GridObject): 14 | is_activated = !is_activated # This also updates the sprite thanks to the setter! 15 | 16 | sprite.scale = Vector2(2.0, 0.5) 17 | sprite.rotation_degrees = 7.0 18 | 19 | # Animate scale and rotation back to normal 20 | var t: Tween = create_tween()\ 21 | .set_trans(Tween.TRANS_EXPO)\ 22 | .set_ease(Tween.EASE_OUT)\ 23 | .set_parallel() 24 | t.tween_property(sprite, ^"scale", Vector2.ONE, 0.2) 25 | t.tween_property(sprite, ^"rotation", 0.0, 0.2) 26 | 27 | 28 | -------------------------------------------------------------------------------- /Sprites/rock.png.import: -------------------------------------------------------------------------------- 1 | [remap] 2 | 3 | importer="texture" 4 | type="CompressedTexture2D" 5 | uid="uid://gnj2art7tanr" 6 | path="res://.godot/imported/rock.png-452459abe115d9c157b550a0a19ea60e.ctex" 7 | metadata={ 8 | "vram_texture": false 9 | } 10 | 11 | [deps] 12 | 13 | source_file="res://Sprites/rock.png" 14 | dest_files=["res://.godot/imported/rock.png-452459abe115d9c157b550a0a19ea60e.ctex"] 15 | 16 | [params] 17 | 18 | compress/mode=0 19 | compress/high_quality=false 20 | compress/lossy_quality=0.7 21 | compress/hdr_compression=1 22 | compress/normal_map=0 23 | compress/channel_pack=0 24 | mipmaps/generate=false 25 | mipmaps/limit=-1 26 | roughness/mode=0 27 | roughness/src_normal="" 28 | process/fix_alpha_border=true 29 | process/premult_alpha=false 30 | process/normal_map_invert_y=false 31 | process/hdr_as_srgb=false 32 | process/hdr_clamp_exposure=false 33 | process/size_limit=0 34 | detect_3d/compress_to=1 35 | -------------------------------------------------------------------------------- /Sprites/tile.png.import: -------------------------------------------------------------------------------- 1 | [remap] 2 | 3 | importer="texture" 4 | type="CompressedTexture2D" 5 | uid="uid://b2ccjc1xfhx8q" 6 | path="res://.godot/imported/tile.png-484556c5e80e89276f13d51f43be63d4.ctex" 7 | metadata={ 8 | "vram_texture": false 9 | } 10 | 11 | [deps] 12 | 13 | source_file="res://Sprites/tile.png" 14 | dest_files=["res://.godot/imported/tile.png-484556c5e80e89276f13d51f43be63d4.ctex"] 15 | 16 | [params] 17 | 18 | compress/mode=0 19 | compress/high_quality=false 20 | compress/lossy_quality=0.7 21 | compress/hdr_compression=1 22 | compress/normal_map=0 23 | compress/channel_pack=0 24 | mipmaps/generate=false 25 | mipmaps/limit=-1 26 | roughness/mode=0 27 | roughness/src_normal="" 28 | process/fix_alpha_border=true 29 | process/premult_alpha=false 30 | process/normal_map_invert_y=false 31 | process/hdr_as_srgb=false 32 | process/hdr_clamp_exposure=false 33 | process/size_limit=0 34 | detect_3d/compress_to=1 35 | -------------------------------------------------------------------------------- /Sprites/tree.png.import: -------------------------------------------------------------------------------- 1 | [remap] 2 | 3 | importer="texture" 4 | type="CompressedTexture2D" 5 | uid="uid://c46pmxvqqrdkr" 6 | path="res://.godot/imported/tree.png-2bedb0b12015dbe361d6af1245334d57.ctex" 7 | metadata={ 8 | "vram_texture": false 9 | } 10 | 11 | [deps] 12 | 13 | source_file="res://Sprites/tree.png" 14 | dest_files=["res://.godot/imported/tree.png-2bedb0b12015dbe361d6af1245334d57.ctex"] 15 | 16 | [params] 17 | 18 | compress/mode=0 19 | compress/high_quality=false 20 | compress/lossy_quality=0.7 21 | compress/hdr_compression=1 22 | compress/normal_map=0 23 | compress/channel_pack=0 24 | mipmaps/generate=false 25 | mipmaps/limit=-1 26 | roughness/mode=0 27 | roughness/src_normal="" 28 | process/fix_alpha_border=true 29 | process/premult_alpha=false 30 | process/normal_map_invert_y=false 31 | process/hdr_as_srgb=false 32 | process/hdr_clamp_exposure=false 33 | process/size_limit=0 34 | detect_3d/compress_to=1 35 | -------------------------------------------------------------------------------- /Sprites/crate.png.import: -------------------------------------------------------------------------------- 1 | [remap] 2 | 3 | importer="texture" 4 | type="CompressedTexture2D" 5 | uid="uid://cy78xgck4yv1e" 6 | path="res://.godot/imported/crate.png-c387de9b883318ae36c9fd5a499c5602.ctex" 7 | metadata={ 8 | "vram_texture": false 9 | } 10 | 11 | [deps] 12 | 13 | source_file="res://Sprites/crate.png" 14 | dest_files=["res://.godot/imported/crate.png-c387de9b883318ae36c9fd5a499c5602.ctex"] 15 | 16 | [params] 17 | 18 | compress/mode=0 19 | compress/high_quality=false 20 | compress/lossy_quality=0.7 21 | compress/hdr_compression=1 22 | compress/normal_map=0 23 | compress/channel_pack=0 24 | mipmaps/generate=false 25 | mipmaps/limit=-1 26 | roughness/mode=0 27 | roughness/src_normal="" 28 | process/fix_alpha_border=true 29 | process/premult_alpha=false 30 | process/normal_map_invert_y=false 31 | process/hdr_as_srgb=false 32 | process/hdr_clamp_exposure=false 33 | process/size_limit=0 34 | detect_3d/compress_to=1 35 | -------------------------------------------------------------------------------- /Sprites/player.png.import: -------------------------------------------------------------------------------- 1 | [remap] 2 | 3 | importer="texture" 4 | type="CompressedTexture2D" 5 | uid="uid://bmsdvr3iur6ma" 6 | path="res://.godot/imported/player.png-b12d81cacd41edd115dbd315254b5ad9.ctex" 7 | metadata={ 8 | "vram_texture": false 9 | } 10 | 11 | [deps] 12 | 13 | source_file="res://Sprites/player.png" 14 | dest_files=["res://.godot/imported/player.png-b12d81cacd41edd115dbd315254b5ad9.ctex"] 15 | 16 | [params] 17 | 18 | compress/mode=0 19 | compress/high_quality=false 20 | compress/lossy_quality=0.7 21 | compress/hdr_compression=1 22 | compress/normal_map=0 23 | compress/channel_pack=0 24 | mipmaps/generate=false 25 | mipmaps/limit=-1 26 | roughness/mode=0 27 | roughness/src_normal="" 28 | process/fix_alpha_border=true 29 | process/premult_alpha=false 30 | process/normal_map_invert_y=false 31 | process/hdr_as_srgb=false 32 | process/hdr_clamp_exposure=false 33 | process/size_limit=0 34 | detect_3d/compress_to=1 35 | -------------------------------------------------------------------------------- /Sprites/switch.png.import: -------------------------------------------------------------------------------- 1 | [remap] 2 | 3 | importer="texture" 4 | type="CompressedTexture2D" 5 | uid="uid://dpsipxb366pba" 6 | path="res://.godot/imported/switch.png-d8d03726c8e2e757d9fd5bd90e30be97.ctex" 7 | metadata={ 8 | "vram_texture": false 9 | } 10 | 11 | [deps] 12 | 13 | source_file="res://Sprites/switch.png" 14 | dest_files=["res://.godot/imported/switch.png-d8d03726c8e2e757d9fd5bd90e30be97.ctex"] 15 | 16 | [params] 17 | 18 | compress/mode=0 19 | compress/high_quality=false 20 | compress/lossy_quality=0.7 21 | compress/hdr_compression=1 22 | compress/normal_map=0 23 | compress/channel_pack=0 24 | mipmaps/generate=false 25 | mipmaps/limit=-1 26 | roughness/mode=0 27 | roughness/src_normal="" 28 | process/fix_alpha_border=true 29 | process/premult_alpha=false 30 | process/normal_map_invert_y=false 31 | process/hdr_as_srgb=false 32 | process/hdr_clamp_exposure=false 33 | process/size_limit=0 34 | detect_3d/compress_to=1 35 | -------------------------------------------------------------------------------- /icon.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /Components/interactable_component.gd: -------------------------------------------------------------------------------- 1 | class_name InteractableComponent extends Node 2 | 3 | ## Emitted when this object was just interacted with 4 | ## See func interact() 5 | signal just_interacted(interacted_by: GridObject) 6 | 7 | # In case we may want to disable it 8 | @export var is_enabled: bool = true 9 | 10 | 11 | func _enter_tree() -> void: 12 | # This component can only be a chld of GridObjects 13 | assert(owner is GridObject) 14 | owner.set_meta(&"InteractableComponent", self) # Register 15 | 16 | func _exit_tree() -> void: 17 | owner.remove_meta(&"InteractableComponent") # Unregister 18 | 19 | 20 | # Let's just have another node handle interactions to keep things simple 21 | # More specialized behavior can also be implemented 22 | # E.g. `BreakableComponent (extends InteractableComponent)` 23 | # See also func move() in player.gd 24 | func interact(interacted_by: GridObject): 25 | if !is_enabled: 26 | return 27 | just_interacted.emit(interacted_by) 28 | -------------------------------------------------------------------------------- /Objects/crate.tscn: -------------------------------------------------------------------------------- 1 | [gd_scene load_steps=5 format=3 uid="uid://dh2ekt12oel2n"] 2 | 3 | [ext_resource type="Script" path="res://Objects/crate.gd" id="1_4v2ot"] 4 | [ext_resource type="Texture2D" uid="uid://cy78xgck4yv1e" path="res://Sprites/crate.png" id="2_t45t3"] 5 | [ext_resource type="Script" path="res://Components/pushable_component.gd" id="3_14jm5"] 6 | [ext_resource type="Script" path="res://Components/interactable_component.gd" id="4_gktfs"] 7 | 8 | [node name="Crate" type="Node2D"] 9 | script = ExtResource("1_4v2ot") 10 | 11 | [node name="Sprite2D" type="Sprite2D" parent="."] 12 | position = Vector2(16, 16) 13 | texture = ExtResource("2_t45t3") 14 | 15 | [node name="PushableComponent" type="Node" parent="."] 16 | script = ExtResource("3_14jm5") 17 | 18 | [node name="InteractableComponent" type="Node" parent="."] 19 | script = ExtResource("4_gktfs") 20 | 21 | [connection signal="just_interacted" from="InteractableComponent" to="." method="_on_just_interacted"] 22 | -------------------------------------------------------------------------------- /Scenes/Levels/level.gd: -------------------------------------------------------------------------------- 1 | class_name Level extends Node2D 2 | 3 | 4 | @onready var tile_map = $TileMap as TileMap 5 | 6 | 7 | func _unhandled_key_input(event: InputEvent) -> void: 8 | if event.is_action_released(&"reset"): 9 | get_tree().reload_current_scene() 10 | # Tell the scene we handled this input 11 | get_viewport().set_input_as_handled() 12 | 13 | 14 | ## Returns the GridObject that the grid pos `pos` 15 | ## Warning: may return null 16 | func get_cellv(pos: Vector2i) -> GridObject: 17 | for node in get_tree().get_nodes_in_group(&"GridObjects"): 18 | if node.grid_pos == pos: 19 | return node 20 | return null 21 | 22 | ## Returns whether the grid pos `pos` is occupied by some GridObject 23 | func is_cellv_occupied(pos: Vector2i) -> bool: 24 | for node in get_tree().get_nodes_in_group(&"GridObjects"): 25 | if node.grid_pos == pos: 26 | return true 27 | return false 28 | 29 | func cellv_exists(pos: Vector2i) -> bool: 30 | return tile_map.get_cell_tile_data(0, pos) != null 31 | -------------------------------------------------------------------------------- /icon.svg.import: -------------------------------------------------------------------------------- 1 | [remap] 2 | 3 | importer="texture" 4 | type="CompressedTexture2D" 5 | uid="uid://cm3s4ekfx0xda" 6 | path="res://.godot/imported/icon.svg-218a8f2b3041327d8a5756f3a245f83b.ctex" 7 | metadata={ 8 | "vram_texture": false 9 | } 10 | 11 | [deps] 12 | 13 | source_file="res://icon.svg" 14 | dest_files=["res://.godot/imported/icon.svg-218a8f2b3041327d8a5756f3a245f83b.ctex"] 15 | 16 | [params] 17 | 18 | compress/mode=0 19 | compress/high_quality=false 20 | compress/lossy_quality=0.7 21 | compress/hdr_compression=1 22 | compress/normal_map=0 23 | compress/channel_pack=0 24 | mipmaps/generate=false 25 | mipmaps/limit=-1 26 | roughness/mode=0 27 | roughness/src_normal="" 28 | process/fix_alpha_border=true 29 | process/premult_alpha=false 30 | process/normal_map_invert_y=false 31 | process/hdr_as_srgb=false 32 | process/hdr_clamp_exposure=false 33 | process/size_limit=0 34 | detect_3d/compress_to=1 35 | svg/scale=1.0 36 | editor/scale_with_editor_scale=false 37 | editor/convert_colors_with_editor_theme=false 38 | -------------------------------------------------------------------------------- /Objects/grid_object.gd: -------------------------------------------------------------------------------- 1 | class_name GridObject extends Node2D 2 | 3 | const GRID_SIZE: Vector2i = Vector2i(32, 32) 4 | 5 | ## Grid pos in grid coordinates 6 | ## Always synced with this nodes's position 7 | var grid_pos: Vector2i: set = set_grid_pos 8 | 9 | 10 | func _enter_tree(): 11 | add_to_group(&"GridObjects") 12 | set_notify_transform(true) # See func _notification() 13 | 14 | 15 | ## This method makes sure the grid_pos is always synced to the position 16 | ## It essentially gets called every time the transform (position, rotation, scale) 17 | ## changes 18 | func _notification(what: int): 19 | if what == NOTIFICATION_TRANSFORM_CHANGED: 20 | grid_pos = (position / 32.0).floor() 21 | 22 | ## This setter method takes care of animating the position when the 23 | ## grid_pos changes 24 | func set_grid_pos(p: Vector2i): 25 | grid_pos = p 26 | create_tween()\ 27 | .set_trans(Tween.TRANS_BACK)\ 28 | .set_ease(Tween.EASE_OUT)\ 29 | .tween_property(self, ^"position", Vector2(grid_pos * GRID_SIZE), 0.2) 30 | 31 | 32 | #region Util - These are only for quality of life / readability 33 | 34 | func get_component(component: StringName) -> Node: 35 | return get_meta(component, null) 36 | 37 | func has_component(component: StringName) -> bool: 38 | return has_meta(component) 39 | 40 | #endregion 41 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Components demo 2 | 3 | Demo in [this video](https://youtu.be/ldxDJpRPCMI) 4 | 5 | Made by Tienne_k in the Godot game engine 6 | 7 | 8 | ### Project structure: 9 | - Player script is in `res://Player/player.gd`. 10 | - Components are inside `res://Components/`. 11 | - Game objects (crates, rocks, etc) are in `res://Objects/`. 12 | - Main scene is `res://Scenes/Levels/test_level.tscn`. 13 | 14 | Some nodes in that scene have editor descriptions (`editor_description` property in the inspector); please be sure to read them to avoid some potential confusion 15 | 16 | --- 17 | 18 | This is definitely not the best implementation, though. 19 | 20 | Due to some coincidences of similar movement mechanics, the player... is technically pushable (i.e. has a PushableComponent)... 21 | 22 | According to this implementation, the player is technically pushing themselves to move... Please don't copy this in your own projects, this was a mere coincidence lol. 23 | 24 | --- 25 | 26 | If you're here because you're genuinely interested in learning this stuff, here are some exercises for you: 27 | 28 | 1. Make the trees pushable 29 | 2. Make it so the trees can only be pushed if no other objects are in the chain 30 | 31 | I.e. player pushing a rock, pushing a crate, pushing a tree wouldn't work; but the player pushing only a tree would 32 | 33 | 3. Make it so switches toggle whether specific crates are pushable or not 34 | 35 | You can share your solutions on our [discord server](https://discord.gg/VTvG7ZQNdQ) 36 | 37 | I hope this helps you on your journey 38 | 39 | -------------------------------------------------------------------------------- /Player/player.gd: -------------------------------------------------------------------------------- 1 | class_name Player extends GridObject 2 | 3 | # Due to some coincidences of similar movement mechanics, the player... 4 | # is technically pushable (i.e. has a PushableComponent)... 5 | # According to this implementation, the player is technically pushing themselves to move... 6 | # Please don't copy this in your own projects, this was a mere coincidence lol 7 | 8 | var level: Level 9 | 10 | @onready var pushable_component = $PushableComponent as PushableComponent 11 | 12 | 13 | func _ready(): 14 | level = get_tree().current_scene as Level 15 | 16 | 17 | func _unhandled_key_input(event: InputEvent): 18 | # We only want to register input events when they're pressed 19 | # Try seeing what happens if we comment this `guard clause` out! 20 | if !event.is_pressed(): 21 | return 22 | 23 | # Move 24 | var movement_dir: Vector2i = Vector2i.ZERO 25 | if event.is_action(&"up", true): 26 | movement_dir = Vector2i.UP 27 | elif event.is_action(&"down", true): 28 | movement_dir = Vector2i.DOWN 29 | elif event.is_action(&"left", true): 30 | movement_dir = Vector2i.LEFT 31 | elif event.is_action(&"right", true): 32 | movement_dir = Vector2i.RIGHT 33 | 34 | if movement_dir != Vector2i.ZERO: 35 | move(movement_dir) 36 | # Tell the scene we handled this input 37 | get_viewport().set_input_as_handled() 38 | 39 | 40 | ## Tries to move in the direction `dir` 41 | func move(dir: Vector2i) -> void: 42 | var target_cell: Vector2i = grid_pos + dir 43 | 44 | if pushable_component.try_push(dir): 45 | return 46 | 47 | # Push failed; try to interact with stuff 48 | var object: GridObject = level.get_cellv(target_cell) 49 | # `Level::get_cellv()` can return null, let's handle that just in case 50 | if object == null: 51 | return 52 | # Object is not interactable; return 53 | if !object.has_component(&"InteractableComponent"): 54 | return 55 | 56 | object.get_component(&"InteractableComponent") .interact(self) 57 | 58 | -------------------------------------------------------------------------------- /project.godot: -------------------------------------------------------------------------------- 1 | ; Engine configuration file. 2 | ; It's best edited using the editor UI and not directly, 3 | ; since the parameters that go here are not all obvious. 4 | ; 5 | ; Format: 6 | ; [section] ; section goes between [] 7 | ; param=value ; assign values to parameters 8 | 9 | config_version=5 10 | 11 | [application] 12 | 13 | config/name="ComponentsDemo" 14 | config/description="made by Tienne_k :)" 15 | run/main_scene="res://Scenes/Levels/test_level.tscn" 16 | config/features=PackedStringArray("4.2", "GL Compatibility") 17 | config/icon="res://icon.svg" 18 | 19 | [editor_plugins] 20 | 21 | enabled=PackedStringArray() 22 | 23 | [input] 24 | 25 | up={ 26 | "deadzone": 0.5, 27 | "events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":87,"key_label":0,"unicode":119,"echo":false,"script":null) 28 | ] 29 | } 30 | down={ 31 | "deadzone": 0.5, 32 | "events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":83,"key_label":0,"unicode":115,"echo":false,"script":null) 33 | ] 34 | } 35 | left={ 36 | "deadzone": 0.5, 37 | "events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":65,"key_label":0,"unicode":97,"echo":false,"script":null) 38 | ] 39 | } 40 | right={ 41 | "deadzone": 0.5, 42 | "events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":68,"key_label":0,"unicode":100,"echo":false,"script":null) 43 | ] 44 | } 45 | reset={ 46 | "deadzone": 0.5, 47 | "events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":82,"key_label":0,"unicode":114,"echo":false,"script":null) 48 | ] 49 | } 50 | 51 | [rendering] 52 | 53 | textures/canvas_textures/default_texture_filter=0 54 | renderer/rendering_method="gl_compatibility" 55 | renderer/rendering_method.mobile="gl_compatibility" 56 | -------------------------------------------------------------------------------- /Components/pushable_component.gd: -------------------------------------------------------------------------------- 1 | class_name PushableComponent extends Node 2 | 3 | # In case we may want to disable it 4 | @export var is_enabled: bool = true 5 | 6 | # Used in animate_push_fail() 7 | var tween: Tween = null 8 | 9 | 10 | func _enter_tree() -> void: 11 | # This component can only be a chld of GridObjects 12 | assert(owner is GridObject, "Owner must be a GridObject") 13 | owner.set_meta(&"PushableComponent", self) # Register 14 | 15 | func _exit_tree() -> void: 16 | owner.remove_meta(&"PushableComponent") # Unregister 17 | 18 | 19 | ## Tries to push its GridObject owner in the direction `dir` 20 | ## Returns whether the push was successful 21 | ## This method is *recursive* (i.e. calls try_push() inside of try_push()) 22 | func try_push(dir: Vector2i) -> bool: 23 | if !is_enabled: 24 | return false 25 | 26 | var target_cell: Vector2i = owner.grid_pos + dir 27 | var level: Level = get_tree().current_scene 28 | 29 | # target_cell is out of bounds! 30 | if !level.cellv_exists(target_cell): 31 | animate_push_fail(dir) 32 | return false 33 | 34 | # Check if there's something in the way 35 | var object: GridObject = level.get_cellv(target_cell) 36 | # There's nothing in the way, we're good to move! 37 | if object == null: 38 | owner.grid_pos += dir 39 | return true 40 | 41 | # Not pushable 42 | if !object.has_component(&"PushableComponent"): 43 | animate_push_fail(dir) 44 | return false 45 | 46 | # At this point, we know there's an object in the way that is pushable 47 | # Try to push that 48 | var pc: PushableComponent = object.get_component(&"PushableComponent") 49 | if pc.try_push(dir): # Recursion happens here! 50 | # Success! 51 | owner.grid_pos += dir 52 | return true 53 | 54 | animate_push_fail(dir) 55 | return false 56 | 57 | 58 | # Animates a little nudge for when a push failed 59 | # For a cheat sheet of easing functions, please refer to this GOD TIER website: 60 | # https://easings.net/ 61 | # by Andrey Sitnik and Ivan Solovev 62 | func animate_push_fail(dir: Vector2): 63 | # Let's *not* interfere with other tweens 64 | # If we don't do this, we'll be creating many Tweens stacking on top of each other, 65 | # leading to an unpleasant visual glitch 66 | if tween != null and tween.is_running(): 67 | return 68 | 69 | const DURATION: float = 0.1 70 | 71 | # Get the position from grid_pos 72 | # We do this instead of using `owner.position` because `owner.position` might be in 73 | # the middle of an animation (see GridObject.set_grid_pos()), which would lead 74 | # to another visual glitch 75 | var pos: Vector2 = Vector2(owner.grid_pos * GridObject.GRID_SIZE) 76 | 77 | # Play the animation 78 | tween = create_tween() .set_trans(Tween.TRANS_SINE) .set_ease(Tween.EASE_OUT) 79 | tween.tween_property(owner, ^"position", pos + Vector2(dir), DURATION) 80 | tween.tween_property(owner, ^"position", pos, DURATION) 81 | 82 | -------------------------------------------------------------------------------- /Scenes/Levels/test_level.tscn: -------------------------------------------------------------------------------- 1 | [gd_scene load_steps=7 format=3 uid="uid://yh85qb6vaadx"] 2 | 3 | [ext_resource type="PackedScene" uid="uid://br40ys4g11vno" path="res://Scenes/Levels/level.tscn" id="1_u3o2e"] 4 | [ext_resource type="PackedScene" uid="uid://d13c0p5fwo72p" path="res://Player/player.tscn" id="2_53px2"] 5 | [ext_resource type="PackedScene" uid="uid://cptsprxqn5b86" path="res://Objects/tree.tscn" id="3_jshd1"] 6 | [ext_resource type="PackedScene" uid="uid://cpiancq5mgv4j" path="res://Objects/rock.tscn" id="4_npq54"] 7 | [ext_resource type="PackedScene" uid="uid://dxvrtbh5gku7w" path="res://Objects/switch.tscn" id="5_2jr3q"] 8 | [ext_resource type="PackedScene" uid="uid://dh2ekt12oel2n" path="res://Objects/crate.tscn" id="6_hmoxr"] 9 | 10 | [node name="TestLevel" instance=ExtResource("1_u3o2e")] 11 | editor_description = "I originally thought I'd add more levels, but stopped after this one..." 12 | 13 | [node name="BG" parent="BGLayer" index="0"] 14 | mouse_filter = 2 15 | 16 | [node name="TileMap" parent="." index="1"] 17 | editor_description = "The tiles of this tileset are all white, and the TimeMap is modulated to the right color. 18 | This is again because I was originally planning on having many levels with different color schemes." 19 | modulate = Color(0.211765, 0.156863, 0.305882, 1) 20 | layer_0/tile_data = PackedInt32Array(0, 1, 0, 1, 1, 0, 2, 1, 0, 65538, 1, 0, 65536, 1, 0, 65537, 1, 0, 3, 1, 0, 65539, 1, 0, 65535, 1, 0, 131071, 1, 0, 131070, 1, 0, 65534, 1, 0, -2, 1, 0, -1, 1, 0, -65536, 1, 0, -65535, 1, 0, -65534, 1, 0, -65533, 1, 0, 131069, 1, 0, 65533, 1, 0, -3, 1, 0, -4, 1, 0, 65532, 1, 0, 131068, 1, 0, -65540, 1, 0, -65539, 1, 0, -65538, 1, 0, -65537, 1, 0, -131072, 1, 0, -131071, 1, 0, -131070, 1, 0, -131069, 1, 0, -196605, 1, 0, -196606, 1, 0, -196607, 1, 0, -196608, 1, 0, -131073, 1, 0, -65541, 1, 0, -5, 1, 0, 65531, 1, 0, 131067, 1, 0, -196604, 1, 0, -131068, 1, 0, -65532, 1, 0, 4, 1, 0, 65540, 1, 0, 131076, 1, 0, 131075, 1, 0, 131074, 1, 0, 131073, 1, 0, 131072, 1, 0, 196607, 1, 0, 196606, 1, 0, 196605, 1, 0, 196604, 1, 0, 196603, 1, 0, -131078, 1, 0, -65542, 1, 0, -6, 1, 0, 65530, 1, 0, 131066, 1, 0, 196602, 1, 0, -196603, 1, 0, -131067, 1, 0, -65531, 1, 0, 5, 1, 0, 65541, 1, 0, 131077, 1, 0, -131077, 1, 0, -131076, 1, 0, -131075, 1, 0, -131074, 1, 0) 21 | 22 | [node name="Objects" parent="." index="2"] 23 | editor_description = "When I started out, I would always make these \"scene objects container\" nodes a Node2D. 24 | That is a slight mistake, as Node2Ds can be moved around. This has many times led to bugs related to mysterious misalignments or offsets. 25 | Using a Node instead of a Node2D prevents that, as regular Nodes don't have a `position` property" 26 | 27 | [node name="Player" parent="Objects" index="0" instance=ExtResource("2_53px2")] 28 | position = Vector2(32, 0) 29 | 30 | [node name="Tree" parent="Objects" index="1" instance=ExtResource("3_jshd1")] 31 | position = Vector2(0, -32) 32 | 33 | [node name="Tree2" parent="Objects" index="2" instance=ExtResource("3_jshd1")] 34 | position = Vector2(96, 0) 35 | 36 | [node name="Rock" parent="Objects" index="3" instance=ExtResource("4_npq54")] 37 | position = Vector2(-32, 0) 38 | 39 | [node name="Switch" parent="Objects" index="4" instance=ExtResource("5_2jr3q")] 40 | position = Vector2(-160, 0) 41 | 42 | [node name="Crate" parent="Objects" index="5" instance=ExtResource("6_hmoxr")] 43 | position = Vector2(-64, 0) 44 | 45 | [node name="Camera2D" type="Camera2D" parent="." index="3"] 46 | zoom = Vector2(2.8, 2.8) 47 | --------------------------------------------------------------------------------