Compare commits

42 Commits

Author SHA1 Message Date
62c96af003 Merge pull request 'network' (#2) from network into main
All checks were successful
Linux arm64 / Build (push) Successful in 1m41s
Reviewed-on: #2
2024-08-22 10:30:02 +02:00
eb85b13ac7 fixed client player going crazy on spawn
All checks were successful
Linux arm64 / Build (pull_request) Successful in 1m35s
2024-08-22 10:25:01 +02:00
d7e80e05de add server side speed check 2024-08-22 10:21:51 +02:00
f7d0103dbf fix warning 2024-08-22 10:19:28 +02:00
1bd053aba3 fix position sync issues
All checks were successful
Linux arm64 / Build (pull_request) Successful in 1m31s
2024-08-21 12:52:00 +02:00
e17387b867 add packet declare syntax check 2024-08-21 12:28:40 +02:00
6ee87733ca network: send player velocity 2024-08-21 10:29:42 +02:00
d3dddff005 very small refactor 2024-08-21 10:22:57 +02:00
03b6e577ea unreliable packets
All checks were successful
Linux arm64 / Build (pull_request) Successful in 1m37s
2024-08-20 19:27:20 +02:00
f557d0dd2d ByteBuffer: handle read errors 2024-08-20 18:51:01 +02:00
a092f6fbc1 make world abstract
All checks were successful
Linux arm64 / Build (pull_request) Successful in 1m36s
2024-08-19 16:19:45 +02:00
3cebb70289 moved server 2024-08-19 15:26:23 +02:00
27a3581af1 moved network interface 2024-08-19 15:24:44 +02:00
5c1793c1e7 move files into client folder 2024-08-19 14:37:36 +02:00
211533d967 remove old PlayerInfo 2024-08-19 14:26:55 +02:00
7948e0ce3a remove unused springarmpivot 2024-08-19 14:25:29 +02:00
770e0281ef use GDREGISTER_CLASS
All checks were successful
Linux arm64 / Build (pull_request) Successful in 1m36s
2024-08-19 11:06:04 +02:00
15a385a825 better networking 2024-08-19 11:05:47 +02:00
665dc4938f add blitz files 2024-08-19 11:04:37 +02:00
ccb6870567 lock godot version 2024-08-19 11:00:26 +02:00
65808e9dee i guess it's working
All checks were successful
Linux arm64 / Build (pull_request) Successful in 1m21s
2024-08-17 17:18:13 +02:00
d261672f51 change node hierarchy 2024-08-17 14:32:53 +02:00
9449b125eb add icon
All checks were successful
Linux arm64 / Build (pull_request) Successful in 1m12s
2024-08-16 19:11:30 +02:00
3769fd3ace begin network 2024-08-16 19:00:45 +02:00
65e2a0b3ce Add main menu (#1)
All checks were successful
Linux arm64 / Build (push) Successful in 1m4s
Il est caché pour l'instant parce que c'est plus rapide à debug le reste pour le moment

Reviewed-on: #1
Co-authored-by: Persson-dev <sim16.prib@gmail.com>
Co-committed-by: Persson-dev <sim16.prib@gmail.com>
2024-08-16 11:13:54 +02:00
960ce2a546 fix firstpersson shadow movement
All checks were successful
Linux arm64 / Build (push) Successful in 1m1s
2024-08-14 15:02:15 +02:00
eb8e3b0888 add cool shadows
All checks were successful
Linux arm64 / Build (push) Successful in 1m3s
2024-08-14 14:31:13 +02:00
d0028dd0f8 fix cam rotation 2024-08-14 14:03:10 +02:00
ef0bcd0a35 format
All checks were successful
Linux arm64 / Build (push) Successful in 1m2s
2024-08-13 20:21:05 +02:00
841e25a3ac fix import warnings 2024-08-13 20:02:31 +02:00
a43269910d real FPS 2024-08-13 20:00:21 +02:00
083bf2f04d xmake things 2024-08-13 19:59:33 +02:00
436e9a27c5 Revert "add a testing camera fps (switching cameras by pressing t)"
This reverts commit 4707d85150.
2024-08-13 17:40:57 +02:00
Morph01
4707d85150 add a testing camera fps (switching cameras by pressing t)
All checks were successful
Linux arm64 / Build (push) Successful in 57s
2024-08-13 10:10:31 +02:00
d59047d0ae remove template project tag
All checks were successful
Linux arm64 / Build (push) Successful in 56s
2024-08-12 13:28:25 +02:00
e93afbeaf1 fix warnings 2024-08-12 11:51:33 +02:00
d9aba1924f third persson prototype 2024-08-12 11:45:42 +02:00
ba66b7b3b4 remove icon import file 2024-08-12 11:45:17 +02:00
26b62dbdd7 ignore import files 2024-08-12 11:45:00 +02:00
fb0a4fc7a5 remove gdexample 2024-08-12 11:44:52 +02:00
cdbe8c3114 add debug define 2024-08-12 11:43:02 +02:00
35708456d4 add clang format 2024-08-12 11:42:47 +02:00
78 changed files with 30016 additions and 164 deletions

37
.clang-format Normal file
View File

@@ -0,0 +1,37 @@
Language: Cpp
BasedOnStyle: LLVM
AlignAfterOpenBracket: DontAlign
BreakConstructorInitializers: AfterColon
ConstructorInitializerAllOnOneLineOrOnePerLine: true
PointerAlignment: Left
SortIncludes: true
SpacesBeforeTrailingComments: 2
UseTab: Always
MaxEmptyLinesToKeep: 5
TabWidth: 4
ConstructorInitializerIndentWidth: 4
ContinuationIndentWidth: 4
IndentWidth: 4
IndentCaseLabels: true
ColumnLimit: 135
AlwaysBreakTemplateDeclarations: Yes
AllowShortFunctionsOnASingleLine: Empty
BreakBeforeBraces: Custom
BraceWrapping:
AfterClass: false
AfterControlStatement: false
AfterEnum: false
AfterExternBlock: false
AfterFunction: false
AfterNamespace: false
AfterStruct: false
AfterUnion: false
BeforeCatch: false
BeforeElse: false
IndentBraces: false
SplitEmptyFunction: true
SplitEmptyRecord: true

1
.gitignore vendored
View File

@@ -14,6 +14,7 @@ build/
*.obj *.obj
*.lib *.lib
*.TMP *.TMP
*.import
godot/.godot godot/.godot
godot/lib godot/lib

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

File diff suppressed because one or more lines are too long

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,7 @@
[gd_scene load_steps=2 format=3 uid="uid://bqv0m8kbr300b"]
[ext_resource type="PackedScene" path="res://Scenes/Levels/world.tscn" id="1_ajsqk"]
[node name="World" type="ClientWorld"]
[node name="WorldContent" parent="." instance=ExtResource("1_ajsqk")]

View File

@@ -0,0 +1,7 @@
[gd_scene load_steps=2 format=3 uid="uid://c2p67anlxe3mk"]
[ext_resource type="PackedScene" path="res://Scenes/Levels/world.tscn" id="1_tecss"]
[node name="World" type="ServerWorld"]
[node name="WorldContent" parent="." instance=ExtResource("1_tecss")]

View File

@@ -0,0 +1,89 @@
[gd_scene load_steps=17 format=3 uid="uid://cl8gww414apoq"]
[ext_resource type="Texture2D" path="res://Assets/Textures/Sky.png" id="1_tcyn8"]
[ext_resource type="Texture2D" path="res://Assets/Textures/Black.png" id="2_j33w8"]
[ext_resource type="Texture2D" path="res://Assets/Textures/Orange.png" id="3_n1lus"]
[ext_resource type="Texture2D" path="res://Assets/Textures/Green.png" id="4_klpsf"]
[sub_resource type="PanoramaSkyMaterial" id="PanoramaSkyMaterial_6c4vd"]
panorama = ExtResource("1_tcyn8")
[sub_resource type="Sky" id="Sky_5ngqa"]
sky_material = SubResource("PanoramaSkyMaterial_6c4vd")
[sub_resource type="Environment" id="Environment_ctwiv"]
background_mode = 2
sky = SubResource("Sky_5ngqa")
tonemap_mode = 2
glow_enabled = true
[sub_resource type="StandardMaterial3D" id="StandardMaterial3D_ajchh"]
albedo_texture = ExtResource("2_j33w8")
uv1_triplanar = true
[sub_resource type="PlaneMesh" id="PlaneMesh_mmup0"]
material = SubResource("StandardMaterial3D_ajchh")
size = Vector2(50, 50)
[sub_resource type="ConcavePolygonShape3D" id="ConcavePolygonShape3D_26ptr"]
data = PackedVector3Array(25, 0, 25, -25, 0, 25, 25, 0, -25, -25, 0, 25, -25, 0, -25, 25, 0, -25)
[sub_resource type="StandardMaterial3D" id="StandardMaterial3D_jkvud"]
albedo_texture = ExtResource("3_n1lus")
uv1_triplanar = true
[sub_resource type="BoxMesh" id="BoxMesh_plpqy"]
material = SubResource("StandardMaterial3D_jkvud")
size = Vector3(10, 3, 1)
[sub_resource type="ConcavePolygonShape3D" id="ConcavePolygonShape3D_v7prx"]
data = PackedVector3Array(-5, 1.5, 0.5, 5, 1.5, 0.5, -5, -1.5, 0.5, 5, 1.5, 0.5, 5, -1.5, 0.5, -5, -1.5, 0.5, 5, 1.5, -0.5, -5, 1.5, -0.5, 5, -1.5, -0.5, -5, 1.5, -0.5, -5, -1.5, -0.5, 5, -1.5, -0.5, 5, 1.5, 0.5, 5, 1.5, -0.5, 5, -1.5, 0.5, 5, 1.5, -0.5, 5, -1.5, -0.5, 5, -1.5, 0.5, -5, 1.5, -0.5, -5, 1.5, 0.5, -5, -1.5, -0.5, -5, 1.5, 0.5, -5, -1.5, 0.5, -5, -1.5, -0.5, 5, 1.5, 0.5, -5, 1.5, 0.5, 5, 1.5, -0.5, -5, 1.5, 0.5, -5, 1.5, -0.5, 5, 1.5, -0.5, -5, -1.5, 0.5, 5, -1.5, 0.5, -5, -1.5, -0.5, 5, -1.5, 0.5, 5, -1.5, -0.5, -5, -1.5, -0.5)
[sub_resource type="PrismMesh" id="PrismMesh_0l7qq"]
left_to_right = -2.0
size = Vector3(5, 5, 5)
[sub_resource type="StandardMaterial3D" id="StandardMaterial3D_pfpgv"]
albedo_texture = ExtResource("4_klpsf")
uv1_triplanar = true
[sub_resource type="ConcavePolygonShape3D" id="ConcavePolygonShape3D_rit6o"]
data = PackedVector3Array(-12.5, 2.5, 2.5, 2.5, -2.5, 2.5, -2.5, -2.5, 2.5, -12.5, 2.5, -2.5, -2.5, -2.5, -2.5, 2.5, -2.5, -2.5, -12.5, 2.5, 2.5, -12.5, 2.5, -2.5, 2.5, -2.5, 2.5, -12.5, 2.5, -2.5, 2.5, -2.5, -2.5, 2.5, -2.5, 2.5, -12.5, 2.5, -2.5, -12.5, 2.5, 2.5, -2.5, -2.5, -2.5, -12.5, 2.5, 2.5, -2.5, -2.5, 2.5, -2.5, -2.5, -2.5, -2.5, -2.5, 2.5, 2.5, -2.5, 2.5, -2.5, -2.5, -2.5, 2.5, -2.5, 2.5, 2.5, -2.5, -2.5, -2.5, -2.5, -2.5)
[node name="World" type="Node3D"]
[node name="WorldEnvironment" type="WorldEnvironment" parent="."]
environment = SubResource("Environment_ctwiv")
[node name="DirectionalLight3D" type="DirectionalLight3D" parent="."]
transform = Transform3D(-0.866026, -0.433013, 0.249999, 0.5, -0.75, 0.433012, -1.3411e-07, 0.499999, 0.866026, 0, 0, 0)
shadow_enabled = true
[node name="Floor" type="MeshInstance3D" parent="."]
mesh = SubResource("PlaneMesh_mmup0")
[node name="StaticBody3D" type="StaticBody3D" parent="Floor"]
[node name="CollisionShape3D" type="CollisionShape3D" parent="Floor/StaticBody3D"]
shape = SubResource("ConcavePolygonShape3D_26ptr")
[node name="Wall" type="MeshInstance3D" parent="."]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 1.5, -3)
mesh = SubResource("BoxMesh_plpqy")
[node name="StaticBody3D" type="StaticBody3D" parent="Wall"]
[node name="CollisionShape3D" type="CollisionShape3D" parent="Wall/StaticBody3D"]
shape = SubResource("ConcavePolygonShape3D_v7prx")
[node name="Slope" type="MeshInstance3D" parent="."]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -2, 1, 4)
mesh = SubResource("PrismMesh_0l7qq")
surface_material_override/0 = SubResource("StandardMaterial3D_pfpgv")
[node name="StaticBody3D" type="StaticBody3D" parent="Slope"]
[node name="CollisionShape3D" type="CollisionShape3D" parent="Slope/StaticBody3D"]
shape = SubResource("ConcavePolygonShape3D_rit6o")
[node name="Players" type="Node" parent="."]

