parent
1d63abc581
commit
bc30cfd6ca
14 changed files with 493 additions and 40 deletions
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 |
||||||
|
|||||||
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 |
||||||
Loading…
Reference in new issue