├── 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 |
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 |
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 |
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 |
--------------------------------------------------------------------------------