View File

@@ -0,0 +1,41 @@
[gd_scene format=3 uid="uid://bqfqg7xwwlxd8"]
[node name="MainMenu" type="MainMenu"]
anchors_preset = 8
anchor_left = 0.5
anchor_top = 0.5
anchor_right = 0.5
anchor_bottom = 0.5
grow_horizontal = 2
grow_vertical = 2
size_flags_horizontal = 4
size_flags_vertical = 4
[node name="Container" type="VBoxContainer" parent="."]
layout_mode = 1
anchors_preset = 8
anchor_left = 0.5
anchor_top = 0.5
anchor_right = 0.5
anchor_bottom = 0.5
offset_left = -228.512
offset_top = -89.5
offset_right = 228.512
offset_bottom = 89.5
grow_horizontal = 2
grow_vertical = 2
[node name="JoinButton" type="Button" parent="Container"]
layout_mode = 2
theme_override_font_sizes/font_size = 35
text = "Join Game"
[node name="CreateButton" type="Button" parent="Container"]
layout_mode = 2
theme_override_font_sizes/font_size = 35
text = "Create Game"
[node name="QuitButton" type="Button" parent="Container"]
layout_mode = 2
theme_override_font_sizes/font_size = 35
text = "Quit"

View File

@@ -0,0 +1,3 @@
[gd_scene format=3 uid="uid://clafls1xhludi"]
[node name="NetworkInterface" type="NetworkInterface"]

View File

@@ -0,0 +1,3 @@
[gd_scene format=3 uid="uid://us5sb4a0kq8d"]
[node name="Server" type="Server"]

10
godot/Scenes/main.tscn Normal file
View File

@@ -0,0 +1,10 @@
[gd_scene load_steps=3 format=3 uid="uid://4jt0v2b2l4rt"]
[ext_resource type="PackedScene" path="res://Scenes/Network/networking.tscn" id="1_06ibn"]
[ext_resource type="PackedScene" path="res://Scenes/Menus/mainmenu.tscn" id="2_lavg1"]
[node name="Main" type="Main"]
[node name="Network" parent="." instance=ExtResource("1_06ibn")]
[node name="MainMenu" parent="." instance=ExtResource("2_lavg1")]

View File

@@ -0,0 +1,63 @@
extends CharacterBody3D
const LERP_VALUE : float = 0.15
var snap_vector : Vector3 = Vector3.DOWN
var speed : float
@export_group("Movement variables")
@export var walk_speed : float = 2.0
@export var run_speed : float = 5.0
@export var jump_strength : float = 15.0
@export var gravity : float = 50.0
const ANIMATION_BLEND : float = 7.0
@onready var player_mesh : Node3D = $Mesh
@onready var spring_arm_pivot : Node3D = $SpringArmPivot
@onready var animator : AnimationTree = $AnimationTree
func _physics_process(delta):
var move_direction : Vector3 = Vector3.ZERO
move_direction.x = Input.get_action_strength("move_right") - Input.get_action_strength("move_left")
move_direction.z = Input.get_action_strength("move_backwards") - Input.get_action_strength("move_forwards")
move_direction = move_direction.rotated(Vector3.UP, spring_arm_pivot.rotation.y)
velocity.y -= gravity * delta
if Input.is_action_pressed("run"):
speed = run_speed
else:
speed = walk_speed
velocity.x = move_direction.x * speed
velocity.z = move_direction.z * speed
if move_direction:
player_mesh.rotation.y = lerp_angle(player_mesh.rotation.y, atan2(velocity.x, velocity.z), LERP_VALUE)
var just_landed := is_on_floor() and snap_vector == Vector3.ZERO
var is_jumping := is_on_floor() and Input.is_action_just_pressed("jump")
if is_jumping:
velocity.y = jump_strength
snap_vector = Vector3.ZERO
elif just_landed:
snap_vector = Vector3.DOWN
apply_floor_snap()
move_and_slide()
animate(delta)
func animate(delta):
if is_on_floor():
animator.set("parameters/ground_air_transition/transition_request", "grounded")
if velocity.length() > 0:
if speed == run_speed:
animator.set("parameters/iwr_blend/blend_amount", lerp(animator.get("parameters/iwr_blend/blend_amount"), 1.0, delta * ANIMATION_BLEND))
else:
animator.set("parameters/iwr_blend/blend_amount", lerp(animator.get("parameters/iwr_blend/blend_amount"), 0.0, delta * ANIMATION_BLEND))
else:
animator.set("parameters/iwr_blend/blend_amount", lerp(animator.get("parameters/iwr_blend/blend_amount"), -1.0, delta * ANIMATION_BLEND))
else:
animator.set("parameters/ground_air_transition/transition_request", "air")

View File

@@ -0,0 +1,30 @@
extends Node3D
@export_group("FOV")
@export var change_fov_on_run : bool
@export var normal_fov : float = 75.0
@export var run_fov : float = 90.0
const CAMERA_BLEND : float = 0.05
@onready var spring_arm : SpringArm3D = $SpringArm3D
@onready var camera : Camera3D = $SpringArm3D/Camera3D
func _ready():
Input.set_mouse_mode(Input.MOUSE_MODE_CAPTURED)
func _unhandled_input(event):
if event is InputEventMouseMotion:
rotate_y(-event.relative.x * 0.005)
spring_arm.rotate_x(-event.relative.y * 0.005)
spring_arm.rotation.x = clamp(spring_arm.rotation.x, -PI/4, PI/4)
func _physics_process(_delta):
if change_fov_on_run:
if owner.is_on_floor():
if Input.is_action_pressed("run"):
camera.fov = lerp(camera.fov, run_fov, CAMERA_BLEND)
else:
camera.fov = lerp(camera.fov, normal_fov, CAMERA_BLEND)
else:
camera.fov = lerp(camera.fov, normal_fov, CAMERA_BLEND)

View File

@@ -1,5 +1,5 @@
[icons] [icons]
Blitz3 = "res://icon.svg" Blitz3 = "res://icon.png"
[configuration] [configuration]

BIN
godot/icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 52 KiB

View File

@@ -1 +0,0 @@
<svg height="128" width="128" xmlns="http://www.w3.org/2000/svg"><rect x="2" y="2" width="124" height="124" rx="14" fill="#363d52" stroke="#212532" stroke-width="4"/><g transform="scale(.101) translate(122 122)"><g fill="#fff"><path d="M105 673v33q407 354 814 0v-33z"/><path d="m105 673 152 14q12 1 15 14l4 67 132 10 8-61q2-11 15-15h162q13 4 15 15l8 61 132-10 4-67q3-13 15-14l152-14V427q30-39 56-81-35-59-83-108-43 20-82 47-40-37-88-64 7-51 8-102-59-28-123-42-26 43-46 89-49-7-98 0-20-46-46-89-64 14-123 42 1 51 8 102-48 27-88 64-39-27-82-47-48 49-83 108 26 42 56 81zm0 33v39c0 276 813 276 814 0v-39l-134 12-5 69q-2 10-14 13l-162 11q-12 0-16-11l-10-65H446l-10 65q-4 11-16 11l-162-11q-12-3-14-13l-5-69z" fill="#478cbf"/><path d="M483 600c0 34 58 34 58 0v-86c0-34-58-34-58 0z"/><circle cx="725" cy="526" r="90"/><circle cx="299" cy="526" r="90"/></g><g fill="#414042"><circle cx="307" cy="532" r="60"/><circle cx="717" cy="532" r="60"/></g></g></svg>

Before

Width:  |  Height:  |  Size: 949 B

View File

@@ -1,37 +0,0 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://cuend5rtkhbnp"
path="res://.godot/imported/icon.svg-218a8f2b3041327d8a5756f3a245f83b.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://icon.svg"
dest_files=["res://.godot/imported/icon.svg-218a8f2b3041327d8a5756f3a245f83b.ctex"]
[params]
compress/mode=0
compress/high_quality=false
compress/lossy_quality=0.7
compress/hdr_compression=1
compress/normal_map=0
compress/channel_pack=0
mipmaps/generate=false
mipmaps/limit=-1
roughness/mode=0
roughness/src_normal=""
process/fix_alpha_border=true
process/premult_alpha=false
process/normal_map_invert_y=false
process/hdr_as_srgb=false
process/hdr_clamp_exposure=false
process/size_limit=0
detect_3d/compress_to=1
svg/scale=1.0
editor/scale_with_editor_scale=false
editor/convert_colors_with_editor_theme=false

View File

@@ -1,10 +0,0 @@
[gd_scene load_steps=2 format=3 uid="uid://uiwigxsdnci1"]
[ext_resource type="Texture2D" uid="uid://cuend5rtkhbnp" path="res://icon.svg" id="1_q81dj"]
[node name="Node2D" type="Node2D"]
[node name="GDExample" type="GDExample" parent="."]
position = Vector2(5.20978, 6.34048)
texture = ExtResource("1_q81dj")
centered = false

View File

@@ -11,6 +11,49 @@ config_version=5
[application] [application]
config/name="Blitz3" config/name="Blitz3"
run/main_scene="res://main.tscn" run/main_scene="res://Scenes/main.tscn"
config/features=PackedStringArray("4.2", "Forward Plus") config/features=PackedStringArray("4.2", "Forward Plus")
config/icon="res://icon.svg" config/icon="res://icon.png"
[display]
window/size/viewport_width=1920
window/size/viewport_height=1080
[input]
move_left={
"deadzone": 0.5,
"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)
]
}
move_right={
"deadzone": 0.5,
"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)
]
}
move_forwards={
"deadzone": 0.5,
"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)
]
}
move_backwards={
"deadzone": 0.5,
"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)
]
}
sprint={
"deadzone": 0.5,
"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":4194325,"key_label":0,"unicode":0,"echo":false,"script":null)
]
}
jump={
"deadzone": 0.5,
"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":32,"key_label":0,"unicode":32,"echo":false,"script":null)
]
}
escape={
"deadzone": 0.5,
"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":0,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":4194305,"key_label":0,"unicode":0,"echo":false,"script":null)
]
}

View File

@@ -0,0 +1,25 @@
#pragma once
/**
* \file NonCopyable.h
* \brief File containing the blitz::NonCopyable class
*/
namespace blitz {
/**
* \class NonCopyable
* \brief Class used to make a class non copyable
* \note Inherit from this class privately to make a class non copyable
*/
class NonCopyable {
public:
NonCopyable(const NonCopyable&) = delete;
NonCopyable& operator=(const NonCopyable&) = delete;
protected:
NonCopyable() {}
~NonCopyable() {}
};
} // namespace blitz

View File

@@ -0,0 +1,11 @@
#pragma once
#include <cstdint>
namespace blitz {
using EntityID = std::uint64_t;
using PeerID = std::int32_t;
using PlayerID = PeerID;
} // namespace blitz

View File

