├── First Game ├── addons │ ├── steam-multiplayer-peer │ │ ├── linux32 │ │ │ ├── .gdignore │ │ │ ├── libsteam_api.so │ │ │ ├── libsteam-multiplayer-peer.linux.template_debug.x86_32.so │ │ │ └── libsteam-multiplayer-peer.linux.template_release.x86_32.so │ │ ├── linux64 │ │ │ ├── .gdignore │ │ │ ├── libsteam_api.so │ │ │ ├── libsteam-multiplayer-peer.linux.template_debug.x86_64.so │ │ │ └── libsteam-multiplayer-peer.linux.template_release.x86_64.so │ │ ├── osx │ │ │ ├── .gdignore │ │ │ ├── libsteam_api.dylib │ │ │ ├── steam-multiplayer-peer.macos.template_debug.framework │ │ │ │ ├── libsteam_api.dylib │ │ │ │ └── steam-multiplayer-peer.macos.template_debug │ │ │ └── steam-multiplayer-peer.macos.template_release.framework │ │ │ │ ├── libsteam_api.dylib │ │ │ │ └── libsteam-multiplayer-peer.macos.template_release │ │ ├── win32 │ │ │ ├── .gdignore │ │ │ ├── steam_api.dll │ │ │ ├── steam-multiplayer-peer.windows.template_debug.x86_32.dll │ │ │ └── steam-multiplayer-peer.windows.template_release.x86_32.dll │ │ ├── win64 │ │ │ ├── .gdignore │ │ │ ├── steam_api64.dll │ │ │ ├── steam_api64.lib │ │ │ ├── steam-multiplayer-peer.windows.template_debug.x86_64.dll │ │ │ ├── ~steam-multiplayer-peer.windows.template_debug.x86_64.dll │ │ │ └── steam-multiplayer-peer.windows.template_release.x86_64.dll │ │ ├── README.md │ │ ├── LICENSE │ │ └── steam-multiplayer-peer.gdextension │ ├── godotsteam │ │ ├── win32 │ │ │ ├── steam_api.dll │ │ │ ├── godotsteam.x86_32.dll │ │ │ └── godotsteam.debug.x86_32.dll │ │ ├── linux32 │ │ │ ├── libsteam_api.so │ │ │ ├── libgodotsteam.x86_32.so │ │ │ └── libgodotsteam.debug.x86_32.so │ │ ├── linux64 │ │ │ ├── libsteam_api.so │ │ │ ├── libgodotsteam.x86_64.so │ │ │ └── libgodotsteam.debug.x86_64.so │ │ ├── osx │ │ │ ├── libsteam_api.dylib │ │ │ ├── libgodotsteam.framework │ │ │ │ ├── libgodotsteam │ │ │ │ ├── libsteam_api.dylib │ │ │ │ └── Resources │ │ │ │ │ └── Info.plist │ │ │ └── libgodotsteam.debug.framework │ │ │ │ ├── libgodotsteam.debug │ │ │ │ ├── libsteam_api.dylib │ │ │ │ └── Resources │ │ │ │ └── Info.plist │ │ ├── win64 │ │ │ ├── steam_api64.dll │ │ │ ├── godotsteam.x86_64.dll │ │ │ ├── godotsteam.debug.x86_64.dll │ │ │ └── ~godotsteam.debug.x86_64.dll │ │ └── godotsteam.gdextension │ ├── netfox │ │ ├── plugin.cfg │ │ ├── rollback │ │ │ ├── rollback-freshness-store.gd │ │ │ ├── network-rollback.gd │ │ │ └── rollback-synchronizer.gd │ │ ├── properties │ │ │ ├── property-snapshot.gd │ │ │ ├── property-entry.gd │ │ │ └── property-cache.gd │ │ ├── icons │ │ │ ├── tick-interpolator.svg.import │ │ │ ├── state-synchronizer.svg.import │ │ │ ├── rollback-synchronizer.svg.import │ │ │ ├── rollback-synchronizer.svg │ │ │ ├── state-synchronizer.svg │ │ │ └── tick-interpolator.svg │ │ ├── LICENSE │ │ ├── README.md │ │ ├── state-synchronizer.gd │ │ ├── interpolators.gd │ │ ├── tick-interpolator.gd │ │ ├── network-performance.gd │ │ ├── netfox.gd │ │ ├── network-events.gd │ │ ├── network-time-synchronizer.gd │ │ └── network-time.gd │ └── netfox.internals │ │ ├── plugin.cfg │ │ ├── README.md │ │ ├── plugin.gd │ │ ├── LICENSE │ │ └── logger.gd ├── .gitignore ├── .gitattributes ├── assets │ ├── sounds │ │ ├── tap.wav │ │ ├── coin.wav │ │ ├── hurt.wav │ │ ├── jump.wav │ │ ├── power_up.wav │ │ ├── explosion.wav │ │ ├── tap.wav.import │ │ ├── coin.wav.import │ │ ├── hurt.wav.import │ │ ├── jump.wav.import │ │ ├── power_up.wav.import │ │ └── explosion.wav.import │ ├── sprites │ │ ├── coin.png │ │ ├── fruit.png │ │ ├── knight.png │ │ ├── platforms.png │ │ ├── slime_green.png │ │ ├── slime_purple.png │ │ ├── world_tileset.png │ │ ├── coin.png.import │ │ ├── fruit.png.import │ │ ├── knight.png.import │ │ ├── platforms.png.import │ │ ├── slime_green.png.import │ │ ├── slime_purple.png.import │ │ └── world_tileset.png.import │ ├── fonts │ │ ├── PixelOperator8.ttf │ │ ├── PixelOperator8-Bold.ttf │ │ ├── PixelOperator8.ttf.import │ │ └── PixelOperator8-Bold.ttf.import │ └── music │ │ ├── time_for_adventure.mp3 │ │ └── time_for_adventure.mp3.import ├── scripts │ ├── multiplayer │ │ ├── multiplayer_manager.gd │ │ ├── multiplayer_input.gd │ │ ├── steam │ │ │ └── steam_manager.gd │ │ ├── networks │ │ │ ├── enet_network.gd │ │ │ ├── network_manager.gd │ │ │ └── steam_network.gd │ │ └── multiplayer_controller.gd │ ├── coin.gd │ ├── finish_line.gd │ ├── platform.gd │ ├── killzone.gd │ ├── slime.gd │ ├── player.gd │ └── game_manager.gd ├── scenes │ ├── multiplayer │ │ └── networks │ │ │ ├── enet_network.tscn │ │ │ └── steam_network.tscn │ ├── music.tscn │ ├── killzone.tscn │ ├── platform.tscn │ ├── slime.tscn │ ├── player.tscn │ ├── coin.tscn │ └── multiplayer_player.tscn ├── default_bus_layout.tres ├── icon.svg ├── icon.svg.import └── project.godot ├── .gitignore ├── .github └── FUNDING.yml ├── README.md └── LICENSE /First Game/addons/steam-multiplayer-peer/linux32/.gdignore: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /First Game/addons/steam-multiplayer-peer/linux64/.gdignore: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /First Game/addons/steam-multiplayer-peer/osx/.gdignore: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /First Game/addons/steam-multiplayer-peer/win32/.gdignore: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /First Game/addons/steam-multiplayer-peer/win64/.gdignore: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /First Game/.gitignore: -------------------------------------------------------------------------------- 1 | # Godot 4+ specific ignores 2 | .godot/ 3 | -------------------------------------------------------------------------------- /First Game/.gitattributes: -------------------------------------------------------------------------------- 1 | # Normalize EOL for all files that Git considers text files. 2 | * text=auto eol=lf 3 | -------------------------------------------------------------------------------- /First Game/assets/sounds/tap.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BatteryAcid/godot-lag-compensation-netfox-1/HEAD/First Game/assets/sounds/tap.wav -------------------------------------------------------------------------------- /First Game/assets/sounds/coin.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BatteryAcid/godot-lag-compensation-netfox-1/HEAD/First Game/assets/sounds/coin.wav -------------------------------------------------------------------------------- /First Game/assets/sounds/hurt.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BatteryAcid/godot-lag-compensation-netfox-1/HEAD/First Game/assets/sounds/hurt.wav -------------------------------------------------------------------------------- /First Game/assets/sounds/jump.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BatteryAcid/godot-lag-compensation-netfox-1/HEAD/First Game/assets/sounds/jump.wav -------------------------------------------------------------------------------- /First Game/assets/sprites/coin.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BatteryAcid/godot-lag-compensation-netfox-1/HEAD/First Game/assets/sprites/coin.png -------------------------------------------------------------------------------- /First Game/assets/sounds/power_up.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BatteryAcid/godot-lag-compensation-netfox-1/HEAD/First Game/assets/sounds/power_up.wav -------------------------------------------------------------------------------- /First Game/assets/sprites/fruit.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BatteryAcid/godot-lag-compensation-netfox-1/HEAD/First Game/assets/sprites/fruit.png -------------------------------------------------------------------------------- /First Game/assets/sprites/knight.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BatteryAcid/godot-lag-compensation-netfox-1/HEAD/First Game/assets/sprites/knight.png -------------------------------------------------------------------------------- /First Game/assets/sounds/explosion.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BatteryAcid/godot-lag-compensation-netfox-1/HEAD/First Game/assets/sounds/explosion.wav -------------------------------------------------------------------------------- /First Game/assets/sprites/platforms.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BatteryAcid/godot-lag-compensation-netfox-1/HEAD/First Game/assets/sprites/platforms.png -------------------------------------------------------------------------------- /First Game/assets/fonts/PixelOperator8.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BatteryAcid/godot-lag-compensation-netfox-1/HEAD/First Game/assets/fonts/PixelOperator8.ttf -------------------------------------------------------------------------------- /First Game/assets/sprites/slime_green.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BatteryAcid/godot-lag-compensation-netfox-1/HEAD/First Game/assets/sprites/slime_green.png -------------------------------------------------------------------------------- /First Game/assets/sprites/slime_purple.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BatteryAcid/godot-lag-compensation-netfox-1/HEAD/First Game/assets/sprites/slime_purple.png -------------------------------------------------------------------------------- /First Game/assets/sprites/world_tileset.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BatteryAcid/godot-lag-compensation-netfox-1/HEAD/First Game/assets/sprites/world_tileset.png -------------------------------------------------------------------------------- /First Game/assets/fonts/PixelOperator8-Bold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BatteryAcid/godot-lag-compensation-netfox-1/HEAD/First Game/assets/fonts/PixelOperator8-Bold.ttf -------------------------------------------------------------------------------- /First Game/assets/music/time_for_adventure.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BatteryAcid/godot-lag-compensation-netfox-1/HEAD/First Game/assets/music/time_for_adventure.mp3 -------------------------------------------------------------------------------- /First Game/addons/godotsteam/win32/steam_api.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BatteryAcid/godot-lag-compensation-netfox-1/HEAD/First Game/addons/godotsteam/win32/steam_api.dll -------------------------------------------------------------------------------- /First Game/addons/godotsteam/linux32/libsteam_api.so: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BatteryAcid/godot-lag-compensation-netfox-1/HEAD/First Game/addons/godotsteam/linux32/libsteam_api.so -------------------------------------------------------------------------------- /First Game/addons/godotsteam/linux64/libsteam_api.so: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BatteryAcid/godot-lag-compensation-netfox-1/HEAD/First Game/addons/godotsteam/linux64/libsteam_api.so -------------------------------------------------------------------------------- /First Game/addons/godotsteam/osx/libsteam_api.dylib: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BatteryAcid/godot-lag-compensation-netfox-1/HEAD/First Game/addons/godotsteam/osx/libsteam_api.dylib -------------------------------------------------------------------------------- /First Game/addons/godotsteam/win64/steam_api64.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BatteryAcid/godot-lag-compensation-netfox-1/HEAD/First Game/addons/godotsteam/win64/steam_api64.dll -------------------------------------------------------------------------------- /First Game/addons/godotsteam/win32/godotsteam.x86_32.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BatteryAcid/godot-lag-compensation-netfox-1/HEAD/First Game/addons/godotsteam/win32/godotsteam.x86_32.dll -------------------------------------------------------------------------------- /First Game/addons/godotsteam/win64/godotsteam.x86_64.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BatteryAcid/godot-lag-compensation-netfox-1/HEAD/First Game/addons/godotsteam/win64/godotsteam.x86_64.dll -------------------------------------------------------------------------------- /First Game/addons/netfox/plugin.cfg: -------------------------------------------------------------------------------- 1 | [plugin] 2 | 3 | name="netfox" 4 | description="Shared internals for netfox addons" 5 | author="Tamas Galffy" 6 | version="1.7.0" 7 | script="netfox.gd" 8 | -------------------------------------------------------------------------------- /First Game/addons/godotsteam/linux32/libgodotsteam.x86_32.so: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BatteryAcid/godot-lag-compensation-netfox-1/HEAD/First Game/addons/godotsteam/linux32/libgodotsteam.x86_32.so -------------------------------------------------------------------------------- /First Game/addons/godotsteam/linux64/libgodotsteam.x86_64.so: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BatteryAcid/godot-lag-compensation-netfox-1/HEAD/First Game/addons/godotsteam/linux64/libgodotsteam.x86_64.so -------------------------------------------------------------------------------- /First Game/addons/godotsteam/win32/godotsteam.debug.x86_32.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BatteryAcid/godot-lag-compensation-netfox-1/HEAD/First Game/addons/godotsteam/win32/godotsteam.debug.x86_32.dll -------------------------------------------------------------------------------- /First Game/addons/godotsteam/win64/godotsteam.debug.x86_64.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BatteryAcid/godot-lag-compensation-netfox-1/HEAD/First Game/addons/godotsteam/win64/godotsteam.debug.x86_64.dll -------------------------------------------------------------------------------- /First Game/addons/steam-multiplayer-peer/win32/steam_api.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BatteryAcid/godot-lag-compensation-netfox-1/HEAD/First Game/addons/steam-multiplayer-peer/win32/steam_api.dll -------------------------------------------------------------------------------- /First Game/addons/steam-multiplayer-peer/win64/steam_api64.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BatteryAcid/godot-lag-compensation-netfox-1/HEAD/First Game/addons/steam-multiplayer-peer/win64/steam_api64.dll -------------------------------------------------------------------------------- /First Game/addons/steam-multiplayer-peer/win64/steam_api64.lib: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BatteryAcid/godot-lag-compensation-netfox-1/HEAD/First Game/addons/steam-multiplayer-peer/win64/steam_api64.lib -------------------------------------------------------------------------------- /First Game/addons/godotsteam/win64/~godotsteam.debug.x86_64.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BatteryAcid/godot-lag-compensation-netfox-1/HEAD/First Game/addons/godotsteam/win64/~godotsteam.debug.x86_64.dll -------------------------------------------------------------------------------- /First Game/addons/steam-multiplayer-peer/linux32/libsteam_api.so: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BatteryAcid/godot-lag-compensation-netfox-1/HEAD/First Game/addons/steam-multiplayer-peer/linux32/libsteam_api.so -------------------------------------------------------------------------------- /First Game/addons/steam-multiplayer-peer/linux64/libsteam_api.so: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BatteryAcid/godot-lag-compensation-netfox-1/HEAD/First Game/addons/steam-multiplayer-peer/linux64/libsteam_api.so -------------------------------------------------------------------------------- /First Game/addons/steam-multiplayer-peer/osx/libsteam_api.dylib: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BatteryAcid/godot-lag-compensation-netfox-1/HEAD/First Game/addons/steam-multiplayer-peer/osx/libsteam_api.dylib -------------------------------------------------------------------------------- /First Game/addons/godotsteam/linux32/libgodotsteam.debug.x86_32.so: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BatteryAcid/godot-lag-compensation-netfox-1/HEAD/First Game/addons/godotsteam/linux32/libgodotsteam.debug.x86_32.so -------------------------------------------------------------------------------- /First Game/addons/godotsteam/linux64/libgodotsteam.debug.x86_64.so: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BatteryAcid/godot-lag-compensation-netfox-1/HEAD/First Game/addons/godotsteam/linux64/libgodotsteam.debug.x86_64.so -------------------------------------------------------------------------------- /First Game/addons/netfox.internals/plugin.cfg: -------------------------------------------------------------------------------- 1 | [plugin] 2 | 3 | name="netfox.internals" 4 | description="Shared internals for netfox addons" 5 | author="Tamas Galffy" 6 | version="1.7.0" 7 | script="plugin.gd" 8 | -------------------------------------------------------------------------------- /First Game/addons/godotsteam/osx/libgodotsteam.framework/libgodotsteam: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BatteryAcid/godot-lag-compensation-netfox-1/HEAD/First Game/addons/godotsteam/osx/libgodotsteam.framework/libgodotsteam -------------------------------------------------------------------------------- /First Game/addons/godotsteam/osx/libgodotsteam.framework/libsteam_api.dylib: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BatteryAcid/godot-lag-compensation-netfox-1/HEAD/First Game/addons/godotsteam/osx/libgodotsteam.framework/libsteam_api.dylib -------------------------------------------------------------------------------- /First Game/addons/godotsteam/osx/libgodotsteam.debug.framework/libgodotsteam.debug: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BatteryAcid/godot-lag-compensation-netfox-1/HEAD/First Game/addons/godotsteam/osx/libgodotsteam.debug.framework/libgodotsteam.debug -------------------------------------------------------------------------------- /First Game/addons/godotsteam/osx/libgodotsteam.debug.framework/libsteam_api.dylib: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BatteryAcid/godot-lag-compensation-netfox-1/HEAD/First Game/addons/godotsteam/osx/libgodotsteam.debug.framework/libsteam_api.dylib -------------------------------------------------------------------------------- /First Game/scripts/multiplayer/multiplayer_manager.gd: -------------------------------------------------------------------------------- 1 | extends Node 2 | 3 | var host_mode_enabled = false 4 | var multiplayer_mode_enabled = false 5 | var respawn_point = Vector2(30, 20) 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /First Game/scripts/coin.gd: -------------------------------------------------------------------------------- 1 | extends Area2D 2 | 3 | @onready var game_manager = %GameManager 4 | @onready var animation_player = $AnimationPlayer 5 | 6 | func _on_body_entered(body): 7 | game_manager.add_point() 8 | animation_player.play("pickup") 9 | -------------------------------------------------------------------------------- /First Game/scripts/finish_line.gd: -------------------------------------------------------------------------------- 1 | extends Area2D 2 | 3 | func _on_body_entered(body): 4 | if MultiplayerManager.multiplayer_mode_enabled && multiplayer.get_unique_id() == body.player_id: 5 | print("Player %s WINS!" % multiplayer.get_unique_id()) 6 | -------------------------------------------------------------------------------- /First Game/addons/netfox.internals/README.md: -------------------------------------------------------------------------------- 1 | # netfox.internals 2 | 3 | Shared utilities for [netfox] addons. Not intended for standalone usage. 4 | 5 | Instead, check out the other addons in the [netfox] repository. 6 | 7 | 8 | [netfox]: https://github.com/foxssake/netfox 9 | -------------------------------------------------------------------------------- /First Game/addons/steam-multiplayer-peer/win32/steam-multiplayer-peer.windows.template_debug.x86_32.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BatteryAcid/godot-lag-compensation-netfox-1/HEAD/First Game/addons/steam-multiplayer-peer/win32/steam-multiplayer-peer.windows.template_debug.x86_32.dll -------------------------------------------------------------------------------- /First Game/addons/steam-multiplayer-peer/win64/steam-multiplayer-peer.windows.template_debug.x86_64.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BatteryAcid/godot-lag-compensation-netfox-1/HEAD/First Game/addons/steam-multiplayer-peer/win64/steam-multiplayer-peer.windows.template_debug.x86_64.dll -------------------------------------------------------------------------------- /First Game/addons/steam-multiplayer-peer/win64/~steam-multiplayer-peer.windows.template_debug.x86_64.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BatteryAcid/godot-lag-compensation-netfox-1/HEAD/First Game/addons/steam-multiplayer-peer/win64/~steam-multiplayer-peer.windows.template_debug.x86_64.dll -------------------------------------------------------------------------------- /First Game/addons/steam-multiplayer-peer/linux32/libsteam-multiplayer-peer.linux.template_debug.x86_32.so: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BatteryAcid/godot-lag-compensation-netfox-1/HEAD/First Game/addons/steam-multiplayer-peer/linux32/libsteam-multiplayer-peer.linux.template_debug.x86_32.so -------------------------------------------------------------------------------- /First Game/addons/steam-multiplayer-peer/linux32/libsteam-multiplayer-peer.linux.template_release.x86_32.so: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BatteryAcid/godot-lag-compensation-netfox-1/HEAD/First Game/addons/steam-multiplayer-peer/linux32/libsteam-multiplayer-peer.linux.template_release.x86_32.so -------------------------------------------------------------------------------- /First Game/addons/steam-multiplayer-peer/linux64/libsteam-multiplayer-peer.linux.template_debug.x86_64.so: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BatteryAcid/godot-lag-compensation-netfox-1/HEAD/First Game/addons/steam-multiplayer-peer/linux64/libsteam-multiplayer-peer.linux.template_debug.x86_64.so -------------------------------------------------------------------------------- /First Game/addons/steam-multiplayer-peer/linux64/libsteam-multiplayer-peer.linux.template_release.x86_64.so: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BatteryAcid/godot-lag-compensation-netfox-1/HEAD/First Game/addons/steam-multiplayer-peer/linux64/libsteam-multiplayer-peer.linux.template_release.x86_64.so -------------------------------------------------------------------------------- /First Game/addons/steam-multiplayer-peer/win32/steam-multiplayer-peer.windows.template_release.x86_32.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BatteryAcid/godot-lag-compensation-netfox-1/HEAD/First Game/addons/steam-multiplayer-peer/win32/steam-multiplayer-peer.windows.template_release.x86_32.dll -------------------------------------------------------------------------------- /First Game/addons/steam-multiplayer-peer/win64/steam-multiplayer-peer.windows.template_release.x86_64.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BatteryAcid/godot-lag-compensation-netfox-1/HEAD/First Game/addons/steam-multiplayer-peer/win64/steam-multiplayer-peer.windows.template_release.x86_64.dll -------------------------------------------------------------------------------- /First Game/addons/steam-multiplayer-peer/osx/steam-multiplayer-peer.macos.template_debug.framework/libsteam_api.dylib: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BatteryAcid/godot-lag-compensation-netfox-1/HEAD/First Game/addons/steam-multiplayer-peer/osx/steam-multiplayer-peer.macos.template_debug.framework/libsteam_api.dylib -------------------------------------------------------------------------------- /First Game/scenes/multiplayer/networks/enet_network.tscn: -------------------------------------------------------------------------------- 1 | [gd_scene load_steps=2 format=3 uid="uid://cay75a4bjl5f0"] 2 | 3 | [ext_resource type="Script" path="res://scripts/multiplayer/networks/enet_network.gd" id="1_g3r4g"] 4 | 5 | [node name="EnetNetwork" type="Node"] 6 | script = ExtResource("1_g3r4g") 7 | -------------------------------------------------------------------------------- /First Game/addons/steam-multiplayer-peer/osx/steam-multiplayer-peer.macos.template_release.framework/libsteam_api.dylib: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BatteryAcid/godot-lag-compensation-netfox-1/HEAD/First Game/addons/steam-multiplayer-peer/osx/steam-multiplayer-peer.macos.template_release.framework/libsteam_api.dylib -------------------------------------------------------------------------------- /First Game/scenes/multiplayer/networks/steam_network.tscn: -------------------------------------------------------------------------------- 1 | [gd_scene load_steps=2 format=3 uid="uid://cay466cydgq0k"] 2 | 3 | [ext_resource type="Script" path="res://scripts/multiplayer/networks/steam_network.gd" id="1_2bv4x"] 4 | 5 | [node name="SteamNetwork" type="Node"] 6 | script = ExtResource("1_2bv4x") 7 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Godot 4+ specific ignores 2 | .godot/ 3 | 4 | # Godot-specific ignores 5 | .import/ 6 | export.cfg 7 | export_presets.cfg 8 | 9 | # Imported translations (automatically generated from CSV files) 10 | *.translation 11 | 12 | # Mono-specific ignores 13 | .mono/ 14 | data_*/ 15 | mono_crash.*.json 16 | -------------------------------------------------------------------------------- /First Game/scenes/music.tscn: -------------------------------------------------------------------------------- 1 | [gd_scene load_steps=2 format=3 uid="uid://4kg150o2entl"] 2 | 3 | [ext_resource type="AudioStream" uid="uid://m73dm4jhs3eq" path="res://assets/music/time_for_adventure.mp3" id="1_q0dvm"] 4 | 5 | [node name="Music" type="AudioStreamPlayer2D"] 6 | stream = ExtResource("1_q0dvm") 7 | bus = &"Music" 8 | -------------------------------------------------------------------------------- /First Game/addons/steam-multiplayer-peer/osx/steam-multiplayer-peer.macos.template_debug.framework/steam-multiplayer-peer.macos.template_debug: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BatteryAcid/godot-lag-compensation-netfox-1/HEAD/First Game/addons/steam-multiplayer-peer/osx/steam-multiplayer-peer.macos.template_debug.framework/steam-multiplayer-peer.macos.template_debug -------------------------------------------------------------------------------- /First Game/addons/steam-multiplayer-peer/osx/steam-multiplayer-peer.macos.template_release.framework/libsteam-multiplayer-peer.macos.template_release: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BatteryAcid/godot-lag-compensation-netfox-1/HEAD/First Game/addons/steam-multiplayer-peer/osx/steam-multiplayer-peer.macos.template_release.framework/libsteam-multiplayer-peer.macos.template_release -------------------------------------------------------------------------------- /First Game/scripts/platform.gd: -------------------------------------------------------------------------------- 1 | extends AnimatableBody2D 2 | 3 | @export var animation_player_optional: AnimationPlayer 4 | 5 | func _on_player_connected(id): 6 | if not multiplayer.is_server(): 7 | animation_player_optional.stop() 8 | animation_player_optional.set_active(false) 9 | 10 | func _ready(): 11 | if animation_player_optional: 12 | multiplayer.peer_connected.connect(_on_player_connected) 13 | -------------------------------------------------------------------------------- /First Game/default_bus_layout.tres: -------------------------------------------------------------------------------- 1 | [gd_resource type="AudioBusLayout" format=3 uid="uid://ctxltu1rd20ra"] 2 | 3 | [resource] 4 | bus/1/name = &"Music" 5 | bus/1/solo = false 6 | bus/1/mute = false 7 | bus/1/bypass_fx = false 8 | bus/1/volume_db = -11.9576 9 | bus/1/send = &"Master" 10 | bus/2/name = &"SFX" 11 | bus/2/solo = false 12 | bus/2/mute = false 13 | bus/2/bypass_fx = false 14 | bus/2/volume_db = 0.0 15 | bus/2/send = &"Master" 16 | -------------------------------------------------------------------------------- /First Game/assets/music/time_for_adventure.mp3.import: -------------------------------------------------------------------------------- 1 | [remap] 2 | 3 | importer="mp3" 4 | type="AudioStreamMP3" 5 | uid="uid://m73dm4jhs3eq" 6 | path="res://.godot/imported/time_for_adventure.mp3-b8a49ae1cfc83b211be9d82e6e985655.mp3str" 7 | 8 | [deps] 9 | 10 | source_file="res://assets/music/time_for_adventure.mp3" 11 | dest_files=["res://.godot/imported/time_for_adventure.mp3-b8a49ae1cfc83b211be9d82e6e985655.mp3str"] 12 | 13 | [params] 14 | 15 | loop=true 16 | loop_offset=0.0 17 | bpm=0.0 18 | beat_count=0 19 | bar_beats=4 20 | -------------------------------------------------------------------------------- /First Game/scenes/killzone.tscn: -------------------------------------------------------------------------------- 1 | [gd_scene load_steps=2 format=3 uid="uid://dtrbka1ef726o"] 2 | 3 | [ext_resource type="Script" path="res://scripts/killzone.gd" id="1_bq345"] 4 | 5 | [node name="Killzone" type="Area2D"] 6 | collision_mask = 2 7 | script = ExtResource("1_bq345") 8 | 9 | [node name="Timer" type="Timer" parent="."] 10 | wait_time = 0.6 11 | one_shot = true 12 | 13 | [connection signal="body_entered" from="." to="." method="_on_body_entered"] 14 | [connection signal="timeout" from="Timer" to="." method="_on_timer_timeout"] 15 | -------------------------------------------------------------------------------- /First Game/scripts/killzone.gd: -------------------------------------------------------------------------------- 1 | extends Area2D 2 | 3 | @onready var timer = $Timer 4 | 5 | func _on_body_entered(body): 6 | if not MultiplayerManager.multiplayer_mode_enabled: 7 | print("You died!") 8 | Engine.time_scale = 0.5 9 | body.get_node("CollisionShape2D").queue_free() 10 | timer.start() 11 | else: 12 | _multiplayer_dead(body) 13 | 14 | func _multiplayer_dead(body): 15 | if multiplayer.is_server() && body.alive: 16 | body.mark_dead() 17 | 18 | func _on_timer_timeout(): 19 | Engine.time_scale = 1.0 20 | get_tree().reload_current_scene() 21 | -------------------------------------------------------------------------------- /First Game/scripts/slime.gd: -------------------------------------------------------------------------------- 1 | extends Node2D 2 | 3 | const SPEED = 60 4 | 5 | var direction = 1 6 | 7 | @onready var ray_cast_right = $RayCastRight 8 | @onready var ray_cast_left = $RayCastLeft 9 | @onready var animated_sprite = $AnimatedSprite2D 10 | 11 | func _ready(): 12 | NetworkTime.on_tick.connect(_tick) 13 | 14 | func _tick(delta, tick): 15 | if ray_cast_right.is_colliding(): 16 | direction = -1 17 | animated_sprite.flip_h = true 18 | if ray_cast_left.is_colliding(): 19 | direction = 1 20 | animated_sprite.flip_h = false 21 | 22 | position.x += direction * SPEED * delta 23 | -------------------------------------------------------------------------------- /First Game/assets/sounds/tap.wav.import: -------------------------------------------------------------------------------- 1 | [remap] 2 | 3 | importer="wav" 4 | type="AudioStreamWAV" 5 | uid="uid://duhe7my8ilfph" 6 | path="res://.godot/imported/tap.wav-78d4c5a48b21a853d89bec74f20510e7.sample" 7 | 8 | [deps] 9 | 10 | source_file="res://assets/sounds/tap.wav" 11 | dest_files=["res://.godot/imported/tap.wav-78d4c5a48b21a853d89bec74f20510e7.sample"] 12 | 13 | [params] 14 | 15 | force/8_bit=false 16 | force/mono=false 17 | force/max_rate=false 18 | force/max_rate_hz=44100 19 | edit/trim=false 20 | edit/normalize=false 21 | edit/loop_mode=0 22 | edit/loop_begin=0 23 | edit/loop_end=-1 24 | compress/mode=0 25 | -------------------------------------------------------------------------------- /First Game/assets/sounds/coin.wav.import: -------------------------------------------------------------------------------- 1 | [remap] 2 | 3 | importer="wav" 4 | type="AudioStreamWAV" 5 | uid="uid://hxv3svfwkg67" 6 | path="res://.godot/imported/coin.wav-9081ee1c6d81d9c34d08bc916297b892.sample" 7 | 8 | [deps] 9 | 10 | source_file="res://assets/sounds/coin.wav" 11 | dest_files=["res://.godot/imported/coin.wav-9081ee1c6d81d9c34d08bc916297b892.sample"] 12 | 13 | [params] 14 | 15 | force/8_bit=false 16 | force/mono=false 17 | force/max_rate=false 18 | force/max_rate_hz=44100 19 | edit/trim=false 20 | edit/normalize=false 21 | edit/loop_mode=0 22 | edit/loop_begin=0 23 | edit/loop_end=-1 24 | compress/mode=0 25 | -------------------------------------------------------------------------------- /First Game/assets/sounds/hurt.wav.import: -------------------------------------------------------------------------------- 1 | [remap] 2 | 3 | importer="wav" 4 | type="AudioStreamWAV" 5 | uid="uid://d3ga5iqgco704" 6 | path="res://.godot/imported/hurt.wav-792baeb99505afd6a1496d4e4330b023.sample" 7 | 8 | [deps] 9 | 10 | source_file="res://assets/sounds/hurt.wav" 11 | dest_files=["res://.godot/imported/hurt.wav-792baeb99505afd6a1496d4e4330b023.sample"] 12 | 13 | [params] 14 | 15 | force/8_bit=false 16 | force/mono=false 17 | force/max_rate=false 18 | force/max_rate_hz=44100 19 | edit/trim=false 20 | edit/normalize=false 21 | edit/loop_mode=0 22 | edit/loop_begin=0 23 | edit/loop_end=-1 24 | compress/mode=0 25 | -------------------------------------------------------------------------------- /First Game/assets/sounds/jump.wav.import: -------------------------------------------------------------------------------- 1 | [remap] 2 | 3 | importer="wav" 4 | type="AudioStreamWAV" 5 | uid="uid://b55xtmvwkoslc" 6 | path="res://.godot/imported/jump.wav-395b727cde98999423d5c020c9c3492f.sample" 7 | 8 | [deps] 9 | 10 | source_file="res://assets/sounds/jump.wav" 11 | dest_files=["res://.godot/imported/jump.wav-395b727cde98999423d5c020c9c3492f.sample"] 12 | 13 | [params] 14 | 15 | force/8_bit=false 16 | force/mono=false 17 | force/max_rate=false 18 | force/max_rate_hz=44100 19 | edit/trim=false 20 | edit/normalize=false 21 | edit/loop_mode=0 22 | edit/loop_begin=0 23 | edit/loop_end=-1 24 | compress/mode=0 25 | -------------------------------------------------------------------------------- /First Game/assets/sounds/power_up.wav.import: -------------------------------------------------------------------------------- 1 | [remap] 2 | 3 | importer="wav" 4 | type="AudioStreamWAV" 5 | uid="uid://chhyiookx3ilk" 6 | path="res://.godot/imported/power_up.wav-8349ffe570559470036ebff4b80f7fc0.sample" 7 | 8 | [deps] 9 | 10 | source_file="res://assets/sounds/power_up.wav" 11 | dest_files=["res://.godot/imported/power_up.wav-8349ffe570559470036ebff4b80f7fc0.sample"] 12 | 13 | [params] 14 | 15 | force/8_bit=false 16 | force/mono=false 17 | force/max_rate=false 18 | force/max_rate_hz=44100 19 | edit/trim=false 20 | edit/normalize=false 21 | edit/loop_mode=0 22 | edit/loop_begin=0 23 | edit/loop_end=-1 24 | compress/mode=0 25 | -------------------------------------------------------------------------------- /First Game/assets/sounds/explosion.wav.import: -------------------------------------------------------------------------------- 1 | [remap] 2 | 3 | importer="wav" 4 | type="AudioStreamWAV" 5 | uid="uid://bv5pmdha600lx" 6 | path="res://.godot/imported/explosion.wav-52e05e8d4b6600106c8dde082c90f915.sample" 7 | 8 | [deps] 9 | 10 | source_file="res://assets/sounds/explosion.wav" 11 | dest_files=["res://.godot/imported/explosion.wav-52e05e8d4b6600106c8dde082c90f915.sample"] 12 | 13 | [params] 14 | 15 | force/8_bit=false 16 | force/mono=false 17 | force/max_rate=false 18 | force/max_rate_hz=44100 19 | edit/trim=false 20 | edit/normalize=false 21 | edit/loop_mode=0 22 | edit/loop_begin=0 23 | edit/loop_end=-1 24 | compress/mode=0 25 | -------------------------------------------------------------------------------- /First Game/addons/netfox/rollback/rollback-freshness-store.gd: -------------------------------------------------------------------------------- 1 | extends RefCounted 2 | class_name RollbackFreshnessStore 3 | 4 | ## This class tracks nodes and whether they have processed any given tick during 5 | ## a rollback. 6 | 7 | var _data: Dictionary = {} 8 | 9 | func is_fresh(node: Node, tick: int) -> bool: 10 | if not _data.has(tick): 11 | return true 12 | 13 | if not _data[tick].has(node): 14 | return true 15 | 16 | return false 17 | 18 | func notify_processed(node: Node, tick: int): 19 | if not _data.has(tick): 20 | _data[tick] = {} 21 | 22 | _data[tick][node] = true 23 | 24 | func trim(): 25 | while _data.size() > NetworkRollback.history_limit: 26 | _data.erase(_data.keys().min()) 27 | -------------------------------------------------------------------------------- /First Game/scripts/multiplayer/multiplayer_input.gd: -------------------------------------------------------------------------------- 1 | class_name PlayerInput extends Node 2 | 3 | var input_direction = Vector2.ZERO 4 | var input_jump = 0 5 | 6 | # Called when the node enters the scene tree for the first time. 7 | func _ready(): 8 | NetworkTime.before_tick_loop.connect(_gather) 9 | 10 | if get_multiplayer_authority() != multiplayer.get_unique_id(): 11 | set_process(false) 12 | set_physics_process(false) 13 | 14 | input_direction = Input.get_axis("move_left", "move_right") 15 | 16 | func _gather(): 17 | if not is_multiplayer_authority(): 18 | return 19 | 20 | input_direction = Input.get_axis("move_left", "move_right") 21 | 22 | func _process(delta): 23 | input_jump = Input.get_action_strength("jump") 24 | -------------------------------------------------------------------------------- /First Game/addons/netfox/properties/property-snapshot.gd: -------------------------------------------------------------------------------- 1 | extends Object 2 | class_name PropertySnapshot 3 | 4 | static func extract(properties: Array[PropertyEntry]) -> Dictionary: 5 | var result = {} 6 | for property in properties: 7 | result[property.to_string()] = property.get_value() 8 | result.make_read_only() 9 | return result 10 | 11 | static func apply(properties: Dictionary, cache: PropertyCache): 12 | for property in properties: 13 | var pe = cache.get_entry(property) 14 | var value = properties[property] 15 | pe.set_value(value) 16 | 17 | static func merge(a: Dictionary, b: Dictionary) -> Dictionary: 18 | var result = {} 19 | for key in a: 20 | result[key] = a[key] 21 | for key in b: 22 | result[key] = b[key] 23 | return result 24 | -------------------------------------------------------------------------------- /First Game/addons/netfox/properties/property-entry.gd: -------------------------------------------------------------------------------- 1 | extends RefCounted 2 | class_name PropertyEntry 3 | 4 | var _path: String 5 | var node: Node 6 | var property: String 7 | 8 | func get_value() -> Variant: 9 | return node.get_indexed(property) 10 | 11 | func set_value(value): 12 | node.set_indexed(property, value) 13 | 14 | func is_valid() -> bool: 15 | if node == null: 16 | return false 17 | 18 | if get_value() == null: 19 | return false 20 | 21 | return true 22 | 23 | func _to_string() -> String: 24 | return _path 25 | 26 | static func parse(root: Node, path: String) -> PropertyEntry: 27 | var result = PropertyEntry.new() 28 | result.node = root.get_node(NodePath(path)) 29 | result.property = path.erase(0, path.find(":") + 1) 30 | result._path = path 31 | return result 32 | -------------------------------------------------------------------------------- /First Game/addons/netfox/properties/property-cache.gd: -------------------------------------------------------------------------------- 1 | extends RefCounted 2 | class_name PropertyCache 3 | 4 | var root: Node 5 | var _cache: Dictionary = {} 6 | 7 | static var _logger: _NetfoxLogger = _NetfoxLogger.for_netfox("PropertyCache") 8 | 9 | func _init(p_root: Node): 10 | root = p_root 11 | 12 | func get_entry(path: String) -> PropertyEntry: 13 | if not _cache.has(path): 14 | var parsed = PropertyEntry.parse(root, path) 15 | if not parsed.is_valid(): 16 | _logger.warning("Invalid property path: %s" % path) 17 | _cache[path] = parsed 18 | return _cache[path] 19 | 20 | func properties() -> Array: 21 | var result: Array[PropertyEntry] 22 | # Can be slow, but no other way to do this with type-safety 23 | # See: https://github.com/godotengine/godot/issues/72627 24 | result.assign(_cache.values()) 25 | return result 26 | -------------------------------------------------------------------------------- /First Game/assets/fonts/PixelOperator8.ttf.import: -------------------------------------------------------------------------------- 1 | [remap] 2 | 3 | importer="font_data_dynamic" 4 | type="FontFile" 5 | uid="uid://dbjl1e6kdxpl6" 6 | path="res://.godot/imported/PixelOperator8.ttf-6f9f01766aff16f52046b880ffb8d367.fontdata" 7 | 8 | [deps] 9 | 10 | source_file="res://assets/fonts/PixelOperator8.ttf" 11 | dest_files=["res://.godot/imported/PixelOperator8.ttf-6f9f01766aff16f52046b880ffb8d367.fontdata"] 12 | 13 | [params] 14 | 15 | Rendering=null 16 | antialiasing=1 17 | generate_mipmaps=false 18 | multichannel_signed_distance_field=false 19 | msdf_pixel_range=8 20 | msdf_size=48 21 | allow_system_fallback=true 22 | force_autohinter=false 23 | hinting=1 24 | subpixel_positioning=1 25 | oversampling=0.0 26 | Fallbacks=null 27 | fallbacks=[] 28 | Compress=null 29 | compress=true 30 | preload=[] 31 | language_support={} 32 | script_support={} 33 | opentype_features={} 34 | -------------------------------------------------------------------------------- /First Game/assets/fonts/PixelOperator8-Bold.ttf.import: -------------------------------------------------------------------------------- 1 | [remap] 2 | 3 | importer="font_data_dynamic" 4 | type="FontFile" 5 | uid="uid://c53kogtyjwsss" 6 | path="res://.godot/imported/PixelOperator8-Bold.ttf-74faf550739674ad3170f08e646e0614.fontdata" 7 | 8 | [deps] 9 | 10 | source_file="res://assets/fonts/PixelOperator8-Bold.ttf" 11 | dest_files=["res://.godot/imported/PixelOperator8-Bold.ttf-74faf550739674ad3170f08e646e0614.fontdata"] 12 | 13 | [params] 14 | 15 | Rendering=null 16 | antialiasing=1 17 | generate_mipmaps=false 18 | multichannel_signed_distance_field=false 19 | msdf_pixel_range=8 20 | msdf_size=48 21 | allow_system_fallback=true 22 | force_autohinter=false 23 | hinting=1 24 | subpixel_positioning=1 25 | oversampling=0.0 26 | Fallbacks=null 27 | fallbacks=[] 28 | Compress=null 29 | compress=true 30 | preload=[] 31 | language_support={} 32 | script_support={} 33 | opentype_features={} 34 | -------------------------------------------------------------------------------- /First Game/addons/steam-multiplayer-peer/README.md: -------------------------------------------------------------------------------- 1 | # Steam Sockets Multiplayer Peer para Godot 4 via GDExtension 2 | 3 | ### Can be added to a project that has GodotSteam 4 | It uses some codes that are close to GodotSteam's SteamNetworkingSockets(), but all aimed at integrating Godot 4's MultiplayerPeerExtension 5 | 6 | ### It does not use lobbies 7 | The extension remains extremely simple and if necessary to have lobbies it must be implemented one level higher, it is recommended to use godotsteam. 8 | 9 | ### Previous contributions 10 | This code was built on top of small experiments by Zennyth[https://github.com/Zennyth] , greenfox1505[https://github.com/greenfox1505] and MichaelMacha[https://github.com/MichaelMacha] 11 | 12 | ### No channel support currently 13 | At some point I intend to integrate channels to be used in rpcs commands, but currently it is only necessary to use channel 0 or the default rpcs 14 | -------------------------------------------------------------------------------- /First Game/addons/godotsteam/osx/libgodotsteam.framework/Resources/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleExecutable 6 | libgodotsteam 7 | CFBundleIdentifier 8 | org.coaguco.godotsteam 9 | CFBundleInfoDictionaryVersion 10 | 6.0 11 | CFBundleName 12 | libgodotsteam 13 | CFBundlePackageType 14 | FMWK 15 | CFBundleShortVersionString 16 | 4.5.2 17 | CFBundleSupportedPlatforms 18 | 19 | MacOSX 20 | 21 | CFBundleVersion 22 | 4.5.2 23 | LSMinimumSystemVersion 24 | 10.12 25 | 26 | -------------------------------------------------------------------------------- /First Game/assets/sprites/coin.png.import: -------------------------------------------------------------------------------- 1 | [remap] 2 | 3 | importer="texture" 4 | type="CompressedTexture2D" 5 | uid="uid://bped01tsjeycn" 6 | path="res://.godot/imported/coin.png-c8309bf0f8fb5f3a7d1e96a4eb3f02ce.ctex" 7 | metadata={ 8 | "vram_texture": false 9 | } 10 | 11 | [deps] 12 | 13 | source_file="res://assets/sprites/coin.png" 14 | dest_files=["res://.godot/imported/coin.png-c8309bf0f8fb5f3a7d1e96a4eb3f02ce.ctex"] 15 | 16 | [params] 17 | 18 | compress/mode=0 19 | compress/high_quality=false 20 | compress/lossy_quality=0.7 21 | compress/hdr_compression=1 22 | compress/normal_map=0 23 | compress/channel_pack=0 24 | mipmaps/generate=false 25 | mipmaps/limit=-1 26 | roughness/mode=0 27 | roughness/src_normal="" 28 | process/fix_alpha_border=true 29 | process/premult_alpha=false 30 | process/normal_map_invert_y=false 31 | process/hdr_as_srgb=false 32 | process/hdr_clamp_exposure=false 33 | process/size_limit=0 34 | detect_3d/compress_to=1 35 | -------------------------------------------------------------------------------- /First Game/icon.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /First Game/addons/godotsteam/osx/libgodotsteam.debug.framework/Resources/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleExecutable 6 | libgodotsteam.debug 7 | CFBundleIdentifier 8 | org.coaguco.godotsteam 9 | CFBundleInfoDictionaryVersion 10 | 6.0 11 | CFBundleName 12 | libgodotsteam.debug 13 | CFBundlePackageType 14 | FMWK 15 | CFBundleShortVersionString 16 | 4.5.2 17 | CFBundleSupportedPlatforms 18 | 19 | MacOSX 20 | 21 | CFBundleVersion 22 | 4.5.2 23 | LSMinimumSystemVersion 24 | 10.12 25 | 26 | -------------------------------------------------------------------------------- /First Game/assets/sprites/fruit.png.import: -------------------------------------------------------------------------------- 1 | [remap] 2 | 3 | importer="texture" 4 | type="CompressedTexture2D" 5 | uid="uid://gx1xihdbejvu" 6 | path="res://.godot/imported/fruit.png-3735163b668af10c2b35b52cba81b68a.ctex" 7 | metadata={ 8 | "vram_texture": false 9 | } 10 | 11 | [deps] 12 | 13 | source_file="res://assets/sprites/fruit.png" 14 | dest_files=["res://.godot/imported/fruit.png-3735163b668af10c2b35b52cba81b68a.ctex"] 15 | 16 | [params] 17 | 18 | compress/mode=0 19 | compress/high_quality=false 20 | compress/lossy_quality=0.7 21 | compress/hdr_compression=1 22 | compress/normal_map=0 23 | compress/channel_pack=0 24 | mipmaps/generate=false 25 | mipmaps/limit=-1 26 | roughness/mode=0 27 | roughness/src_normal="" 28 | process/fix_alpha_border=true 29 | process/premult_alpha=false 30 | process/normal_map_invert_y=false 31 | process/hdr_as_srgb=false 32 | process/hdr_clamp_exposure=false 33 | process/size_limit=0 34 | detect_3d/compress_to=1 35 | -------------------------------------------------------------------------------- /First Game/assets/sprites/knight.png.import: -------------------------------------------------------------------------------- 1 | [remap] 2 | 3 | importer="texture" 4 | type="CompressedTexture2D" 5 | uid="uid://b8cmjj8vq3r8d" 6 | path="res://.godot/imported/knight.png-7c67c83d34932624952797d9e971a644.ctex" 7 | metadata={ 8 | "vram_texture": false 9 | } 10 | 11 | [deps] 12 | 13 | source_file="res://assets/sprites/knight.png" 14 | dest_files=["res://.godot/imported/knight.png-7c67c83d34932624952797d9e971a644.ctex"] 15 | 16 | [params] 17 | 18 | compress/mode=0 19 | compress/high_quality=false 20 | compress/lossy_quality=0.7 21 | compress/hdr_compression=1 22 | compress/normal_map=0 23 | compress/channel_pack=0 24 | mipmaps/generate=false 25 | mipmaps/limit=-1 26 | roughness/mode=0 27 | roughness/src_normal="" 28 | process/fix_alpha_border=true 29 | process/premult_alpha=false 30 | process/normal_map_invert_y=false 31 | process/hdr_as_srgb=false 32 | process/hdr_clamp_exposure=false 33 | process/size_limit=0 34 | detect_3d/compress_to=1 35 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] 4 | patreon: BatteryAcidDev # Replace with a single Patreon username 5 | open_collective: # Replace with a single Open Collective username 6 | ko_fi: # Replace with a single Ko-fi username 7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 9 | liberapay: # Replace with a single Liberapay username 10 | issuehunt: # Replace with a single IssueHunt username 11 | lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry 12 | polar: # Replace with a single Polar username 13 | buy_me_a_coffee: batteryaciddev # Replace with a single Buy Me a Coffee username 14 | custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] 15 | -------------------------------------------------------------------------------- /First Game/assets/sprites/platforms.png.import: -------------------------------------------------------------------------------- 1 | [remap] 2 | 3 | importer="texture" 4 | type="CompressedTexture2D" 5 | uid="uid://cxuqjimd7csiq" 6 | path="res://.godot/imported/platforms.png-3869606db457611ed4193d705dc364e4.ctex" 7 | metadata={ 8 | "vram_texture": false 9 | } 10 | 11 | [deps] 12 | 13 | source_file="res://assets/sprites/platforms.png" 14 | dest_files=["res://.godot/imported/platforms.png-3869606db457611ed4193d705dc364e4.ctex"] 15 | 16 | [params] 17 | 18 | compress/mode=0 19 | compress/high_quality=false 20 | compress/lossy_quality=0.7 21 | compress/hdr_compression=1 22 | compress/normal_map=0 23 | compress/channel_pack=0 24 | mipmaps/generate=false 25 | mipmaps/limit=-1 26 | roughness/mode=0 27 | roughness/src_normal="" 28 | process/fix_alpha_border=true 29 | process/premult_alpha=false 30 | process/normal_map_invert_y=false 31 | process/hdr_as_srgb=false 32 | process/hdr_clamp_exposure=false 33 | process/size_limit=0 34 | detect_3d/compress_to=1 35 | -------------------------------------------------------------------------------- /First Game/assets/sprites/slime_green.png.import: -------------------------------------------------------------------------------- 1 | [remap] 2 | 3 | importer="texture" 4 | type="CompressedTexture2D" 5 | uid="uid://bl53gpvg5mh1y" 6 | path="res://.godot/imported/slime_green.png-f6349164bf3a0f5189bb927b97af9c58.ctex" 7 | metadata={ 8 | "vram_texture": false 9 | } 10 | 11 | [deps] 12 | 13 | source_file="res://assets/sprites/slime_green.png" 14 | dest_files=["res://.godot/imported/slime_green.png-f6349164bf3a0f5189bb927b97af9c58.ctex"] 15 | 16 | [params] 17 | 18 | compress/mode=0 19 | compress/high_quality=false 20 | compress/lossy_quality=0.7 21 | compress/hdr_compression=1 22 | compress/normal_map=0 23 | compress/channel_pack=0 24 | mipmaps/generate=false 25 | mipmaps/limit=-1 26 | roughness/mode=0 27 | roughness/src_normal="" 28 | process/fix_alpha_border=true 29 | process/premult_alpha=false 30 | process/normal_map_invert_y=false 31 | process/hdr_as_srgb=false 32 | process/hdr_clamp_exposure=false 33 | process/size_limit=0 34 | detect_3d/compress_to=1 35 | -------------------------------------------------------------------------------- /First Game/assets/sprites/slime_purple.png.import: -------------------------------------------------------------------------------- 1 | [remap] 2 | 3 | importer="texture" 4 | type="CompressedTexture2D" 5 | uid="uid://bwoec51f6mei0" 6 | path="res://.godot/imported/slime_purple.png-26dc5ddef235ce6a400e78e0d532b050.ctex" 7 | metadata={ 8 | "vram_texture": false 9 | } 10 | 11 | [deps] 12 | 13 | source_file="res://assets/sprites/slime_purple.png" 14 | dest_files=["res://.godot/imported/slime_purple.png-26dc5ddef235ce6a400e78e0d532b050.ctex"] 15 | 16 | [params] 17 | 18 | compress/mode=0 19 | compress/high_quality=false 20 | compress/lossy_quality=0.7 21 | compress/hdr_compression=1 22 | compress/normal_map=0 23 | compress/channel_pack=0 24 | mipmaps/generate=false 25 | mipmaps/limit=-1 26 | roughness/mode=0 27 | roughness/src_normal="" 28 | process/fix_alpha_border=true 29 | process/premult_alpha=false 30 | process/normal_map_invert_y=false 31 | process/hdr_as_srgb=false 32 | process/hdr_clamp_exposure=false 33 | process/size_limit=0 34 | detect_3d/compress_to=1 35 | -------------------------------------------------------------------------------- /First Game/assets/sprites/world_tileset.png.import: -------------------------------------------------------------------------------- 1 | [remap] 2 | 3 | importer="texture" 4 | type="CompressedTexture2D" 5 | uid="uid://d0q2g65ahgok" 6 | path="res://.godot/imported/world_tileset.png-61a32465f33c3d9d3bfecb75b6485009.ctex" 7 | metadata={ 8 | "vram_texture": false 9 | } 10 | 11 | [deps] 12 | 13 | source_file="res://assets/sprites/world_tileset.png" 14 | dest_files=["res://.godot/imported/world_tileset.png-61a32465f33c3d9d3bfecb75b6485009.ctex"] 15 | 16 | [params] 17 | 18 | compress/mode=0 19 | compress/high_quality=false 20 | compress/lossy_quality=0.7 21 | compress/hdr_compression=1 22 | compress/normal_map=0 23 | compress/channel_pack=0 24 | mipmaps/generate=false 25 | mipmaps/limit=-1 26 | roughness/mode=0 27 | roughness/src_normal="" 28 | process/fix_alpha_border=true 29 | process/premult_alpha=false 30 | process/normal_map_invert_y=false 31 | process/hdr_as_srgb=false 32 | process/hdr_clamp_exposure=false 33 | process/size_limit=0 34 | detect_3d/compress_to=1 35 | -------------------------------------------------------------------------------- /First Game/addons/godotsteam/godotsteam.gdextension: -------------------------------------------------------------------------------- 1 | [configuration] 2 | entry_symbol = "godotsteam_init" 3 | compatibility_minimum = 4.1 4 | 5 | [libraries] 6 | macos.debug = "osx/libgodotsteam.debug.framework" 7 | macos.release = "osx/libgodotsteam.framework" 8 | windows.debug.x86_64 = "win64/godotsteam.debug.x86_64.dll" 9 | windows.debug.x86_32 = "win32/godotsteam.debug.x86_32.dll" 10 | windows.release.x86_64 = "win64/godotsteam.x86_64.dll" 11 | windows.release.x86_32 = "win32/godotsteam.x86_32.dll" 12 | linux.debug.x86_64 = "linux64/libgodotsteam.debug.x86_64.so" 13 | linux.debug.x86_32 = "linux32/libgodotsteam.debug.x86_32.so" 14 | linux.release.x86_64 = "linux64/libgodotsteam.x86_64.so" 15 | linux.release.x86_32 = "linux32/libgodotsteam.x86_32.so" 16 | 17 | [dependencies] 18 | windows.x86_64 = { "win64/steam_api64.dll": "" } 19 | windows.x86_32 = { "win32/steam_api.dll": "" } 20 | linux.x86_64 = { "linux64/libsteam_api.so": "" } 21 | linux.x86_32 = { "linux32/libsteam_api.so": "" } 22 | -------------------------------------------------------------------------------- /First Game/icon.svg.import: -------------------------------------------------------------------------------- 1 | [remap] 2 | 3 | importer="texture" 4 | type="CompressedTexture2D" 5 | uid="uid://cyfxuch6qa87f" 6 | path="res://.godot/imported/icon.svg-218a8f2b3041327d8a5756f3a245f83b.ctex" 7 | metadata={ 8 | "vram_texture": false 9 | } 10 | 11 | [deps] 12 | 13 | source_file="res://icon.svg" 14 | dest_files=["res://.godot/imported/icon.svg-218a8f2b3041327d8a5756f3a245f83b.ctex"] 15 | 16 | [params] 17 | 18 | compress/mode=0 19 | compress/high_quality=false 20 | compress/lossy_quality=0.7 21 | compress/hdr_compression=1 22 | compress/normal_map=0 23 | compress/channel_pack=0 24 | mipmaps/generate=false 25 | mipmaps/limit=-1 26 | roughness/mode=0 27 | roughness/src_normal="" 28 | process/fix_alpha_border=true 29 | process/premult_alpha=false 30 | process/normal_map_invert_y=false 31 | process/hdr_as_srgb=false 32 | process/hdr_clamp_exposure=false 33 | process/size_limit=0 34 | detect_3d/compress_to=1 35 | svg/scale=1.0 36 | editor/scale_with_editor_scale=false 37 | editor/convert_colors_with_editor_theme=false 38 | -------------------------------------------------------------------------------- /First Game/addons/netfox.internals/plugin.gd: -------------------------------------------------------------------------------- 1 | @tool 2 | extends EditorPlugin 3 | 4 | var SETTINGS = [ 5 | _NetfoxLogger.make_setting("netfox/logging/log_level") 6 | ] 7 | 8 | func _enter_tree(): 9 | for setting in SETTINGS: 10 | add_setting(setting) 11 | 12 | func _exit_tree(): 13 | if ProjectSettings.get_setting("netfox/general/clear_settings", false): 14 | for setting in SETTINGS: 15 | remove_setting(setting) 16 | 17 | func add_setting(setting: Dictionary): 18 | if ProjectSettings.has_setting(setting.name): 19 | return 20 | 21 | ProjectSettings.set_setting(setting.name, setting.value) 22 | ProjectSettings.set_initial_value(setting.name, setting.value) 23 | ProjectSettings.add_property_info({ 24 | "name": setting.get("name"), 25 | "type": setting.get("type"), 26 | "hint": setting.get("hint", PROPERTY_HINT_NONE), 27 | "hint_string": setting.get("hint_string", "") 28 | }) 29 | 30 | func remove_setting(setting: Dictionary): 31 | if not ProjectSettings.has_setting(setting.name): 32 | return 33 | 34 | ProjectSettings.clear(setting.name) 35 | -------------------------------------------------------------------------------- /First Game/scripts/multiplayer/steam/steam_manager.gd: -------------------------------------------------------------------------------- 1 | extends Node 2 | 3 | var is_owned: bool = false 4 | var steam_app_id: int = 480 # Test game app id 5 | var steam_id: int = 0 6 | var steam_username: String = "" 7 | 8 | var lobby_id = 0 9 | var lobby_max_members = 10 10 | 11 | func _init(): 12 | print("Init Steam") 13 | OS.set_environment("SteamAppId", str(steam_app_id)) 14 | OS.set_environment("SteamGameId", str(steam_app_id)) 15 | 16 | func _process(delta): 17 | Steam.run_callbacks() 18 | 19 | func initialize_steam(): 20 | var initialize_response: Dictionary = Steam.steamInitEx() 21 | print("Did Steam Initialize?: %s " % initialize_response) 22 | 23 | if initialize_response['status'] > 0: 24 | print("Failed to init Steam! Shutting down. %s" % initialize_response) 25 | get_tree().quit() 26 | 27 | is_owned = Steam.isSubscribed() 28 | steam_id = Steam.getSteamID() 29 | steam_username = Steam.getPersonaName() 30 | 31 | print("steam_id %s" % steam_id) 32 | 33 | if is_owned == false: 34 | print("User does not own game!") 35 | get_tree().quit() 36 | -------------------------------------------------------------------------------- /First Game/addons/netfox/icons/tick-interpolator.svg.import: -------------------------------------------------------------------------------- 1 | [remap] 2 | 3 | importer="texture" 4 | type="CompressedTexture2D" 5 | uid="uid://cpaqxcohxtb68" 6 | path="res://.godot/imported/tick-interpolator.svg-c60124cd7d287f516c89a6022efef330.ctex" 7 | metadata={ 8 | "vram_texture": false 9 | } 10 | 11 | [deps] 12 | 13 | source_file="res://addons/netfox/icons/tick-interpolator.svg" 14 | dest_files=["res://.godot/imported/tick-interpolator.svg-c60124cd7d287f516c89a6022efef330.ctex"] 15 | 16 | [params] 17 | 18 | compress/mode=0 19 | compress/high_quality=false 20 | compress/lossy_quality=0.7 21 | compress/hdr_compression=1 22 | compress/normal_map=0 23 | compress/channel_pack=0 24 | mipmaps/generate=false 25 | mipmaps/limit=-1 26 | roughness/mode=0 27 | roughness/src_normal="" 28 | process/fix_alpha_border=true 29 | process/premult_alpha=false 30 | process/normal_map_invert_y=false 31 | process/hdr_as_srgb=false 32 | process/hdr_clamp_exposure=false 33 | process/size_limit=0 34 | detect_3d/compress_to=1 35 | svg/scale=1.0 36 | editor/scale_with_editor_scale=false 37 | editor/convert_colors_with_editor_theme=false 38 | -------------------------------------------------------------------------------- /First Game/addons/netfox/icons/state-synchronizer.svg.import: -------------------------------------------------------------------------------- 1 | [remap] 2 | 3 | importer="texture" 4 | type="CompressedTexture2D" 5 | uid="uid://ogbi1hffcoyh" 6 | path="res://.godot/imported/state-synchronizer.svg-9cb9447ba79f114a58e468a24d17b860.ctex" 7 | metadata={ 8 | "vram_texture": false 9 | } 10 | 11 | [deps] 12 | 13 | source_file="res://addons/netfox/icons/state-synchronizer.svg" 14 | dest_files=["res://.godot/imported/state-synchronizer.svg-9cb9447ba79f114a58e468a24d17b860.ctex"] 15 | 16 | [params] 17 | 18 | compress/mode=0 19 | compress/high_quality=false 20 | compress/lossy_quality=0.7 21 | compress/hdr_compression=1 22 | compress/normal_map=0 23 | compress/channel_pack=0 24 | mipmaps/generate=false 25 | mipmaps/limit=-1 26 | roughness/mode=0 27 | roughness/src_normal="" 28 | process/fix_alpha_border=true 29 | process/premult_alpha=false 30 | process/normal_map_invert_y=false 31 | process/hdr_as_srgb=false 32 | process/hdr_clamp_exposure=false 33 | process/size_limit=0 34 | detect_3d/compress_to=1 35 | svg/scale=1.0 36 | editor/scale_with_editor_scale=false 37 | editor/convert_colors_with_editor_theme=false 38 | -------------------------------------------------------------------------------- /First Game/addons/netfox/icons/rollback-synchronizer.svg.import: -------------------------------------------------------------------------------- 1 | [remap] 2 | 3 | importer="texture" 4 | type="CompressedTexture2D" 5 | uid="uid://csg26ysqqb4xe" 6 | path="res://.godot/imported/rollback-synchronizer.svg-99c6071e1009de5a35a481b2f486c380.ctex" 7 | metadata={ 8 | "vram_texture": false 9 | } 10 | 11 | [deps] 12 | 13 | source_file="res://addons/netfox/icons/rollback-synchronizer.svg" 14 | dest_files=["res://.godot/imported/rollback-synchronizer.svg-99c6071e1009de5a35a481b2f486c380.ctex"] 15 | 16 | [params] 17 | 18 | compress/mode=0 19 | compress/high_quality=false 20 | compress/lossy_quality=0.7 21 | compress/hdr_compression=1 22 | compress/normal_map=0 23 | compress/channel_pack=0 24 | mipmaps/generate=false 25 | mipmaps/limit=-1 26 | roughness/mode=0 27 | roughness/src_normal="" 28 | process/fix_alpha_border=true 29 | process/premult_alpha=false 30 | process/normal_map_invert_y=false 31 | process/hdr_as_srgb=false 32 | process/hdr_clamp_exposure=false 33 | process/size_limit=0 34 | detect_3d/compress_to=1 35 | svg/scale=1.0 36 | editor/scale_with_editor_scale=false 37 | editor/convert_colors_with_editor_theme=false 38 | -------------------------------------------------------------------------------- /First Game/addons/netfox/icons/rollback-synchronizer.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /First Game/addons/netfox/LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2023 Gálffy Tamás 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of 4 | this software and associated documentation files (the "Software"), to deal in 5 | the Software without restriction, including without limitation the rights to 6 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 7 | the Software, and to permit persons to whom the Software is furnished to do so, 8 | subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 15 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 16 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 17 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 18 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 19 | -------------------------------------------------------------------------------- /First Game/addons/netfox.internals/LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2023 Gálffy Tamás 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of 4 | this software and associated documentation files (the "Software"), to deal in 5 | the Software without restriction, including without limitation the rights to 6 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 7 | the Software, and to permit persons to whom the Software is furnished to do so, 8 | subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 15 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 16 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 17 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 18 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 19 | -------------------------------------------------------------------------------- /First Game/scenes/platform.tscn: -------------------------------------------------------------------------------- 1 | [gd_scene load_steps=5 format=3 uid="uid://bgv5v2fxqlswe"] 2 | 3 | [ext_resource type="Texture2D" uid="uid://cxuqjimd7csiq" path="res://assets/sprites/platforms.png" id="1_7q5sl"] 4 | [ext_resource type="Script" path="res://scripts/platform.gd" id="1_bwx77"] 5 | 6 | [sub_resource type="SceneReplicationConfig" id="SceneReplicationConfig_ijbck"] 7 | properties/0/path = NodePath(".:position") 8 | properties/0/spawn = true 9 | properties/0/replication_mode = 1 10 | 11 | [sub_resource type="RectangleShape2D" id="RectangleShape2D_13ihi"] 12 | size = Vector2(32, 8) 13 | 14 | [node name="Platform" type="AnimatableBody2D"] 15 | script = ExtResource("1_bwx77") 16 | 17 | [node name="PlatformSynchronizer" type="MultiplayerSynchronizer" parent="."] 18 | replication_config = SubResource("SceneReplicationConfig_ijbck") 19 | 20 | [node name="Sprite2D" type="Sprite2D" parent="."] 21 | texture = ExtResource("1_7q5sl") 22 | region_enabled = true 23 | region_rect = Rect2(16, 0, 32, 9) 24 | 25 | [node name="CollisionShape2D" type="CollisionShape2D" parent="."] 26 | shape = SubResource("RectangleShape2D_13ihi") 27 | one_way_collision = true 28 | -------------------------------------------------------------------------------- /First Game/addons/steam-multiplayer-peer/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Expresso Bits 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /First Game/addons/netfox/README.md: -------------------------------------------------------------------------------- 1 | # netfox 2 | 3 | The core addon of [netfox], providing responsive multiplayer features for the 4 | [Godot Engine]. 5 | 6 | ## Features 7 | 8 | * ⏲️ Synchronized time 9 | * Runs game logic at a fixed, configurable tickrate 10 | * Time synchronized to game host 11 | * 🧈 State interpolation 12 | * Render 24fps tickrate at buttery smooth 60fps or more 13 | * Add a `TickInterpolator` node and it just works 14 | * 💨 Lag compensation with CSP 15 | * Implement responsive player motion with little to no extra code 16 | * Just use the `RollbackSynchronizer` node for state synchronization 17 | 18 | ## Install 19 | 20 | See the root [README](../../README.md). 21 | 22 | ## Usage 23 | 24 | See the [docs](https://foxssake.github.io/netfox/). 25 | 26 | ## License 27 | 28 | netfox is under the [MIT license](LICENSE). 29 | 30 | ## Issues 31 | 32 | In case of any issues, comments, or questions, please feel free to [open an issue]! 33 | 34 | [netfox]: https://github.com/foxssake/netfox 35 | [source]: https://github.com/foxssake/netfox/archive/refs/heads/main.zip 36 | [Godot engine]: https://godotengine.org/ 37 | [open an issue]: https://github.com/foxssake/netfox/issues 38 | -------------------------------------------------------------------------------- /First Game/addons/netfox/icons/state-synchronizer.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | 10 | 11 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /First Game/scripts/player.gd: -------------------------------------------------------------------------------- 1 | extends CharacterBody2D 2 | 3 | 4 | const SPEED = 130.0 5 | const JUMP_VELOCITY = -300.0 6 | 7 | # Get the gravity from the project settings to be synced with RigidBody nodes. 8 | var gravity = ProjectSettings.get_setting("physics/2d/default_gravity") 9 | 10 | @onready var animated_sprite = $AnimatedSprite2D 11 | 12 | func _physics_process(delta): 13 | # Add the gravity. 14 | if not is_on_floor(): 15 | velocity.y += gravity * delta 16 | 17 | # Handle jump. 18 | if Input.is_action_just_pressed("jump") and is_on_floor(): 19 | velocity.y = JUMP_VELOCITY 20 | 21 | # Get the input direction: -1, 0, 1 22 | var direction = Input.get_axis("move_left", "move_right") 23 | 24 | # Flip the Sprite 25 | if direction > 0: 26 | animated_sprite.flip_h = false 27 | elif direction < 0: 28 | animated_sprite.flip_h = true 29 | 30 | # Play animations 31 | if is_on_floor(): 32 | if direction == 0: 33 | animated_sprite.play("idle") 34 | else: 35 | animated_sprite.play("run") 36 | else: 37 | animated_sprite.play("jump") 38 | 39 | # Apply movement 40 | if direction: 41 | velocity.x = direction * SPEED 42 | else: 43 | velocity.x = move_toward(velocity.x, 0, SPEED) 44 | 45 | move_and_slide() 46 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Godot Lag Compensation: Netfox 2 | 3 | This is a continuation of the Brackeys-based multiplayer game where we add support for lag compensation using Netfox. Netfox uses Client-side Prediction and Server reconciliation as a means to provide responsive client movement while maintaining server authority over the player. 4 | 5 | So you no longer have to wait for that "round trip" from hitting a key and waiting for it to come back from the server before moving the player. 6 | 7 | > Tutorial: https://youtu.be/GqHTNmRspjU 8 | 9 | --- 10 | 11 | ## Netfox 12 | 13 | Documentation: 14 | > https://foxssake.github.io/netfox/ 15 | 16 | Discord: 17 | > https://discord.gg/yEFAZKhB 18 | 19 | Discussion: 20 | > https://github.com/foxssake/netfox/discussions 21 | 22 | 23 | --- 24 | 25 | ### Note: 26 | This project was built on top off the project with Steam support using the Steam Multiplayer Peer extension: 27 | > https://github.com/BatteryAcid/godot-steam-multiplayer-peer-extension 28 | 29 | --- 30 | 31 | ## Original Source: First game in Godot 32 | Project files for our video on making your first game in Godot. 33 | 34 | Check out the videos on the [Brackeys YouTube Channel](http://youtube.com/brackeys). 35 | 36 | Everything is free to use, also commercially (public domain). -------------------------------------------------------------------------------- /First Game/addons/netfox/icons/tick-interpolator.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | 11 | 12 | 13 | 14 | 15 | 16 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | This is free and unencumbered software released into the public domain. 2 | 3 | Anyone is free to copy, modify, publish, use, compile, sell, or 4 | distribute this software, either in source code form or as a compiled 5 | binary, for any purpose, commercial or non-commercial, and by any 6 | means. 7 | 8 | In jurisdictions that recognize copyright laws, the author or authors 9 | of this software dedicate any and all copyright interest in the 10 | software to the public domain. We make this dedication for the benefit 11 | of the public at large and to the detriment of our heirs and 12 | successors. We intend this dedication to be an overt act of 13 | relinquishment in perpetuity of all present and future rights to this 14 | software under copyright law. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 19 | IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR 20 | OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, 21 | ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 22 | OTHER DEALINGS IN THE SOFTWARE. 23 | 24 | For more information, please refer to 25 | -------------------------------------------------------------------------------- /First Game/addons/netfox/state-synchronizer.gd: -------------------------------------------------------------------------------- 1 | extends Node 2 | class_name StateSynchronizer 3 | 4 | ## Synchronizes state from authority. 5 | 6 | @export var root: Node 7 | @export var properties: Array[String] 8 | 9 | var _property_cache: PropertyCache 10 | var _props: Array[PropertyEntry] 11 | 12 | var _last_received_tick: int = -1 13 | var _last_received_state: Dictionary = {} 14 | 15 | ## Process settings. 16 | ## 17 | ## Call this after any change to configuration. 18 | func process_settings(): 19 | _property_cache = PropertyCache.new(root) 20 | _props = [] 21 | 22 | for property in properties: 23 | var pe = _property_cache.get_entry(property) 24 | _props.push_back(pe) 25 | 26 | func _ready(): 27 | process_settings() 28 | NetworkTime.after_tick.connect(_after_tick) 29 | 30 | func _after_tick(_dt, tick): 31 | if is_multiplayer_authority(): 32 | # Submit snapshot 33 | var state = PropertySnapshot.extract(_props) 34 | rpc("_submit_state", state, tick) 35 | else: 36 | # Apply last received state 37 | PropertySnapshot.apply(_last_received_state, _property_cache) 38 | 39 | @rpc("authority", "unreliable", "call_remote") 40 | func _submit_state(state: Dictionary, tick: int): 41 | if tick <= _last_received_tick: 42 | return 43 | 44 | _last_received_state = state 45 | _last_received_tick = tick 46 | -------------------------------------------------------------------------------- /First Game/addons/steam-multiplayer-peer/steam-multiplayer-peer.gdextension: -------------------------------------------------------------------------------- 1 | [configuration] 2 | entry_symbol = "steam_multiplayer_peer_init" 3 | compatibility_minimum = 4.1 4 | 5 | [libraries] 6 | macos.debug = "osx/steam-multiplayer-peer.macos.template_debug.framework" 7 | macos.release = "osx/steam-multiplayer-peer.macos.template_release.framework" 8 | windows.debug.x86_64 = "win64/steam-multiplayer-peer.windows.template_debug.x86_64.dll" 9 | windows.debug.x86_32 = "win32/steam-multiplayer-peer.windows.template_debug.x86_32.dll" 10 | windows.release.x86_64 = "win64/steam-multiplayer-peer.windows.template_release.x86_64.dll" 11 | windows.release.x86_32 = "win32/steam-multiplayer-peer.windows.template_release.x86_32.dll" 12 | linux.debug.x86_64 = "linux64/libsteam-multiplayer-peer.linux.template_debug.x86_64.so" 13 | linux.debug.x86_32 = "linux32/libsteam-multiplayer-peer.linux.template_debug.x86_32.so" 14 | linux.release.x86_64 = "linux64/libsteam-multiplayer-peer.linux.template_release.x86_64.so" 15 | linux.release.x86_32 = "linux32/libsteam-multiplayer-peer.linux.template_release.x86_32.so" 16 | 17 | [dependencies] 18 | macos.universal = { "osx/libsteam_api.dylib": "" } 19 | windows.x86_64 = { "win64/steam_api64.dll": "" } 20 | windows.x86_32 = { "win32/steam_api.dll": "" } 21 | linux.x86_64 = { "linux64/libsteam_api.so": "" } 22 | linux.x86_32 = { "linux32/libsteam_api.so": "" } -------------------------------------------------------------------------------- /First Game/scripts/multiplayer/networks/enet_network.gd: -------------------------------------------------------------------------------- 1 | extends Node 2 | 3 | const SERVER_PORT = 8080 4 | const SERVER_IP = "127.0.0.1" 5 | 6 | var multiplayer_scene = preload("res://scenes/multiplayer_player.tscn") 7 | var multiplayer_peer: ENetMultiplayerPeer = ENetMultiplayerPeer.new() 8 | var _players_spawn_node 9 | 10 | func become_host(): 11 | print("Starting host!") 12 | 13 | multiplayer_peer.create_server(SERVER_PORT) 14 | multiplayer.multiplayer_peer = multiplayer_peer 15 | 16 | multiplayer.peer_connected.connect(_add_player_to_game) 17 | multiplayer.peer_disconnected.connect(_del_player) 18 | 19 | if not OS.has_feature("dedicated_server"): 20 | _add_player_to_game(1) 21 | 22 | func join_as_client(lobby_id): 23 | print("Player 2 joining") 24 | 25 | multiplayer_peer.create_client(SERVER_IP, SERVER_PORT) 26 | multiplayer.multiplayer_peer = multiplayer_peer 27 | 28 | func _add_player_to_game(id: int): 29 | print("Player %s joined the game!" % id) 30 | 31 | var player_to_add = multiplayer_scene.instantiate() 32 | player_to_add.player_id = id 33 | player_to_add.name = str(id) 34 | 35 | _players_spawn_node.add_child(player_to_add, true) 36 | 37 | func _del_player(id: int): 38 | print("Player %s left the game!" % id) 39 | if not _players_spawn_node.has_node(str(id)): 40 | return 41 | _players_spawn_node.get_node(str(id)).queue_free() 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | -------------------------------------------------------------------------------- /First Game/scripts/multiplayer/networks/network_manager.gd: -------------------------------------------------------------------------------- 1 | extends Node 2 | 3 | enum MULTIPLAYER_NETWORK_TYPE { ENET, STEAM } 4 | 5 | @export var _players_spawn_node: Node2D 6 | 7 | var active_network_type: MULTIPLAYER_NETWORK_TYPE = MULTIPLAYER_NETWORK_TYPE.ENET 8 | var enet_network_scene := preload("res://scenes/multiplayer/networks/enet_network.tscn") 9 | var steam_network_scene := preload("res://scenes/multiplayer/networks/steam_network.tscn") 10 | var active_network 11 | 12 | func _build_multiplayer_network(): 13 | if not active_network: 14 | print("Setting active_network") 15 | 16 | MultiplayerManager.multiplayer_mode_enabled = true 17 | 18 | match active_network_type: 19 | MULTIPLAYER_NETWORK_TYPE.ENET: 20 | print("Setting network type to ENet") 21 | _set_active_network(enet_network_scene) 22 | MULTIPLAYER_NETWORK_TYPE.STEAM: 23 | print("Setting network type to Steam") 24 | _set_active_network(steam_network_scene) 25 | _: 26 | print("No match for network type!") 27 | 28 | func _set_active_network(active_network_scene): 29 | var network_scene_initialized = active_network_scene.instantiate() 30 | active_network = network_scene_initialized 31 | active_network._players_spawn_node = _players_spawn_node 32 | add_child(active_network) 33 | 34 | func become_host(is_dedicated_server = false): 35 | _build_multiplayer_network() 36 | MultiplayerManager.host_mode_enabled = true if is_dedicated_server == false else false 37 | active_network.become_host() 38 | 39 | func join_as_client(lobby_id = 0): 40 | _build_multiplayer_network() 41 | active_network.join_as_client(lobby_id) 42 | 43 | func list_lobbies(): 44 | _build_multiplayer_network() 45 | active_network.list_lobbies() 46 | -------------------------------------------------------------------------------- /First Game/addons/netfox/interpolators.gd: -------------------------------------------------------------------------------- 1 | extends Object 2 | class_name Interpolators 3 | 4 | class Interpolator: 5 | var is_applicable: Callable 6 | var apply: Callable 7 | 8 | static func make(is_applicable: Callable, apply: Callable) -> Interpolator: 9 | var result = Interpolator.new() 10 | result.is_applicable = is_applicable 11 | result.apply = apply 12 | return result 13 | 14 | static var DEFAULT_INTERPOLATOR = Interpolator.make( 15 | func (v): return true, 16 | func (a, b, f): return a if f < 0.5 else b 17 | ) 18 | 19 | static var interpolators: Array[Interpolator] 20 | static var default_apply: Callable = func(a, b, f): a if f < 0.5 else b 21 | 22 | ## Register an interpolator. 23 | ## 24 | ## New interpolators are pushed to the front of the list, making them have 25 | ## precedence over existing ones. This can be useful in case you want to override 26 | ## the built-in interpolators. 27 | static func register(is_applicable: Callable, apply: Callable): 28 | interpolators.push_front(Interpolator.make(is_applicable, apply)) 29 | 30 | ## Find the appropriate interpolator for the given value. 31 | ## 32 | ## If none was found, the default interpolator is returned. 33 | static func find_for(value) -> Callable: 34 | for interpolator in interpolators: 35 | if interpolator.is_applicable.call(value): 36 | return interpolator.apply 37 | 38 | return DEFAULT_INTERPOLATOR.apply 39 | 40 | ## Interpolate between two values. 41 | ## 42 | ## Note, that it is usually faster to just cache the Callable returned by find_for 43 | ## and call that, instead of calling interpolate repeatedly. The latter will have 44 | ## to lookup the appropriate interpolator on every call. 45 | static func interpolate(a, b, f: float): 46 | return find_for(a).call(a, b, f) 47 | 48 | static func _static_init(): 49 | # Register built-in interpolators 50 | # Float 51 | register( 52 | func(a): return a is float, 53 | func(a: float, b: float, f: float): return lerpf(a, b, f) 54 | ) 55 | 56 | # Vector 57 | register( 58 | func(a): return a is Vector2, 59 | func(a: Vector2, b: Vector2, f: float): return a.lerp(b, f) 60 | ) 61 | register( 62 | func(a): return a is Vector3, 63 | func(a: Vector3, b: Vector3, f: float): return a.lerp(b, f) 64 | ) 65 | 66 | # Transform 67 | register( 68 | func(a): return a is Transform2D, 69 | func(a: Transform2D, b: Transform2D, f: float): return a.interpolate_with(b, f) 70 | ) 71 | register( 72 | func(a): return a is Transform3D, 73 | func(a: Transform3D, b: Transform3D, f: float): return a.interpolate_with(b, f) 74 | ) 75 | -------------------------------------------------------------------------------- /First Game/addons/netfox/tick-interpolator.gd: -------------------------------------------------------------------------------- 1 | extends Node 2 | class_name TickInterpolator 3 | 4 | @export var root: Node 5 | @export var enabled: bool = true 6 | @export var properties: Array[String] 7 | @export var record_first_state: bool = true 8 | @export var enable_recording: bool = true 9 | 10 | var _state_from: Dictionary = {} 11 | var _state_to: Dictionary = {} 12 | var _props: Array[PropertyEntry] = [] 13 | var _interpolators: Dictionary = {} 14 | 15 | var _property_cache: PropertyCache 16 | 17 | ## Process settings. 18 | ## 19 | ## Call this after any change to configuration. 20 | func process_settings(): 21 | _property_cache = PropertyCache.new(root) 22 | _props.clear() 23 | _interpolators.clear() 24 | 25 | _state_from = {} 26 | _state_to = {} 27 | 28 | for property in properties: 29 | var pe = _property_cache.get_entry(property) 30 | _props.push_back(pe) 31 | _interpolators[property] = Interpolators.find_for(pe.get_value()) 32 | 33 | ## Check if interpolation can be done. 34 | ## 35 | ## Even if it's enabled, no interpolation will be done if there are no 36 | ## properties to interpolate. 37 | func can_interpolate() -> bool: 38 | return enabled and not properties.is_empty() 39 | 40 | ## Record current state for interpolation. 41 | ## 42 | ## Note that this will rotate the states, so the previous target becomes the new 43 | ## starting point for the interpolation. This is automatically called if 44 | ## [code]enable_recording[/code] is true. 45 | func push_state(): 46 | _state_from = _state_to 47 | _state_to = PropertySnapshot.extract(_props) 48 | 49 | ## Record current state and transition without interpolation. 50 | func teleport(): 51 | _state_from = PropertySnapshot.extract(_props) 52 | _state_to = _state_from 53 | 54 | func _ready(): 55 | process_settings() 56 | NetworkTime.before_tick_loop.connect(_before_tick_loop) 57 | NetworkTime.after_tick_loop.connect(_after_tick_loop) 58 | 59 | # Wait a frame for any initial setup before recording first state 60 | if record_first_state: 61 | await get_tree().process_frame 62 | teleport() 63 | 64 | func _process(_delta): 65 | _interpolate(_state_from, _state_to, NetworkTime.tick_factor) 66 | 67 | func _before_tick_loop(): 68 | PropertySnapshot.apply(_state_to, _property_cache) 69 | 70 | func _after_tick_loop(): 71 | if enable_recording: 72 | push_state() 73 | 74 | func _interpolate(from: Dictionary, to: Dictionary, f: float): 75 | if not can_interpolate(): 76 | return 77 | 78 | for property in from: 79 | if not to.has(property): continue 80 | 81 | var pe = _property_cache.get_entry(property) 82 | var a = from[property] 83 | var b = to[property] 84 | var interpolate = _interpolators[property] as Callable 85 | 86 | pe.set_value(interpolate.call(a, b, f)) 87 | -------------------------------------------------------------------------------- /First Game/scenes/slime.tscn: -------------------------------------------------------------------------------- 1 | [gd_scene load_steps=12 format=3 uid="uid://d2w2i5h0lhc6l"] 2 | 3 | [ext_resource type="Script" path="res://scripts/slime.gd" id="1_fxbww"] 4 | [ext_resource type="Texture2D" uid="uid://bl53gpvg5mh1y" path="res://assets/sprites/slime_green.png" id="1_nuk1g"] 5 | [ext_resource type="PackedScene" uid="uid://dtrbka1ef726o" path="res://scenes/killzone.tscn" id="2_aiwyj"] 6 | [ext_resource type="Script" path="res://addons/netfox/state-synchronizer.gd" id="2_ovs8h"] 7 | [ext_resource type="Script" path="res://addons/netfox/tick-interpolator.gd" id="3_sy1pc"] 8 | 9 | [sub_resource type="AtlasTexture" id="AtlasTexture_ayoyu"] 10 | atlas = ExtResource("1_nuk1g") 11 | region = Rect2(0, 24, 24, 24) 12 | 13 | [sub_resource type="AtlasTexture" id="AtlasTexture_2ponx"] 14 | atlas = ExtResource("1_nuk1g") 15 | region = Rect2(24, 24, 24, 24) 16 | 17 | [sub_resource type="AtlasTexture" id="AtlasTexture_x57lo"] 18 | atlas = ExtResource("1_nuk1g") 19 | region = Rect2(48, 24, 24, 24) 20 | 21 | [sub_resource type="AtlasTexture" id="AtlasTexture_1a8la"] 22 | atlas = ExtResource("1_nuk1g") 23 | region = Rect2(72, 24, 24, 24) 24 | 25 | [sub_resource type="SpriteFrames" id="SpriteFrames_a8uf2"] 26 | animations = [{ 27 | "frames": [{ 28 | "duration": 1.0, 29 | "texture": SubResource("AtlasTexture_ayoyu") 30 | }, { 31 | "duration": 1.0, 32 | "texture": SubResource("AtlasTexture_2ponx") 33 | }, { 34 | "duration": 1.0, 35 | "texture": SubResource("AtlasTexture_x57lo") 36 | }, { 37 | "duration": 1.0, 38 | "texture": SubResource("AtlasTexture_1a8la") 39 | }], 40 | "loop": true, 41 | "name": &"default", 42 | "speed": 10.0 43 | }] 44 | 45 | [sub_resource type="RectangleShape2D" id="RectangleShape2D_u4xjq"] 46 | size = Vector2(10, 14) 47 | 48 | [node name="Slime" type="Node2D"] 49 | script = ExtResource("1_fxbww") 50 | 51 | [node name="StateSynchronizer" type="Node" parent="." node_paths=PackedStringArray("root")] 52 | script = ExtResource("2_ovs8h") 53 | root = NodePath("..") 54 | properties = Array[String]([":position"]) 55 | 56 | [node name="TickInterpolator" type="Node" parent="." node_paths=PackedStringArray("root")] 57 | script = ExtResource("3_sy1pc") 58 | root = NodePath("..") 59 | properties = Array[String]([":position"]) 60 | 61 | [node name="AnimatedSprite2D" type="AnimatedSprite2D" parent="."] 62 | position = Vector2(0, -12) 63 | sprite_frames = SubResource("SpriteFrames_a8uf2") 64 | autoplay = "default" 65 | frame = 2 66 | frame_progress = 0.722655 67 | 68 | [node name="Killzone" parent="." instance=ExtResource("2_aiwyj")] 69 | 70 | [node name="CollisionShape2D" type="CollisionShape2D" parent="Killzone"] 71 | position = Vector2(0, -7) 72 | shape = SubResource("RectangleShape2D_u4xjq") 73 | 74 | [node name="RayCastRight" type="RayCast2D" parent="."] 75 | position = Vector2(0, -7) 76 | target_position = Vector2(9, 0) 77 | 78 | [node name="RayCastLeft" type="RayCast2D" parent="."] 79 | position = Vector2(0, -7) 80 | target_position = Vector2(-9, 0) 81 | -------------------------------------------------------------------------------- /First Game/scripts/game_manager.gd: -------------------------------------------------------------------------------- 1 | extends Node 2 | 3 | var score = 0 4 | 5 | @onready var score_label = $ScoreLabel 6 | 7 | func _ready(): 8 | if OS.has_feature("dedicated_server"): 9 | print("Starting dedicated server...") 10 | _remove_single_player() 11 | %NetworkManager.become_host(true) 12 | 13 | func add_point(): 14 | score += 1 15 | score_label.text = "You collected " + str(score) + " coins." 16 | 17 | func become_host(): 18 | print("Become host pressed") 19 | _remove_single_player() 20 | %MultiplayerHUD.hide() 21 | %SteamHUD.hide() 22 | %NetworkManager.become_host() 23 | 24 | func join_as_client(): 25 | print("Join as player 2") 26 | join_lobby() 27 | 28 | func use_steam(): 29 | print("Using Steam!") 30 | %MultiplayerHUD.hide() 31 | %SteamHUD.show() 32 | SteamManager.initialize_steam() 33 | Steam.lobby_match_list.connect(_on_lobby_match_list) 34 | %NetworkManager.active_network_type = %NetworkManager.MULTIPLAYER_NETWORK_TYPE.STEAM 35 | 36 | func list_steam_lobbies(): 37 | print("List Steam lobbies") 38 | %NetworkManager.list_lobbies() 39 | 40 | func join_lobby(lobby_id = 0): 41 | print("Joining lobby %s" % lobby_id) 42 | _remove_single_player() 43 | %MultiplayerHUD.hide() 44 | %SteamHUD.hide() 45 | %NetworkManager.join_as_client(lobby_id) 46 | 47 | func _on_lobby_match_list(lobbies: Array): 48 | print("On lobby match list") 49 | 50 | for lobby_child in $"../SteamHUD/Panel/Lobbies/VBoxContainer".get_children(): 51 | lobby_child.queue_free() 52 | 53 | for lobby in lobbies: 54 | var lobby_name: String = Steam.getLobbyData(lobby, "name") 55 | 56 | if lobby_name != "": 57 | var lobby_mode: String = Steam.getLobbyData(lobby, "mode") 58 | 59 | var lobby_button: Button = Button.new() 60 | lobby_button.set_text(lobby_name + " | " + lobby_mode) 61 | lobby_button.set_size(Vector2(100, 30)) 62 | lobby_button.add_theme_font_size_override("font_size", 8) 63 | 64 | var fv = FontVariation.new() 65 | fv.set_base_font(load("res://assets/fonts/PixelOperator8.ttf")) 66 | lobby_button.add_theme_font_override("font", fv) 67 | lobby_button.set_name("lobby_%s" % lobby) 68 | lobby_button.alignment = HORIZONTAL_ALIGNMENT_LEFT 69 | lobby_button.connect("pressed", Callable(self, "join_lobby").bind(lobby)) 70 | 71 | $"../SteamHUD/Panel/Lobbies/VBoxContainer".add_child(lobby_button) 72 | 73 | 74 | func _remove_single_player(): 75 | print("Remove single player") 76 | var player_to_remove = get_tree().get_current_scene().get_node("Player") 77 | player_to_remove.queue_free() 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | -------------------------------------------------------------------------------- /First Game/addons/netfox.internals/logger.gd: -------------------------------------------------------------------------------- 1 | extends RefCounted 2 | class_name _NetfoxLogger 3 | 4 | enum { 5 | LOG_MIN, 6 | LOG_TRACE, 7 | LOG_DEBUG, 8 | LOG_INFO, 9 | LOG_WARN, 10 | LOG_ERROR, 11 | LOG_MAX 12 | } 13 | 14 | static var log_level: int 15 | static var module_log_level: Dictionary 16 | 17 | var module: String 18 | var name: String 19 | 20 | const level_prefixes: Array[String] = [ 21 | "", 22 | "TRC", 23 | "DBG", 24 | "INF", 25 | "WRN", 26 | "ERR", 27 | "" 28 | ] 29 | 30 | static func _static_init(): 31 | log_level = ProjectSettings.get_setting("netfox/logging/log_level", LOG_MIN) 32 | module_log_level = { 33 | "netfox": ProjectSettings.get_setting("netfox/logging/netfox_log_level", LOG_MIN), 34 | "netfox.noray": ProjectSettings.get_setting("netfox/logging/netfox_noray_log_level", LOG_MIN), 35 | "netfox.extras": ProjectSettings.get_setting("netfox/logging/netfox_extras_log_level", LOG_MIN) 36 | } 37 | 38 | static func for_netfox(p_name: String) -> _NetfoxLogger: 39 | return _NetfoxLogger.new("netfox", p_name) 40 | 41 | static func for_noray(p_name: String) -> _NetfoxLogger: 42 | return _NetfoxLogger.new("netfox.noray", p_name) 43 | 44 | static func for_extras(p_name: String) -> _NetfoxLogger: 45 | return _NetfoxLogger.new("netfox.extras", p_name) 46 | 47 | static func make_setting(name: String) -> Dictionary: 48 | return { 49 | "name": name, 50 | "value": LOG_MIN, 51 | "type": TYPE_INT, 52 | "hint": PROPERTY_HINT_ENUM, 53 | "hint_string": "All,Trace,Debug,Info,Warning,Error,None" 54 | } 55 | 56 | func _init(p_module: String, p_name: String): 57 | module = p_module 58 | name = p_name 59 | 60 | func _check_log_level(level: int) -> bool: 61 | var cmp_level = log_level 62 | if level < cmp_level: 63 | return false 64 | 65 | if module_log_level.has(module): 66 | var module_level = module_log_level.get(module) 67 | return level >= module_level 68 | 69 | return true 70 | 71 | func _format_text(text: String, level: int) -> String: 72 | level = clampi(level, LOG_MIN, LOG_MAX) 73 | return "[%s][%s::%s] %s" % [level_prefixes[level], module,name, text] 74 | 75 | func _log_text(text: String, level: int): 76 | if _check_log_level(level): 77 | print(_format_text(text, level)) 78 | 79 | func trace(text: String): 80 | _log_text(text, LOG_TRACE) 81 | 82 | func debug(text: String): 83 | _log_text(text, LOG_DEBUG) 84 | 85 | func info(text: String): 86 | _log_text(text, LOG_INFO) 87 | 88 | func warning(text: String): 89 | if _check_log_level(LOG_WARN): 90 | var formatted_text = _format_text(text, LOG_WARN) 91 | push_warning(formatted_text) 92 | # Print so it shows up in the Output panel too 93 | print(formatted_text) 94 | 95 | func error(text: String): 96 | if _check_log_level(LOG_ERROR): 97 | var formatted_text = _format_text(text, LOG_ERROR) 98 | push_error(formatted_text) 99 | # Print so it shows up in the Output panel too 100 | print(formatted_text) 101 | -------------------------------------------------------------------------------- /First Game/project.godot: -------------------------------------------------------------------------------- 1 | ; Engine configuration file. 2 | ; It's best edited using the editor UI and not directly, 3 | ; since the parameters that go here are not all obvious. 4 | ; 5 | ; Format: 6 | ; [section] ; section goes between [] 7 | ; param=value ; assign values to parameters 8 | 9 | config_version=5 10 | 11 | [application] 12 | 13 | config/name="First Game" 14 | run/main_scene="res://scenes/game.tscn" 15 | config/features=PackedStringArray("4.2", "Forward Plus") 16 | config/icon="res://icon.svg" 17 | 18 | [autoload] 19 | 20 | Music="*res://scenes/music.tscn" 21 | MultiplayerManager="*res://scripts/multiplayer/multiplayer_manager.gd" 22 | SteamManager="*res://scripts/multiplayer/steam/steam_manager.gd" 23 | NetworkTime="*res://addons/netfox/network-time.gd" 24 | NetworkTimeSynchronizer="*res://addons/netfox/network-time-synchronizer.gd" 25 | NetworkRollback="*res://addons/netfox/rollback/network-rollback.gd" 26 | NetworkEvents="*res://addons/netfox/network-events.gd" 27 | NetworkPerformance="*res://addons/netfox/network-performance.gd" 28 | 29 | [display] 30 | 31 | window/size/always_on_top=true 32 | window/vsync/vsync_mode=0 33 | 34 | [editor_plugins] 35 | 36 | enabled=PackedStringArray("res://addons/netfox.internals/plugin.cfg", "res://addons/netfox/plugin.cfg") 37 | 38 | [input] 39 | 40 | jump={ 41 | "deadzone": 0.5, 42 | "events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":32,"key_label":0,"unicode":32,"echo":false,"script":null) 43 | ] 44 | } 45 | move_left={ 46 | "deadzone": 0.5, 47 | "events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":4194319,"key_label":0,"unicode":0,"echo":false,"script":null) 48 | , 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) 49 | ] 50 | } 51 | move_right={ 52 | "deadzone": 0.5, 53 | "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":4194321,"key_label":0,"unicode":0,"echo":false,"script":null) 54 | , 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) 55 | ] 56 | } 57 | 58 | [rendering] 59 | 60 | textures/canvas_textures/default_texture_filter=0 61 | textures/vram_compression/import_etc2_astc=true 62 | -------------------------------------------------------------------------------- /First Game/scripts/multiplayer/multiplayer_controller.gd: -------------------------------------------------------------------------------- 1 | extends CharacterBody2D 2 | 3 | const SPEED = 130.0 4 | const JUMP_VELOCITY = -300.0 5 | 6 | @onready var animated_sprite = $AnimatedSprite2D 7 | @onready var rollback_synchronizer = $RollbackSynchronizer 8 | 9 | # Get the gravity from the project settings to be synced with RigidBody nodes. 10 | var gravity = ProjectSettings.get_setting("physics/2d/default_gravity") 11 | 12 | var _respawning = false 13 | var alive = true 14 | 15 | @export var input: PlayerInput 16 | 17 | @export var player_id := 1: 18 | set(id): 19 | player_id = id 20 | input.set_multiplayer_authority(id) 21 | 22 | func _ready(): 23 | if multiplayer.get_unique_id() == player_id: 24 | $Camera2D.make_current() 25 | else: 26 | $Camera2D.enabled = false 27 | 28 | rollback_synchronizer.process_settings() 29 | 30 | func _apply_animations(delta): 31 | 32 | var direction = input.input_direction 33 | 34 | # Flip the Sprite 35 | if direction > 0: 36 | animated_sprite.flip_h = false 37 | elif direction < 0: 38 | animated_sprite.flip_h = true 39 | 40 | # Play animations 41 | if is_on_floor(): 42 | if direction == 0: 43 | animated_sprite.play("idle") 44 | else: 45 | animated_sprite.play("run") 46 | else: 47 | animated_sprite.play("jump") 48 | 49 | func _rollback_tick(delta, tick, is_fresh): 50 | if not _respawning: 51 | _apply_movement_from_input(delta) 52 | else: 53 | _respawning = false 54 | position = MultiplayerManager.respawn_point 55 | velocity = Vector2.ZERO 56 | $TickInterpolator.teleport() 57 | 58 | await get_tree().create_timer(0.5).timeout 59 | alive = true 60 | 61 | func _apply_movement_from_input(delta): 62 | 63 | _force_update_is_on_floor() 64 | 65 | # Add the gravity. 66 | if not is_on_floor(): 67 | velocity.y += gravity * delta 68 | elif input.input_jump > 0: 69 | # Handle jump. 70 | velocity.y = JUMP_VELOCITY * input.input_jump 71 | 72 | # Get the input direction: -1, 0, 1 73 | var direction = input.input_direction 74 | 75 | # Apply movement 76 | if direction: 77 | velocity.x = direction * SPEED 78 | else: 79 | velocity.x = move_toward(velocity.x, 0, SPEED) 80 | 81 | velocity *= NetworkTime.physics_factor 82 | move_and_slide() 83 | velocity /= NetworkTime.physics_factor 84 | 85 | func _force_update_is_on_floor(): 86 | var old_velocity = velocity 87 | velocity = Vector2.ZERO 88 | move_and_slide() 89 | velocity = old_velocity 90 | 91 | func _process(delta): 92 | if not multiplayer.is_server() || MultiplayerManager.host_mode_enabled: 93 | _apply_animations(delta) 94 | 95 | func mark_dead(): 96 | print("Mark player dead!") 97 | $CollisionShape2D.set_deferred("disabled", true) 98 | alive = false 99 | $RespawnTimer.start() 100 | 101 | func _respawn(): 102 | print("Respawned!") 103 | $CollisionShape2D.set_deferred("disabled", false) 104 | _respawning = true 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | -------------------------------------------------------------------------------- /First Game/addons/netfox/network-performance.gd: -------------------------------------------------------------------------------- 1 | extends Node 2 | 3 | const NETWORK_LOOP_DURATION_MONITOR: StringName = "netfox/Network loop duration (ms)" 4 | const ROLLBACK_LOOP_DURATION_MONITOR: StringName = "netfox/Rollback loop duration (ms)" 5 | const NETWORK_TICKS_MONITOR: StringName = "netfox/Network ticks simulated" 6 | const ROLLBACK_TICKS_MONITOR: StringName = "netfox/Rollback ticks simulated" 7 | const ROLLBACK_TICK_DURATION_MONITOR: StringName = "netfox/Rollback tick duration (ms)" 8 | 9 | var _network_loop_start: float = 0 10 | var _network_loop_duration: float = 0 11 | 12 | var _network_ticks: int = 0 13 | var _network_ticks_accum: int = 0 14 | 15 | var _rollback_loop_start: float = 0 16 | var _rollback_loop_duration: float = 0 17 | 18 | var _rollback_ticks: int = 0 19 | var _rollback_ticks_accum: int = 0 20 | 21 | static var _logger: _NetfoxLogger = _NetfoxLogger.for_netfox("NetworkPerformance") 22 | 23 | func is_enabled(): 24 | if OS.has_feature("netfox_noperf"): 25 | return false 26 | 27 | if OS.has_feature("netfox_perf"): 28 | return true 29 | 30 | # This returns true in the editor too 31 | return OS.is_debug_build() 32 | 33 | ## Get time spent in the last network tick loop, in millisec. 34 | ## [br] 35 | ## Note that this also includes time spent in the rollback tick loop. 36 | func get_network_loop_duration_ms() -> float: 37 | return _network_loop_duration * 1000 38 | 39 | ## Get the number of ticks simulated in the last network tick loop. 40 | func get_network_ticks() -> int: 41 | return _network_ticks 42 | 43 | ## Get time spent in the last rollback tick loop, in millisec. 44 | func get_rollback_loop_duration_ms() -> float: 45 | return _rollback_loop_duration * 1000 46 | 47 | ## Get the number of ticks resimulated in the last rollback tick loop. 48 | func get_rollback_ticks() -> int: 49 | return _rollback_ticks 50 | 51 | ## Get the average amount of time spent in a rollback tick during the last 52 | ## rollback loop, in millisec. 53 | func get_rollback_tick_duration_ms() -> float: 54 | return _rollback_loop_duration * 1000 / maxi(_rollback_ticks, 1) 55 | 56 | func _ready(): 57 | if not is_enabled(): 58 | _logger.debug("Network performance disabled") 59 | return 60 | 61 | _logger.debug("Network performance enabled, registering performance monitors") 62 | Performance.add_custom_monitor(NETWORK_LOOP_DURATION_MONITOR, get_network_loop_duration_ms) 63 | Performance.add_custom_monitor(ROLLBACK_LOOP_DURATION_MONITOR, get_rollback_loop_duration_ms) 64 | Performance.add_custom_monitor(NETWORK_TICKS_MONITOR, get_network_ticks) 65 | Performance.add_custom_monitor(ROLLBACK_TICKS_MONITOR, get_rollback_ticks) 66 | Performance.add_custom_monitor(ROLLBACK_TICK_DURATION_MONITOR, get_rollback_tick_duration_ms) 67 | 68 | NetworkTime.before_tick_loop.connect(_before_tick_loop) 69 | NetworkTime.on_tick.connect(_on_network_tick) 70 | NetworkTime.after_tick_loop.connect(_after_tick_loop) 71 | 72 | NetworkRollback.before_loop.connect(_before_rollback_loop) 73 | NetworkRollback.on_process_tick.connect(_on_rollback_tick) 74 | NetworkRollback.after_loop.connect(_after_rollback_loop) 75 | 76 | func _before_tick_loop(): 77 | _network_loop_start = _time() 78 | _network_ticks_accum = 0 79 | 80 | func _on_network_tick(_dt, _t): 81 | _network_ticks_accum += 1 82 | 83 | func _after_tick_loop(): 84 | _network_loop_duration = _time() - _network_loop_start 85 | _network_ticks = _network_ticks_accum 86 | 87 | func _before_rollback_loop(): 88 | _rollback_loop_start = _time() 89 | _rollback_ticks_accum = 0 90 | 91 | func _on_rollback_tick(_t): 92 | _rollback_ticks_accum += 1 93 | 94 | func _after_rollback_loop(): 95 | _rollback_loop_duration = _time() - _rollback_loop_start 96 | _rollback_ticks = _rollback_ticks_accum 97 | 98 | func _time() -> float: 99 | return Time.get_unix_time_from_system() 100 | -------------------------------------------------------------------------------- /First Game/scripts/multiplayer/networks/steam_network.gd: -------------------------------------------------------------------------------- 1 | extends Node 2 | 3 | var multiplayer_scene = preload("res://scenes/multiplayer_player.tscn") 4 | var multiplayer_peer: SteamMultiplayerPeer = SteamMultiplayerPeer.new() 5 | var _players_spawn_node 6 | var _hosted_lobby_id = 0 7 | 8 | const LOBBY_NAME = "BAD2233" 9 | const LOBBY_MODE = "CoOP" 10 | 11 | func _ready(): 12 | #multiplayer_peer.lobby_created.connect(_on_lobby_created) 13 | Steam.lobby_created.connect(_on_lobby_created.bind()) 14 | 15 | func become_host(): 16 | print("Starting host!") 17 | 18 | multiplayer.peer_connected.connect(_add_player_to_game) 19 | multiplayer.peer_disconnected.connect(_del_player) 20 | 21 | Steam.lobby_joined.connect(_on_lobby_joined.bind()) 22 | Steam.createLobby(Steam.LOBBY_TYPE_PUBLIC, SteamManager.lobby_max_members) 23 | 24 | func join_as_client(lobby_id): 25 | print("Joining lobby %s" % lobby_id) 26 | Steam.lobby_joined.connect(_on_lobby_joined.bind()) 27 | Steam.joinLobby(int(lobby_id)) 28 | 29 | func _on_lobby_created(connect: int, lobby_id): 30 | print("On lobby created") 31 | if connect == 1: 32 | _hosted_lobby_id = lobby_id 33 | print("Created lobby: %s" % _hosted_lobby_id) 34 | 35 | Steam.setLobbyJoinable(_hosted_lobby_id, true) 36 | 37 | Steam.setLobbyData(_hosted_lobby_id, "name", LOBBY_NAME) 38 | Steam.setLobbyData(_hosted_lobby_id, "mode", LOBBY_MODE) 39 | 40 | _create_host() 41 | 42 | func _create_host(): 43 | print("Create Host") 44 | 45 | var error = multiplayer_peer.create_host(0, []) 46 | 47 | if error == OK: 48 | multiplayer.set_multiplayer_peer(multiplayer_peer) 49 | 50 | if not OS.has_feature("dedicated_server"): 51 | _add_player_to_game(1) 52 | else: 53 | print("error creating host: %s" % str(error)) 54 | 55 | func _on_lobby_joined(lobby: int, permissions: int, locked: bool, response: int): 56 | print("On lobby joined") 57 | 58 | if response == 1: 59 | var id = Steam.getLobbyOwner(lobby) 60 | if id != Steam.getSteamID(): 61 | print("Connecting client to socket...") 62 | connect_socket(id) 63 | else: 64 | # Get the failure reason 65 | var FAIL_REASON: String 66 | match response: 67 | 2: FAIL_REASON = "This lobby no longer exists." 68 | 3: FAIL_REASON = "You don't have permission to join this lobby." 69 | 4: FAIL_REASON = "The lobby is now full." 70 | 5: FAIL_REASON = "Uh... something unexpected happened!" 71 | 6: FAIL_REASON = "You are banned from this lobby." 72 | 7: FAIL_REASON = "You cannot join due to having a limited account." 73 | 8: FAIL_REASON = "This lobby is locked or disabled." 74 | 9: FAIL_REASON = "This lobby is community locked." 75 | 10: FAIL_REASON = "A user in the lobby has blocked you from joining." 76 | 11: FAIL_REASON = "A user you have blocked is in the lobby." 77 | print(FAIL_REASON) 78 | 79 | func connect_socket(steam_id: int): 80 | var error = multiplayer_peer.create_client(steam_id, 0, []) 81 | if error == OK: 82 | print("Connecting peer to host...") 83 | multiplayer.set_multiplayer_peer(multiplayer_peer) 84 | else: 85 | print("Error creating client: %s" % str(error)) 86 | 87 | func list_lobbies(): 88 | Steam.addRequestLobbyListDistanceFilter(Steam.LOBBY_DISTANCE_FILTER_WORLDWIDE) 89 | # NOTE: If you are using the test app id, you will need to apply a filter on your game name 90 | # Otherwise, it may not show up in the lobby list of your clients 91 | Steam.addRequestLobbyListStringFilter("name", LOBBY_NAME, Steam.LOBBY_COMPARISON_EQUAL) 92 | Steam.requestLobbyList() 93 | 94 | func _add_player_to_game(id: int): 95 | print("Player %s joined the game!" % id) 96 | 97 | var player_to_add = multiplayer_scene.instantiate() 98 | player_to_add.player_id = id 99 | player_to_add.name = str(id) 100 | 101 | _players_spawn_node.add_child(player_to_add, true) 102 | 103 | func _del_player(id: int): 104 | print("Player %s left the game!" % id) 105 | if not _players_spawn_node.has_node(str(id)): 106 | return 107 | _players_spawn_node.get_node(str(id)).queue_free() 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | -------------------------------------------------------------------------------- /First Game/addons/netfox/netfox.gd: -------------------------------------------------------------------------------- 1 | @tool 2 | extends EditorPlugin 3 | 4 | const ROOT = "res://addons/netfox" 5 | var SETTINGS = [ 6 | { 7 | # Setting this to false will make Netfox keep its settings even when 8 | # disabling the plugin. Useful for developing the plugin. 9 | "name": "netfox/general/clear_settings", 10 | "value": true, 11 | "type": TYPE_BOOL 12 | }, 13 | # Logging 14 | _NetfoxLogger.make_setting("netfox/logging/netfox_log_level"), 15 | # Time settings 16 | { 17 | "name": "netfox/time/tickrate", 18 | "value": 30, 19 | "type": TYPE_INT 20 | }, 21 | { 22 | "name": "netfox/time/max_ticks_per_frame", 23 | "value": 8, 24 | "type": TYPE_INT 25 | }, 26 | { 27 | "name": "netfox/time/recalibrate_threshold", 28 | "value": 8.0, 29 | "type": TYPE_FLOAT 30 | }, 31 | { 32 | # Time to wait between time syncs 33 | "name": "netfox/time/sync_interval", 34 | "value": 1.0, 35 | "type": TYPE_FLOAT 36 | }, 37 | { 38 | "name": "netfox/time/sync_samples", 39 | "value": 8, 40 | "type": TYPE_INT 41 | }, 42 | { 43 | # Time to wait between time sync samples 44 | "name": "netfox/time/sync_sample_interval", 45 | "value": 0.1, 46 | "type": TYPE_FLOAT 47 | }, 48 | { 49 | "name": "netfox/time/sync_to_physics", 50 | "value": false, 51 | "type": TYPE_BOOL 52 | }, 53 | # Rollback settings 54 | { 55 | "name": "netfox/rollback/enabled", 56 | "value": true, 57 | "type": TYPE_BOOL 58 | }, 59 | { 60 | "name": "netfox/rollback/history_limit", 61 | "value": 64, 62 | "type": TYPE_INT 63 | }, 64 | { 65 | "name": "netfox/rollback/input_redundancy", 66 | "value": 3, 67 | "type": TYPE_INT 68 | }, 69 | { 70 | "name": "netfox/rollback/display_offset", 71 | "value": 0, 72 | "type": TYPE_INT 73 | }, 74 | # Events 75 | { 76 | "name": "netfox/events/enabled", 77 | "value": true, 78 | "type": TYPE_BOOL 79 | } 80 | ] 81 | 82 | const AUTOLOADS = [ 83 | { 84 | "name": "NetworkTime", 85 | "path": ROOT + "/network-time.gd" 86 | }, 87 | { 88 | "name": "NetworkTimeSynchronizer", 89 | "path": ROOT + "/network-time-synchronizer.gd" 90 | }, 91 | { 92 | "name": "NetworkRollback", 93 | "path": ROOT + "/rollback/network-rollback.gd" 94 | }, 95 | { 96 | "name": "NetworkEvents", 97 | "path": ROOT + "/network-events.gd" 98 | }, 99 | { 100 | "name": "NetworkPerformance", 101 | "path": ROOT + "/network-performance.gd" 102 | } 103 | ] 104 | 105 | const TYPES = [ 106 | { 107 | "name": "RollbackSynchronizer", 108 | "base": "Node", 109 | "script": ROOT + "/rollback/rollback-synchronizer.gd", 110 | "icon": ROOT + "/icons/rollback-synchronizer.svg" 111 | }, 112 | { 113 | "name": "StateSynchronizer", 114 | "base": "Node", 115 | "script": ROOT + "/state-synchronizer.gd", 116 | "icon": ROOT + "/icons/state-synchronizer.svg" 117 | }, 118 | { 119 | "name": "TickInterpolator", 120 | "base": "Node", 121 | "script": ROOT + "/tick-interpolator.gd", 122 | "icon": ROOT + "/icons/tick-interpolator.svg" 123 | }, 124 | ] 125 | 126 | func _enter_tree(): 127 | for setting in SETTINGS: 128 | add_setting(setting) 129 | 130 | for autoload in AUTOLOADS: 131 | add_autoload_singleton(autoload.name, autoload.path) 132 | 133 | for type in TYPES: 134 | add_custom_type(type.name, type.base, load(type.script), load(type.icon)) 135 | 136 | func _exit_tree(): 137 | if ProjectSettings.get_setting("netfox/general/clear_settings", false): 138 | for setting in SETTINGS: 139 | remove_setting(setting) 140 | 141 | for autoload in AUTOLOADS: 142 | remove_autoload_singleton(autoload.name) 143 | 144 | for type in TYPES: 145 | remove_custom_type(type.name) 146 | 147 | func add_setting(setting: Dictionary): 148 | if ProjectSettings.has_setting(setting.name): 149 | return 150 | 151 | ProjectSettings.set_setting(setting.name, setting.value) 152 | ProjectSettings.set_initial_value(setting.name, setting.value) 153 | ProjectSettings.add_property_info({ 154 | "name": setting.get("name"), 155 | "type": setting.get("type"), 156 | "hint": setting.get("hint", PROPERTY_HINT_NONE), 157 | "hint_string": setting.get("hint_string", "") 158 | }) 159 | 160 | func remove_setting(setting: Dictionary): 161 | if not ProjectSettings.has_setting(setting.name): 162 | return 163 | 164 | ProjectSettings.clear(setting.name) 165 | -------------------------------------------------------------------------------- /First Game/addons/netfox/network-events.gd: -------------------------------------------------------------------------------- 1 | extends Node 2 | ## This class provides convenience signals for multiplayer games. 3 | ## 4 | ## While the client start/stop and peer join/leave events are trivial, the 5 | ## server side has no similar events. This means that if you'd like to add some 6 | ## funcionality that should happen on server start, you either have to couple 7 | ## the code ( i.e. call it wherever you start the server ) or introduce a custom 8 | ## event to decouple your code from your network init code. 9 | ## 10 | ## By providing these convenience events, you can forego all that and instead 11 | ## just listen to a single signal that should work no matter what. 12 | ## 13 | ## [i]Note:[/i] This class also manages [NetworkTime] start/stop, so as long as 14 | ## network events are enabled, you don't need to manually call start/stop. 15 | 16 | ## Event emitted when the [MultiplayerAPI] is changed 17 | signal on_multiplayer_change(old: MultiplayerAPI, new: MultiplayerAPI) 18 | 19 | ## Event emitted when the server starts 20 | signal on_server_start() 21 | 22 | ## Event emitted when the server stops for any reason 23 | signal on_server_stop() 24 | 25 | ## Event emitted when the client starts 26 | signal on_client_start(id: int) 27 | 28 | ## Event emitted when the client stops. 29 | ## 30 | ## This can happen due to either the client itself or the server disconnecting 31 | ## for whatever reason. 32 | signal on_client_stop() 33 | 34 | ## Event emitted when a new peer joins the game. 35 | signal on_peer_join(id: int) 36 | 37 | ## Event emitted when a peer leaves the game. 38 | signal on_peer_leave(id: int) 39 | 40 | ## Whether the events are enabled. 41 | ## 42 | ## Events are only emitted when it's enabled. Disabling this can free up some 43 | ## performance, as when enabled, the multiplayer API and the host are 44 | ## continuously checked for changes. 45 | ## 46 | ## The initial value is taken from the Netfox project settings. 47 | var enabled: bool: 48 | get: return _enabled 49 | set(v): _set_enabled(v) 50 | 51 | var _is_server: bool = false 52 | var _multiplayer: MultiplayerAPI 53 | var _enabled = false 54 | 55 | ## Check if we're running as server. 56 | func is_server() -> bool: 57 | if multiplayer == null: 58 | return false 59 | 60 | var peer = multiplayer.multiplayer_peer 61 | if peer == null: 62 | return false 63 | 64 | if peer is OfflineMultiplayerPeer: 65 | return false 66 | 67 | if peer.get_connection_status() != MultiplayerPeer.CONNECTION_CONNECTED: 68 | return false 69 | 70 | if not multiplayer.is_server(): 71 | return false 72 | 73 | return true 74 | 75 | func _ready(): 76 | enabled = ProjectSettings.get_setting("netfox/events/enabled", true) 77 | 78 | # Automatically start ticking when entering multiplayer and stop when 79 | # leaving multiplayer 80 | on_server_start.connect(NetworkTime.start) 81 | on_server_stop.connect(NetworkTime.stop) 82 | on_client_start.connect(func(id): NetworkTime.start()) 83 | on_client_stop.connect(NetworkTime.stop) 84 | 85 | func _process(_delta): 86 | if multiplayer != _multiplayer: 87 | _disconnect_handlers(_multiplayer) 88 | _connect_handlers(multiplayer) 89 | 90 | on_multiplayer_change.emit(_multiplayer, multiplayer) 91 | _multiplayer = multiplayer 92 | 93 | if not _is_server and is_server(): 94 | _is_server = true 95 | on_server_start.emit() 96 | 97 | if _is_server and not is_server(): 98 | _is_server = false 99 | on_server_stop.emit() 100 | 101 | func _connect_handlers(mp: MultiplayerAPI): 102 | if mp == null: 103 | return 104 | 105 | mp.connected_to_server.connect(_handle_connected_to_server) 106 | mp.server_disconnected.connect(_handle_server_disconnected) 107 | mp.peer_connected.connect(_handle_peer_connected) 108 | mp.peer_disconnected.connect(_handle_peer_disconnected) 109 | 110 | func _disconnect_handlers(mp: MultiplayerAPI): 111 | if mp == null: 112 | return 113 | 114 | mp.connected_to_server.disconnect(_handle_connected_to_server) 115 | mp.server_disconnected.disconnect(_handle_server_disconnected) 116 | mp.peer_connected.disconnect(_handle_peer_connected) 117 | mp.peer_disconnected.disconnect(_handle_peer_disconnected) 118 | 119 | func _handle_connected_to_server(): 120 | on_client_start.emit(multiplayer.get_unique_id()) 121 | 122 | func _handle_server_disconnected(): 123 | on_client_stop.emit() 124 | 125 | func _handle_peer_connected(id: int): 126 | on_peer_join.emit(id) 127 | 128 | func _handle_peer_disconnected(id: int): 129 | on_peer_leave.emit(id) 130 | 131 | func _set_enabled(enable: bool): 132 | if _enabled and not enable: 133 | _disconnect_handlers(_multiplayer) 134 | _multiplayer = null 135 | if not _enabled and enable: 136 | _multiplayer = multiplayer 137 | _connect_handlers(_multiplayer) 138 | 139 | _enabled = enable 140 | set_process(enable) 141 | -------------------------------------------------------------------------------- /First Game/addons/netfox/network-time-synchronizer.gd: -------------------------------------------------------------------------------- 1 | extends Node 2 | 3 | ## Time between syncs, in seconds. 4 | ## 5 | ## [i]read-only[/i], you can change this in the Netfox project settings 6 | var sync_interval: float: 7 | get: 8 | return ProjectSettings.get_setting("netfox/time/sync_interval", 1.0) 9 | set(v): 10 | push_error("Trying to set read-only variable sync_interval") 11 | 12 | ## Number of measurements ( samples ) to take to guess latency. 13 | ## 14 | ## [i]read-only[/i], you can change this in the Netfox project settings 15 | var sync_samples: int: 16 | get: 17 | return ProjectSettings.get_setting("netfox/time/sync_samples", 8) 18 | set(v): 19 | push_error("Trying to set read-only variable sync_samples") 20 | 21 | ## Time between samples in a single sync process. 22 | ## 23 | ## [i]read-only[/i], you can change this in the Netfox project settings 24 | var sync_sample_interval: float: 25 | get: 26 | return ProjectSettings.get_setting("netfox/time/sync_sample_interval", 0.1) 27 | set(v): 28 | push_error("Trying to set read-only variable sync_sample_interval") 29 | 30 | var _remote_rtt: Dictionary = {} 31 | var _remote_time: Dictionary = {} 32 | var _remote_tick: Dictionary = {} 33 | var _active: bool = false 34 | 35 | ## Event emitted when a time sync process completes 36 | signal on_sync(server_time: float, server_tick: int, rtt: float) 37 | 38 | ## Event emitted when a response to a ping request arrives. 39 | signal on_ping(peer_id: int, peer_time: float, peer_tick: int) 40 | 41 | ## Start the time synchronization loop. 42 | ## 43 | ## Starting multiple times has no effect. 44 | func start(): 45 | if _active: 46 | return 47 | 48 | _active = true 49 | _sync_time_loop(sync_interval) 50 | 51 | ## Stop the time synchronization loop. 52 | func stop(): 53 | _active = false 54 | 55 | ## Get the amount of time passed since Godot has started, in seconds. 56 | func get_real_time(): 57 | return Time.get_ticks_msec() / 1000.0 58 | 59 | ## Estimate the time at the given peer, in seconds. 60 | ## 61 | ## While this is a coroutine, so it won't block your game, this can take multiple 62 | ## seconds, depending on latency, number of samples and sample interval. 63 | ## 64 | ## Returns a triplet of the following: 65 | ## [ol] 66 | ## last_remote_time - Latest timestamp received from target 67 | ## rtt - Estimated roundtrip time to target 68 | ## synced_time - Estimated time at target 69 | ## [/ol] 70 | func sync_time(id: int) -> Array[float]: 71 | _remote_rtt.clear() 72 | _remote_time.clear() 73 | _remote_tick.clear() 74 | 75 | for i in range(sync_samples): 76 | get_rtt(id, i) 77 | await get_tree().create_timer(sync_sample_interval).timeout 78 | 79 | # Wait for all samples to run through 80 | while _remote_rtt.size() != sync_samples: 81 | await get_tree().process_frame 82 | 83 | var samples = _remote_rtt.values().duplicate() 84 | var last_remote_time = _remote_time.values().max() 85 | samples.sort() 86 | var average = samples.reduce(func(a, b): return a + b) / samples.size() 87 | 88 | # Reject samples that are too far away from average 89 | var deviation_threshold = 1 90 | samples = samples.filter(func(s): return (s - average) / average < deviation_threshold) 91 | 92 | # Return NAN if none of the samples fit within threshold 93 | # Should be rare, but technically possible 94 | if samples.is_empty(): 95 | return [NAN, NAN, NAN] 96 | 97 | average = samples.reduce(func(a, b): return a + b) / samples.size() 98 | var rtt = average 99 | var latency = rtt / 2.0 100 | 101 | return [last_remote_time, rtt, last_remote_time + latency] 102 | 103 | ## Get roundtrip time to a given peer, in seconds. 104 | func get_rtt(id: int, sample_id: int = -1) -> float: 105 | if id == multiplayer.get_unique_id(): 106 | return 0 107 | 108 | var trip_start = get_real_time() 109 | rpc_id(id, "_request_ping") 110 | var response = await on_ping 111 | var trip_end = get_real_time() 112 | var rtt = trip_end - trip_start 113 | 114 | _remote_rtt[sample_id] = rtt 115 | _remote_time[sample_id] = response[1] 116 | _remote_tick[sample_id] = response[2] 117 | return rtt 118 | 119 | func _sync_time_loop(interval: float): 120 | while true: 121 | var sync_result = await sync_time(1) 122 | var rtt = sync_result[1] 123 | var new_time = sync_result[2] 124 | 125 | if not _active: 126 | # Make sure we don't emit any events if we've been stopped since 127 | break 128 | if new_time == NAN: 129 | # Skip if sync has failed 130 | continue 131 | 132 | var new_tick = floor(new_time * NetworkTime.tickrate) 133 | new_time = NetworkTime.ticks_to_seconds(new_tick) # Sync to tick 134 | 135 | on_sync.emit(new_time, new_tick, rtt) 136 | await get_tree().create_timer(interval).timeout 137 | 138 | @rpc("any_peer", "reliable", "call_remote") 139 | func _request_ping(): 140 | var sender = multiplayer.get_remote_sender_id() 141 | rpc_id(sender, "_respond_ping", NetworkTime.time, NetworkTime.tick) 142 | 143 | @rpc("any_peer", "reliable", "call_remote") 144 | func _respond_ping(peer_time: float, peer_tick: int): 145 | var sender = multiplayer.get_remote_sender_id() 146 | on_ping.emit(sender, peer_time, peer_tick) 147 | -------------------------------------------------------------------------------- /First Game/scenes/player.tscn: -------------------------------------------------------------------------------- 1 | [gd_scene load_steps=26 format=3 uid="uid://dqnaqj8yb6hwd"] 2 | 3 | [ext_resource type="Script" path="res://scripts/player.gd" id="1_ofpvf"] 4 | [ext_resource type="Texture2D" uid="uid://b8cmjj8vq3r8d" path="res://assets/sprites/knight.png" id="1_p3dy5"] 5 | 6 | [sub_resource type="AtlasTexture" id="AtlasTexture_eq0sc"] 7 | atlas = ExtResource("1_p3dy5") 8 | region = Rect2(0, 0, 32, 32) 9 | 10 | [sub_resource type="AtlasTexture" id="AtlasTexture_euih6"] 11 | atlas = ExtResource("1_p3dy5") 12 | region = Rect2(32, 0, 32, 32) 13 | 14 | [sub_resource type="AtlasTexture" id="AtlasTexture_bvckt"] 15 | atlas = ExtResource("1_p3dy5") 16 | region = Rect2(64, 0, 32, 32) 17 | 18 | [sub_resource type="AtlasTexture" id="AtlasTexture_5qbp8"] 19 | atlas = ExtResource("1_p3dy5") 20 | region = Rect2(96, 0, 32, 32) 21 | 22 | [sub_resource type="AtlasTexture" id="AtlasTexture_m4p6y"] 23 | atlas = ExtResource("1_p3dy5") 24 | region = Rect2(64, 160, 32, 32) 25 | 26 | [sub_resource type="AtlasTexture" id="AtlasTexture_sbdhl"] 27 | atlas = ExtResource("1_p3dy5") 28 | region = Rect2(0, 64, 32, 32) 29 | 30 | [sub_resource type="AtlasTexture" id="AtlasTexture_cixn3"] 31 | atlas = ExtResource("1_p3dy5") 32 | region = Rect2(32, 64, 32, 32) 33 | 34 | [sub_resource type="AtlasTexture" id="AtlasTexture_wlmx5"] 35 | atlas = ExtResource("1_p3dy5") 36 | region = Rect2(64, 64, 32, 32) 37 | 38 | [sub_resource type="AtlasTexture" id="AtlasTexture_2mug6"] 39 | atlas = ExtResource("1_p3dy5") 40 | region = Rect2(96, 64, 32, 32) 41 | 42 | [sub_resource type="AtlasTexture" id="AtlasTexture_7fabi"] 43 | atlas = ExtResource("1_p3dy5") 44 | region = Rect2(128, 64, 32, 32) 45 | 46 | [sub_resource type="AtlasTexture" id="AtlasTexture_8kjed"] 47 | atlas = ExtResource("1_p3dy5") 48 | region = Rect2(160, 64, 32, 32) 49 | 50 | [sub_resource type="AtlasTexture" id="AtlasTexture_36jc6"] 51 | atlas = ExtResource("1_p3dy5") 52 | region = Rect2(192, 64, 32, 32) 53 | 54 | [sub_resource type="AtlasTexture" id="AtlasTexture_xdrox"] 55 | atlas = ExtResource("1_p3dy5") 56 | region = Rect2(224, 64, 32, 32) 57 | 58 | [sub_resource type="AtlasTexture" id="AtlasTexture_c7dpx"] 59 | atlas = ExtResource("1_p3dy5") 60 | region = Rect2(0, 96, 32, 32) 61 | 62 | [sub_resource type="AtlasTexture" id="AtlasTexture_pyrft"] 63 | atlas = ExtResource("1_p3dy5") 64 | region = Rect2(32, 96, 32, 32) 65 | 66 | [sub_resource type="AtlasTexture" id="AtlasTexture_dkv65"] 67 | atlas = ExtResource("1_p3dy5") 68 | region = Rect2(64, 96, 32, 32) 69 | 70 | [sub_resource type="AtlasTexture" id="AtlasTexture_312ld"] 71 | atlas = ExtResource("1_p3dy5") 72 | region = Rect2(96, 96, 32, 32) 73 | 74 | [sub_resource type="AtlasTexture" id="AtlasTexture_08i1f"] 75 | atlas = ExtResource("1_p3dy5") 76 | region = Rect2(128, 96, 32, 32) 77 | 78 | [sub_resource type="AtlasTexture" id="AtlasTexture_ajbxf"] 79 | atlas = ExtResource("1_p3dy5") 80 | region = Rect2(160, 96, 32, 32) 81 | 82 | [sub_resource type="AtlasTexture" id="AtlasTexture_ducdv"] 83 | atlas = ExtResource("1_p3dy5") 84 | region = Rect2(192, 96, 32, 32) 85 | 86 | [sub_resource type="AtlasTexture" id="AtlasTexture_wq37r"] 87 | atlas = ExtResource("1_p3dy5") 88 | region = Rect2(224, 96, 32, 32) 89 | 90 | [sub_resource type="SpriteFrames" id="SpriteFrames_81gkq"] 91 | animations = [{ 92 | "frames": [{ 93 | "duration": 1.0, 94 | "texture": SubResource("AtlasTexture_eq0sc") 95 | }, { 96 | "duration": 1.0, 97 | "texture": SubResource("AtlasTexture_euih6") 98 | }, { 99 | "duration": 1.0, 100 | "texture": SubResource("AtlasTexture_bvckt") 101 | }, { 102 | "duration": 1.0, 103 | "texture": SubResource("AtlasTexture_5qbp8") 104 | }], 105 | "loop": true, 106 | "name": &"idle", 107 | "speed": 10.0 108 | }, { 109 | "frames": [{ 110 | "duration": 1.0, 111 | "texture": SubResource("AtlasTexture_m4p6y") 112 | }], 113 | "loop": true, 114 | "name": &"jump", 115 | "speed": 5.0 116 | }, { 117 | "frames": [{ 118 | "duration": 1.0, 119 | "texture": SubResource("AtlasTexture_sbdhl") 120 | }, { 121 | "duration": 1.0, 122 | "texture": SubResource("AtlasTexture_cixn3") 123 | }, { 124 | "duration": 1.0, 125 | "texture": SubResource("AtlasTexture_wlmx5") 126 | }, { 127 | "duration": 1.0, 128 | "texture": SubResource("AtlasTexture_2mug6") 129 | }, { 130 | "duration": 1.0, 131 | "texture": SubResource("AtlasTexture_7fabi") 132 | }, { 133 | "duration": 1.0, 134 | "texture": SubResource("AtlasTexture_8kjed") 135 | }, { 136 | "duration": 1.0, 137 | "texture": SubResource("AtlasTexture_36jc6") 138 | }, { 139 | "duration": 1.0, 140 | "texture": SubResource("AtlasTexture_xdrox") 141 | }, { 142 | "duration": 1.0, 143 | "texture": SubResource("AtlasTexture_c7dpx") 144 | }, { 145 | "duration": 1.0, 146 | "texture": SubResource("AtlasTexture_pyrft") 147 | }, { 148 | "duration": 1.0, 149 | "texture": SubResource("AtlasTexture_dkv65") 150 | }, { 151 | "duration": 1.0, 152 | "texture": SubResource("AtlasTexture_312ld") 153 | }, { 154 | "duration": 1.0, 155 | "texture": SubResource("AtlasTexture_08i1f") 156 | }, { 157 | "duration": 1.0, 158 | "texture": SubResource("AtlasTexture_ajbxf") 159 | }, { 160 | "duration": 1.0, 161 | "texture": SubResource("AtlasTexture_ducdv") 162 | }, { 163 | "duration": 1.0, 164 | "texture": SubResource("AtlasTexture_wq37r") 165 | }], 166 | "loop": true, 167 | "name": &"run", 168 | "speed": 10.0 169 | }] 170 | 171 | [sub_resource type="CircleShape2D" id="CircleShape2D_bvcm0"] 172 | radius = 5.0 173 | 174 | [node name="Player" type="CharacterBody2D"] 175 | z_index = 5 176 | collision_layer = 2 177 | script = ExtResource("1_ofpvf") 178 | 179 | [node name="AnimatedSprite2D" type="AnimatedSprite2D" parent="."] 180 | position = Vector2(0, -12) 181 | sprite_frames = SubResource("SpriteFrames_81gkq") 182 | animation = &"jump" 183 | autoplay = "idle" 184 | 185 | [node name="CollisionShape2D" type="CollisionShape2D" parent="."] 186 | position = Vector2(0, -5) 187 | shape = SubResource("CircleShape2D_bvcm0") 188 | -------------------------------------------------------------------------------- /First Game/scenes/coin.tscn: -------------------------------------------------------------------------------- 1 | [gd_scene load_steps=21 format=3 uid="uid://pqp5qaw561fr"] 2 | 3 | [ext_resource type="Texture2D" uid="uid://bped01tsjeycn" path="res://assets/sprites/coin.png" id="1_hrxtj"] 4 | [ext_resource type="Script" path="res://scripts/coin.gd" id="1_ix6bn"] 5 | [ext_resource type="AudioStream" uid="uid://hxv3svfwkg67" path="res://assets/sounds/coin.wav" id="3_ag2jy"] 6 | 7 | [sub_resource type="AtlasTexture" id="AtlasTexture_1fm7y"] 8 | atlas = ExtResource("1_hrxtj") 9 | region = Rect2(0, 0, 16, 16) 10 | 11 | [sub_resource type="AtlasTexture" id="AtlasTexture_mbwas"] 12 | atlas = ExtResource("1_hrxtj") 13 | region = Rect2(16, 0, 16, 16) 14 | 15 | [sub_resource type="AtlasTexture" id="AtlasTexture_ex1m6"] 16 | atlas = ExtResource("1_hrxtj") 17 | region = Rect2(32, 0, 16, 16) 18 | 19 | [sub_resource type="AtlasTexture" id="AtlasTexture_2u7hg"] 20 | atlas = ExtResource("1_hrxtj") 21 | region = Rect2(48, 0, 16, 16) 22 | 23 | [sub_resource type="AtlasTexture" id="AtlasTexture_rjnp2"] 24 | atlas = ExtResource("1_hrxtj") 25 | region = Rect2(64, 0, 16, 16) 26 | 27 | [sub_resource type="AtlasTexture" id="AtlasTexture_hvebx"] 28 | atlas = ExtResource("1_hrxtj") 29 | region = Rect2(80, 0, 16, 16) 30 | 31 | [sub_resource type="AtlasTexture" id="AtlasTexture_pwl53"] 32 | atlas = ExtResource("1_hrxtj") 33 | region = Rect2(96, 0, 16, 16) 34 | 35 | [sub_resource type="AtlasTexture" id="AtlasTexture_jj66p"] 36 | atlas = ExtResource("1_hrxtj") 37 | region = Rect2(112, 0, 16, 16) 38 | 39 | [sub_resource type="AtlasTexture" id="AtlasTexture_6x2no"] 40 | atlas = ExtResource("1_hrxtj") 41 | region = Rect2(128, 0, 16, 16) 42 | 43 | [sub_resource type="AtlasTexture" id="AtlasTexture_tba44"] 44 | atlas = ExtResource("1_hrxtj") 45 | region = Rect2(144, 0, 16, 16) 46 | 47 | [sub_resource type="AtlasTexture" id="AtlasTexture_wxbvu"] 48 | atlas = ExtResource("1_hrxtj") 49 | region = Rect2(160, 0, 16, 16) 50 | 51 | [sub_resource type="AtlasTexture" id="AtlasTexture_nfuqw"] 52 | atlas = ExtResource("1_hrxtj") 53 | region = Rect2(176, 0, 16, 16) 54 | 55 | [sub_resource type="SpriteFrames" id="SpriteFrames_i3y1m"] 56 | animations = [{ 57 | "frames": [{ 58 | "duration": 1.0, 59 | "texture": SubResource("AtlasTexture_1fm7y") 60 | }, { 61 | "duration": 1.0, 62 | "texture": SubResource("AtlasTexture_mbwas") 63 | }, { 64 | "duration": 1.0, 65 | "texture": SubResource("AtlasTexture_ex1m6") 66 | }, { 67 | "duration": 1.0, 68 | "texture": SubResource("AtlasTexture_2u7hg") 69 | }, { 70 | "duration": 1.0, 71 | "texture": SubResource("AtlasTexture_rjnp2") 72 | }, { 73 | "duration": 1.0, 74 | "texture": SubResource("AtlasTexture_hvebx") 75 | }, { 76 | "duration": 1.0, 77 | "texture": SubResource("AtlasTexture_pwl53") 78 | }, { 79 | "duration": 1.0, 80 | "texture": SubResource("AtlasTexture_jj66p") 81 | }, { 82 | "duration": 1.0, 83 | "texture": SubResource("AtlasTexture_6x2no") 84 | }, { 85 | "duration": 1.0, 86 | "texture": SubResource("AtlasTexture_tba44") 87 | }, { 88 | "duration": 1.0, 89 | "texture": SubResource("AtlasTexture_wxbvu") 90 | }, { 91 | "duration": 1.0, 92 | "texture": SubResource("AtlasTexture_nfuqw") 93 | }], 94 | "loop": true, 95 | "name": &"default", 96 | "speed": 10.0 97 | }] 98 | 99 | [sub_resource type="CircleShape2D" id="CircleShape2D_osao3"] 100 | radius = 5.0 101 | 102 | [sub_resource type="Animation" id="Animation_0t7oa"] 103 | resource_name = "pickup" 104 | tracks/0/type = "value" 105 | tracks/0/imported = false 106 | tracks/0/enabled = true 107 | tracks/0/path = NodePath("AnimatedSprite2D:visible") 108 | tracks/0/interp = 1 109 | tracks/0/loop_wrap = true 110 | tracks/0/keys = { 111 | "times": PackedFloat32Array(0), 112 | "transitions": PackedFloat32Array(1), 113 | "update": 1, 114 | "values": [false] 115 | } 116 | tracks/1/type = "value" 117 | tracks/1/imported = false 118 | tracks/1/enabled = true 119 | tracks/1/path = NodePath("CollisionShape2D:disabled") 120 | tracks/1/interp = 1 121 | tracks/1/loop_wrap = true 122 | tracks/1/keys = { 123 | "times": PackedFloat32Array(0), 124 | "transitions": PackedFloat32Array(1), 125 | "update": 1, 126 | "values": [true] 127 | } 128 | tracks/2/type = "value" 129 | tracks/2/imported = false 130 | tracks/2/enabled = true 131 | tracks/2/path = NodePath("PickupSound:playing") 132 | tracks/2/interp = 1 133 | tracks/2/loop_wrap = true 134 | tracks/2/keys = { 135 | "times": PackedFloat32Array(0), 136 | "transitions": PackedFloat32Array(1), 137 | "update": 1, 138 | "values": [true] 139 | } 140 | tracks/3/type = "method" 141 | tracks/3/imported = false 142 | tracks/3/enabled = true 143 | tracks/3/path = NodePath(".") 144 | tracks/3/interp = 1 145 | tracks/3/loop_wrap = true 146 | tracks/3/keys = { 147 | "times": PackedFloat32Array(1), 148 | "transitions": PackedFloat32Array(1), 149 | "values": [{ 150 | "args": [], 151 | "method": &"queue_free" 152 | }] 153 | } 154 | 155 | [sub_resource type="Animation" id="Animation_66sdr"] 156 | length = 0.001 157 | tracks/0/type = "value" 158 | tracks/0/imported = false 159 | tracks/0/enabled = true 160 | tracks/0/path = NodePath("AnimatedSprite2D:visible") 161 | tracks/0/interp = 1 162 | tracks/0/loop_wrap = true 163 | tracks/0/keys = { 164 | "times": PackedFloat32Array(0), 165 | "transitions": PackedFloat32Array(1), 166 | "update": 1, 167 | "values": [true] 168 | } 169 | tracks/1/type = "value" 170 | tracks/1/imported = false 171 | tracks/1/enabled = true 172 | tracks/1/path = NodePath("CollisionShape2D:disabled") 173 | tracks/1/interp = 1 174 | tracks/1/loop_wrap = true 175 | tracks/1/keys = { 176 | "times": PackedFloat32Array(0), 177 | "transitions": PackedFloat32Array(1), 178 | "update": 1, 179 | "values": [false] 180 | } 181 | tracks/2/type = "value" 182 | tracks/2/imported = false 183 | tracks/2/enabled = true 184 | tracks/2/path = NodePath("PickupSound:playing") 185 | tracks/2/interp = 1 186 | tracks/2/loop_wrap = true 187 | tracks/2/keys = { 188 | "times": PackedFloat32Array(0), 189 | "transitions": PackedFloat32Array(1), 190 | "update": 1, 191 | "values": [false] 192 | } 193 | 194 | [sub_resource type="AnimationLibrary" id="AnimationLibrary_gmtgn"] 195 | _data = { 196 | "RESET": SubResource("Animation_66sdr"), 197 | "pickup": SubResource("Animation_0t7oa") 198 | } 199 | 200 | [node name="Coin" type="Area2D"] 201 | collision_mask = 2 202 | script = ExtResource("1_ix6bn") 203 | 204 | [node name="AnimatedSprite2D" type="AnimatedSprite2D" parent="."] 205 | sprite_frames = SubResource("SpriteFrames_i3y1m") 206 | autoplay = "default" 207 | 208 | [node name="CollisionShape2D" type="CollisionShape2D" parent="."] 209 | shape = SubResource("CircleShape2D_osao3") 210 | 211 | [node name="PickupSound" type="AudioStreamPlayer2D" parent="."] 212 | stream = ExtResource("3_ag2jy") 213 | bus = &"SFX" 214 | 215 | [node name="AnimationPlayer" type="AnimationPlayer" parent="."] 216 | libraries = { 217 | "": SubResource("AnimationLibrary_gmtgn") 218 | } 219 | 220 | [connection signal="body_entered" from="." to="." method="_on_body_entered"] 221 | -------------------------------------------------------------------------------- /First Game/addons/netfox/rollback/network-rollback.gd: -------------------------------------------------------------------------------- 1 | extends Node 2 | 3 | ## Whether rollback is enabled. 4 | var enabled: bool = ProjectSettings.get_setting("netfox/rollback/enabled", true) 5 | 6 | ## How many ticks to store as history. 7 | ## 8 | ## The larger the history limit, the further we can roll back into the past, 9 | ## thus the more latency we can manage. 10 | ## 11 | ## [i]read-only[/i], you can change this in the project settings 12 | var history_limit: int: 13 | get: 14 | return ProjectSettings.get_setting("netfox/rollback/history_limit", 64) 15 | set(v): 16 | push_error("Trying to set read-only variable history_limit") 17 | 18 | ## Offset into the past for display. 19 | ## 20 | ## After the rollback, we have the option to not display the absolute latest 21 | ## state of the game, but let's say the state two frames ago ( offset = 2 ). 22 | ## This can help with hiding latency, by giving more time for an up-to-date 23 | ## state to arrive before we try to display it. 24 | ## 25 | ## [i]read-only[/i], you can change this in the project settings 26 | var display_offset: int: 27 | get: 28 | return ProjectSettings.get_setting("netfox/rollback/display_offset", 0) 29 | set(v): 30 | push_error("Trying to set read-only variable display_offset") 31 | 32 | ## How many previous input frames to send along with the current one. 33 | ## 34 | ## Input data is sent unreliably over UDP for speed. 35 | ## Some packets may be lost, some arrive late or out of order. 36 | ## To mitigate this, we can send the current and previous n ticks of input data. 37 | ## 38 | ## [i]read-only[/i], you can change this in the project settings 39 | var input_redundancy: int: 40 | get: 41 | return ProjectSettings.get_setting("netfox/rollback/input_redundancy", 3) 42 | set(v): 43 | push_error("Trying to set read-only variable input_redundancy") 44 | 45 | var tick: int: 46 | get: 47 | return _tick 48 | set(v): 49 | push_error("Trying to set read-only variable tick") 50 | 51 | ## Event emitted before running the network rollback loop 52 | signal before_loop() 53 | 54 | ## Event emitted in preparation of each rollback tick. 55 | ## 56 | ## Handlers should apply the state and input corresponding to the given tick. 57 | signal on_prepare_tick(tick: int) 58 | 59 | ## Event emitted to process the given rollback tick. 60 | ## 61 | ## Handlers should check if they *need* to resimulate the given tick, and if so, 62 | ## generate the next state based on the current data ( applied in the prepare 63 | ## tick phase ). 64 | signal on_process_tick(tick: int) 65 | 66 | ## Event emitted to record the given rollback tick. 67 | ## 68 | ## By this time, the tick is advanced from the simulation, handlers should save 69 | ## their resulting states for the given tick. 70 | signal on_record_tick(tick: int) 71 | 72 | ## Event emitted after running the network rollback loop 73 | signal after_loop() 74 | 75 | var _tick: int = 0 76 | var _resim_from: int 77 | 78 | var _is_rollback: bool = false 79 | var _simulated_nodes: Dictionary = {} 80 | 81 | ## Submit the resimulation start tick for the current loop. 82 | ## 83 | ## This is used to determine the resimulation range during each loop. 84 | func notify_resimulation_start(tick: int): 85 | _resim_from = min(_resim_from, tick) 86 | 87 | ## Submit node for simulation. 88 | ## 89 | ## This is used mostly internally by [RollbackSynchronizer]. The idea is to 90 | ## submit each affected node while preparing the tick, and then run only the 91 | ## nodes that need to be resimulated. 92 | func notify_simulated(node: Node): 93 | _simulated_nodes[node] = true 94 | 95 | 96 | ## Check if node was submitted for simulation. 97 | ## 98 | ## This is used mostly internally by [RollbackSynchronizer]. The idea is to 99 | ## submit each affected node while preparing the tick, and then use 100 | ## [code]is_simulated[/code] to run only the nodes that need to be resimulated. 101 | func is_simulated(node: Node): 102 | return _simulated_nodes.has(node) 103 | 104 | ## Check if a network rollback is currently active. 105 | func is_rollback() -> bool: 106 | return _is_rollback 107 | 108 | ## Checks if a given object is rollback-aware, i.e. has the 109 | ## [code]_rollback_tick[/code] method implemented. 110 | ## 111 | ## This is used by [RollbackSynchronizer] to see if it should simulate the 112 | ## given object during rollback. 113 | func is_rollback_aware(what: Object) -> bool: 114 | return what.has_method("_rollback_tick") 115 | 116 | ## Calls the [code]_rollback_tick[/code] method on the target, running its 117 | ## simulation for the given rollback tick. 118 | ## 119 | ## This is used by [RollbackSynchronizer] to resimulate ticks during rollback. 120 | ## While the _rollback_tick method could be called directly as well, this method 121 | ## exists to future-proof the code a bit, so the method name is not repeated all 122 | ## over the place. 123 | ## 124 | ## [i]Note:[/i] Make sure to check if the target is rollback-aware, because if 125 | ## it's not, this method will run into an error. 126 | func process_rollback(target: Object, delta: float, p_tick: int, is_fresh: bool): 127 | target._rollback_tick(delta, p_tick, is_fresh) 128 | 129 | func _ready(): 130 | NetworkTime.after_tick_loop.connect(_rollback) 131 | 132 | func _rollback(): 133 | if not enabled: 134 | return 135 | 136 | _is_rollback = true 137 | 138 | # Ask all rewindables to submit their earliest inputs 139 | _resim_from = NetworkTime.tick 140 | before_loop.emit() 141 | 142 | # from = Earliest input amongst all rewindables 143 | var from = _resim_from 144 | 145 | # to = Current tick 146 | var to = NetworkTime.tick 147 | 148 | # for tick in from .. to: 149 | for tick in range(from, to): 150 | _tick = tick 151 | _simulated_nodes.clear() 152 | 153 | # Prepare state 154 | # Done individually by Rewindables ( usually Rollback Synchronizers ) 155 | # Restore input and state for tick 156 | on_prepare_tick.emit(tick) 157 | 158 | # Simulate rollback tick 159 | # Method call on rewindables 160 | # Rollback synchronizers go through each node they manage 161 | # If current tick is in node's range, tick 162 | # If authority: Latest input >= tick >= Latest state 163 | # If not: Latest input >= tick >= Earliest input 164 | on_process_tick.emit(tick) 165 | 166 | # Record state for tick + 1 167 | on_record_tick.emit(tick + 1) 168 | 169 | # Restore display state 170 | after_loop.emit() 171 | _is_rollback = false 172 | 173 | # Insight 1: 174 | # state(x) = simulate(state(x - 1), input(x - 1)) 175 | # state(x + 1) = simulate(state(x), input(x)) 176 | # Insight 2: 177 | # Server is authorative over all state, client over its own input, i.e. 178 | # Server broadcasts state 179 | # Client sends input to server 180 | # Flow: 181 | # Clients send in their inputs 182 | # Server simulates frames from earliest input to current 183 | # Server broadcasts simulated frames 184 | # Clients receive authorative states 185 | # Clients simulate local frames 186 | -------------------------------------------------------------------------------- /First Game/scenes/multiplayer_player.tscn: -------------------------------------------------------------------------------- 1 | [gd_scene load_steps=30 format=3 uid="uid://d226nufmlvivj"] 2 | 3 | [ext_resource type="Script" path="res://scripts/multiplayer/multiplayer_controller.gd" id="1_ij7lf"] 4 | [ext_resource type="Texture2D" uid="uid://b8cmjj8vq3r8d" path="res://assets/sprites/knight.png" id="2_5qtv2"] 5 | [ext_resource type="Script" path="res://scripts/multiplayer/multiplayer_input.gd" id="2_75sn2"] 6 | [ext_resource type="Script" path="res://addons/netfox/rollback/rollback-synchronizer.gd" id="3_qb0r7"] 7 | [ext_resource type="Script" path="res://addons/netfox/tick-interpolator.gd" id="4_3ymtc"] 8 | 9 | [sub_resource type="SceneReplicationConfig" id="SceneReplicationConfig_bcfu3"] 10 | properties/0/path = NodePath(".:player_id") 11 | properties/0/spawn = true 12 | properties/0/replication_mode = 0 13 | 14 | [sub_resource type="AtlasTexture" id="AtlasTexture_eq0sc"] 15 | atlas = ExtResource("2_5qtv2") 16 | region = Rect2(0, 0, 32, 32) 17 | 18 | [sub_resource type="AtlasTexture" id="AtlasTexture_euih6"] 19 | atlas = ExtResource("2_5qtv2") 20 | region = Rect2(32, 0, 32, 32) 21 | 22 | [sub_resource type="AtlasTexture" id="AtlasTexture_bvckt"] 23 | atlas = ExtResource("2_5qtv2") 24 | region = Rect2(64, 0, 32, 32) 25 | 26 | [sub_resource type="AtlasTexture" id="AtlasTexture_5qbp8"] 27 | atlas = ExtResource("2_5qtv2") 28 | region = Rect2(96, 0, 32, 32) 29 | 30 | [sub_resource type="AtlasTexture" id="AtlasTexture_m4p6y"] 31 | atlas = ExtResource("2_5qtv2") 32 | region = Rect2(64, 160, 32, 32) 33 | 34 | [sub_resource type="AtlasTexture" id="AtlasTexture_sbdhl"] 35 | atlas = ExtResource("2_5qtv2") 36 | region = Rect2(0, 64, 32, 32) 37 | 38 | [sub_resource type="AtlasTexture" id="AtlasTexture_cixn3"] 39 | atlas = ExtResource("2_5qtv2") 40 | region = Rect2(32, 64, 32, 32) 41 | 42 | [sub_resource type="AtlasTexture" id="AtlasTexture_wlmx5"] 43 | atlas = ExtResource("2_5qtv2") 44 | region = Rect2(64, 64, 32, 32) 45 | 46 | [sub_resource type="AtlasTexture" id="AtlasTexture_2mug6"] 47 | atlas = ExtResource("2_5qtv2") 48 | region = Rect2(96, 64, 32, 32) 49 | 50 | [sub_resource type="AtlasTexture" id="AtlasTexture_7fabi"] 51 | atlas = ExtResource("2_5qtv2") 52 | region = Rect2(128, 64, 32, 32) 53 | 54 | [sub_resource type="AtlasTexture" id="AtlasTexture_8kjed"] 55 | atlas = ExtResource("2_5qtv2") 56 | region = Rect2(160, 64, 32, 32) 57 | 58 | [sub_resource type="AtlasTexture" id="AtlasTexture_36jc6"] 59 | atlas = ExtResource("2_5qtv2") 60 | region = Rect2(192, 64, 32, 32) 61 | 62 | [sub_resource type="AtlasTexture" id="AtlasTexture_xdrox"] 63 | atlas = ExtResource("2_5qtv2") 64 | region = Rect2(224, 64, 32, 32) 65 | 66 | [sub_resource type="AtlasTexture" id="AtlasTexture_c7dpx"] 67 | atlas = ExtResource("2_5qtv2") 68 | region = Rect2(0, 96, 32, 32) 69 | 70 | [sub_resource type="AtlasTexture" id="AtlasTexture_pyrft"] 71 | atlas = ExtResource("2_5qtv2") 72 | region = Rect2(32, 96, 32, 32) 73 | 74 | [sub_resource type="AtlasTexture" id="AtlasTexture_dkv65"] 75 | atlas = ExtResource("2_5qtv2") 76 | region = Rect2(64, 96, 32, 32) 77 | 78 | [sub_resource type="AtlasTexture" id="AtlasTexture_312ld"] 79 | atlas = ExtResource("2_5qtv2") 80 | region = Rect2(96, 96, 32, 32) 81 | 82 | [sub_resource type="AtlasTexture" id="AtlasTexture_08i1f"] 83 | atlas = ExtResource("2_5qtv2") 84 | region = Rect2(128, 96, 32, 32) 85 | 86 | [sub_resource type="AtlasTexture" id="AtlasTexture_ajbxf"] 87 | atlas = ExtResource("2_5qtv2") 88 | region = Rect2(160, 96, 32, 32) 89 | 90 | [sub_resource type="AtlasTexture" id="AtlasTexture_ducdv"] 91 | atlas = ExtResource("2_5qtv2") 92 | region = Rect2(192, 96, 32, 32) 93 | 94 | [sub_resource type="AtlasTexture" id="AtlasTexture_wq37r"] 95 | atlas = ExtResource("2_5qtv2") 96 | region = Rect2(224, 96, 32, 32) 97 | 98 | [sub_resource type="SpriteFrames" id="SpriteFrames_jsyko"] 99 | animations = [{ 100 | "frames": [{ 101 | "duration": 1.0, 102 | "texture": SubResource("AtlasTexture_eq0sc") 103 | }, { 104 | "duration": 1.0, 105 | "texture": SubResource("AtlasTexture_euih6") 106 | }, { 107 | "duration": 1.0, 108 | "texture": SubResource("AtlasTexture_bvckt") 109 | }, { 110 | "duration": 1.0, 111 | "texture": SubResource("AtlasTexture_5qbp8") 112 | }], 113 | "loop": true, 114 | "name": &"idle", 115 | "speed": 10.0 116 | }, { 117 | "frames": [{ 118 | "duration": 1.0, 119 | "texture": SubResource("AtlasTexture_m4p6y") 120 | }], 121 | "loop": true, 122 | "name": &"jump", 123 | "speed": 5.0 124 | }, { 125 | "frames": [{ 126 | "duration": 1.0, 127 | "texture": SubResource("AtlasTexture_sbdhl") 128 | }, { 129 | "duration": 1.0, 130 | "texture": SubResource("AtlasTexture_cixn3") 131 | }, { 132 | "duration": 1.0, 133 | "texture": SubResource("AtlasTexture_wlmx5") 134 | }, { 135 | "duration": 1.0, 136 | "texture": SubResource("AtlasTexture_2mug6") 137 | }, { 138 | "duration": 1.0, 139 | "texture": SubResource("AtlasTexture_7fabi") 140 | }, { 141 | "duration": 1.0, 142 | "texture": SubResource("AtlasTexture_8kjed") 143 | }, { 144 | "duration": 1.0, 145 | "texture": SubResource("AtlasTexture_36jc6") 146 | }, { 147 | "duration": 1.0, 148 | "texture": SubResource("AtlasTexture_xdrox") 149 | }, { 150 | "duration": 1.0, 151 | "texture": SubResource("AtlasTexture_c7dpx") 152 | }, { 153 | "duration": 1.0, 154 | "texture": SubResource("AtlasTexture_pyrft") 155 | }, { 156 | "duration": 1.0, 157 | "texture": SubResource("AtlasTexture_dkv65") 158 | }, { 159 | "duration": 1.0, 160 | "texture": SubResource("AtlasTexture_312ld") 161 | }, { 162 | "duration": 1.0, 163 | "texture": SubResource("AtlasTexture_08i1f") 164 | }, { 165 | "duration": 1.0, 166 | "texture": SubResource("AtlasTexture_ajbxf") 167 | }, { 168 | "duration": 1.0, 169 | "texture": SubResource("AtlasTexture_ducdv") 170 | }, { 171 | "duration": 1.0, 172 | "texture": SubResource("AtlasTexture_wq37r") 173 | }], 174 | "loop": true, 175 | "name": &"run", 176 | "speed": 10.0 177 | }] 178 | 179 | [sub_resource type="CircleShape2D" id="CircleShape2D_7dch5"] 180 | radius = 5.0 181 | 182 | [node name="MultiplayerPlayer" type="CharacterBody2D" node_paths=PackedStringArray("input")] 183 | collision_layer = 2 184 | script = ExtResource("1_ij7lf") 185 | input = NodePath("Input") 186 | 187 | [node name="PlayerSynchronizer" type="MultiplayerSynchronizer" parent="."] 188 | replication_config = SubResource("SceneReplicationConfig_bcfu3") 189 | 190 | [node name="Input" type="Node" parent="."] 191 | script = ExtResource("2_75sn2") 192 | 193 | [node name="RollbackSynchronizer" type="Node" parent="." node_paths=PackedStringArray("root")] 194 | script = ExtResource("3_qb0r7") 195 | root = NodePath("..") 196 | state_properties = Array[String]([":global_transform", ":velocity"]) 197 | input_properties = Array[String](["Input:input_direction", "Input:input_jump"]) 198 | 199 | [node name="TickInterpolator" type="Node" parent="." node_paths=PackedStringArray("root")] 200 | script = ExtResource("4_3ymtc") 201 | root = NodePath("..") 202 | properties = Array[String]([":global_transform"]) 203 | 204 | [node name="AnimatedSprite2D" type="AnimatedSprite2D" parent="."] 205 | position = Vector2(0, -12) 206 | sprite_frames = SubResource("SpriteFrames_jsyko") 207 | animation = &"jump" 208 | autoplay = "idle" 209 | 210 | [node name="CollisionShape2D" type="CollisionShape2D" parent="."] 211 | position = Vector2(0, -5) 212 | shape = SubResource("CircleShape2D_7dch5") 213 | 214 | [node name="Camera2D" type="Camera2D" parent="."] 215 | position = Vector2(0, -7) 216 | zoom = Vector2(4, 4) 217 | limit_bottom = 120 218 | limit_smoothed = true 219 | position_smoothing_enabled = true 220 | 221 | [node name="RespawnTimer" type="Timer" parent="."] 222 | wait_time = 0.6 223 | one_shot = true 224 | 225 | [node name="Username" type="Label" parent="."] 226 | anchors_preset = 15 227 | anchor_right = 1.0 228 | anchor_bottom = 1.0 229 | offset_top = -42.0 230 | offset_right = 1.0 231 | offset_bottom = -19.0 232 | grow_horizontal = 2 233 | grow_vertical = 2 234 | horizontal_alignment = 1 235 | 236 | [connection signal="timeout" from="RespawnTimer" to="." method="_respawn"] 237 | -------------------------------------------------------------------------------- /First Game/addons/netfox/rollback/rollback-synchronizer.gd: -------------------------------------------------------------------------------- 1 | extends Node 2 | class_name RollbackSynchronizer 3 | 4 | ## Similar to [MultiplayerSynchronizer], this class is responsible for 5 | ## synchronizing data between players, but with support for rollback. 6 | 7 | @export var root: Node = get_parent() 8 | @export var state_properties: Array[String] 9 | @export var input_properties: Array[String] 10 | 11 | var _record_state_props: Array[PropertyEntry] = [] 12 | var _record_input_props: Array[PropertyEntry] = [] 13 | var _auth_state_props: Array[PropertyEntry] = [] 14 | var _auth_input_props: Array[PropertyEntry] = [] 15 | var _nodes: Array[Node] = [] 16 | 17 | var _states: Dictionary = {} 18 | var _inputs: Dictionary = {} 19 | var _latest_state: int = -1 20 | var _earliest_input: int 21 | 22 | var _property_cache: PropertyCache 23 | var _freshness_store: RollbackFreshnessStore 24 | 25 | static var _logger: _NetfoxLogger = _NetfoxLogger.for_netfox("RollbackSynchronizer") 26 | 27 | ## Process settings. 28 | ## 29 | ## Call this after any change to configuration. Updates based on authority too 30 | ## ( calls process_authority ). 31 | func process_settings(): 32 | _property_cache = PropertyCache.new(root) 33 | _freshness_store = RollbackFreshnessStore.new() 34 | 35 | _nodes.clear() 36 | _record_state_props.clear() 37 | 38 | _states.clear() 39 | _inputs.clear() 40 | _latest_state = NetworkTime.tick - 1 41 | _earliest_input = NetworkTime.tick 42 | 43 | # Gather state props - all state props are recorded 44 | for property in state_properties: 45 | var pe = _property_cache.get_entry(property) 46 | _record_state_props.push_back(pe) 47 | 48 | process_authority() 49 | 50 | # Gather all rollback-aware nodes to simulate during rollbacks 51 | _nodes = root.find_children("*") 52 | _nodes.push_front(root) 53 | _nodes = _nodes.filter(func(it): return NetworkRollback.is_rollback_aware(it)) 54 | _nodes.erase(self) 55 | 56 | ## Process settings based on authority. 57 | ## 58 | ## Call this whenever the authority of any of the nodes managed by 59 | ## RollbackSynchronizer changes. Make sure to do this at the same time on all 60 | ## peers. 61 | func process_authority(): 62 | _record_input_props.clear() 63 | _auth_input_props.clear() 64 | _auth_state_props.clear() 65 | 66 | # Gather state properties that we own 67 | # i.e. it's the state of a node that belongs to the local peer 68 | for property in state_properties: 69 | var pe = _property_cache.get_entry(property) 70 | if pe.node.is_multiplayer_authority(): 71 | _auth_state_props.push_back(pe) 72 | 73 | # Gather input properties that we own 74 | # Only record input that is our own 75 | for property in input_properties: 76 | var pe = _property_cache.get_entry(property) 77 | if pe.node.is_multiplayer_authority(): 78 | _record_input_props.push_back(pe) 79 | _auth_input_props.push_back(pe) 80 | 81 | func _ready(): 82 | process_settings() 83 | 84 | if not NetworkTime.is_initial_sync_done(): 85 | # Wait for time sync to complete 86 | await NetworkTime.after_sync 87 | _latest_state = NetworkTime.tick - 1 88 | 89 | NetworkTime.before_tick.connect(_before_tick) 90 | NetworkTime.after_tick.connect(_after_tick) 91 | NetworkRollback.before_loop.connect(_before_loop) 92 | NetworkRollback.on_prepare_tick.connect(_prepare_tick) 93 | NetworkRollback.on_process_tick.connect(_process_tick) 94 | NetworkRollback.on_record_tick.connect(_record_tick) 95 | NetworkRollback.after_loop.connect(_after_loop) 96 | 97 | func _before_loop(): 98 | if _auth_input_props.is_empty(): 99 | # We don't have any inputs we own, simulate from earliest we've received 100 | NetworkRollback.notify_resimulation_start(_earliest_input) 101 | else: 102 | # We own inputs, simulate from latest authorative state 103 | NetworkRollback.notify_resimulation_start(_latest_state) 104 | 105 | func _prepare_tick(tick: int): 106 | # Prepare state 107 | # Done individually by Rewindables ( usually Rollback Synchronizers ) 108 | # Restore input and state for tick 109 | var state = _get_history(_states, tick) 110 | var input = _get_history(_inputs, tick) 111 | 112 | PropertySnapshot.apply(state, _property_cache) 113 | PropertySnapshot.apply(input, _property_cache) 114 | 115 | for node in _nodes: 116 | if _can_simulate(node, tick): 117 | NetworkRollback.notify_simulated(node) 118 | 119 | func _can_simulate(node: Node, tick: int) -> bool: 120 | if node.is_multiplayer_authority(): 121 | # Simulate from earliest input 122 | # Don't simulate frames we don't have input for 123 | return tick >= _earliest_input and _inputs.has(tick) 124 | else: 125 | # Simulate ONLY if we have state from server 126 | # Simulate from latest authorative state - anything the server confirmed we don't rerun 127 | # Don't simulate frames we don't have input for 128 | return tick >= _latest_state and _inputs.has(tick) 129 | 130 | func _process_tick(tick: int): 131 | # Simulate rollback tick 132 | # Method call on rewindables 133 | # Rollback synchronizers go through each node they manage 134 | # If current tick is in node's range, tick 135 | # If authority: Latest input >= tick >= Latest state 136 | # If not: Latest input >= tick >= Earliest input 137 | for node in _nodes: 138 | if NetworkRollback.is_simulated(node): 139 | var is_fresh = _freshness_store.is_fresh(node, tick) 140 | NetworkRollback.process_rollback(node, NetworkTime.ticktime, tick, is_fresh) 141 | _freshness_store.notify_processed(node, tick) 142 | 143 | func _record_tick(tick: int): 144 | # Broadcast state we own 145 | if not _auth_state_props.is_empty(): 146 | var broadcast = {} 147 | 148 | for property in _auth_state_props: 149 | if _can_simulate(property.node, tick - 1): 150 | # Only broadcast if we've simulated the node 151 | broadcast[property.to_string()] = property.get_value() 152 | 153 | if broadcast.size() > 0: 154 | # Broadcast as new state 155 | _latest_state = max(_latest_state, tick) 156 | _states[tick] = PropertySnapshot.merge(_states.get(tick, {}), broadcast) 157 | rpc("_submit_state", broadcast, tick) 158 | 159 | # Record state for specified tick ( current + 1 ) 160 | if not _record_state_props.is_empty() and tick > _latest_state: 161 | _states[tick] = PropertySnapshot.extract(_record_state_props) 162 | 163 | func _after_loop(): 164 | _earliest_input = NetworkTime.tick 165 | 166 | # Apply display state 167 | var display_state = _get_history(_states, NetworkTime.tick - NetworkRollback.display_offset) 168 | PropertySnapshot.apply(display_state, _property_cache) 169 | 170 | func _before_tick(_delta, tick): 171 | # Apply state for tick 172 | var state = _get_history(_states, tick) 173 | PropertySnapshot.apply(state, _property_cache) 174 | 175 | func _after_tick(_delta, _tick): 176 | if not _auth_input_props.is_empty(): 177 | var input = PropertySnapshot.extract(_auth_input_props) 178 | _inputs[NetworkTime.tick] = input 179 | 180 | #Send the last n inputs for each property 181 | var inputs = {} 182 | for i in range(0, NetworkRollback.input_redundancy): 183 | var tick_input = _inputs.get(NetworkTime.tick - i, {}) 184 | for property in tick_input: 185 | if not inputs.has(property): 186 | inputs[property] = [] 187 | inputs[property].push_back(tick_input[property]) 188 | 189 | rpc("_submit_input", inputs, NetworkTime.tick) 190 | 191 | while _states.size() > NetworkRollback.history_limit: 192 | _states.erase(_states.keys().min()) 193 | 194 | while _inputs.size() > NetworkRollback.history_limit: 195 | _inputs.erase(_inputs.keys().min()) 196 | 197 | _freshness_store.trim() 198 | 199 | func _get_history(buffer: Dictionary, tick: int) -> Dictionary: 200 | if buffer.has(tick): 201 | return buffer[tick] 202 | 203 | if buffer.is_empty(): 204 | return {} 205 | 206 | var earliest = buffer.keys().min() 207 | var latest = buffer.keys().max() 208 | 209 | if tick < earliest: 210 | return buffer[earliest] 211 | 212 | if tick > latest: 213 | return buffer[latest] 214 | 215 | var before = buffer.keys() \ 216 | .filter(func (key): return key < tick) \ 217 | .max() 218 | 219 | return buffer[before] 220 | 221 | @rpc("any_peer", "unreliable", "call_remote") 222 | func _submit_input(input: Dictionary, tick: int): 223 | var sender = multiplayer.get_remote_sender_id() 224 | var sanitized = {} 225 | for property in input: 226 | var pe = _property_cache.get_entry(property) 227 | var value = input[property] 228 | var input_owner = pe.node.get_multiplayer_authority() 229 | 230 | if input_owner != sender: 231 | _logger.warning("Received input for node owned by %s from %s, sender has no authority!" \ 232 | % [input_owner, sender]) 233 | continue 234 | 235 | sanitized[property] = value 236 | 237 | if sanitized.size() > 0: 238 | for property in sanitized: 239 | for i in range(0, sanitized[property].size()): 240 | var t = tick - i 241 | var old_input = _inputs.get(t, {}).get(property) 242 | var new_input = sanitized[property][i] 243 | 244 | if old_input == null: 245 | # We received an array of current and previous inputs, merge them into our history. 246 | _inputs[t] = _inputs.get(t, {}) 247 | _inputs[t][property] = new_input 248 | _earliest_input = min(_earliest_input, t) 249 | else: 250 | _logger.warning("Received invalid input from %s for tick %s for %s" % [sender, tick, root.name]) 251 | 252 | @rpc("any_peer", "unreliable_ordered", "call_remote") 253 | func _submit_state(state: Dictionary, tick: int): 254 | if tick > NetworkTime.tick: 255 | # This used to be weird, but is now expected due to estimating remote time 256 | # push_warning("Received state from the future %s / %s - adding nonetheless" % [tick, NetworkTime.tick]) 257 | pass 258 | 259 | if tick < NetworkTime.tick - NetworkRollback.history_limit and _latest_state >= 0: 260 | # State too old! 261 | _logger.error("Received state for %s, rejecting because older than %s frames" % [tick, NetworkRollback.history_limit]) 262 | return 263 | 264 | var sender = multiplayer.get_remote_sender_id() 265 | var sanitized = {} 266 | for property in state: 267 | var pe = _property_cache.get_entry(property) 268 | var value = state[property] 269 | var state_owner = pe.node.get_multiplayer_authority() 270 | 271 | if state_owner != sender: 272 | _logger.warning("Received state for node owned by %s from %s, sender has no authority!" \ 273 | % [state_owner, sender]) 274 | continue 275 | 276 | sanitized[property] = value 277 | 278 | if sanitized.size() > 0: 279 | _states[tick] = PropertySnapshot.merge(_states.get(tick, {}), sanitized) 280 | # _latest_state = max(_latest_state, tick) 281 | _latest_state = tick 282 | else: 283 | _logger.warning("Received invalid state from %s for tick %s" % [sender, tick]) 284 | -------------------------------------------------------------------------------- /First Game/addons/netfox/network-time.gd: -------------------------------------------------------------------------------- 1 | extends Node 2 | ## This class handles timing. 3 | ## 4 | ## @tutorial(NetworkTime Guide): https://foxssake.github.io/netfox/netfox/guides/network-time/ 5 | 6 | ## Number of ticks per second. 7 | ## 8 | ## [i]read-only[/i], you can change this in the project settings 9 | var tickrate: int: 10 | get: 11 | if sync_to_physics: 12 | return Engine.physics_ticks_per_second 13 | else: 14 | return ProjectSettings.get_setting("netfox/time/tickrate", 30) 15 | set(v): 16 | push_error("Trying to set read-only variable tickrate") 17 | 18 | ## Whether to sync the network ticks to physics updates. 19 | ## 20 | ## When set to true, tickrate and the custom timer is ignored, and a network 21 | ## tick will be done on every physics frame. 22 | ## 23 | ## [i]read-only[/i], you can change this in the project settings 24 | var sync_to_physics: bool: 25 | get: 26 | return ProjectSettings.get_setting("netfox/time/sync_to_physics", false) 27 | set(v): 28 | push_error("Trying to set read-only variable sync_to_physics") 29 | 30 | ## Maximum number of ticks to simulate per frame. 31 | ## 32 | ## If the game itself runs slower than the configured tickrate, multiple ticks 33 | ## will be run in a single go. However, to avoid an endless feedback loop of 34 | ## running too many ticks in a frame, which makes the game even slower, which 35 | ## results in even more ticks and so on, this setting is an upper limit on how 36 | ## many ticks can be simulated in a single go. 37 | ## 38 | ## [i]read-only[/i], you can change this in the project settings 39 | var max_ticks_per_frame: int: 40 | get: 41 | return ProjectSettings.get_setting("netfox/time/max_ticks_per_frame", 8) 42 | set(v): 43 | push_error("Trying to set read-only variable max_ticks_per_frame") 44 | 45 | ## Current network time in seconds. 46 | ## 47 | ## Time is measured from the start of NetworkTime, in practice this is often the 48 | ## time from the server's start. 49 | ## 50 | ## Use this value in cases where timestamps need to be shared with the server. 51 | ## 52 | ## [i]Note:[/i] Time is continuously synced with the server. If the difference 53 | ## between local and server time is above a certain threshold, this value will 54 | ## be adjusted. 55 | ## 56 | ## See [NetworkTimeSynchronizer]. 57 | ## See the setting [code]"netfox/time/recalibrate_threshold"[/code]. 58 | ## 59 | ## [i]read-only[/i] 60 | var time: float: 61 | get: 62 | return float(_tick) / tickrate 63 | set(v): 64 | push_error("Trying to set read-only variable time") 65 | 66 | ## Current network time in ticks. 67 | ## 68 | ## Time is measured from the start of NetworkTime, in practice this is often the 69 | ## time from the server's start. 70 | ## 71 | ## Use this value in cases where timestamps need to be shared with the server. 72 | ## 73 | ## [i]Note:[/i] Time is continuously synced with the server. If the difference 74 | ## between local and server time is above a certain threshold, this value will 75 | ## be adjusted. 76 | ## 77 | ## See [NetworkTimeSynchronizer]. 78 | ## See the setting [code]"netfox/time/recalibrate_threshold"[/code]. 79 | ## 80 | ## [i]read-only[/i] 81 | var tick: int: 82 | get: 83 | return _tick 84 | set(v): 85 | push_error("Trying to set read-only variable tick") 86 | 87 | ## Threshold before recalibrating [code]tick[/code] and [code]time[/code]. 88 | ## 89 | ## Time is continuously synced to the server. In case the time difference is 90 | ## excessive between local and the server, both [code]tick[/code] and 91 | ## [code]time[/code] will be reset to the estimated server values. 92 | ## 93 | ## This property determines the difference threshold in seconds for 94 | ## recalibration. 95 | ## 96 | ## [i]read-only[/i], you can change this in the project settings 97 | var recalibrate_threshold: float: 98 | get: 99 | return ProjectSettings.get_setting("netfox/time/recalibrate_threshold", 8.0) 100 | set(v): 101 | push_error("Trying to set read-only variable recalibrate_threshold") 102 | 103 | ## Current network time in ticks on the server. 104 | ## 105 | ## This is value is only an estimate, and is regularly updated. This means that 106 | ## this value can and probably will change depending on network conditions. 107 | ## 108 | ## [i]read-only[/i] 109 | var remote_tick: int: 110 | get: 111 | return _remote_tick 112 | set(v): 113 | push_error("Trying to set read-only variable remote_tick") 114 | 115 | ## Current network time in seconds on the server. 116 | ## 117 | ## This is value is only an estimate, and is regularly updated. This means that 118 | ## this value can and probably will change depending on network conditions. 119 | ## 120 | ## [i]read-only[/i] 121 | var remote_time: float: 122 | get: 123 | return float(_remote_tick) / tickrate 124 | set(v): 125 | push_error("Trying to set read-only variable remote_time") 126 | 127 | ## Estimated roundtrip time to server. 128 | ## 129 | ## This value is updated regularly, during server time sync. Latency can be 130 | ## estimated as half of the roundtrip time. 131 | ## 132 | ## Will always be 0 on servers. 133 | ## 134 | ## [i]read-only[/i] 135 | var remote_rtt: float: 136 | get: 137 | return _remote_rtt 138 | set(v): 139 | push_error("Trying to set read-only variable remote_rtt") 140 | 141 | ## Current network time in ticks. 142 | ## 143 | ## On clients, this value is synced to the server [i]only once[/i] when joining 144 | ## the game. After that, it will increase monotonically, incrementing every 145 | ## single tick. 146 | ## 147 | ## When hosting, this value is simply the number of ticks since game start. 148 | ## 149 | ## This property can be used for things that require a timer that is guaranteed 150 | ## to be linear, i.e. no jumps in time. 151 | ## 152 | ## [i]read-only[/i] 153 | var local_tick: int: 154 | get: 155 | return _local_tick 156 | set(v): 157 | push_error("Trying to set read-only variable local_tick") 158 | 159 | ## Current network time in seconds. 160 | ## 161 | ## On clients, this value is synced to the server [i]only once[/i] when joining 162 | ## the game. After that, it will increase monotonically, incrementing every 163 | ## single tick. 164 | ## 165 | ## When hosting, this value is simply the seconds elapsed since game start. 166 | ## 167 | ## This property can be used for things that require a timer that is guaranteed 168 | ## to be linear, i.e. no jumps in time. 169 | ## 170 | ## [i]read-only[/i] 171 | var local_time: float: 172 | get: 173 | return float(_local_tick) / tickrate 174 | set(v): 175 | push_error("Trying to set read-only variable local_time") 176 | 177 | 178 | ## Amount of time a single tick takes, in seconds. 179 | ## 180 | ## This is the reverse of tickrate 181 | ## 182 | ## [i]read-only[/i] 183 | var ticktime: float: 184 | get: 185 | return 1.0 / tickrate 186 | set(v): 187 | push_error("Trying to set read-only variable ticktime") 188 | 189 | ## Percentage of where we are in time for the current tick. 190 | ## 191 | ## 0.0 - the current tick just happened[br] 192 | ## 0.5 - we're halfway to the next tick[br] 193 | ## 1.0 - the next tick is right about to be simulated[br] 194 | ## 195 | ## [i]read-only[/i] 196 | var tick_factor: float: 197 | get: 198 | if not sync_to_physics: 199 | return 1.0 - clampf((_next_tick_time - _last_process_time) * tickrate, 0, 1) 200 | else: 201 | return Engine.get_physics_interpolation_fraction() 202 | set(v): 203 | push_error("Trying to set read-only variable tick_factor") 204 | 205 | ## Multiplier to get from physics process speeds to tick speeds. 206 | ## 207 | ## Some methods, like CharacterBody's move_and_slide take velocity in units/sec 208 | ## and figure out the time delta on their own. However, they are not aware of 209 | ## netfox's time, so motion is all wrong in a network tick. For example, the 210 | ## network ticks run at 30 fps, while the game is running at 60fps, thus 211 | ## move_and_slide will also assume that it's running on 60fps, resulting in 212 | ## slower than expected movement. 213 | ## 214 | ## To circument this, you can multiply any velocities with this variable, and 215 | ## get the desired speed. Don't forget to then divide by this value if it's a 216 | ## persistent variable ( e.g. CharacterBody's velocity ). 217 | ## 218 | ## NOTE: This works correctly both in regular and in physics frames, but may 219 | ## yield different values. 220 | ## 221 | ## [i]read-only[/i] 222 | var physics_factor: float: 223 | get: 224 | if Engine.is_in_physics_frame(): 225 | return Engine.physics_ticks_per_second / tickrate 226 | else: 227 | return ticktime / _process_delta 228 | set(v): 229 | push_error("Trying to set read-only variable physics_factor") 230 | 231 | ## Emitted before a tick loop is run. 232 | signal before_tick_loop() 233 | 234 | ## Emitted before a tick is run. 235 | signal before_tick(delta: float, tick: int) 236 | 237 | ## Emitted for every network tick. 238 | signal on_tick(delta: float, tick: int) 239 | 240 | ## Emitted after every network tick. 241 | signal after_tick(delta: float, tick: int) 242 | 243 | ## Emitted after the tick loop is run. 244 | signal after_tick_loop() 245 | 246 | ## Emitted after time is synchronized. 247 | ## 248 | ## This happens once the NetworkTime is started, and the first time sync process 249 | ## concludes. When running as server, this is emitted instantly after started. 250 | signal after_sync() 251 | 252 | ## Emitted after a client synchronizes their time. 253 | ## 254 | ## This is only emitted on the server, and is emitted when the client concludes 255 | ## their time sync process. This is useful as this event means that the client 256 | ## is ticking and gameplay has started on their end. 257 | signal after_client_sync(peer_id: int) 258 | 259 | var _tick: int = 0 260 | var _active: bool = false 261 | var _initial_sync_done = false 262 | var _process_delta: float = 0 263 | 264 | var _next_tick_time: float = 0 265 | var _last_process_time: float = 0. 266 | 267 | var _remote_rtt: float = 0 268 | var _remote_tick: int = 0 269 | var _local_tick: int = 0 270 | 271 | # Cache the synced clients, as the rpc call itself may arrive multiple times 272 | # ( for some reason? ) 273 | var _synced_clients: Dictionary = {} 274 | 275 | static var _logger: _NetfoxLogger = _NetfoxLogger.for_netfox("NetworkTime") 276 | 277 | ## Start NetworkTime. 278 | ## 279 | ## Once this is called, time will be synchronized and ticks will be consistently 280 | ## emitted. 281 | ## 282 | ## On clients, the initial time sync must complete before any ticks are emitted. 283 | ## 284 | ## To check if this initial sync is done, see [method is_initial_sync_done]. If 285 | ## you need a signal, see [signal after_sync]. 286 | func start(): 287 | if _active: 288 | return 289 | 290 | _tick = 0 291 | _remote_tick = 0 292 | _local_tick = 0 293 | _remote_rtt = 0 294 | _initial_sync_done = false 295 | 296 | after_client_sync.connect(func(pid): 297 | _logger.debug("Client #%s is now on time!" % [pid]) 298 | ) 299 | 300 | if not multiplayer.is_server(): 301 | NetworkTimeSynchronizer.start() 302 | await NetworkTimeSynchronizer.on_sync 303 | _tick = _remote_tick 304 | _local_tick = _remote_tick 305 | _initial_sync_done = true 306 | _active = true 307 | _next_tick_time = _get_os_time() 308 | after_sync.emit() 309 | 310 | rpc_id(1, "_submit_sync_success") 311 | else: 312 | _active = true 313 | _initial_sync_done = true 314 | _next_tick_time = _get_os_time() 315 | after_sync.emit() 316 | 317 | # Remove clients from the synced cache when disconnected 318 | multiplayer.peer_disconnected.connect(func(peer): _synced_clients.erase(peer)) 319 | 320 | ## Stop NetworkTime. 321 | ## 322 | ## This will stop the time sync in the background, and no more ticks will be 323 | ## emitted until the next start. 324 | func stop(): 325 | NetworkTimeSynchronizer.stop() 326 | _active = false 327 | 328 | ## Check if the initial time sync is done. 329 | func is_initial_sync_done() -> bool: 330 | return _initial_sync_done 331 | 332 | ## Check if client's time sync is complete. 333 | ## 334 | ## Using this from a client is considered an error. 335 | func is_client_synced(peer_id: int) -> bool: 336 | if not multiplayer.is_server(): 337 | _logger.error("Trying to check if client is synced from another client!") 338 | return false 339 | else: 340 | return _synced_clients.has(peer_id) 341 | 342 | ## Convert a duration of ticks to seconds. 343 | func ticks_to_seconds(ticks: int) -> float: 344 | return ticks * ticktime 345 | 346 | ## Convert a duration of seconds to ticks. 347 | func seconds_to_ticks(seconds: float) -> int: 348 | return int(seconds * tickrate) 349 | 350 | ## Calculate the duration between two ticks in seconds 351 | ## 352 | ## [i]Note:[/i] Returns negative values if tick_to is smaller than tick_from 353 | func seconds_between(tick_from: int, tick_to: int) -> float: 354 | return ticks_to_seconds(tick_to - tick_from) 355 | 356 | ## Calculate the duration between two points in time as ticks 357 | ## 358 | ## [i]Note:[/i] Returns negative values if seconds_to is smaller than seconds_from 359 | func ticks_between(seconds_from: float, seconds_to: float) -> int: 360 | return seconds_to_ticks(seconds_to - seconds_from) 361 | 362 | func _ready(): 363 | NetworkTimeSynchronizer.on_sync.connect(_handle_sync) 364 | 365 | func _process(delta): 366 | # Use OS delta to determine if the game's paused from editor, or through the SceneTree 367 | var os_delta = _get_os_time() - _last_process_time 368 | var is_delta_mismatch = os_delta / delta > 4. and os_delta > .5 369 | 370 | # Adjust next tick time if the game is paused, so we don't try to "catch up" after unpausing 371 | if (is_delta_mismatch and Engine.is_editor_hint()) or get_tree().paused: 372 | _next_tick_time += os_delta 373 | 374 | _process_delta = delta 375 | _last_process_time += os_delta 376 | 377 | # Run tick loop if needed 378 | if _active and not sync_to_physics: 379 | var ticks_in_loop = 0 380 | while _next_tick_time < _last_process_time and ticks_in_loop < max_ticks_per_frame: 381 | if ticks_in_loop == 0: 382 | before_tick_loop.emit() 383 | 384 | _run_tick() 385 | 386 | ticks_in_loop += 1 387 | _next_tick_time += ticktime 388 | 389 | if ticks_in_loop > 0: 390 | after_tick_loop.emit() 391 | 392 | func _physics_process(delta): 393 | if _active and sync_to_physics and not get_tree().paused: 394 | # Run a single tick every physics frame 395 | before_tick_loop.emit() 396 | _run_tick() 397 | after_tick_loop.emit() 398 | 399 | func _run_tick(): 400 | before_tick.emit(ticktime, tick) 401 | on_tick.emit(ticktime, tick) 402 | after_tick.emit(ticktime, tick) 403 | 404 | _tick += 1 405 | _remote_tick +=1 406 | _local_tick += 1 407 | 408 | func _get_os_time() -> float: 409 | return Time.get_ticks_msec() / 1000. 410 | 411 | func _handle_sync(server_time: float, server_tick: int, rtt: float): 412 | _remote_tick = server_tick 413 | _remote_rtt = rtt 414 | 415 | # Adjust tick if it's too far away from remote 416 | if absf(seconds_between(tick, remote_tick)) > recalibrate_threshold and _initial_sync_done: 417 | _logger.error("Large difference between estimated remote time and local time!") 418 | _logger.error("Local time: %s; Remote time: %s" % [time, remote_time]) 419 | _tick = _remote_tick 420 | 421 | @rpc("any_peer", "reliable", "call_remote") 422 | func _submit_sync_success(): 423 | var peer_id = multiplayer.get_remote_sender_id() 424 | 425 | if not _synced_clients.has(peer_id): 426 | _synced_clients[peer_id] = true 427 | after_client_sync.emit(multiplayer.get_remote_sender_id()) 428 | --------------------------------------------------------------------------------