Health and attacks!
This commit is contained in:
@@ -32,8 +32,8 @@ open_scenes=PackedStringArray("res://level/scenes/level.tscn", "res://level/scen
|
|||||||
current_scene="res://level/scenes/player.tscn"
|
current_scene="res://level/scenes/player.tscn"
|
||||||
center_split_offset=0
|
center_split_offset=0
|
||||||
selected_default_debugger_tab_idx=1
|
selected_default_debugger_tab_idx=1
|
||||||
selected_main_editor_idx=3
|
selected_main_editor_idx=2
|
||||||
selected_bottom_panel_item=0
|
selected_bottom_panel_item=1
|
||||||
|
|
||||||
[EditorWindow]
|
[EditorWindow]
|
||||||
|
|
||||||
@@ -44,8 +44,8 @@ size=Vector2i(1152, 648)
|
|||||||
|
|
||||||
[ScriptEditor]
|
[ScriptEditor]
|
||||||
|
|
||||||
open_scripts=["res://level/scripts/3d_godot_robot.gd", "res://level/scripts/level.gd", "res://level/scripts/player.gd", "res://README.md"]
|
open_scripts=["res://level/scripts/3d_godot_robot.gd", "res://level/scripts/level.gd", "res://level/scripts/player.gd", "res://README.md", "res://level/scripts/spring_arm_offset.gd"]
|
||||||
selected_script="res://level/scripts/player.gd"
|
selected_script="res://level/scripts/spring_arm_offset.gd"
|
||||||
open_help=[]
|
open_help=[]
|
||||||
script_split_offset=200
|
script_split_offset=200
|
||||||
list_split_offset=0
|
list_split_offset=0
|
||||||
@@ -53,8 +53,8 @@ zoom_factor=1.0
|
|||||||
|
|
||||||
[GameView]
|
[GameView]
|
||||||
|
|
||||||
floating_window_rect=Rect2i(634, 315, 1292, 767)
|
floating_window_rect=Rect2i(2677, 131, 1292, 767)
|
||||||
floating_window_screen=1
|
floating_window_screen=0
|
||||||
|
|
||||||
[ShaderEditor]
|
[ShaderEditor]
|
||||||
|
|
||||||
|
|||||||
Binary file not shown.
@@ -1,30 +1,32 @@
|
|||||||
63f7b34db8d8cdea90c76aacccf841ec
|
63f7b34db8d8cdea90c76aacccf841ec
|
||||||
::res://::1762971203
|
::res://::1762974779
|
||||||
|
CLAUDE.md::TextFile::-1::1762974557::0::1::::<><><>0<>0<><>::
|
||||||
CONTRIBUTING.md::TextFile::-1::1762703890::0::1::::<><><>0<>0<><>::
|
CONTRIBUTING.md::TextFile::-1::1762703890::0::1::::<><><>0<>0<><>::
|
||||||
icon.png::CompressedTexture2D::7745181525272711891::1762703890::1762703997::1::::<><><>0<>0<>896be222f4526152412fc7150ec81337<>res://.godot/imported/icon.png-487276ed1e3a0c39cad0279d744ee560.ctex::
|
icon.png::CompressedTexture2D::7745181525272711891::1762703890::1762703997::1::::<><><>0<>0<>896be222f4526152412fc7150ec81337<>res://.godot/imported/icon.png-487276ed1e3a0c39cad0279d744ee560.ctex::
|
||||||
README.md::TextFile::-1::1762703890::0::1::::<><><>0<>0<><>::
|
README.md::TextFile::-1::1762972834::0::1::::<><><>0<>0<><>::
|
||||||
::res://assets/::1762703936
|
::res://assets/::1762972860
|
||||||
::res://assets/characters/::1762972117
|
::res://assets/characters/::1762972860
|
||||||
::res://assets/characters/Lobster/::1762972124
|
::res://assets/characters/Lobster/::1762972860
|
||||||
10029_Lobster_v1_Diffuse.jpg::CompressedTexture2D::1636629097341229697::1762972122::1762972126::1::::<><><>0<>0<>6340ceb996fa69b121c26459d93e8863<>res://.godot/imported/10029_Lobster_v1_Diffuse.jpg-042296e567cd8e99b847d125c0bf3e99.s3tc.ctex::
|
10029_Lobster_v1_Diffuse.jpg::CompressedTexture2D::1636629097341229697::1762972122::1762972126::1::::<><><>0<>0<>6340ceb996fa69b121c26459d93e8863<>res://.godot/imported/10029_Lobster_v1_Diffuse.jpg-042296e567cd8e99b847d125c0bf3e99.s3tc.ctex::
|
||||||
10029_Lobster_v1_iterations-2.obj::Mesh::8031617903903645591::1762972122::1762972124::1::::<><><>0<>0<>6f735946f3e6cee1022fc19030e5e510<>res://.godot/imported/10029_Lobster_v1_iterations-2.obj-007e872ca27f7329d9bece1ae9a73118.mesh<*>res://.godot/imported/10029_Lobster_v1_iterations-2.obj-007e872ca27f7329d9bece1ae9a73118.mesh::uid://xk2boxo0jwrp::::res://assets/characters/Lobster/10029_Lobster_v1_Diffuse.jpg
|
10029_Lobster_v1_iterations-2.obj::Mesh::8031617903903645591::1762972122::1762972124::1::::<><><>0<>0<>6f735946f3e6cee1022fc19030e5e510<>res://.godot/imported/10029_Lobster_v1_iterations-2.obj-007e872ca27f7329d9bece1ae9a73118.mesh<*>res://.godot/imported/10029_Lobster_v1_iterations-2.obj-007e872ca27f7329d9bece1ae9a73118.mesh::uid://xk2boxo0jwrp::::res://assets/characters/Lobster/10029_Lobster_v1_Diffuse.jpg
|
||||||
::res://assets/characters/player/::1762704000
|
::res://assets/characters/player/::1762972860
|
||||||
3DGodotRobot.glb::PackedScene::5656896519777153630::1762703890::1762704000::1::::<><><>0<>0<>bb220f20c8949e4b43553be149732094<>res://.godot/imported/3DGodotRobot.glb-c7060093cec96d07cab7f0bfb35f5061.scn::uid://bdg48m6l86q8i::::res://assets/characters/player/3DGodotRobot_GodotPalette.png
|
3DGodotRobot.glb::PackedScene::5656896519777153630::1762703890::1762704000::1::::<><><>0<>0<>bb220f20c8949e4b43553be149732094<>res://.godot/imported/3DGodotRobot.glb-c7060093cec96d07cab7f0bfb35f5061.scn::uid://bdg48m6l86q8i::::res://assets/characters/player/3DGodotRobot_GodotPalette.png
|
||||||
3DGodotRobot_GodotPalette.png::CompressedTexture2D::2611193883834911314::1762703890::1762703998::1::::<><><>0<>0<>a2b865fe920c4004f33acb6ec9f0a050<>res://.godot/imported/3DGodotRobot_GodotPalette.png-db2cb37c015bbbe580a404413e47cf86.s3tc.ctex::
|
3DGodotRobot_GodotPalette.png::CompressedTexture2D::2611193883834911314::1762703890::1762703998::1::::<><><>0<>0<>a2b865fe920c4004f33acb6ec9f0a050<>res://.godot/imported/3DGodotRobot_GodotPalette.png-db2cb37c015bbbe580a404413e47cf86.s3tc.ctex::
|
||||||
::res://assets/characters/player/GodotRobotPaletteSwap/::1762703998
|
::res://assets/characters/player/GodotRobotPaletteSwap/::1762972860
|
||||||
GodotGreenPalette.png::CompressedTexture2D::2473362712891021041::1762703890::1762703998::1::::<><><>0<>0<>b098d3d8ca31773160ee1f4035b79a4e<>res://.godot/imported/GodotGreenPalette.png-8c760c09d8ace0593bb63002a2ed8eae.s3tc.ctex::
|
GodotGreenPalette.png::CompressedTexture2D::2473362712891021041::1762703890::1762703998::1::::<><><>0<>0<>b098d3d8ca31773160ee1f4035b79a4e<>res://.godot/imported/GodotGreenPalette.png-8c760c09d8ace0593bb63002a2ed8eae.s3tc.ctex::
|
||||||
GodotPalette.png::CompressedTexture2D::382661063308374971::1762703890::1762703998::1::::<><><>0<>0<>2bdb257df9dcc0460d586ea92204ceb3<>res://.godot/imported/GodotPalette.png-a2d735eec81ad2647941b719336144cd.s3tc.ctex::
|
GodotPalette.png::CompressedTexture2D::382661063308374971::1762703890::1762703998::1::::<><><>0<>0<>2bdb257df9dcc0460d586ea92204ceb3<>res://.godot/imported/GodotPalette.png-a2d735eec81ad2647941b719336144cd.s3tc.ctex::
|
||||||
GodotRedPalette.png::CompressedTexture2D::3612187353401932242::1762703890::1762703996::1::::<><><>0<>0<>12aa761020ebe4c596572a3c09f2ee0b<>res://.godot/imported/GodotRedPalette.png-08bd2ae0a0cfe45c50f42f0c3a248d1b.ctex::
|
GodotRedPalette.png::CompressedTexture2D::3612187353401932242::1762703890::1762703996::1::::<><><>0<>0<>12aa761020ebe4c596572a3c09f2ee0b<>res://.godot/imported/GodotRedPalette.png-08bd2ae0a0cfe45c50f42f0c3a248d1b.ctex::
|
||||||
GodotYellowPalette.png::CompressedTexture2D::6381944650169859036::1762703890::1762703996::1::::<><><>0<>0<>96b97d4593ec92e6955954d301295c0a<>res://.godot/imported/GodotYellowPalette.png-827e66defded795634af82b4a85f17de.ctex::
|
GodotYellowPalette.png::CompressedTexture2D::6381944650169859036::1762703890::1762703996::1::::<><><>0<>0<>96b97d4593ec92e6955954d301295c0a<>res://.godot/imported/GodotYellowPalette.png-827e66defded795634af82b4a85f17de.ctex::
|
||||||
::res://assets/fonts/::1762703995
|
::res://assets/fonts/::1762972860
|
||||||
Kurland.ttf::FontFile::7721683626151354491::1762703891::1762703995::1::::<><><>0<>0<>dacc2e7578d27658799cf58f30bac804<>res://.godot/imported/Kurland.ttf-14f9cbbd8657b37f475042f6a32feeab.fontdata::
|
Kurland.ttf::FontFile::7721683626151354491::1762703891::1762703995::1::::<><><>0<>0<>dacc2e7578d27658799cf58f30bac804<>res://.godot/imported/Kurland.ttf-14f9cbbd8657b37f475042f6a32feeab.fontdata::
|
||||||
::res://level/::1762703936
|
::res://level/::1762972860
|
||||||
::res://level/scenes/::1762703936
|
::res://level/scenes/::1762972860
|
||||||
level.tscn::PackedScene::8575440581694956823::1762703891::0::1::::<><><>0<>0<><>::uid://d0dgljwwl463n::::res://level/scripts/level.gd<>uid://cffjduipbb3s5::::res://level/scenes/player.tscn<>uid://diapabmalpcrj::::res://assets/fonts/Kurland.ttf
|
level.tscn::PackedScene::8575440581694956823::1762703891::0::1::::<><><>0<>0<><>::uid://d0dgljwwl463n::::res://level/scripts/level.gd<>uid://cffjduipbb3s5::::res://level/scenes/player.tscn<>uid://diapabmalpcrj::::res://assets/fonts/Kurland.ttf
|
||||||
player.tscn::PackedScene::5134660348171653994::1762703891::0::1::::<><><>0<>0<><>::uid://c2si8gkbnde0c::::res://level/scripts/player.gd<>uid://bdg48m6l86q8i::::res://assets/characters/player/3DGodotRobot_GodotPalette.png<>uid://v8j54rbc0ik::::res://level/scripts/3d_godot_robot.gd<>uid://cw6pxwst2gh85::::res://assets/characters/player/GodotRobotPaletteSwap/GodotYellowPalette.png<>uid://bbid6mowxhd5b::::res://assets/characters/player/GodotRobotPaletteSwap/GodotGreenPalette.png<>uid://fpmmv2oxjcdv::::res://assets/characters/player/GodotRobotPaletteSwap/GodotPalette.png<>uid://bj7yrijm7bppq::::res://level/scripts/spring_arm_offset.gd<>uid://brp1gy30s4rks::::res://assets/characters/player/GodotRobotPaletteSwap/GodotRedPalette.png
|
player.tscn::PackedScene::5134660348171653994::1762972234::0::1::::<><><>0<>0<><>::uid://c2si8gkbnde0c::::res://level/scripts/player.gd<>uid://bdg48m6l86q8i::::res://assets/characters/player/3DGodotRobot_GodotPalette.png<>uid://v8j54rbc0ik::::res://level/scripts/3d_godot_robot.gd<>uid://cw6pxwst2gh85::::res://assets/characters/player/GodotRobotPaletteSwap/GodotYellowPalette.png<>uid://bbid6mowxhd5b::::res://assets/characters/player/GodotRobotPaletteSwap/GodotGreenPalette.png<>uid://fpmmv2oxjcdv::::res://assets/characters/player/GodotRobotPaletteSwap/GodotPalette.png<>uid://bj7yrijm7bppq::::res://level/scripts/spring_arm_offset.gd<>uid://brp1gy30s4rks::::res://assets/characters/player/GodotRobotPaletteSwap/GodotRedPalette.png<>uid://dmottq5u3my52::::res://assets/characters/Lobster/10029_Lobster_v1_iterations-2.obj
|
||||||
::res://level/scripts/::1762703936
|
::res://level/scripts/::1762974651
|
||||||
3d_godot_robot.gd::GDScript::45373287015371390::1762703891::0::1::::Body<>Node3D<><>0<>0<><>::
|
3d_godot_robot.gd::GDScript::45373287015371390::1762703891::0::1::::Body<>Node3D<><>0<>0<><>::
|
||||||
|
base_unit.gd::GDScript::928298549479668245::1762974583::0::1::::BaseUnit<>CharacterBody3D<><>0<>0<><>::
|
||||||
level.gd::GDScript::8920560728693148825::1762703891::0::1::::<>Node3D<><>0<>0<><>::
|
level.gd::GDScript::8920560728693148825::1762703891::0::1::::<>Node3D<><>0<>0<><>::
|
||||||
network.gd::GDScript::5061006158354274844::1762703891::0::1::::<>Node<><>0<>0<><>::
|
network.gd::GDScript::5061006158354274844::1762703891::0::1::::<>Node<><>0<>0<><>::
|
||||||
player.gd::GDScript::6705643942978221932::1762703891::0::1::::Character<>CharacterBody3D<><>0<>0<><>::
|
player.gd::GDScript::6705643942978221932::1762974651::0::1::::Character<>BaseUnit<><>0<>0<><>::
|
||||||
spring_arm_offset.gd::GDScript::3085668365566169618::1762703891::0::1::::SpringArmCharacter<>Node3D<><>0<>0<><>::
|
spring_arm_offset.gd::GDScript::3085668365566169618::1762703891::0::1::::SpringArmCharacter<>Node3D<><>0<>0<><>::
|
||||||
|
|||||||
@@ -1 +1,2 @@
|
|||||||
res://level/scenes/player.tscn
|
res://level/scripts/player.gd
|
||||||
|
res://level/scripts/spring_arm_offset.gd
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ executable_path="C:/Users/James/Desktop/Godot_v4.5-stable_win64.exe"
|
|||||||
[recent_files]
|
[recent_files]
|
||||||
|
|
||||||
scenes=["res://level/scenes/player.tscn", "res://level/scenes/level.tscn"]
|
scenes=["res://level/scenes/player.tscn", "res://level/scenes/level.tscn"]
|
||||||
scripts=["res://README.md", "res://level/scripts/level.gd", "res://level/scripts/3d_godot_robot.gd", "res://level/scripts/player.gd"]
|
scripts=["res://level/scripts/spring_arm_offset.gd", "res://README.md", "res://level/scripts/level.gd", "res://level/scripts/3d_godot_robot.gd", "res://level/scripts/player.gd"]
|
||||||
|
|
||||||
[debug_options]
|
[debug_options]
|
||||||
|
|
||||||
|
|||||||
@@ -3,12 +3,16 @@
|
|||||||
state={
|
state={
|
||||||
"bookmarks": PackedInt32Array(),
|
"bookmarks": PackedInt32Array(),
|
||||||
"breakpoints": PackedInt32Array(),
|
"breakpoints": PackedInt32Array(),
|
||||||
"column": 73,
|
"column": 18,
|
||||||
"folded_lines": Array[int]([]),
|
"folded_lines": Array[int]([]),
|
||||||
"h_scroll_position": 0,
|
"h_scroll_position": 0,
|
||||||
"row": 112,
|
"row": 169,
|
||||||
"scroll_position": 90.99999999999999,
|
"scroll_position": 169.0,
|
||||||
"selection": false,
|
"selection": true,
|
||||||
|
"selection_from_column": 18,
|
||||||
|
"selection_from_line": 169,
|
||||||
|
"selection_to_column": 48,
|
||||||
|
"selection_to_line": 174,
|
||||||
"syntax_highlighter": "GDScript"
|
"syntax_highlighter": "GDScript"
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -45,11 +49,25 @@ state={
|
|||||||
state={
|
state={
|
||||||
"bookmarks": PackedInt32Array(),
|
"bookmarks": PackedInt32Array(),
|
||||||
"breakpoints": PackedInt32Array(),
|
"breakpoints": PackedInt32Array(),
|
||||||
"column": 98,
|
"column": 18,
|
||||||
"folded_lines": Array[int]([]),
|
"folded_lines": Array[int]([]),
|
||||||
"h_scroll_position": 0,
|
"h_scroll_position": 0,
|
||||||
"row": 22,
|
"row": 2,
|
||||||
"scroll_position": 0.0,
|
"scroll_position": 0.0,
|
||||||
"selection": false,
|
"selection": false,
|
||||||
"syntax_highlighter": "Markdown"
|
"syntax_highlighter": "Markdown"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[res://level/scripts/spring_arm_offset.gd]
|
||||||
|
|
||||||
|
state={
|
||||||
|
"bookmarks": PackedInt32Array(),
|
||||||
|
"breakpoints": PackedInt32Array(),
|
||||||
|
"column": 0,
|
||||||
|
"folded_lines": Array[int]([]),
|
||||||
|
"h_scroll_position": 0,
|
||||||
|
"row": 10,
|
||||||
|
"scroll_position": 0.0,
|
||||||
|
"selection": false,
|
||||||
|
"syntax_highlighter": "GDScript"
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,4 +1,12 @@
|
|||||||
list=[{
|
list=[{
|
||||||
|
"base": &"CharacterBody3D",
|
||||||
|
"class": &"BaseUnit",
|
||||||
|
"icon": "",
|
||||||
|
"is_abstract": false,
|
||||||
|
"is_tool": false,
|
||||||
|
"language": &"GDScript",
|
||||||
|
"path": "res://level/scripts/base_unit.gd"
|
||||||
|
}, {
|
||||||
"base": &"Node3D",
|
"base": &"Node3D",
|
||||||
"class": &"Body",
|
"class": &"Body",
|
||||||
"icon": "",
|
"icon": "",
|
||||||
@@ -7,7 +15,7 @@ list=[{
|
|||||||
"language": &"GDScript",
|
"language": &"GDScript",
|
||||||
"path": "res://level/scripts/3d_godot_robot.gd"
|
"path": "res://level/scripts/3d_godot_robot.gd"
|
||||||
}, {
|
}, {
|
||||||
"base": &"CharacterBody3D",
|
"base": &"BaseUnit",
|
||||||
"class": &"Character",
|
"class": &"Character",
|
||||||
"icon": "",
|
"icon": "",
|
||||||
"is_abstract": false,
|
"is_abstract": false,
|
||||||
|
|||||||
Binary file not shown.
@@ -0,0 +1,111 @@
|
|||||||
|
# CLAUDE.md
|
||||||
|
|
||||||
|
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
||||||
|
|
||||||
|
## Project Overview
|
||||||
|
|
||||||
|
This is a 3D multiplayer fighting game built in Godot 4.5 using an inheritance-based architecture. The game uses ENet for client-server networking with the Network singleton (autoload) managing all multiplayer connections.
|
||||||
|
|
||||||
|
## Key Commands
|
||||||
|
|
||||||
|
### Running the Game
|
||||||
|
- Open the project in Godot Editor and press F5 to run
|
||||||
|
- Or use: `godot --path . res://level/scenes/level.tscn`
|
||||||
|
|
||||||
|
### Testing Multiplayer Locally
|
||||||
|
1. Run the main scene (level.tscn)
|
||||||
|
2. Click "Host" on one instance to create a server (default port 8080)
|
||||||
|
3. Run another instance and click "Join" to connect as a client
|
||||||
|
|
||||||
|
## Architecture
|
||||||
|
|
||||||
|
### Inheritance Structure
|
||||||
|
The codebase follows an inheritance-based design pattern:
|
||||||
|
- **BaseUnit** (base class) - Common functionality for all game entities that can take damage/have health
|
||||||
|
- **Character** extends **CharacterBody3D** - Player character class (class_name: Character)
|
||||||
|
- Currently implements movement, jumping, sprinting, respawning, and skin customization
|
||||||
|
- Uses multiplayer authority for input handling (only the owner processes input)
|
||||||
|
|
||||||
|
### Networking (Network.gd)
|
||||||
|
- **Type**: Autoload singleton (accessible via `Network` globally)
|
||||||
|
- **Protocol**: ENet (UDP-based)
|
||||||
|
- **Architecture**: Client-Server (one host, multiple clients up to MAX_PLAYERS=10)
|
||||||
|
- **Player Data**: Stored in `Network.players` dictionary keyed by peer_id
|
||||||
|
- Contains: `{"nick": String, "skin": Character.SkinColor}`
|
||||||
|
|
||||||
|
### RPC Patterns
|
||||||
|
The project uses Godot 4's RPC system for multiplayer synchronization:
|
||||||
|
- Use `@rpc("any_peer", "reliable")` for critical data (health, damage)
|
||||||
|
- Use `@rpc("any_peer", "unreliable")` for frequent position updates
|
||||||
|
- The server (multiplayer.is_server()) is authoritative for game state
|
||||||
|
- Use `multiplayer.get_remote_sender_id()` to identify RPC sender
|
||||||
|
- Use `rpc_id(peer_id, "method_name")` for targeted RPCs
|
||||||
|
|
||||||
|
### Scene Structure
|
||||||
|
- **level/scenes/level.tscn** - Main scene with menu UI, chat, and PlayersContainer
|
||||||
|
- level.gd spawns players dynamically when they connect
|
||||||
|
- **level/scenes/player.tscn** - Player character scene
|
||||||
|
- Instantiated by server when players connect
|
||||||
|
- Named with peer_id for easy lookup
|
||||||
|
|
||||||
|
### Physics Layers
|
||||||
|
- Layer 1: "player" - Player collision
|
||||||
|
- Layer 2: "world" - Environment collision
|
||||||
|
|
||||||
|
### Input Actions
|
||||||
|
Defined in project.godot:
|
||||||
|
- `move_left`, `move_right`, `move_forward`, `move_backward` (WASD)
|
||||||
|
- `jump` (Space)
|
||||||
|
- `shift` (Left Shift for sprinting)
|
||||||
|
- `quit` (Escape)
|
||||||
|
- `toggle_chat` (Enter)
|
||||||
|
|
||||||
|
## Development Guidelines
|
||||||
|
|
||||||
|
### Adding New Components
|
||||||
|
1. Create scripts in `level/scripts/`
|
||||||
|
2. For multiplayer-synchronized components:
|
||||||
|
- Always check `is_multiplayer_authority()` before processing input
|
||||||
|
- Use RPC for state changes that need to replicate
|
||||||
|
- Server should validate all important state changes
|
||||||
|
|
||||||
|
### Adding New Unit Types (Enemies, NPCs)
|
||||||
|
1. Create a new class that extends BaseUnit
|
||||||
|
2. Implement required virtual methods
|
||||||
|
3. Add any unique behavior in _physics_process/_process
|
||||||
|
4. Ensure multiplayer authority is set correctly
|
||||||
|
|
||||||
|
### Testing Multiplayer
|
||||||
|
- Always test with at least 2 instances (1 host, 1 client)
|
||||||
|
- Test edge cases: late joins, disconnections, packet loss
|
||||||
|
- Verify state is synchronized correctly across all clients
|
||||||
|
|
||||||
|
## Common Patterns
|
||||||
|
|
||||||
|
### Spawning Networked Entities
|
||||||
|
```gdscript
|
||||||
|
# Server-side only
|
||||||
|
if multiplayer.is_server():
|
||||||
|
var entity = entity_scene.instantiate()
|
||||||
|
entity.name = str(unique_id) # Name with ID for easy lookup
|
||||||
|
entity.set_multiplayer_authority(owner_peer_id)
|
||||||
|
container.add_child(entity, true) # true = force readable name
|
||||||
|
```
|
||||||
|
|
||||||
|
### Multiplayer Authority Check
|
||||||
|
```gdscript
|
||||||
|
func _physics_process(delta):
|
||||||
|
if not is_multiplayer_authority():
|
||||||
|
return
|
||||||
|
# Only the authority processes input/logic
|
||||||
|
```
|
||||||
|
|
||||||
|
### RPC for State Changes
|
||||||
|
```gdscript
|
||||||
|
@rpc("any_peer", "reliable")
|
||||||
|
func take_damage(amount: int, attacker_id: int):
|
||||||
|
if not multiplayer.is_server():
|
||||||
|
return # Server validates
|
||||||
|
# Apply damage logic
|
||||||
|
rpc("sync_health", current_health) # Broadcast to all
|
||||||
|
```
|
||||||
@@ -0,0 +1,117 @@
|
|||||||
|
extends CharacterBody3D
|
||||||
|
class_name BaseUnit
|
||||||
|
|
||||||
|
## Base class for all units (players, enemies, NPCs) in the game
|
||||||
|
## Provides common functionality like health management and taking damage
|
||||||
|
|
||||||
|
signal health_changed(old_health: float, new_health: float)
|
||||||
|
signal died(killer_id: int)
|
||||||
|
signal respawned()
|
||||||
|
|
||||||
|
@export var max_health: float = 100.0
|
||||||
|
@export var can_respawn: bool = true
|
||||||
|
@export var respawn_delay: float = 3.0
|
||||||
|
|
||||||
|
var current_health: float = 100.0
|
||||||
|
var is_dead: bool = false
|
||||||
|
var _respawn_point: Vector3 = Vector3.ZERO
|
||||||
|
|
||||||
|
func _ready():
|
||||||
|
current_health = max_health
|
||||||
|
_respawn_point = global_position
|
||||||
|
|
||||||
|
func _enter_tree():
|
||||||
|
set_multiplayer_authority(str(name).to_int())
|
||||||
|
|
||||||
|
## Take damage from an attacker
|
||||||
|
## Should only be called on the server for authority
|
||||||
|
@rpc("any_peer", "reliable")
|
||||||
|
func take_damage(amount: float, attacker_id: int = -1):
|
||||||
|
# Only server can process damage
|
||||||
|
if not multiplayer.is_server():
|
||||||
|
return
|
||||||
|
|
||||||
|
if is_dead:
|
||||||
|
return
|
||||||
|
|
||||||
|
var old_health = current_health
|
||||||
|
current_health = max(0, current_health - amount)
|
||||||
|
|
||||||
|
# Broadcast health change to all clients
|
||||||
|
rpc("sync_health", current_health)
|
||||||
|
health_changed.emit(old_health, current_health)
|
||||||
|
|
||||||
|
if current_health <= 0:
|
||||||
|
_die(attacker_id)
|
||||||
|
|
||||||
|
## Heal the unit
|
||||||
|
@rpc("any_peer", "reliable")
|
||||||
|
func heal(amount: float):
|
||||||
|
if not multiplayer.is_server():
|
||||||
|
return
|
||||||
|
|
||||||
|
if is_dead:
|
||||||
|
return
|
||||||
|
|
||||||
|
var old_health = current_health
|
||||||
|
current_health = min(max_health, current_health + amount)
|
||||||
|
|
||||||
|
rpc("sync_health", current_health)
|
||||||
|
health_changed.emit(old_health, current_health)
|
||||||
|
|
||||||
|
## Sync health across all clients
|
||||||
|
@rpc("any_peer", "call_local", "reliable")
|
||||||
|
func sync_health(new_health: float):
|
||||||
|
var old_health = current_health
|
||||||
|
current_health = new_health
|
||||||
|
health_changed.emit(old_health, current_health)
|
||||||
|
|
||||||
|
## Handle death
|
||||||
|
func _die(killer_id: int):
|
||||||
|
if is_dead:
|
||||||
|
return
|
||||||
|
|
||||||
|
is_dead = true
|
||||||
|
died.emit(killer_id)
|
||||||
|
rpc("sync_death", killer_id)
|
||||||
|
|
||||||
|
if can_respawn and multiplayer.is_server():
|
||||||
|
await get_tree().create_timer(respawn_delay).timeout
|
||||||
|
_respawn()
|
||||||
|
|
||||||
|
## Sync death state to all clients
|
||||||
|
@rpc("any_peer", "call_local", "reliable")
|
||||||
|
func sync_death(killer_id: int):
|
||||||
|
is_dead = true
|
||||||
|
died.emit(killer_id)
|
||||||
|
# Subclasses should override to add visual effects, disable collision, etc.
|
||||||
|
|
||||||
|
## Respawn the unit
|
||||||
|
func _respawn():
|
||||||
|
if not multiplayer.is_server():
|
||||||
|
return
|
||||||
|
|
||||||
|
is_dead = false
|
||||||
|
current_health = max_health
|
||||||
|
global_position = _respawn_point
|
||||||
|
velocity = Vector3.ZERO
|
||||||
|
|
||||||
|
rpc("sync_respawn", _respawn_point)
|
||||||
|
respawned.emit()
|
||||||
|
|
||||||
|
## Sync respawn to all clients
|
||||||
|
@rpc("any_peer", "call_local", "reliable")
|
||||||
|
func sync_respawn(spawn_pos: Vector3):
|
||||||
|
is_dead = false
|
||||||
|
current_health = max_health
|
||||||
|
global_position = spawn_pos
|
||||||
|
velocity = Vector3.ZERO
|
||||||
|
respawned.emit()
|
||||||
|
|
||||||
|
## Set the respawn point
|
||||||
|
func set_respawn_point(point: Vector3):
|
||||||
|
_respawn_point = point
|
||||||
|
|
||||||
|
## Get health percentage (0.0 to 1.0)
|
||||||
|
func get_health_percent() -> float:
|
||||||
|
return current_health / max_health if max_health > 0 else 0.0
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://nhw7amcyksft
|
||||||
+199
-12
@@ -1,4 +1,4 @@
|
|||||||
extends CharacterBody3D
|
extends BaseUnit
|
||||||
class_name Character
|
class_name Character
|
||||||
|
|
||||||
const NORMAL_SPEED = 6.0
|
const NORMAL_SPEED = 6.0
|
||||||
@@ -8,6 +8,7 @@ const JUMP_VELOCITY = 10
|
|||||||
enum SkinColor { BLUE, YELLOW, GREEN, RED }
|
enum SkinColor { BLUE, YELLOW, GREEN, RED }
|
||||||
|
|
||||||
@onready var nickname: Label3D = $PlayerNick/Nickname
|
@onready var nickname: Label3D = $PlayerNick/Nickname
|
||||||
|
@onready var health_label: Label3D = null # Optional 3D health label
|
||||||
|
|
||||||
@export_category("Objects")
|
@export_category("Objects")
|
||||||
@export var _body: Node3D = null
|
@export var _body: Node3D = null
|
||||||
@@ -25,18 +26,45 @@ enum SkinColor { BLUE, YELLOW, GREEN, RED }
|
|||||||
@onready var _limbs_head_mesh: MeshInstance3D = get_node("3DGodotRobot/RobotArmature/Skeleton3D/Llimbs and head")
|
@onready var _limbs_head_mesh: MeshInstance3D = get_node("3DGodotRobot/RobotArmature/Skeleton3D/Llimbs and head")
|
||||||
|
|
||||||
var _current_speed: float
|
var _current_speed: float
|
||||||
var _respawn_point = Vector3(0, 5, 0)
|
|
||||||
var gravity = ProjectSettings.get_setting("physics/3d/default_gravity")
|
var gravity = ProjectSettings.get_setting("physics/3d/default_gravity")
|
||||||
|
|
||||||
|
# Attack system
|
||||||
|
@export var attack_damage: float = 10.0
|
||||||
|
@export var attack_range: float = 3.0
|
||||||
|
@export var attack_cooldown: float = 0.5
|
||||||
|
var _attack_timer: float = 0.0
|
||||||
|
|
||||||
func _enter_tree():
|
func _enter_tree():
|
||||||
set_multiplayer_authority(str(name).to_int())
|
super._enter_tree()
|
||||||
$SpringArmOffset/SpringArm3D/Camera3D.current = is_multiplayer_authority()
|
$SpringArmOffset/SpringArm3D/Camera3D.current = is_multiplayer_authority()
|
||||||
|
|
||||||
func _ready():
|
func _ready():
|
||||||
return
|
super._ready()
|
||||||
|
set_respawn_point(Vector3(0, 5, 0))
|
||||||
|
|
||||||
|
# Try to get optional health label
|
||||||
|
if has_node("PlayerNick/HealthLabel"):
|
||||||
|
health_label = get_node("PlayerNick/HealthLabel")
|
||||||
|
|
||||||
|
# Connect health signals
|
||||||
|
health_changed.connect(_on_health_changed)
|
||||||
|
died.connect(_on_died)
|
||||||
|
respawned.connect(_on_respawned)
|
||||||
|
|
||||||
|
# Update health display
|
||||||
|
_update_health_display()
|
||||||
|
|
||||||
|
# Create 2D UI health bar for local player
|
||||||
|
if is_multiplayer_authority():
|
||||||
|
_create_health_ui()
|
||||||
|
|
||||||
func _physics_process(delta):
|
func _physics_process(delta):
|
||||||
if not is_multiplayer_authority(): return
|
# Check if multiplayer is ready
|
||||||
|
if multiplayer.multiplayer_peer == null:
|
||||||
|
return
|
||||||
|
|
||||||
|
if not is_multiplayer_authority():
|
||||||
|
return
|
||||||
|
|
||||||
var current_scene = get_tree().get_current_scene()
|
var current_scene = get_tree().get_current_scene()
|
||||||
if current_scene and current_scene.has_method("is_chat_visible") and current_scene.is_chat_visible() and is_on_floor():
|
if current_scene and current_scene.has_method("is_chat_visible") and current_scene.is_chat_visible() and is_on_floor():
|
||||||
@@ -57,10 +85,24 @@ func _physics_process(delta):
|
|||||||
move_and_slide()
|
move_and_slide()
|
||||||
_body.animate(velocity)
|
_body.animate(velocity)
|
||||||
|
|
||||||
func _process(_delta):
|
func _process(delta):
|
||||||
if not is_multiplayer_authority(): return
|
# Check if multiplayer is ready
|
||||||
|
if multiplayer.multiplayer_peer == null:
|
||||||
|
return
|
||||||
|
|
||||||
|
if not is_multiplayer_authority():
|
||||||
|
return
|
||||||
|
|
||||||
_check_fall_and_respawn()
|
_check_fall_and_respawn()
|
||||||
|
|
||||||
|
# Update attack cooldown
|
||||||
|
if _attack_timer > 0:
|
||||||
|
_attack_timer -= delta
|
||||||
|
|
||||||
|
# Handle attack input
|
||||||
|
if Input.is_action_just_pressed("attack") and _attack_timer <= 0 and not is_dead:
|
||||||
|
_perform_attack()
|
||||||
|
|
||||||
func freeze():
|
func freeze():
|
||||||
velocity.x = 0
|
velocity.x = 0
|
||||||
velocity.z = 0
|
velocity.z = 0
|
||||||
@@ -98,13 +140,10 @@ func is_running() -> bool:
|
|||||||
return false
|
return false
|
||||||
|
|
||||||
func _check_fall_and_respawn():
|
func _check_fall_and_respawn():
|
||||||
if global_transform.origin.y < -15.0:
|
if global_transform.origin.y < -15.0 and multiplayer.is_server():
|
||||||
|
# Use BaseUnit's respawn system
|
||||||
_respawn()
|
_respawn()
|
||||||
|
|
||||||
func _respawn():
|
|
||||||
global_transform.origin = _respawn_point
|
|
||||||
velocity = Vector3.ZERO
|
|
||||||
|
|
||||||
@rpc("any_peer", "reliable")
|
@rpc("any_peer", "reliable")
|
||||||
func change_nick(new_nick: String):
|
func change_nick(new_nick: String):
|
||||||
if nickname:
|
if nickname:
|
||||||
@@ -134,3 +173,151 @@ func set_mesh_texture(mesh_instance: MeshInstance3D, texture: CompressedTexture2
|
|||||||
var new_material := material
|
var new_material := material
|
||||||
new_material.albedo_texture = texture
|
new_material.albedo_texture = texture
|
||||||
mesh_instance.set_surface_override_material(0, new_material)
|
mesh_instance.set_surface_override_material(0, new_material)
|
||||||
|
|
||||||
|
## Attack system
|
||||||
|
func _perform_attack():
|
||||||
|
if not is_multiplayer_authority() or is_dead:
|
||||||
|
return
|
||||||
|
|
||||||
|
_attack_timer = attack_cooldown
|
||||||
|
|
||||||
|
# Find nearest enemy in range
|
||||||
|
var space_state = get_world_3d().direct_space_state
|
||||||
|
var query = PhysicsShapeQueryParameters3D.new()
|
||||||
|
var sphere = SphereShape3D.new()
|
||||||
|
sphere.radius = attack_range
|
||||||
|
query.shape = sphere
|
||||||
|
query.transform = global_transform
|
||||||
|
query.collision_mask = 1 # Player layer
|
||||||
|
|
||||||
|
var results = space_state.intersect_shape(query)
|
||||||
|
|
||||||
|
for result in results:
|
||||||
|
var hit_body = result["collider"]
|
||||||
|
if hit_body != self and hit_body is BaseUnit:
|
||||||
|
var attacker_id = multiplayer.get_unique_id()
|
||||||
|
|
||||||
|
# If we're the server, apply damage directly
|
||||||
|
if multiplayer.is_server():
|
||||||
|
_server_apply_damage(hit_body.name, attack_damage, attacker_id)
|
||||||
|
else:
|
||||||
|
# Otherwise, request server to apply damage
|
||||||
|
rpc_id(1, "_server_apply_damage", hit_body.name, attack_damage, attacker_id)
|
||||||
|
break # Only hit one target per attack
|
||||||
|
|
||||||
|
## Server-side damage application
|
||||||
|
@rpc("any_peer", "reliable")
|
||||||
|
func _server_apply_damage(target_name: String, damage: float, attacker_id: int):
|
||||||
|
if not multiplayer.is_server():
|
||||||
|
return
|
||||||
|
|
||||||
|
# Get the target from the players container
|
||||||
|
var level = get_tree().get_current_scene()
|
||||||
|
if not level or not level.has_node("PlayersContainer"):
|
||||||
|
return
|
||||||
|
|
||||||
|
var players_container = level.get_node("PlayersContainer")
|
||||||
|
if not players_container.has_node(target_name):
|
||||||
|
return
|
||||||
|
|
||||||
|
var target = players_container.get_node(target_name)
|
||||||
|
if target and target is BaseUnit:
|
||||||
|
target.take_damage(damage, attacker_id)
|
||||||
|
|
||||||
|
## Health display and callbacks
|
||||||
|
func _create_health_ui():
|
||||||
|
# Create a 2D UI for the local player's health
|
||||||
|
var canvas = CanvasLayer.new()
|
||||||
|
canvas.name = "HealthUI"
|
||||||
|
add_child(canvas)
|
||||||
|
|
||||||
|
# Health bar background
|
||||||
|
var health_bg = ColorRect.new()
|
||||||
|
health_bg.name = "HealthBG"
|
||||||
|
health_bg.color = Color(0.2, 0.2, 0.2, 0.8)
|
||||||
|
health_bg.position = Vector2(20, 20)
|
||||||
|
health_bg.size = Vector2(200, 30)
|
||||||
|
canvas.add_child(health_bg)
|
||||||
|
|
||||||
|
# Health bar (current health)
|
||||||
|
var health_bar = ColorRect.new()
|
||||||
|
health_bar.name = "HealthBar"
|
||||||
|
health_bar.color = Color(0.0, 0.8, 0.0, 1.0) # Green
|
||||||
|
health_bar.position = Vector2(22, 22)
|
||||||
|
health_bar.size = Vector2(196, 26)
|
||||||
|
canvas.add_child(health_bar)
|
||||||
|
|
||||||
|
# Health text
|
||||||
|
var health_text = Label.new()
|
||||||
|
health_text.name = "HealthText"
|
||||||
|
health_text.position = Vector2(20, 20)
|
||||||
|
health_text.size = Vector2(200, 30)
|
||||||
|
health_text.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
|
||||||
|
health_text.vertical_alignment = VERTICAL_ALIGNMENT_CENTER
|
||||||
|
health_text.add_theme_color_override("font_color", Color.WHITE)
|
||||||
|
health_text.add_theme_color_override("font_outline_color", Color.BLACK)
|
||||||
|
health_text.add_theme_constant_override("outline_size", 2)
|
||||||
|
health_text.text = "HP: %d/%d" % [int(current_health), int(max_health)]
|
||||||
|
canvas.add_child(health_text)
|
||||||
|
|
||||||
|
func _update_health_display():
|
||||||
|
# Update 3D label if it exists
|
||||||
|
if health_label:
|
||||||
|
health_label.text = "HP: %d/%d" % [int(current_health), int(max_health)]
|
||||||
|
|
||||||
|
# Update 2D UI for local player
|
||||||
|
if is_multiplayer_authority() and has_node("HealthUI"):
|
||||||
|
var health_bar = get_node_or_null("HealthUI/HealthBar")
|
||||||
|
var health_text = get_node_or_null("HealthUI/HealthText")
|
||||||
|
|
||||||
|
if health_bar:
|
||||||
|
var health_percent = get_health_percent()
|
||||||
|
health_bar.size.x = 196 * health_percent
|
||||||
|
|
||||||
|
# Change color based on health
|
||||||
|
if health_percent > 0.6:
|
||||||
|
health_bar.color = Color(0.0, 0.8, 0.0) # Green
|
||||||
|
elif health_percent > 0.3:
|
||||||
|
health_bar.color = Color(1.0, 0.8, 0.0) # Yellow
|
||||||
|
else:
|
||||||
|
health_bar.color = Color(0.8, 0.0, 0.0) # Red
|
||||||
|
|
||||||
|
if health_text:
|
||||||
|
health_text.text = "HP: %d/%d" % [int(current_health), int(max_health)]
|
||||||
|
|
||||||
|
func _on_health_changed(_old_health: float, _new_health: float):
|
||||||
|
_update_health_display()
|
||||||
|
|
||||||
|
func _on_died(killer_id: int):
|
||||||
|
# Disable player when dead
|
||||||
|
set_physics_process(false)
|
||||||
|
set_process(false)
|
||||||
|
|
||||||
|
# Visual feedback - could add death animation here
|
||||||
|
if _body:
|
||||||
|
_body.visible = false
|
||||||
|
|
||||||
|
# Print death message
|
||||||
|
var killer_name = "Unknown"
|
||||||
|
if Network.players.has(killer_id):
|
||||||
|
killer_name = Network.players[killer_id]["nick"]
|
||||||
|
|
||||||
|
if is_multiplayer_authority():
|
||||||
|
print("You were killed by ", killer_name)
|
||||||
|
|
||||||
|
# Show death message on UI
|
||||||
|
if has_node("HealthUI/HealthText"):
|
||||||
|
get_node("HealthUI/HealthText").text = "DEAD - Respawning..."
|
||||||
|
|
||||||
|
func _on_respawned():
|
||||||
|
# Re-enable player
|
||||||
|
set_physics_process(true)
|
||||||
|
set_process(true)
|
||||||
|
|
||||||
|
if _body:
|
||||||
|
_body.visible = true
|
||||||
|
|
||||||
|
_update_health_display()
|
||||||
|
|
||||||
|
if is_multiplayer_authority():
|
||||||
|
print("You respawned!")
|
||||||
|
|||||||
@@ -7,6 +7,9 @@ const MOUSE_SENSIBILITY: float = 0.005
|
|||||||
@export var _spring_arm: SpringArm3D = null
|
@export var _spring_arm: SpringArm3D = null
|
||||||
|
|
||||||
func _unhandled_input(_event) -> void:
|
func _unhandled_input(_event) -> void:
|
||||||
|
# Check if multiplayer is ready
|
||||||
|
if multiplayer.multiplayer_peer == null:
|
||||||
|
return
|
||||||
|
|
||||||
if (_event is InputEventMouseMotion) and is_multiplayer_authority():
|
if (_event is InputEventMouseMotion) and is_multiplayer_authority():
|
||||||
rotate_y(-_event.relative.x * MOUSE_SENSIBILITY)
|
rotate_y(-_event.relative.x * MOUSE_SENSIBILITY)
|
||||||
|
|||||||
@@ -68,6 +68,11 @@ toggle_chat={
|
|||||||
"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":4194326,"key_label":0,"unicode":0,"location":0,"echo":false,"script":null)
|
"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":4194326,"key_label":0,"unicode":0,"location":0,"echo":false,"script":null)
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
attack={
|
||||||
|
"deadzone": 0.5,
|
||||||
|
"events": [Object(InputEventMouseButton,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"button_mask":1,"position":Vector2(0, 0),"global_position":Vector2(0, 0),"factor":1.0,"button_index":1,"canceled":false,"pressed":true,"double_click":false,"script":null)
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
[layer_names]
|
[layer_names]
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user