@@ -0,0 +1,40 @@
#pragma once
#include <blitz/protocol/PacketDispatcher.h>
#include <blitz/protocol/Packets.h>
#include <godot_cpp/classes/node.hpp>
namespace blitz {
class NetworkInterface : public godot::Node, public protocol::PacketDispatcher {
GDCLASS(NetworkInterface, godot::Node)
protected:
static void _bind_methods();
public:
NetworkInterface();
~NetworkInterface();
void BroadcastPacket(const protocol::Packet& a_Packet);
void SendPacket(PeerID a_Peer, const protocol::Packet& a_Packet);
godot::Error JoinGame(const godot::String& a_Address, uint16_t a_Port);
godot::Error CreateGame(uint16_t a_Port, bool a_Dedicated = false);
void ShutdownNetwork();
void _ready() override;
private:
void RecievePacketDataReliable(godot::PackedByteArray a_PacketData);
void RecievePacketDataUnreliable(godot::PackedByteArray a_PacketData);
void RecievePacketDataUnreliableOrdered(godot::PackedByteArray a_PacketData);
void OnPlayerConnected(PeerID a_PeerId);
void OnPlayerDisconnected(PeerID a_PeerId);
void OnConnectOk();
void OnConnectFail();
void OnServerDisconnected();
};
} // namespace blitz

View File

@@ -0,0 +1,40 @@
#pragma once
#include <godot_cpp/classes/node3d.hpp>
#include <blitz/protocol/PacketHandler.h>
namespace blitz {
class Player;
class NetworkInterface;
class World : public godot::Node3D, public protocol::PacketHandler {
GDCLASS(World, godot::Node3D)
protected:
static void _bind_methods();
World();
~World();
public:
// Godot overrides
void _ready() override;
Player* GetPlayerById(PlayerID a_PlayerId);
void HandlePacket(const protocol::packets::PlayerJoin&) override;
void HandlePacket(const protocol::packets::PlayerLeave&) override;
protected:
NetworkInterface* m_NetworkInterface;
godot::Node* m_Players;
float m_PassedTime;
virtual void AddPlayer(PlayerID a_PlayerId, godot::String a_PlayerName);
virtual void RemovePlayer(PlayerID a_PlayerId);
virtual void SetPlayerPositionAndRotation(
PlayerID a_PlayerId, const godot::Vector3& a_Position, const godot::Vector3& a_Rotation, const godot::Vector3& a_Velocity);
};
} // namespace blitz

View File

@@ -0,0 +1,94 @@
#pragma once
#include <godot_cpp/variant/packed_byte_array.hpp>
#include <godot_cpp/variant/string.hpp>
#include <stdexcept>
#include <vector>
namespace blitz {
namespace protocol {
class PlayerInfo;
class ByteBuffer {
private:
godot::PackedByteArray m_Buffer;
std::size_t m_ReadOffset;
public:
class ReadError : public std::runtime_error {
public:
ReadError(const std::string& msg) : std::runtime_error(msg) {}
};
ByteBuffer(godot::PackedByteArray&& a_Buffer) : m_Buffer(std::move(a_Buffer)), m_ReadOffset(0) {}
ByteBuffer() : m_ReadOffset(0) {
m_Buffer.resize(0);
}
const godot::PackedByteArray& GetByteArray() const {
return m_Buffer;
}
godot::PackedByteArray& GetByteArray() {
return m_Buffer;
}
// Integers
ByteBuffer& operator<<(int8_t a_Data);
ByteBuffer& operator>>(int8_t& a_Data);
ByteBuffer& operator<<(uint8_t a_Data);
ByteBuffer& operator>>(uint8_t& a_Data);
ByteBuffer& operator<<(int16_t a_Data);
ByteBuffer& operator>>(int16_t& a_Data);
ByteBuffer& operator<<(uint16_t a_Data);
ByteBuffer& operator>>(uint16_t& a_Data);
ByteBuffer& operator<<(int32_t a_Data);
ByteBuffer& operator>>(int32_t& a_Data);
ByteBuffer& operator<<(uint32_t a_Data);
ByteBuffer& operator>>(uint32_t& a_Data);
ByteBuffer& operator<<(int64_t a_Data);
ByteBuffer& operator>>(int64_t& a_Data);
ByteBuffer& operator<<(uint64_t a_Data);
ByteBuffer& operator>>(uint64_t& a_Data);
ByteBuffer& operator<<(float a_Data);
ByteBuffer& operator>>(float& a_Data);
ByteBuffer& operator<<(double a_Data);
ByteBuffer& operator>>(double& a_Data);
ByteBuffer& operator<<(const godot::String& a_Data);
ByteBuffer& operator>>(godot::String& a_Data);
ByteBuffer& operator<<(const godot::Vector3& a_Data);
ByteBuffer& operator>>(godot::Vector3& a_Data);
template <typename T>
ByteBuffer& operator<<(const std::vector<T>& a_Data) {
*this << static_cast<std::uint32_t>(a_Data.size());
for (const T& data : a_Data) {
*this << data;
}
return *this;
}
template <typename T>
ByteBuffer& operator>>(std::vector<T>& a_Data) {
std::uint32_t arraySize;
*this >> arraySize;
a_Data.resize(arraySize);
for (std::uint32_t i = 0; i < arraySize; i++) {
*this >> a_Data[i];
}
return *this;
}
ByteBuffer& operator<<(const PlayerInfo& a_Data);
ByteBuffer& operator>>(PlayerInfo& a_Data);
};
} // namespace protocol
} // namespace blitz

View File

@@ -0,0 +1,73 @@
#pragma once
#include <blitz/common/Types.h>
#include <vector>
#include <godot_cpp/variant/string.hpp>
#include <godot_cpp/variant/vector3.hpp>
namespace blitz {
namespace protocol {
struct PlayerInfo {
PlayerID m_PlayerId;
godot::String m_PlayerName;
};
namespace data {
struct PlayerLogin {
godot::String m_PlayerName;
};
struct UpdateHealth {
float m_NewHealth;
};
struct LoggingSuccess {
PlayerID m_PlayerId;
};
struct PlayerDeath {};
struct PlayerJoin {
PlayerInfo m_Player;
};
struct PlayerLeave {
PlayerID m_PlayerId;
};
struct PlayerStats {};
struct PlayerList {
std::vector<PlayerInfo> m_Players;
};
struct ServerConfig {};
struct ServerTps {};
struct UpdateGameState {};
struct KeepAlive {
std::uint64_t m_KeepAliveId;
};
struct Disconnect {};
struct ChatMessage {
godot::String m_Text;
};
struct PlayerPositionAndRotation {
PlayerID m_Player;
godot::Vector3 m_Position;
godot::Vector3 m_Rotation;
godot::Vector3 m_Velocity;
};
struct PlayerShoot {};
} // namespace data
} // namespace protocol
} // namespace blitz

View File

@@ -0,0 +1,55 @@
#pragma once
/**
* \file PacketDeclare.h
* \brief Holds the definitions of the packets (but not their content)
*/
namespace blitz {
namespace protocol {
/**
* \enum PacketSender
* \brief Indicate who should send a packet
*/
enum class PacketSenderType {
/** Sent by clients and server */
Both = 1,
/** Sent by clients to the server */
Client,
/** Sent by server to the clients */
Server,
};
enum class PacketSendType {
Reliable = 1,
Unreliable,
UnreliableOrdered,
};
/**
* \def DeclareAllPacket
* \brief Avoids repetitive operations on packets
*/
#define DeclareAllPacket() \
DeclarePacket(ChatMessage, Reliable, Both) \
DeclarePacket(Disconnect, Reliable, Both) \
DeclarePacket(KeepAlive, Reliable, Both) \
DeclarePacket(LoggingSuccess, Reliable, Server) \
DeclarePacket(PlayerDeath, Reliable, Server) \
DeclarePacket(PlayerJoin, Reliable, Server) \
DeclarePacket(PlayerLeave, Reliable, Server) \
DeclarePacket(PlayerList, Reliable, Server) \
DeclarePacket(PlayerLogin, Reliable, Client) \
DeclarePacket(PlayerPositionAndRotation, Unreliable, Both) \
DeclarePacket(PlayerShoot, Reliable, Both) \
DeclarePacket(PlayerStats, Reliable, Server) \
DeclarePacket(ServerConfig, Reliable, Server) \
DeclarePacket(ServerTps, Reliable, Server) \
DeclarePacket(UpdateGameState, Reliable, Server) \
DeclarePacket(UpdateHealth, Reliable, Client)
} // namespace protocol
} // namespace blitz

View File

@@ -0,0 +1,58 @@
#pragma once
/**
* \file PacketDispatcher.h
* \brief File containing the blitz::protocol::PacketDispatcher class
*/
#include <blitz/common/NonCopyable.h>
#include <blitz/protocol/Packets.h>
#include <map>
namespace blitz {
namespace protocol {
class PacketHandler;
/**
* \class PacketDispatcher
* \brief Class used to dispatch packets
*/
class PacketDispatcher : private NonCopyable {
private:
std::map<PacketType, std::vector<PacketHandler*>> m_Handlers;
public:
/**
* \brief Constructor
*/
PacketDispatcher() {}
/**
* \brief Dispatch a packet
* \param packet The packet to dispatch
*/
void Dispatch(const Packet& packet);
/**
* \brief Register a packet handler
* \param type The packet type
* \param handler The packet handler
*/
void RegisterHandler(PacketType type, PacketHandler& handler);
/**
* \brief Unregister a packet handler
* \param type The packet type
* \param handler The packet handler
*/
void UnregisterHandler(PacketType type, PacketHandler& handler);
/**
* \brief Unregister a packet handler
* \param handler The packet handler
*/
void UnregisterHandler(PacketHandler& handler);
};
} // namespace protocol
} // namespace blitz

View File

@@ -0,0 +1,21 @@
#pragma once
#include <blitz/protocol/Packets.h>
#include <memory>
namespace blitz {
namespace protocol {
namespace PacketFactory {
template<typename PacketDerived, typename = typename std::enable_if<std::is_base_of<Packet, PacketDerived>::value>::type>
std::unique_ptr<PacketDerived> CreatePacket() {
return std::make_unique<PacketDerived>();
}
const std::unique_ptr<Packet>& CreateReadOnlyPacket(PacketType a_Type);
void Init();
} // namespace PacketFactory
} // namespace protocol
} // namespace blitz

View File

@@ -0,0 +1,34 @@
#pragma once
/**
* \file PacketHandler.h
* \brief File containing the blitz::protocol::PacketHandler class
*/
#include <blitz/protocol/Packets.h>
#include <blitz/protocol/PacketVisitor.h>
namespace blitz {
namespace protocol {
class PacketDispatcher;
#define DeclarePacket(PacketName, ...) virtual void Visit(const packets::PacketName&); virtual void HandlePacket(const packets::PacketName&) {}
/**
* \class PacketHandler
* \brief Class used to handle packets
*/
class PacketHandler : public PacketVisitor {
public:
PacketHandler() {}
~PacketHandler() {}
DeclareAllPacket()
};
#undef DeclarePacket
} // namespace protocol
} // namespace blitz

View File

@@ -0,0 +1,60 @@
#pragma once
#include <blitz/protocol/PacketVisitor.h>
namespace blitz {
class NetworkInterface;
namespace protocol {
#define DeclarePacket(PacketName, Reliability, ...) void Visit(const protocol::packets::PacketName& a_Packet) override;
///////////////////////
/* PacketBroadcaster */
///////////////////////
class PacketBroadcaster : public protocol::PacketVisitor {
private:
NetworkInterface& m_NetworkInterface;
public:
PacketBroadcaster(NetworkInterface& a_NetworkInterface) : m_NetworkInterface(a_NetworkInterface) {}
void BroadcastPacket(const protocol::Packet& a_Packet) {
Check(a_Packet);
}
DeclareAllPacket()
};
//////////////////
/* PacketSender */
//////////////////
class PacketSender : public protocol::PacketVisitor {
private:
NetworkInterface& m_NetworkInterface;
PeerID m_PeerId;
public:
PacketSender(PeerID a_PeerId, NetworkInterface& a_NetworkInterface) : m_PeerId(a_PeerId), m_NetworkInterface(a_NetworkInterface) {}
void SendPacket(const protocol::Packet& a_Packet) {
Check(a_Packet);
}
DeclareAllPacket()
};
#undef DeclarePacket
} // namespace protocol
} // namespace blitz

View File

@@ -0,0 +1,20 @@
#pragma once
#include <blitz/protocol/Packets.h>
#include <godot_cpp/variant/packed_byte_array.hpp>
#include <memory>
namespace blitz {
namespace protocol {
using PacketPtr = std::unique_ptr<Packet>;
namespace PacketSerializer {
godot::PackedByteArray Serialize(const Packet& a_Packet);
std::unique_ptr<Packet> Deserialize(godot::PackedByteArray& a_Data);
}
} // namespace protocol
} // namespace blitz

View File

@@ -0,0 +1,39 @@
#pragma once
/**
* \file PacketVisitor.h
* \brief File containing the blitz::protocol::PacketVisitor class
*/
#include <blitz/protocol/Packets.h>
namespace blitz {
namespace protocol {
#define DeclarePacket(PacketName, ...) \
/** This function is called when the packet processed by PacketVisitor::Check is a PacketName */ \
virtual void Visit(const packets::PacketName&) {}
/**
* \class PacketVisitor
* \brief This class uses double-dispatch in order to find the real type of a packet
*/
class PacketVisitor : private NonCopyable {
protected:
PacketVisitor() {}
virtual ~PacketVisitor() {}
public:
/**
* \brief Calls the right PacketVisitor::Visit method corresponding to the real type of the packet
* \param packet the Packet to visit
*/
void Check(const Packet& packet);
DeclareAllPacket()
};
#undef DeclarePacket
} // namespace protocol
} // namespace blitz

View File

@@ -0,0 +1,112 @@
#pragma once
/**
* \file Packets.h
* \brief File containing the definitions of the packets
*/
#include <blitz/common/NonCopyable.h>
#include <blitz/protocol/PacketData.h>
#include <blitz/protocol/PacketDeclare.h>
namespace blitz {
namespace protocol {
class PacketVisitor;
/** A Packet id is 8 bits wide */
using PacketID = std::uint8_t;
#define DeclarePacket(PacketName, ...) /** PacketName */ PacketName,
/**
* \enum PacketType
* \brief Map a Packet to an id
*/
enum class PacketType : PacketID {
DeclareAllPacket()
/** The number of packets */
PACKET_COUNT
};
#undef DeclarePacket
class Packet : private NonCopyable {
public:
/**
* \return The real type of the packet
*/
virtual PacketType GetType() const = 0;
/**
* \brief The network peer who sent the packet
*/
PeerID m_Sender;
private:
/** Use a PacketVisitor to make double-dispatch possible */
virtual void Accept(PacketVisitor& a_Visitor) const = 0;
friend class PacketVisitor;
};
namespace packets {
/**
* \class ConcretePacket
* \brief A Packet associated with an id and holding data
* \tparam PT The packet type
* \tparam Data The structure holding the data of the packet (in blitz::protocol::data namespace)
*/
template <PacketType PT, typename Data>
class ConcretePacket : public Packet {
public:
/** The type of the struct holding the data */
using PacketDataType = Data;
/** The structure holding the actual data */
PacketDataType m_Data;
/** Construct the packet with data of type PacketDataType */
ConcretePacket(const PacketDataType& a_Data = {});
constexpr PacketType GetType() const override {
return PT;
};
private:
void Accept(PacketVisitor& a_Visitor) const override;
};
// define BLITZ_INSTANCIATE_PACKETS
// before including this file
// if you want to instantiate templates
#ifdef BLITZ_INSTANCIATE_PACKETS
#define DeclarePacket(PacketName, ...) \
using PacketName = ConcretePacket<PacketType::PacketName, data::PacketName>; \
template class ConcretePacket<PacketType::PacketName, data::PacketName>;
#else
#define DeclarePacket(PacketName, ...) /** Defines the PacketName packet */ \
using PacketName = ConcretePacket<PacketType::PacketName, data::PacketName>;
#endif
DeclareAllPacket()
#undef DeclarePacket
} // namespace packets
} // namespace protocol
} // namespace blitz

View File

@@ -0,0 +1,21 @@
#include <blitz/godot/World.h>
namespace blitz {
class ClientWorld : public World {
GDCLASS(ClientWorld, World)
protected:
static void _bind_methods();
public:
ClientWorld();
~ClientWorld();
void _process(float delta);
void HandlePacket(const protocol::packets::PlayerPositionAndRotation&) override;
private:
void UpdatePlayerPos();
};
} // namespace blitz

View File

@@ -0,0 +1,36 @@
#pragma once
#include <client/Player.h>
#include <godot_cpp/classes/input_event_mouse_motion.hpp>
namespace blitz {
class FirstPersonPlayer : public Player {
GDCLASS(FirstPersonPlayer, godot::CharacterBody3D)
protected:
static void _bind_methods();
public:
FirstPersonPlayer();
~FirstPersonPlayer();
// Godot overrides
void _unhandled_input(const godot::Ref<godot::InputEvent>&);
void _physics_process(float delta) override;
void _ready() override;
private:
godot::Camera3D* m_Camera;
godot::Node3D* m_Head;
float m_BobTime;
float m_Speed;
void UpdateBobbing(float delta);
void UpdateFOV(float delta);
void UpdateCamera(const godot::InputEventMouseMotion&);
void UpdatePosition(float delta);
void UpdateAnimation(float delta);
};
} // namespace blitz

21
include/client/Main.h Normal file
View File

@@ -0,0 +1,21 @@
#pragma once
#include <godot_cpp/classes/node.hpp>
namespace blitz {
class Main : public godot::Node {
GDCLASS(Main, godot::Node)
protected:
static void _bind_methods();
public:
Main();
~Main();
void _ready() override;
void ChangeScene(bool a_Server);
};
} // namespace blitz

39
include/client/MainMenu.h Normal file
View File

@@ -0,0 +1,39 @@
#pragma once
#include <godot_cpp/classes/button.hpp>
#include <godot_cpp/classes/control.hpp>
#include <blitz/godot/NetworkInterface.h>
namespace blitz {
class MainMenu : public godot::Control {
GDCLASS(MainMenu, godot::Control)
protected:
static void _bind_methods();
public:
MainMenu();
~MainMenu();
// Godot overrides
void _ready() override;
private:
godot::Button* m_JoinButton;
godot::Button* m_CreateButton;
godot::Button* m_QuitButton;
NetworkInterface* m_NetworkInterface;
void OnConnected();
void OnDisconnected();
void OnJoinPressed();
void OnCreatePressed();
void OnQuitPressed();
void DisableButtons();
void EnableButtons();
};
} // namespace blitz

43
include/client/Player.h Normal file
View File

@@ -0,0 +1,43 @@
#pragma once
#include <godot_cpp/classes/animation_tree.hpp>
#include <godot_cpp/classes/character_body3d.hpp>
#include <godot_cpp/classes/node3d.hpp>
#include <blitz/common/Types.h>
namespace blitz {
class World;
class Player : public godot::CharacterBody3D {
GDCLASS(Player, godot::CharacterBody3D);
protected:
static void _bind_methods();
public:
Player();
~Player();
void _ready() override;
virtual void _physics_process(float delta);
void animate(float delta);
godot::Vector3 GetCameraRotation() const;
void SetCameraRotation(const godot::Vector3& a_Rotation);
PlayerID GetId() const {
return m_PeerId;
}
protected:
godot::Node3D* m_Mesh;
godot::AnimationTree* m_AnimationTree;
godot::Vector3 m_SnapVector;
PeerID m_PeerId;
friend class World;
};
} // namespace blitz

32
include/server/Server.h Normal file
View File

@@ -0,0 +1,32 @@
#pragma once
#include <blitz/common/Types.h>
#include <godot_cpp/classes/node.hpp>
namespace blitz {
class Lobby;
class NetworkInterface;
class Server : public godot::Node {
GDCLASS(Server, godot::Node)
protected:
static void _bind_methods();
public:
Server();
~Server();
void _ready() override;
void OnPlayerConnect(PeerID a_PeerId);
void OnPlayerDisconnect(PeerID a_PeerId);
private:
Lobby* m_Lobby;
NetworkInterface* m_NetworkInterface;
godot::TypedArray<PeerID> m_Peers;
};
} // namespace blitz

View File

@@ -0,0 +1,23 @@
#include <blitz/godot/World.h>
namespace blitz {
class ServerWorld : public World {
GDCLASS(ServerWorld, World)
protected:
static void _bind_methods();
public:
ServerWorld();
~ServerWorld();
void _process(float delta);
void HandlePacket(const protocol::packets::PlayerPositionAndRotation&) override;
void SyncPlayersPos();
protected:
virtual void AddPlayer(PlayerID a_PlayerId, godot::String a_PlayerName);
};
} // namespace blitz

View File

@@ -0,0 +1,160 @@
#include <blitz/godot/NetworkInterface.h>
#include <blitz/protocol/PacketFactory.h>
#include <blitz/protocol/PacketSender.h>
#include <blitz/protocol/PacketSerializer.h>
#include <blitz/protocol/PacketVisitor.h>
#include <godot_cpp/classes/e_net_multiplayer_peer.hpp>
#include <godot_cpp/classes/multiplayer_api.hpp>
#include <godot_cpp/classes/packed_scene.hpp>
#include <godot_cpp/classes/resource_loader.hpp>
#include <godot_cpp/variant/utility_functions.hpp>
namespace blitz {
#define RPC_CONFIG(functionName, rpc_mode, transfer_mode, call_local, channel) \
{ \
Dictionary config; \
config["rpc_mode"] = rpc_mode; \
config["transfer_mode"] = transfer_mode; \
config["call_local"] = call_local; \
config["channel"] = channel; \
rpc_config(functionName, config); \
}
static const char ServerScenePath[] = "res://Scenes/Network/server.tscn";
using namespace godot;
void NetworkInterface::_bind_methods() {
ClassDB::bind_method(D_METHOD("RecievePacketDataReliable", "a_PacketData"), &NetworkInterface::RecievePacketDataReliable);
ClassDB::bind_method(D_METHOD("RecievePacketDataUnreliable", "a_PacketData"), &NetworkInterface::RecievePacketDataUnreliable);
ClassDB::bind_method(
D_METHOD("RecievePacketDataUnreliableOrdered", "a_PacketData"), &NetworkInterface::RecievePacketDataUnreliableOrdered);
// server
ADD_SIGNAL(MethodInfo("player_connected", PropertyInfo(Variant::INT, "peer_id")));
ADD_SIGNAL(MethodInfo("player_disconnected", PropertyInfo(Variant::INT, "peer_id")));
// client
ADD_SIGNAL(MethodInfo("server_disconnected"));
ADD_SIGNAL(MethodInfo("local_player_connected"));
protocol::PacketFactory::Init();
}
NetworkInterface::NetworkInterface() {}
NetworkInterface::~NetworkInterface() {}
void NetworkInterface::_ready() {
RPC_CONFIG("RecievePacketDataReliable", MultiplayerAPI::RPC_MODE_ANY_PEER, MultiplayerPeer::TRANSFER_MODE_RELIABLE, true, 0);
RPC_CONFIG("RecievePacketDataUnreliable", MultiplayerAPI::RPC_MODE_ANY_PEER, MultiplayerPeer::TRANSFER_MODE_UNRELIABLE, true, 1);
RPC_CONFIG("RecievePacketDataUnreliableOrdered", MultiplayerAPI::RPC_MODE_ANY_PEER,
MultiplayerPeer::TRANSFER_MODE_UNRELIABLE_ORDERED, true, 2);
get_multiplayer()->connect("peer_connected", callable_mp(this, &NetworkInterface::OnPlayerConnected));
get_multiplayer()->connect("peer_disconnected", callable_mp(this, &NetworkInterface::OnPlayerDisconnected));
get_multiplayer()->connect("connected_to_server", callable_mp(this, &NetworkInterface::OnConnectOk));
get_multiplayer()->connect("connection_failed", callable_mp(this, &NetworkInterface::OnConnectFail));
get_multiplayer()->connect("server_disconnected", callable_mp(this, &NetworkInterface::OnServerDisconnected));
}
void NetworkInterface::BroadcastPacket(const protocol::Packet& a_Packet) {
protocol::PacketBroadcaster packetBroadcaster(*this);
packetBroadcaster.BroadcastPacket(a_Packet);
}
void NetworkInterface::SendPacket(PeerID a_Peer, const protocol::Packet& a_Packet) {
protocol::PacketSender packetSender(a_Peer, *this);
packetSender.SendPacket(a_Packet);
}
void NetworkInterface::RecievePacketDataReliable(godot::PackedByteArray a_PacketData) {
auto packet = protocol::PacketSerializer::Deserialize(a_PacketData);
if (packet) {
packet->m_Sender = get_multiplayer()->get_remote_sender_id();
Dispatch(*packet);
}
}
void NetworkInterface::RecievePacketDataUnreliable(godot::PackedByteArray a_PacketData) {
// we have to copy the function body in order to preserve the remote sender id
auto packet = protocol::PacketSerializer::Deserialize(a_PacketData);
if (packet) {
packet->m_Sender = get_multiplayer()->get_remote_sender_id();
Dispatch(*packet);
}
}
void NetworkInterface::RecievePacketDataUnreliableOrdered(godot::PackedByteArray a_PacketData) {
// we have to copy the function body in order to preserve the remote sender id
auto packet = protocol::PacketSerializer::Deserialize(a_PacketData);
if (packet) {
packet->m_Sender = get_multiplayer()->get_remote_sender_id();
Dispatch(*packet);
}
}
Error NetworkInterface::JoinGame(const String& a_Address, uint16_t a_Port) {
auto* peer = memnew(ENetMultiplayerPeer);
Error error = peer->create_client(a_Address, a_Port, 3);
if (error) {
OnConnectFail();
return error;
}
get_multiplayer()->set_multiplayer_peer(peer);
return Error::OK;
}
Error NetworkInterface::CreateGame(uint16_t a_Port, bool a_Dedicated) {
auto* peer = memnew(ENetMultiplayerPeer);
Error error = peer->create_server(a_Port, 50, 3);
if (error)
return error;
get_multiplayer()->set_multiplayer_peer(peer);
Ref<PackedScene> serverScene = ResourceLoader::get_singleton()->load(ServerScenePath);
add_child(serverScene->instantiate());
if (!a_Dedicated) {
emit_signal("local_player_connected");
emit_signal("player_connected", get_multiplayer()->get_unique_id());
}
return Error::OK;
}
void NetworkInterface::OnPlayerConnected(PeerID a_PeerId) {
emit_signal("player_connected", a_PeerId);
}
void NetworkInterface::OnPlayerDisconnected(PeerID a_PeerId) {
emit_signal("player_disconnected", a_PeerId);
}
void NetworkInterface::OnConnectOk() {
emit_signal("local_player_connected");
}
void NetworkInterface::OnConnectFail() {
ShutdownNetwork();
emit_signal("server_disconnected");
}
void NetworkInterface::OnServerDisconnected() {
ShutdownNetwork();
emit_signal("server_disconnected");
}
void NetworkInterface::ShutdownNetwork() {
get_multiplayer()->set_multiplayer_peer(nullptr);
if (auto* server = find_child("Server")) {
server->queue_free();
}
}
} // namespace blitz

102
src/blitz/godot/World.cpp Normal file
View File

@@ -0,0 +1,102 @@
#include <blitz/godot/World.h>
#include <blitz/godot/NetworkInterface.h>
#include <client/FirstPersonPlayer.h>
#include <client/Player.h>
#include <godot_cpp/classes/engine.hpp>
#include <godot_cpp/classes/multiplayer_api.hpp>
#include <godot_cpp/classes/packed_scene.hpp>
#include <godot_cpp/classes/resource_loader.hpp>
#include <godot_cpp/variant/utility_functions.hpp>
using namespace godot;
namespace blitz {
static const char FirstPersonPlayerScenePath[] = "res://Scenes/Characters/first_person_player.tscn";
static const char PlayerScenePath[] = "res://Scenes/Characters/player.tscn";
void World::_bind_methods() {}
void World::_ready() {
if (Engine::get_singleton()->is_editor_hint())
return;
m_Players = find_child("Players");
DEV_ASSERT(m_Players);
m_NetworkInterface = Object::cast_to<NetworkInterface>(get_parent()->find_child("Network"));
DEV_ASSERT(m_NetworkInterface);
m_NetworkInterface->RegisterHandler(protocol::PacketType::PlayerJoin, *this);
m_NetworkInterface->RegisterHandler(protocol::PacketType::PlayerLeave, *this);
m_NetworkInterface->RegisterHandler(protocol::PacketType::PlayerPositionAndRotation, *this);
}
World::World() {}
World::~World() {
if (Engine::get_singleton()->is_editor_hint())
return;
m_NetworkInterface->UnregisterHandler(*this);
}
Player* World::GetPlayerById(PlayerID a_PlayerId) {
String stringId = UtilityFunctions::var_to_str(a_PlayerId);
for (int i = 0; i < m_Players->get_child_count(); i++) {
Node* player = m_Players->get_child(i);
if (player->get_name() == stringId) {
return Object::cast_to<Player>(player);
}
}
return nullptr;
}
void World::HandlePacket(const protocol::packets::PlayerJoin& a_PlayerJoin) {
const protocol::PlayerInfo& playerInfo = a_PlayerJoin.m_Data.m_Player;
AddPlayer(playerInfo.m_PlayerId, playerInfo.m_PlayerName);
}
void World::HandlePacket(const protocol::packets::PlayerLeave& a_PlayerLeave) {
RemovePlayer(a_PlayerLeave.m_Data.m_PlayerId);
}
void World::AddPlayer(PlayerID a_PlayerId, String a_PlayerName) {
UtilityFunctions::print("New Player with id : ", a_PlayerId, " and name ", a_PlayerName);
if (a_PlayerId == get_multiplayer()->get_unique_id()) {
Ref<PackedScene> serverScene = ResourceLoader::get_singleton()->load(FirstPersonPlayerScenePath);
FirstPersonPlayer* player = Object::cast_to<FirstPersonPlayer>(serverScene->instantiate());
player->set_name(UtilityFunctions::var_to_str(a_PlayerId));
player->m_PeerId = a_PlayerId;
m_Players->add_child(player);
} else {
Ref<PackedScene> serverScene = ResourceLoader::get_singleton()->load(PlayerScenePath);
Player* player = Object::cast_to<Player>(serverScene->instantiate());
player->set_name(UtilityFunctions::var_to_str(a_PlayerId));
player->m_PeerId = a_PlayerId;
m_Players->add_child(player);
}
}
void World::RemovePlayer(PlayerID a_PlayerId) {
UtilityFunctions::print("Removing Player with id : ", a_PlayerId);
Player* player = GetPlayerById(a_PlayerId);
if (player) {
player->queue_free();
}
}
void World::SetPlayerPositionAndRotation(
PlayerID a_PlayerId, const Vector3& a_Position, const Vector3& a_Rotation, const godot::Vector3& a_Velocity) {
Player* player = GetPlayerById(a_PlayerId);
if (player) {
player->set_position(a_Position);
player->SetCameraRotation(a_Rotation);
player->set_velocity(a_Velocity);
}
}
} // namespace blitz

View File

@@ -0,0 +1,79 @@
#include <blitz/protocol/ByteBuffer.h>
#include <blitz/protocol/PacketData.h>
namespace blitz {
namespace protocol {
#define Operators(Type, GodotType) \
ByteBuffer& ByteBuffer::operator>>(Type& a_Data) { \
if (sizeof(a_Data) + m_ReadOffset > m_Buffer.size()) { \
throw ReadError("Buffer is too small ! Can't read " #Type); \
} \
a_Data = m_Buffer.decode_##GodotType(m_ReadOffset); \
m_ReadOffset += sizeof(a_Data); \
return *this; \
} \
\
ByteBuffer& ByteBuffer::operator<<(Type a_Data) { \
m_Buffer.resize(m_Buffer.size() + sizeof(a_Data)); \
m_Buffer.encode_##GodotType(m_Buffer.size() - sizeof(a_Data), a_Data); \
return *this; \
}
// Integers
Operators(int8_t, s8);
Operators(uint8_t, u8);
Operators(int16_t, s16);
Operators(uint16_t, u16);
Operators(int32_t, s32);
Operators(uint32_t, u32);
Operators(int64_t, s64);
Operators(uint64_t, u64);
// Reals
Operators(float, float);
Operators(double, double);
ByteBuffer& ByteBuffer::operator>>(PlayerInfo& a_Data) {
*this >> a_Data.m_PlayerId >> a_Data.m_PlayerName;
return *this;
}
ByteBuffer& ByteBuffer::operator<<(const PlayerInfo& a_Data) {
*this << a_Data.m_PlayerId << a_Data.m_PlayerName;
return *this;
}
ByteBuffer& ByteBuffer::operator<<(const godot::Vector3& a_Data) {
*this << a_Data.x << a_Data.y << a_Data.z;
return *this;
}
ByteBuffer& ByteBuffer::operator>>(godot::Vector3& a_Data) {
*this >> a_Data.x >> a_Data.y >> a_Data.z;
return *this;
}
ByteBuffer& ByteBuffer::operator>>(godot::String& a_Data) {
int nullPos = m_Buffer.find(0, m_ReadOffset);
if (nullPos < 0)
throw ReadError("String does not have an and in buffer !");
godot::PackedByteArray stringBuffer = m_Buffer.slice(m_ReadOffset, nullPos);
a_Data = stringBuffer.get_string_from_utf8();
m_ReadOffset = nullPos + 1;
return *this;
}
ByteBuffer& ByteBuffer::operator<<(const godot::String& a_Data) {
godot::PackedByteArray stringBuffer = a_Data.to_utf8_buffer();
m_Buffer.append_array(stringBuffer);
// ends the string
*this << static_cast<std::uint8_t>(0);
return *this;
}
} // namespace protocol
} // namespace blitz

View File

@@ -0,0 +1,37 @@
#include <blitz/protocol/PacketDispatcher.h>
#include <algorithm>
#include <blitz/protocol/PacketHandler.h>
namespace blitz {
namespace protocol {
void PacketDispatcher::RegisterHandler(PacketType type, PacketHandler& handler) {
auto found = std::find(m_Handlers[type].begin(), m_Handlers[type].end(), &handler);
if (found == m_Handlers[type].end())
m_Handlers[type].push_back(&handler);
}
void PacketDispatcher::UnregisterHandler(PacketType type, PacketHandler& handler) {
m_Handlers[type].erase(std::remove(m_Handlers[type].begin(), m_Handlers[type].end(), &handler), m_Handlers[type].end());
}
void PacketDispatcher::UnregisterHandler(PacketHandler& handler) {
for (auto& pair : m_Handlers) {
if (pair.second.empty())
continue;
PacketType type = pair.first;
m_Handlers[type].erase(std::remove(m_Handlers[type].begin(), m_Handlers[type].end(), &handler), m_Handlers[type].end());
}
}
void PacketDispatcher::Dispatch(const Packet& packet) {
PacketType type = packet.GetType();
for (PacketHandler* handler : m_Handlers[type])
handler->Check(packet);
}
} // namespace protocol
} // namespace blitz

View File

@@ -0,0 +1,32 @@
#include <blitz/protocol/PacketFactory.h>
#include <array>
#include <cassert>
#include <functional>
namespace blitz {
namespace protocol {
namespace PacketFactory {
using PacketCreator = std::function<std::unique_ptr<Packet>()>;
#define DeclarePacket(PacketName, ...) std::make_unique<packets::PacketName>(),
static std::array<std::unique_ptr<Packet>, static_cast<std::size_t>(PacketType::PACKET_COUNT)> Packets;
void Init() {
Packets = {
DeclareAllPacket()
};
}
const std::unique_ptr<Packet>& CreateReadOnlyPacket(PacketType a_Type) {
assert(a_Type < PacketType::PACKET_COUNT);
return Packets[static_cast<std::size_t>(a_Type)];
}
} // namespace PacketFactory
} // namespace protocol
} // namespace blitz

View File

@@ -0,0 +1,14 @@
#include <blitz/protocol/PacketHandler.h>
namespace blitz {
namespace protocol {
#define DeclarePacket(PacketName, ...) \
void PacketHandler::Visit(const packets::PacketName& a_Packet) { \
HandlePacket(a_Packet); \
}
DeclareAllPacket()
} // namespace protocol
} // namespace blitz

View File

@@ -0,0 +1,39 @@
#include <blitz/protocol/PacketSender.h>
#include <blitz/godot/NetworkInterface.h>
#include <blitz/protocol/PacketSerializer.h>
namespace blitz {
namespace protocol {
///////////////////////
/* PacketBroadcaster */
///////////////////////
#define DeclarePacket(PacketName, Reliability, ...) \
void PacketBroadcaster::Visit(const protocol::packets::PacketName& a_Packet) { \
m_NetworkInterface.rpc("RecievePacketData" #Reliability, protocol::PacketSerializer::Serialize(a_Packet)); \
}
DeclareAllPacket()
#undef DeclarePacket
//////////////////
/* PacketSender */
//////////////////
#define DeclarePacket(PacketName, Reliability, ...) \
void PacketSender::Visit(const protocol::packets::PacketName& a_Packet) { \
m_NetworkInterface.rpc_id(m_PeerId, "RecievePacketData" #Reliability, protocol::PacketSerializer::Serialize(a_Packet)); \
}
DeclareAllPacket()
#undef DeclarePacket
} // namespace protocol
} // namespace blitz

View File

@@ -0,0 +1,285 @@
#include <blitz/protocol/PacketSerializer.h>
#include <blitz/protocol/ByteBuffer.h>
#include <blitz/protocol/PacketFactory.h>
#include <blitz/protocol/PacketVisitor.h>
#include <godot_cpp/variant/utility_functions.hpp>
#include <godot_cpp/variant/variant.hpp>
namespace blitz {
namespace protocol {
namespace PacketSerializer {
#define DeclarePacket(PacketName, ...) \
void Visit(const packets::PacketName& a_Packet) override { \
const auto& packetData = a_Packet.m_Data; \
SerializePacketData(packetData); \
} \
\
void SerializePacketData(const packets::PacketName::PacketDataType& a_Packet);
class Serializer : public PacketVisitor {
private:
ByteBuffer& m_Buffer;
public:
Serializer(ByteBuffer& a_Buffer) : m_Buffer(a_Buffer) {}
void Serialize(const Packet& a_Packet) {
m_Buffer << static_cast<PacketID>(a_Packet.GetType());
Check(a_Packet);
}
DeclareAllPacket()
};
#undef DeclarePacket
#define DeclarePacket(PacketName, ...) \
void Visit(const packets::PacketName& a_Packet) override { \
auto packetPtr = PacketFactory::CreatePacket<packets::PacketName>(); \
auto& packetData = packetPtr->m_Data; \
\
DeserializePacketData(packetData); \
\
m_Packet = std::move(packetPtr); \
} \
\
void DeserializePacketData(packets::PacketName::PacketDataType& a_Packet);
class Deserializer : public PacketVisitor {
private:
ByteBuffer& m_Buffer;
PacketPtr m_Packet;
public:
Deserializer(ByteBuffer&& a_Buffer) : m_Buffer(a_Buffer) {}
bool Deserialize(const PacketPtr& a_Packet) {
try {
Check(*a_Packet.get());
} catch (ByteBuffer::ReadError& e) {
godot::UtilityFunctions::printerr("[PacketSerializer::Deserializer] ", e.what());
return false;
}
return true;
}
PacketPtr& GetPacket() {
return m_Packet;
}
DeclareAllPacket()
};
godot::PackedByteArray Serialize(const Packet& a_Packet) {
ByteBuffer stream;
Serializer serializer(stream);
serializer.Serialize(a_Packet);
return stream.GetByteArray();
}
std::unique_ptr<Packet> Deserialize(godot::PackedByteArray& a_Data) {
ByteBuffer stream(std::move(a_Data));
PacketID packetId;
stream >> packetId;
if (packetId >= static_cast<PacketID>(PacketType::PACKET_COUNT))
return nullptr;
PacketType packetType = PacketType(packetId);
// for double-dispatch
const PacketPtr& emptyPacket = PacketFactory::CreateReadOnlyPacket(packetType);
Deserializer deserializer(std::move(stream));
if (deserializer.Deserialize(emptyPacket)) {
PacketPtr packet = std::move(deserializer.GetPacket());
return packet;
}
return nullptr;
}
//---------------------------------------------
// Packet serializer implementation
//----------------------------------------------
void Serializer::SerializePacketData(const data::PlayerLogin& a_Packet) {
m_Buffer << a_Packet.m_PlayerName;
}
void Deserializer::DeserializePacketData(data::PlayerLogin& a_Packet) {
m_Buffer >> a_Packet.m_PlayerName;
}
void Serializer::SerializePacketData(const data::UpdateHealth& a_Packet) {
m_Buffer << a_Packet.m_NewHealth;
}
void Deserializer::DeserializePacketData(data::UpdateHealth& a_Packet) {
m_Buffer >> a_Packet.m_NewHealth;
}
void Serializer::SerializePacketData(const data::LoggingSuccess& a_Packet) {
m_Buffer << a_Packet.m_PlayerId;
}
void Deserializer::DeserializePacketData(data::LoggingSuccess& a_Packet) {
m_Buffer >> a_Packet.m_PlayerId;
}
void Serializer::SerializePacketData(const data::PlayerDeath& a_Packet) {}
void Deserializer::DeserializePacketData(data::PlayerDeath& a_Packet) {}
void Serializer::SerializePacketData(const data::PlayerJoin& a_Packet) {
m_Buffer << a_Packet.m_Player;
}
void Deserializer::DeserializePacketData(data::PlayerJoin& a_Packet) {
m_Buffer >> a_Packet.m_Player;
}
void Serializer::SerializePacketData(const data::PlayerLeave& a_Packet) {
m_Buffer << a_Packet.m_PlayerId;
}
void Deserializer::DeserializePacketData(data::PlayerLeave& a_Packet) {
m_Buffer >> a_Packet.m_PlayerId;
}
void Serializer::SerializePacketData(const data::PlayerList& a_Packet) {
m_Buffer << a_Packet.m_Players;
}
void Deserializer::DeserializePacketData(data::PlayerList& a_Packet) {
m_Buffer >> a_Packet.m_Players;
}
void Serializer::SerializePacketData(const data::PlayerStats& a_Packet) {}
void Deserializer::DeserializePacketData(data::PlayerStats& a_Packet) {}
void Serializer::SerializePacketData(const data::ServerConfig& a_Packet) {}
void Deserializer::DeserializePacketData(data::ServerConfig& a_Packet) {}
void Serializer::SerializePacketData(const data::ServerTps& a_Packet) {}
void Deserializer::DeserializePacketData(data::ServerTps& a_Packet) {}
void Serializer::SerializePacketData(const data::UpdateGameState& a_Packet) {}
void Deserializer::DeserializePacketData(data::UpdateGameState& a_Packet) {}
void Serializer::SerializePacketData(const data::KeepAlive& a_Packet) {
m_Buffer << a_Packet.m_KeepAliveId;
}
void Deserializer::DeserializePacketData(data::KeepAlive& a_Packet) {
m_Buffer >> a_Packet.m_KeepAliveId;
}
void Serializer::SerializePacketData(const data::Disconnect& a_Packet) {}
void Deserializer::DeserializePacketData(data::Disconnect& a_Packet) {}
void Serializer::SerializePacketData(const data::ChatMessage& a_Packet) {
m_Buffer << a_Packet.m_Text;
}
void Deserializer::DeserializePacketData(data::ChatMessage& a_Packet) {
m_Buffer >> a_Packet.m_Text;
}
void Serializer::SerializePacketData(const data::PlayerPositionAndRotation& a_Packet) {
m_Buffer << a_Packet.m_Player << a_Packet.m_Position << a_Packet.m_Rotation << a_Packet.m_Velocity;
}
void Deserializer::DeserializePacketData(data::PlayerPositionAndRotation& a_Packet) {
m_Buffer >> a_Packet.m_Player >> a_Packet.m_Position >> a_Packet.m_Rotation >> a_Packet.m_Velocity;
}
void Serializer::SerializePacketData(const data::PlayerShoot& a_Packet) {}
void Deserializer::DeserializePacketData(data::PlayerShoot& a_Packet) {}
} // namespace PacketSerializer
} // namespace protocol
} // namespace blitz

View File

@@ -0,0 +1,11 @@
#include <blitz/protocol/PacketVisitor.h>
namespace blitz {
namespace protocol {
void PacketVisitor::Check(const Packet& a_Packet) {
a_Packet.Accept(*this);
}
} // namespace protocol
} // namespace blitz

View File

@@ -0,0 +1,23 @@
#define BLITZ_INSTANCIATE_PACKETS
#include <blitz/protocol/Packets.h>
#include <blitz/protocol/PacketVisitor.h>
namespace blitz {
namespace protocol {
template <PacketType PT, typename Data>
packets::ConcretePacket<PT, Data>::ConcretePacket(const PacketDataType& a_Data) : m_Data(a_Data) {}
template <PacketType PT, typename Data>
void packets::ConcretePacket<PT, Data>::Accept(PacketVisitor& a_Visitor) const {
a_Visitor.Visit(*this);
}
#define DeclarePacket(PacketName, packetSendType, packetSenderType) \
static_assert(static_cast<unsigned>(PacketSendType::packetSendType) && static_cast<unsigned>(PacketSenderType::packetSenderType));
DeclareAllPacket()
} // namespace protocol
} // namespace blitz

View File

@@ -0,0 +1,57 @@
#include <client/ClientWorld.h>
#include <blitz/godot/NetworkInterface.h>
#include <client/Player.h>
#include <godot_cpp/classes/engine.hpp>
#include <godot_cpp/classes/multiplayer_api.hpp>
#include <godot_cpp/variant/utility_functions.hpp>
namespace blitz {
using namespace godot;
void ClientWorld::_bind_methods() {}
ClientWorld::ClientWorld() {}
ClientWorld::~ClientWorld() {}
void ClientWorld::_process(float delta) {
#if DEBUG_ENABLED
if (Engine::get_singleton()->is_editor_hint())
return;
#endif
m_PassedTime += delta;
if (m_PassedTime < 0.05f)
return;
m_PassedTime = 0.0f;
UpdatePlayerPos();
}
void ClientWorld::UpdatePlayerPos() {
Player* player = GetPlayerById(get_multiplayer()->get_unique_id());
if (player) {
m_NetworkInterface->BroadcastPacket(protocol::packets::PlayerPositionAndRotation(
{get_multiplayer()->get_unique_id(), player->get_position(), player->GetCameraRotation(), player->get_velocity()}));
}
}
void ClientWorld::HandlePacket(const protocol::packets::PlayerPositionAndRotation& a_PlayerPos) {
const auto& data = a_PlayerPos.m_Data;
if (data.m_Player == get_multiplayer()->get_unique_id()) {
Player* player = GetPlayerById(get_multiplayer()->get_unique_id());
if (player && (a_PlayerPos.m_Data.m_Position - player->get_position()).length() > 10) {
SetPlayerPositionAndRotation(data.m_Player, data.m_Position, data.m_Rotation, data.m_Velocity);
godot::UtilityFunctions::print("Teleported to : ", data.m_Position);
}
return;
}
SetPlayerPositionAndRotation(data.m_Player, data.m_Position, data.m_Rotation, data.m_Velocity);
}
} // namespace blitz

View File

@@ -0,0 +1,179 @@
#include <client/FirstPersonPlayer.h>
#include <godot_cpp/classes/camera3d.hpp>
#include <godot_cpp/classes/engine.hpp>
#include <godot_cpp/classes/input.hpp>
#include <godot_cpp/classes/input_event_mouse_motion.hpp>
#include <godot_cpp/classes/input_map.hpp>
#include <godot_cpp/core/math.hpp>
#include <godot_cpp/variant/utility_functions.hpp>
using namespace godot;
namespace blitz {
static constexpr float WALK_SPEED = 5.0f;
static constexpr float SPRINT_SPEED = 7.0f;
static constexpr float JUMP_VELOCITY = 4.5f;
static constexpr float GRAVITY = 9.81f;
static constexpr float SENSITIVITY = 0.003f;
static constexpr float BOB_FREQ = 2.0f;
static constexpr float BOB_AMP = 0.08f;
static constexpr float AIR_MOVEMENT = 3.0f;
static constexpr float GROUND_FRICTION = 7.0f;
static constexpr float BASE_FOV = 75.0f;
static constexpr float FOV_CHANGE = 1.5f;
static constexpr float FOV_TRANSITION = 8.0f;
static constexpr float MIN_FOV_VELOCITY = 0.5;
static constexpr float MAX_FOV_VELOCITY = SPRINT_SPEED * 2.0f;
static const float LerpValue = 0.10;
static const float AnimationBlend = 7.0;
void FirstPersonPlayer::_bind_methods() {}
FirstPersonPlayer::FirstPersonPlayer() : Player(), m_BobTime(0) {}
FirstPersonPlayer::~FirstPersonPlayer() {}
void FirstPersonPlayer::_ready() {
InputMap::get_singleton()->load_from_project_settings();
if (!Engine::get_singleton()->is_editor_hint()) {
Input::get_singleton()->set_mouse_mode(Input::MOUSE_MODE_CAPTURED);
}
m_Head = Object::cast_to<Node3D>(find_child("Head"));
m_Camera = Object::cast_to<Camera3D>(m_Head->find_child("Camera"));
m_AnimationTree = Object::cast_to<AnimationTree>(find_child("AnimationTree"));
m_Mesh = Object::cast_to<Node3D>(find_child("Mesh"));
set_position({0, 0, 0});
set_velocity({0, 0, 0});
}
void FirstPersonPlayer::_unhandled_input(const godot::Ref<godot::InputEvent>& a_Event) {
auto* event = Object::cast_to<InputEventMouseMotion>(a_Event.ptr());
if (event)
UpdateCamera(*event);
// TODO: remove
if (Input::get_singleton()->is_action_just_pressed("escape")) {
Input::MouseMode current = Input::get_singleton()->get_mouse_mode();
Input::get_singleton()->set_mouse_mode(
(current == Input::MOUSE_MODE_CAPTURED) ? Input::MOUSE_MODE_VISIBLE : Input::MOUSE_MODE_CAPTURED);
}
}
void FirstPersonPlayer::_physics_process(float a_Delta) {
#if DEBUG_ENABLED
if (Engine::get_singleton()->is_editor_hint()) {
return;
}
#endif
auto* Input = Input::get_singleton();
if (!is_on_floor())
set_velocity(get_velocity() - Vector3{0, GRAVITY * a_Delta, 0});
if (Input->is_action_pressed("jump") && is_on_floor())
set_velocity({get_velocity().x, JUMP_VELOCITY, get_velocity().z});
m_Speed = Input->is_action_pressed("sprint") ? SPRINT_SPEED : WALK_SPEED;
UpdatePosition(a_Delta);
UpdateFOV(a_Delta);
UpdateBobbing(a_Delta);
move_and_slide();
UpdateAnimation(a_Delta);
}
void FirstPersonPlayer::UpdateBobbing(float a_Delta) {
m_BobTime += a_Delta * get_velocity().length() * is_on_floor();
Vector3 newPos{static_cast<float>(Math::cos(m_BobTime * BOB_FREQ / 2.0) * BOB_AMP),
static_cast<float>(Math::sin(m_BobTime * BOB_FREQ) * BOB_AMP), 0};
m_Camera->set_transform({m_Camera->get_transform().basis, newPos});
}
void FirstPersonPlayer::UpdateCamera(const InputEventMouseMotion& a_Event) {
m_Head->rotate_y(-a_Event.get_relative().x * SENSITIVITY);
m_Mesh->rotate_y(-a_Event.get_relative().x * SENSITIVITY);
m_Camera->rotate_x(-a_Event.get_relative().y * SENSITIVITY);
float rotationX = m_Camera->get_rotation().x;
rotationX = CLAMP(rotationX, Math::deg_to_rad(-80.0), Math::deg_to_rad(80.0));
m_Camera->set_rotation({rotationX, get_rotation().y, get_rotation().z});
}
void FirstPersonPlayer::UpdatePosition(float delta) {
auto* Input = Input::get_singleton();
Vector2 inputDirection = Input->get_vector("move_left", "move_right", "move_forwards", "move_backwards");
Vector3 direction = (m_Head->get_transform().basis.xform(Vector3(inputDirection.x, 0, inputDirection.y))).normalized();
if (is_on_floor()) {
if (!direction.is_zero_approx()) {
set_velocity({direction.x * m_Speed, get_velocity().y, direction.z * m_Speed});
} else {
set_velocity({Math::lerp(static_cast<float>(get_velocity().x), static_cast<float>(direction.x * m_Speed),
static_cast<float>(delta * GROUND_FRICTION)),
get_velocity().y,
Math::lerp(static_cast<float>(get_velocity().z), static_cast<float>(direction.z * m_Speed),
static_cast<float>(delta * GROUND_FRICTION))});
}
} else {
set_velocity({Math::lerp(static_cast<float>(get_velocity().x), static_cast<float>(direction.x * m_Speed),
static_cast<float>(delta * AIR_MOVEMENT)),
get_velocity().y,
Math::lerp(static_cast<float>(get_velocity().z), static_cast<float>(direction.z * m_Speed),
static_cast<float>(delta * AIR_MOVEMENT))});
}
if (!direction.is_zero_approx()) {
godot::Vector3 newRotation = m_Mesh->get_rotation();
newRotation.y = godot::UtilityFunctions::lerp_angle(
newRotation.y, godot::UtilityFunctions::atan2(get_velocity().x, get_velocity().z), LerpValue);
m_Mesh->set_rotation(newRotation);
}
}
void FirstPersonPlayer::UpdateFOV(float a_Delta) {
float velocityClamped = Math::clamp(get_velocity().length(), MIN_FOV_VELOCITY, MAX_FOV_VELOCITY);
float targetFOV = BASE_FOV + FOV_CHANGE * velocityClamped;
m_Camera->set_fov(Math::lerp(static_cast<float>(m_Camera->get_fov()), targetFOV, a_Delta * FOV_TRANSITION));
}
void FirstPersonPlayer::UpdateAnimation(float delta) {
if (is_on_floor()) {
m_AnimationTree->set("parameters/ground_air_transition/transition_request", "grounded");
if (get_velocity().length() > 0.2f) {
if (m_Speed == SPRINT_SPEED) {
m_AnimationTree->set("parameters/iwr_blend/blend_amount",
UtilityFunctions::lerp(m_AnimationTree->get("parameters/iwr_blend/blend_amount"), 1.0, delta * AnimationBlend));
} else {
m_AnimationTree->set("parameters/iwr_blend/blend_amount",
UtilityFunctions::lerp(m_AnimationTree->get("parameters/iwr_blend/blend_amount"), 0.0, delta * AnimationBlend));
}
} else {
m_AnimationTree->set("parameters/iwr_blend/blend_amount",
UtilityFunctions::lerp(m_AnimationTree->get("parameters/iwr_blend/blend_amount"), -1.0, delta * AnimationBlend));
}
} else {
m_AnimationTree->set("parameters/ground_air_transition/transition_request", "air");
}
}
} // namespace blitz

42
src/client/Main.cpp Normal file
View File

@@ -0,0 +1,42 @@
#include <client/Main.h>
#include <godot_cpp/classes/engine.hpp>
#include <godot_cpp/classes/packed_scene.hpp>
#include <godot_cpp/classes/resource_loader.hpp>
#include <godot_cpp/classes/scene_tree.hpp>
#include <godot_cpp/classes/window.hpp>
#include <godot_cpp/variant/utility_functions.hpp>
#include <client/ClientWorld.h>
#include <server/ServerWorld.h>
using namespace godot;
namespace blitz {
static constexpr char ClientWorldScenePath[] = "res://Scenes/Levels/client_world.tscn";
static constexpr char ServerWorldScenePath[] = "res://Scenes/Levels/server_world.tscn";
void Main::_bind_methods() {}
void Main::_ready() {
auto* mainMenu = find_child("MainMenu");
DEV_ASSERT(mainMenu);
mainMenu->connect("change_scene_to_game", callable_mp(this, &Main::ChangeScene));
}
Main::Main() {}
Main::~Main() {}
void Main::ChangeScene(bool a_Server) {
Ref<PackedScene> sceneData;
if (a_Server)
sceneData = ResourceLoader::get_singleton()->load(ServerWorldScenePath);
else
sceneData = ResourceLoader::get_singleton()->load(ClientWorldScenePath);
add_child(sceneData->instantiate());
}
} // namespace blitz

81
src/client/MainMenu.cpp Normal file
View File

@@ -0,0 +1,81 @@
#include <client/MainMenu.h>
#include <godot_cpp/classes/engine.hpp>
#include <godot_cpp/classes/multiplayer_api.hpp>
#include <godot_cpp/classes/resource_loader.hpp>
#include <godot_cpp/classes/scene_tree.hpp>
using namespace godot;
namespace blitz {
void MainMenu::_bind_methods() {
godot::ClassDB::bind_method(godot::D_METHOD("on_connected"), &MainMenu::OnConnected);
ADD_SIGNAL(MethodInfo("change_scene_to_game", PropertyInfo(Variant::BOOL, "server")));
}
MainMenu::MainMenu() {}
MainMenu::~MainMenu() {}
void MainMenu::_ready() {
Node* container = find_child("Container");
DEV_ASSERT(container);
m_JoinButton = Object::cast_to<Button>(container->find_child("JoinButton"));
m_CreateButton = Object::cast_to<Button>(container->find_child("CreateButton"));
m_QuitButton = Object::cast_to<Button>(container->find_child("QuitButton"));
DEV_ASSERT(m_JoinButton);
DEV_ASSERT(m_CreateButton);
DEV_ASSERT(m_QuitButton);
m_JoinButton->connect("pressed", callable_mp(this, &MainMenu::OnJoinPressed));
m_CreateButton->connect("pressed", callable_mp(this, &MainMenu::OnCreatePressed));
m_QuitButton->connect("pressed", callable_mp(this, &MainMenu::OnQuitPressed));
if (!Engine::get_singleton()->is_editor_hint()) {
m_NetworkInterface = Object::cast_to<NetworkInterface>(get_parent()->find_child("Network"));
DEV_ASSERT(m_NetworkInterface);
m_NetworkInterface->connect("local_player_connected", callable_mp(this, &MainMenu::OnConnected));
m_NetworkInterface->connect("server_disconnected", callable_mp(this, &MainMenu::OnDisconnected));
}
}
void MainMenu::OnConnected() {
emit_signal("change_scene_to_game", get_multiplayer()->is_server());
set_visible(false);
}
void MainMenu::OnDisconnected() {
set_visible(true);
EnableButtons();
}
void MainMenu::OnJoinPressed() {
DisableButtons();
m_NetworkInterface->JoinGame("localhost", 25565);
}
void MainMenu::OnCreatePressed() {
DisableButtons();
m_NetworkInterface->CreateGame(25565);
}
void MainMenu::OnQuitPressed() {
get_tree()->quit();
}
void MainMenu::DisableButtons() {
m_JoinButton->set_disabled(true);
m_CreateButton->set_disabled(true);
}
void MainMenu::EnableButtons() {
m_JoinButton->set_disabled(false);
m_CreateButton->set_disabled(false);
}
} // namespace blitz

82
src/client/Player.cpp Normal file
View File

@@ -0,0 +1,82 @@
#include <client/Player.h>
#include <godot_cpp/classes/engine.hpp>
#include <godot_cpp/classes/input.hpp>
#include <godot_cpp/classes/input_map.hpp>
#include <godot_cpp/core/class_db.hpp>
#include <godot_cpp/variant/utility_functions.hpp>
static const float WalkSpeed = 2.0;
static const float RunSpeed = 7.0;
static const float JumpStrength = 15.0;
static const float Gravity = 50.0;
static const float LerpValue = 0.15;
static const float AnimationBlend = 7.0;
namespace blitz {
using namespace godot;
void Player::_bind_methods() {}
Player::Player() : m_PeerId(0) {
// we set the player to an invalid position
set_position({-99999, -999999, -999999});
}
Player::~Player() {}
void Player::_ready() {
godot::InputMap::get_singleton()->load_from_project_settings();
m_Mesh = Object::cast_to<godot::Node3D>(find_child("Mesh"));
m_AnimationTree = Object::cast_to<godot::AnimationTree>(find_child("AnimationTree"));
DEV_ASSERT(m_Mesh);
DEV_ASSERT(m_AnimationTree);
animate(0);
}
void Player::_physics_process(float delta) {
if (godot::Engine::get_singleton()->is_editor_hint())
return;
move_and_slide();
animate(delta);
}
void Player::animate(float delta) {
if (is_on_floor()) {
m_AnimationTree->set("parameters/ground_air_transition/transition_request", "grounded");
float speed = get_velocity().length();
if (speed > 0.2f) {
if (speed >= RunSpeed) {
m_AnimationTree->set("parameters/iwr_blend/blend_amount",
godot::UtilityFunctions::lerp(
m_AnimationTree->get("parameters/iwr_blend/blend_amount"), 1.0, delta * AnimationBlend));
} else {
m_AnimationTree->set("parameters/iwr_blend/blend_amount",
godot::UtilityFunctions::lerp(
m_AnimationTree->get("parameters/iwr_blend/blend_amount"), 0.0, delta * AnimationBlend));
}
} else {
m_AnimationTree->set("parameters/iwr_blend/blend_amount",
godot::UtilityFunctions::lerp(
m_AnimationTree->get("parameters/iwr_blend/blend_amount"), -1.0, delta * AnimationBlend));
}
} else {
m_AnimationTree->set("parameters/ground_air_transition/transition_request", "air");
}
}
Vector3 Player::GetCameraRotation() const {
return m_Mesh->get_rotation();
}
void Player::SetCameraRotation(const Vector3& a_Rotation) {
m_Mesh->set_rotation(a_Rotation);
}
} // namespace blitz

View File

@@ -0,0 +1,51 @@
#include <blitz/godot/NetworkInterface.h>
#include <client/ClientWorld.h>
#include <client/FirstPersonPlayer.h>
#include <client/Main.h>
#include <client/MainMenu.h>
#include <client/Player.h>
#include <server/Server.h>
#include <server/ServerWorld.h>
#include <gdextension_interface.h>
#include <godot_cpp/core/defs.hpp>
#include <godot_cpp/godot.hpp>
using namespace godot;
static void RegisterClasses() {
GDREGISTER_CLASS(blitz::Player);
GDREGISTER_CLASS(blitz::FirstPersonPlayer);
GDREGISTER_CLASS(blitz::MainMenu);
GDREGISTER_CLASS(blitz::Main);
GDREGISTER_CLASS(blitz::NetworkInterface);
GDREGISTER_CLASS(blitz::Server);
GDREGISTER_ABSTRACT_CLASS(blitz::World);
GDREGISTER_CLASS(blitz::ClientWorld);
GDREGISTER_CLASS(blitz::ServerWorld);
}
static void initialize_blitz_module(ModuleInitializationLevel p_level) {
if (p_level != MODULE_INITIALIZATION_LEVEL_SCENE) {
return;
}
RegisterClasses();
}
static void uninitialize_blitz_module(ModuleInitializationLevel p_level) {
if (p_level != MODULE_INITIALIZATION_LEVEL_SCENE) {
return;
}
}
extern "C" GDExtensionBool GDE_EXPORT library_init(GDExtensionInterfaceGetProcAddress p_get_proc,
const GDExtensionClassLibraryPtr p_library, GDExtensionInitialization* r_initialization) {
godot::GDExtensionBinding::InitObject init_obj(p_get_proc, p_library, r_initialization);
init_obj.register_initializer(initialize_blitz_module);
init_obj.register_terminator(uninitialize_blitz_module);
init_obj.set_minimum_library_initialization_level(MODULE_INITIALIZATION_LEVEL_SCENE);
return init_obj.init();
}

View File

@@ -1,28 +0,0 @@
#include "gdexample.h"
#include <godot_cpp/core/class_db.hpp>
using namespace godot;
void GDExample::_bind_methods() {
// ClassDB::bind_method(D_METHOD("_process", "delta"), &GDExample::_process);
}
GDExample::GDExample()
{
time_passed = 0.0;
}
GDExample::~GDExample() {}
void GDExample::_process(float delta)
{
time_passed += delta;
auto new_position = Vector2(
10.0 + (10.0 * sin(time_passed * 2.0)),
10.0 + (10.0 * cos(time_passed * 1.5)));
set_position(new_position);
}

View File

@@ -1,24 +0,0 @@
#ifndef GDEXAMPLE_H
#define GDEXAMPLE_H
#include <godot_cpp/classes/sprite2d.hpp>
namespace godot
{
class GDExample : public Sprite2D
{
GDCLASS(GDExample, Sprite2D)
private:
double time_passed;
protected:
static void _bind_methods();
public:
GDExample();
~GDExample();
void _process(float delta);
};
}
#endif

View File

@@ -1,44 +0,0 @@
#include "register_types.h"
#include "gdexample.h"
#include <gdextension_interface.h>
#include <godot_cpp/core/defs.hpp>
#include <godot_cpp/godot.hpp>
using namespace godot;
void initialize_example_module(ModuleInitializationLevel p_level)
{
if (p_level != MODULE_INITIALIZATION_LEVEL_SCENE)
{
return;
}
ClassDB::register_class<GDExample>();
}
void uninitialize_example_module(ModuleInitializationLevel p_level)
{
if (p_level != MODULE_INITIALIZATION_LEVEL_SCENE)
{
return;
}
}
extern "C"
{
GDExtensionBool GDE_EXPORT library_init(
GDExtensionInterfaceGetProcAddress p_get_proc,
const GDExtensionClassLibraryPtr p_library,
GDExtensionInitialization *r_initialization)
{
godot::GDExtensionBinding::InitObject init_obj(p_get_proc, p_library, r_initialization);
init_obj.register_initializer(initialize_example_module);
init_obj.register_terminator(uninitialize_example_module);
init_obj.set_minimum_library_initialization_level(MODULE_INITIALIZATION_LEVEL_SCENE);
return init_obj.init();
}
}

View File

@@ -1,11 +0,0 @@
#ifndef GDEXAMPLE_REGISTER_TYPES_H
#define GDEXAMPLE_REGISTER_TYPES_H
#include <godot_cpp/core/class_db.hpp>
using namespace godot;
void initialize_example_module(ModuleInitializationLevel p_level);
void uninitialize_example_module(ModuleInitializationLevel p_level);
#endif

42
src/server/Server.cpp Normal file
View File

@@ -0,0 +1,42 @@
#include <server/Server.h>
#include <blitz/godot/NetworkInterface.h>
#include <godot_cpp/classes/engine.hpp>
using namespace godot;
namespace blitz {
void Server::_bind_methods() {}
Server::Server() {}
Server::~Server() {}
void Server::_ready() {
if (Engine::get_singleton()->is_editor_hint())
return;
m_NetworkInterface = Object::cast_to<NetworkInterface>(get_parent());
DEV_ASSERT(m_NetworkInterface);
m_NetworkInterface->connect("player_connected", callable_mp(this, &Server::OnPlayerConnect));
m_NetworkInterface->connect("player_disconnected", callable_mp(this, &Server::OnPlayerDisconnect));
}
void Server::OnPlayerConnect(PeerID a_PeerId) {
protocol::PlayerInfo playerInfo{a_PeerId, "whoami"};
for (int i = 0; i < m_Peers.size(); i++) {
m_NetworkInterface->SendPacket(a_PeerId, protocol::packets::PlayerJoin({m_Peers[i], "whoami"}));
}
m_Peers.push_back(a_PeerId);
m_NetworkInterface->BroadcastPacket(protocol::packets::PlayerJoin({playerInfo}));
}
void Server::OnPlayerDisconnect(PeerID a_PeerId) {
m_Peers.erase(a_PeerId);
m_NetworkInterface->BroadcastPacket(protocol::packets::PlayerLeave({a_PeerId}));
}
} // namespace blitz

View File

@@ -0,0 +1,64 @@
#include <server/ServerWorld.h>
#include <blitz/godot/NetworkInterface.h>
#include <client/Player.h>
#include <godot_cpp/classes/engine.hpp>
#include <godot_cpp/variant/utility_functions.hpp>
namespace blitz {
using namespace godot;
void ServerWorld::_bind_methods() {}
ServerWorld::ServerWorld() {}
ServerWorld::~ServerWorld() {}
void ServerWorld::_process(float delta) {
#if DEBUG_ENABLED
if (Engine::get_singleton()->is_editor_hint())
return;
#endif
m_PassedTime += delta;
if (m_PassedTime < 0.05f)
return;
m_PassedTime = 0.0f;
SyncPlayersPos();
}
void ServerWorld::SyncPlayersPos() {
for (int i = 0; i < m_Players->get_child_count(); i++) {
Player* player = Object::cast_to<Player>(m_Players->get_child(i));
DEV_ASSERT(player);
m_NetworkInterface->BroadcastPacket(protocol::packets::PlayerPositionAndRotation(
{player->GetId(), player->get_position(), player->GetCameraRotation(), player->get_velocity()}));
}
}
void ServerWorld::HandlePacket(const protocol::packets::PlayerPositionAndRotation& a_PlayerPos) {
const auto& data = a_PlayerPos.m_Data;
if (data.m_Player != a_PlayerPos.m_Sender)
return;
Player* player = GetPlayerById(data.m_Player);
if (!player)
return;
if ((data.m_Position - player->get_position()).length() > 10) {
UtilityFunctions::print(
"Player ", data.m_Player, " moved too fast ! (from ", player->get_position(), " to ", data.m_Position, ")");
return;
}
SetPlayerPositionAndRotation(data.m_Player, data.m_Position, data.m_Rotation, data.m_Velocity);
}
void ServerWorld::AddPlayer(PlayerID a_PlayerId, godot::String a_PlayerName) {
World::AddPlayer(a_PlayerId, a_PlayerName);
Player* player = GetPlayerById(a_PlayerId);
player->set_position({0, 0, 0});
}
} // namespace blitz

View File

@@ -16,7 +16,7 @@ add_rules("mode.debug", "mode.release")
set_languages("c++20") set_languages("c++20")
-- use latest 4.x version by default -- use latest 4.x version by default
add_requires("godotcpp4") add_requires("godotcpp4 4.2")
includes("tasks.lua") includes("tasks.lua")

View File

@@ -1,3 +1,9 @@
if is_mode("debug") then
add_defines("DEBUG_ENABLED")
end
-- more on https://xmake.io/#/manual/project_target -- more on https://xmake.io/#/manual/project_target
target(PROJECT_NAME) target(PROJECT_NAME)
set_kind("shared") set_kind("shared")
@@ -8,6 +14,8 @@ target(PROJECT_NAME)
-- more on https://xmake.io/#/manual/project_target?id=targetadd_files -- more on https://xmake.io/#/manual/project_target?id=targetadd_files
add_files("../src/**.cpp") add_files("../src/**.cpp")
add_includedirs("../include")
-- change the output name -- change the output name
set_basename(PROJECT_NAME .. ".$(os)_$(mode)_$(arch)") set_basename(PROJECT_NAME .. ".$(os)_$(mode)_$(arch)")
@@ -35,8 +43,9 @@ target(PROJECT_NAME)
on_run( on_run(
(function(godot_project_folder) (function(godot_project_folder)
return function(target) return function(target)
os.execv("echo", {"godot --path", godot_project_folder}) local cmd = format("godot --path %s", godot_project_folder)
os.exec("godot --path " .. godot_project_folder) os.execv("echo", {cmd})
os.exec(cmd)
end end
end)(GODOT_PROJECT_FOLDER) end)(GODOT_PROJECT_FOLDER)
) )

View File

@@ -37,8 +37,10 @@ task("export")
local export_path = path.absolute(path.join(publish_folder, project_name)) local export_path = path.absolute(path.join(publish_folder, project_name))
os.execv("echo", {"godot", godot_project_file, export_mode, target_platform, export_path}) local cmd = format("godot %s --headless --%s %s %s", godot_project_file, export_mode, "\"" .. target_platform .. "\"", export_path)
os.exec("godot %s --headless --%s %s %s", godot_project_file, export_mode, "\"" .. target_platform .. "\"", export_path)
os.execv("echo", {cmd})
os.exec(cmd)
end end
end)(GODOT_PROJECT_FOLDER, PUBLISH_FOLDER, PROJECT_NAME)) end)(GODOT_PROJECT_FOLDER, PUBLISH_FOLDER, PROJECT_NAME))
@@ -202,3 +204,22 @@ android.release.arm64 = "res://lib/libProjectName.android_release_arm64.so"
description = "Generate gdextension file", description = "Generate gdextension file",
} }
task_end() task_end()
task("import-assets")
on_run((function(godot_project_folder, project_name)
return function ()
local godot_project_file = path.join(godot_project_folder, "project.godot")
local cmd = format("godot --headless --import %s", godot_project_file)
os.execv("echo", {cmd})
os.exec(cmd)
end
end)(GODOT_PROJECT_FOLDER, PROJECT_NAME))
set_menu {
usage = "xmake import-assets",
description = "Import project assets",
}
task_end()