├── 2D ├── Wall2D.tscn ├── NetPlayer2D.gd ├── Player2D.tscn ├── Stage2D.tscn └── LocalPlayer2D.gd ├── 3D ├── NetPlayer3D.gd ├── Player.tscn ├── Wall.tscn ├── Floor.tscn ├── LocalPlayer3D.gd └── Stage.tscn ├── default_env.tres ├── 2DWithCollisions ├── Wall2DCollisions.tscn ├── Player2DCollisions.tscn ├── NetPlayer2DCollisions.gd ├── Stage2DCollisions.tscn ├── LocalPlayer2DCollisions.gd └── InputControlCollisions.gd ├── 3DWithCollisions ├── PlayerCollisions.tscn ├── Display.gd ├── Wall.tscn ├── Floor.tscn ├── NetPlayer3DCollisions.gd ├── StageCollisions.tscn ├── LocalPlayer3DCollisions.gd └── InputControlCollisions.gd ├── project.godot ├── README.md └── InputControl.gd /2D/Wall2D.tscn: -------------------------------------------------------------------------------- 1 | [gd_scene load_steps=2 format=2] 2 | 3 | [sub_resource type="RectangleShape2D" id=1] 4 | extents = Vector2( 10, 30 ) 5 | 6 | [node name="StaticBody2D" type="StaticBody2D"] 7 | 8 | [node name="Polygon2D" type="Polygon2D" parent="."] 9 | color = Color( 0.176471, 0.239216, 0.756863, 1 ) 10 | polygon = PoolVector2Array( -10, -30, 10, -30, 10, 30, -10, 30 ) 11 | 12 | [node name="CollisionShape2D" type="CollisionShape2D" parent="."] 13 | shape = SubResource( 1 ) 14 | -------------------------------------------------------------------------------- /2D/NetPlayer2D.gd: -------------------------------------------------------------------------------- 1 | #By Jon Chau 2 | extends "res://2D/LocalPlayer2D.gd" 3 | 4 | func input_update(input): 5 | #calculate state of object for the current frame 6 | if input.net_input[0]: #W 7 | updateY += 7 8 | 9 | if input.net_input[1]: #A 10 | updateX += 7 11 | 12 | if input.net_input[2]: #S 13 | updateY -= 7 14 | 15 | if input.net_input[3]: #D 16 | updateX -= 7 17 | 18 | if !input.net_input[4]: #SPACE 19 | updateCounter += 1 20 | else: 21 | updateCounter = updateCounter/2 -------------------------------------------------------------------------------- /3D/NetPlayer3D.gd: -------------------------------------------------------------------------------- 1 | #By Jon Chau 2 | extends "res://3D/LocalPlayer3D.gd" 3 | 4 | func input_update(input): 5 | #calculate state of object for the current frame 6 | if input.net_input[0]: #W 7 | updateZ += 0.5 8 | 9 | if input.net_input[1]: #A 10 | updateX += 0.5 11 | 12 | if input.net_input[2]: #S 13 | updateZ -= 0.5 14 | 15 | if input.net_input[3]: #D 16 | updateX -= 0.5 17 | 18 | if !input.net_input[4]: #SPACE 19 | updateCounter += 1 20 | else: 21 | updateCounter = updateCounter/2 -------------------------------------------------------------------------------- /2D/Player2D.tscn: -------------------------------------------------------------------------------- 1 | [gd_scene load_steps=2 format=2] 2 | 3 | [sub_resource type="RectangleShape2D" id=1] 4 | extents = Vector2( 40, 40 ) 5 | 6 | [node name="KinematicBody2D" type="KinematicBody2D"] 7 | 8 | [node name="Polygon2D" type="Polygon2D" parent="."] 9 | scale = Vector2( 2, 2 ) 10 | color = Color( 0.439216, 0.760784, 0.219608, 1 ) 11 | polygon = PoolVector2Array( 20, 0, 20, -20, -20, -20, -20, 20, 20, 20 ) 12 | 13 | [node name="CollisionShape2D" type="CollisionShape2D" parent="."] 14 | shape = SubResource( 1 ) 15 | -------------------------------------------------------------------------------- /default_env.tres: -------------------------------------------------------------------------------- 1 | [gd_resource type="Environment" load_steps=2 format=2] 2 | 3 | [sub_resource type="ProceduralSky" id=1] 4 | sky_top_color = Color( 0.0470588, 0.454902, 0.976471, 1 ) 5 | sky_horizon_color = Color( 0.556863, 0.823529, 0.909804, 1 ) 6 | sky_curve = 0.25 7 | ground_bottom_color = Color( 0.101961, 0.145098, 0.188235, 1 ) 8 | ground_horizon_color = Color( 0.482353, 0.788235, 0.952941, 1 ) 9 | ground_curve = 0.01 10 | sun_energy = 16.0 11 | 12 | [resource] 13 | background_mode = 2 14 | background_sky = SubResource( 1 ) 15 | -------------------------------------------------------------------------------- /2DWithCollisions/Wall2DCollisions.tscn: -------------------------------------------------------------------------------- 1 | [gd_scene load_steps=2 format=2] 2 | 3 | [sub_resource type="RectangleShape2D" id=1] 4 | extents = Vector2( 10, 30 ) 5 | 6 | [node name="StaticBody2D" type="StaticBody2D"] 7 | collision_layer = 2 8 | collision_mask = 0 9 | 10 | [node name="Polygon2D" type="Polygon2D" parent="."] 11 | color = Color( 0.176471, 0.239216, 0.756863, 1 ) 12 | polygon = PoolVector2Array( -10, -30, 10, -30, 10, 30, -10, 30 ) 13 | 14 | [node name="CollisionShape2D" type="CollisionShape2D" parent="."] 15 | shape = SubResource( 1 ) 16 | -------------------------------------------------------------------------------- /3D/Player.tscn: -------------------------------------------------------------------------------- 1 | [gd_scene load_steps=4 format=2] 2 | 3 | [sub_resource type="SpatialMaterial" id=1] 4 | albedo_color = Color( 0.914063, 0.881565, 0.0821228, 1 ) 5 | roughness = 0.0 6 | 7 | [sub_resource type="CapsuleMesh" id=2] 8 | material = SubResource( 1 ) 9 | 10 | [sub_resource type="CapsuleShape" id=3] 11 | 12 | [node name="KinematicBody" type="KinematicBody"] 13 | transform = Transform( 1, 0, 0, 0, -4.37114e-008, -1, 0, 1, -4.37114e-008, 0, 0, 0 ) 14 | 15 | [node name="MeshInstance" type="MeshInstance" parent="."] 16 | mesh = SubResource( 2 ) 17 | material/0 = null 18 | 19 | [node name="CollisionShape" type="CollisionShape" parent="."] 20 | shape = SubResource( 3 ) 21 | -------------------------------------------------------------------------------- /2DWithCollisions/Player2DCollisions.tscn: -------------------------------------------------------------------------------- 1 | [gd_scene load_steps=2 format=2] 2 | 3 | [sub_resource type="RectangleShape2D" id=1] 4 | extents = Vector2( 40, 40 ) 5 | 6 | [node name="KinematicBody2D" type="KinematicBody2D"] 7 | collision_mask = 2 8 | 9 | [node name="Polygon2D" type="Polygon2D" parent="."] 10 | scale = Vector2( 2, 2 ) 11 | color = Color( 0.439216, 0.760784, 0.219608, 1 ) 12 | polygon = PoolVector2Array( 20, 0, 20, -20, -20, -20, -20, 20, 20, 20 ) 13 | 14 | [node name="CollisionShape2D" type="CollisionShape2D" parent="."] 15 | shape = SubResource( 1 ) 16 | 17 | [node name="Label" type="Label" parent="."] 18 | margin_right = 40.0 19 | margin_bottom = 36.0 20 | custom_colors/font_color = Color( 0, 0, 0, 1 ) 21 | -------------------------------------------------------------------------------- /3DWithCollisions/PlayerCollisions.tscn: -------------------------------------------------------------------------------- 1 | [gd_scene load_steps=4 format=2] 2 | 3 | [sub_resource type="SpatialMaterial" id=1] 4 | albedo_color = Color( 0.914063, 0.881565, 0.0821228, 1 ) 5 | roughness = 0.0 6 | 7 | [sub_resource type="CapsuleMesh" id=2] 8 | material = SubResource( 1 ) 9 | 10 | [sub_resource type="CapsuleShape" id=3] 11 | 12 | [node name="KinematicBody" type="KinematicBody"] 13 | transform = Transform( 1, 0, 0, 0, -4.37114e-008, -1, 0, 1, -4.37114e-008, 0, 0, 0 ) 14 | collision_layer = 0 15 | 16 | [node name="MeshInstance" type="MeshInstance" parent="."] 17 | mesh = SubResource( 2 ) 18 | material/0 = null 19 | 20 | [node name="CollisionShape" type="CollisionShape" parent="."] 21 | shape = SubResource( 3 ) 22 | -------------------------------------------------------------------------------- /3DWithCollisions/Display.gd: -------------------------------------------------------------------------------- 1 | extends Spatial 2 | 3 | var localPlayer = null 4 | var netPlayer = null 5 | var localLabel = null 6 | var netLabel = null 7 | var camera = null 8 | 9 | func _ready(): 10 | localPlayer = get_node("InputControl/LocalPlayer") 11 | netPlayer = get_node("InputControl/NetPlayer") 12 | localLabel = get_node("Display/LocalLabel") 13 | netLabel = get_node("Display/NetLabel") 14 | camera = get_node("Camera") 15 | 16 | # warning-ignore:unused_argument 17 | func _process(delta): 18 | localLabel.text = str(localPlayer.counter) 19 | netLabel.text = str(netPlayer.counter) 20 | localLabel.set_position(camera.unproject_position(localPlayer.get_translation())) 21 | netLabel.set_position(camera.unproject_position(netPlayer.get_translation())) -------------------------------------------------------------------------------- /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=4 10 | 11 | _global_script_classes=[ ] 12 | _global_script_class_icons={ 13 | 14 | } 15 | 16 | [application] 17 | 18 | config/name="Rollback_Netcode" 19 | run/main_scene="res://2DWithCollisions/Stage2DCollisions.tscn" 20 | 21 | [debug] 22 | 23 | gdscript/warnings/unsafe_cast=true 24 | 25 | [layer_names] 26 | 27 | 2d_physics/layer_1="Player" 28 | 2d_physics/layer_2="Wall" 29 | 30 | [rendering] 31 | 32 | environment/default_environment="res://default_env.tres" 33 | -------------------------------------------------------------------------------- /3D/Wall.tscn: -------------------------------------------------------------------------------- 1 | [gd_scene load_steps=4 format=2] 2 | 3 | [sub_resource type="SpatialMaterial" id=1] 4 | albedo_color = Color( 0.103989, 0.74627, 0.917969, 1 ) 5 | roughness = 0.0 6 | 7 | [sub_resource type="CubeMesh" id=2] 8 | material = SubResource( 1 ) 9 | 10 | [sub_resource type="ConvexPolygonShape" id=3] 11 | points = PoolVector3Array( -1, 1, 1, 1, 1, -1, 1, 1, 1, -1, 1, -1, -1, -1, 1, 1, -1, -1, 1, -1, 1, -1, -1, -1, 1, 1, 1, -1, 1, -1, 1, 1, -1, -1, 1, 1, 1, -1, 1, -1, -1, -1, 1, -1, -1, -1, -1, 1, 1, 1, 1, -1, -1, 1, -1, 1, 1, 1, -1, 1, 1, 1, -1, -1, -1, -1, -1, 1, -1, 1, -1, -1 ) 12 | 13 | [node name="Wall" type="Spatial"] 14 | 15 | [node name="MeshInstance" type="MeshInstance" parent="."] 16 | transform = Transform( 1, 0, 0, 0, 1, 0, 0, 0, 5, 0, 0, 0 ) 17 | mesh = SubResource( 2 ) 18 | material/0 = null 19 | 20 | [node name="StaticBody" type="StaticBody" parent="MeshInstance"] 21 | 22 | [node name="CollisionShape" type="CollisionShape" parent="MeshInstance/StaticBody"] 23 | shape = SubResource( 3 ) 24 | -------------------------------------------------------------------------------- /3D/Floor.tscn: -------------------------------------------------------------------------------- 1 | [gd_scene load_steps=4 format=2] 2 | 3 | [sub_resource type="SpatialMaterial" id=1] 4 | albedo_color = Color( 0.0546875, 1, 0, 1 ) 5 | roughness = 0.0 6 | 7 | [sub_resource type="CubeMesh" id=2] 8 | material = SubResource( 1 ) 9 | 10 | [sub_resource type="ConvexPolygonShape" id=3] 11 | points = PoolVector3Array( -1, 1, 1, 1, 1, -1, 1, 1, 1, -1, 1, -1, -1, -1, 1, 1, -1, -1, 1, -1, 1, -1, -1, -1, 1, 1, 1, -1, 1, -1, 1, 1, -1, -1, 1, 1, 1, -1, 1, -1, -1, -1, 1, -1, -1, -1, -1, 1, 1, 1, 1, -1, -1, 1, -1, 1, 1, 1, -1, 1, 1, 1, -1, -1, -1, -1, -1, 1, -1, 1, -1, -1 ) 12 | 13 | [node name="Floor" type="Spatial"] 14 | 15 | [node name="MeshInstance" type="MeshInstance" parent="."] 16 | transform = Transform( 10, 0, 4.86383e-007, 0, 1, 0, -4.86383e-007, 0, 10, 0, 0, 0 ) 17 | mesh = SubResource( 2 ) 18 | material/0 = null 19 | 20 | [node name="StaticBody" type="StaticBody" parent="MeshInstance"] 21 | 22 | [node name="CollisionShape" type="CollisionShape" parent="MeshInstance/StaticBody"] 23 | shape = SubResource( 3 ) 24 | -------------------------------------------------------------------------------- /3DWithCollisions/Wall.tscn: -------------------------------------------------------------------------------- 1 | [gd_scene load_steps=4 format=2] 2 | 3 | [sub_resource type="SpatialMaterial" id=1] 4 | albedo_color = Color( 0.103989, 0.74627, 0.917969, 1 ) 5 | roughness = 0.0 6 | 7 | [sub_resource type="CubeMesh" id=2] 8 | material = SubResource( 1 ) 9 | 10 | [sub_resource type="ConvexPolygonShape" id=3] 11 | points = PoolVector3Array( -1, 1, 1, 1, 1, -1, 1, 1, 1, -1, 1, -1, -1, -1, 1, 1, -1, -1, 1, -1, 1, -1, -1, -1, 1, 1, 1, -1, 1, -1, 1, 1, -1, -1, 1, 1, 1, -1, 1, -1, -1, -1, 1, -1, -1, -1, -1, 1, 1, 1, 1, -1, -1, 1, -1, 1, 1, 1, -1, 1, 1, 1, -1, -1, -1, -1, -1, 1, -1, 1, -1, -1 ) 12 | 13 | [node name="Wall" type="Spatial"] 14 | 15 | [node name="MeshInstance" type="MeshInstance" parent="."] 16 | transform = Transform( 1, 0, 0, 0, 1, 0, 0, 0, 5, 0, 0, 0 ) 17 | mesh = SubResource( 2 ) 18 | material/0 = null 19 | 20 | [node name="StaticBody" type="StaticBody" parent="MeshInstance"] 21 | collision_mask = 0 22 | 23 | [node name="CollisionShape" type="CollisionShape" parent="MeshInstance/StaticBody"] 24 | shape = SubResource( 3 ) 25 | -------------------------------------------------------------------------------- /3DWithCollisions/Floor.tscn: -------------------------------------------------------------------------------- 1 | [gd_scene load_steps=4 format=2] 2 | 3 | [sub_resource type="SpatialMaterial" id=1] 4 | albedo_color = Color( 0.0546875, 1, 0, 1 ) 5 | roughness = 0.0 6 | 7 | [sub_resource type="CubeMesh" id=2] 8 | material = SubResource( 1 ) 9 | 10 | [sub_resource type="ConvexPolygonShape" id=3] 11 | points = PoolVector3Array( -1, 1, 1, 1, 1, -1, 1, 1, 1, -1, 1, -1, -1, -1, 1, 1, -1, -1, 1, -1, 1, -1, -1, -1, 1, 1, 1, -1, 1, -1, 1, 1, -1, -1, 1, 1, 1, -1, 1, -1, -1, -1, 1, -1, -1, -1, -1, 1, 1, 1, 1, -1, -1, 1, -1, 1, 1, 1, -1, 1, 1, 1, -1, -1, -1, -1, -1, 1, -1, 1, -1, -1 ) 12 | 13 | [node name="Floor" type="Spatial"] 14 | 15 | [node name="MeshInstance" type="MeshInstance" parent="."] 16 | transform = Transform( 10, 0, 4.86383e-007, 0, 1, 0, -4.86383e-007, 0, 10, 0, 0, 0 ) 17 | mesh = SubResource( 2 ) 18 | material/0 = null 19 | 20 | [node name="StaticBody" type="StaticBody" parent="MeshInstance"] 21 | collision_mask = 0 22 | 23 | [node name="CollisionShape" type="CollisionShape" parent="MeshInstance/StaticBody"] 24 | shape = SubResource( 3 ) 25 | -------------------------------------------------------------------------------- /2DWithCollisions/NetPlayer2DCollisions.gd: -------------------------------------------------------------------------------- 1 | #By Jon Chau 2 | extends "res://2DWithCollisions/LocalPlayer2DCollisions.gd" 3 | 4 | func input_update(input, game_state): 5 | #calculate state of object for the current frame 6 | var vect = Vector2(0, 0) 7 | 8 | #Rect2 intersection for moving objects that can pass through 9 | for sibling in game_state: 10 | if sibling != name: 11 | if collisionMask.intersects(game_state[sibling]['collisionMask']): 12 | updateCounter += 1 13 | # print("NetPlayer) Rect2 intersection! counter is: " + str(counter) + ", updateCounter is: " + str(updateCounter)) 14 | 15 | if input.net_input[0]: #W 16 | vect.y += 7 17 | 18 | if input.net_input[1]: #A 19 | vect.x += 7 20 | 21 | if input.net_input[2]: #S 22 | vect.y -= 7 23 | 24 | if input.net_input[3]: #D 25 | vect.x -= 7 26 | 27 | if input.local_input[4]: #SPACE 28 | updateCounter = updateCounter/2 29 | 30 | #move_and_collide for "solid" stationary objects 31 | var collision = move_and_collide(vect) 32 | if collision: 33 | vect = vect.slide(collision.normal) 34 | move_and_collide(vect) 35 | 36 | collisionMask = Rect2(Vector2(position.x - rectExtents.x, position.y - rectExtents.y), Vector2(rectExtents.x, rectExtents.y) * 2) -------------------------------------------------------------------------------- /2D/Stage2D.tscn: -------------------------------------------------------------------------------- 1 | [gd_scene load_steps=6 format=2] 2 | 3 | [ext_resource path="res://InputControl.gd" type="Script" id=1] 4 | [ext_resource path="res://2D/Player2D.tscn" type="PackedScene" id=2] 5 | [ext_resource path="res://2D/LocalPlayer2D.gd" type="Script" id=3] 6 | [ext_resource path="res://2D/NetPlayer2D.gd" type="Script" id=4] 7 | [ext_resource path="res://2D/Wall2D.tscn" type="PackedScene" id=5] 8 | 9 | [node name="Node2D" type="Node2D"] 10 | 11 | [node name="InputControl" type="Node" parent="."] 12 | script = ExtResource( 1 ) 13 | 14 | [node name="LocalPlayer" parent="InputControl" instance=ExtResource( 2 )] 15 | position = Vector2( 512, 490 ) 16 | script = ExtResource( 3 ) 17 | 18 | [node name="NetPlayer" parent="InputControl" instance=ExtResource( 2 )] 19 | position = Vector2( 512, 110 ) 20 | script = ExtResource( 4 ) 21 | 22 | [node name="Wall" parent="." instance=ExtResource( 5 )] 23 | position = Vector2( 512, 30 ) 24 | scale = Vector2( 50, 1 ) 25 | 26 | [node name="Wall2" parent="." instance=ExtResource( 5 )] 27 | position = Vector2( 512, 570 ) 28 | scale = Vector2( 50, 1 ) 29 | 30 | [node name="Wall3" parent="." instance=ExtResource( 5 )] 31 | position = Vector2( 984, 300 ) 32 | scale = Vector2( 4, 10 ) 33 | 34 | [node name="Wall4" parent="." instance=ExtResource( 5 )] 35 | position = Vector2( 40, 300 ) 36 | scale = Vector2( 4, 10 ) 37 | -------------------------------------------------------------------------------- /2DWithCollisions/Stage2DCollisions.tscn: -------------------------------------------------------------------------------- 1 | [gd_scene load_steps=6 format=2] 2 | 3 | [ext_resource path="res://2DWithCollisions/InputControlCollisions.gd" type="Script" id=1] 4 | [ext_resource path="res://2DWithCollisions/Player2DCollisions.tscn" type="PackedScene" id=2] 5 | [ext_resource path="res://2DWithCollisions/LocalPlayer2DCollisions.gd" type="Script" id=3] 6 | [ext_resource path="res://2DWithCollisions/NetPlayer2DCollisions.gd" type="Script" id=4] 7 | [ext_resource path="res://2DWithCollisions/Wall2DCollisions.tscn" type="PackedScene" id=5] 8 | 9 | [node name="Node2D" type="Node2D"] 10 | 11 | [node name="InputControl" type="Node" parent="."] 12 | script = ExtResource( 1 ) 13 | 14 | [node name="LocalPlayer" parent="InputControl" instance=ExtResource( 2 )] 15 | position = Vector2( 512, 490 ) 16 | script = ExtResource( 3 ) 17 | 18 | [node name="NetPlayer" parent="InputControl" instance=ExtResource( 2 )] 19 | position = Vector2( 512, 110 ) 20 | script = ExtResource( 4 ) 21 | 22 | [node name="StaticBody2D" parent="." instance=ExtResource( 5 )] 23 | position = Vector2( 512, 30 ) 24 | scale = Vector2( 50, 1 ) 25 | 26 | [node name="StaticBody2D2" parent="." instance=ExtResource( 5 )] 27 | position = Vector2( 512, 570 ) 28 | scale = Vector2( 50, 1 ) 29 | 30 | [node name="StaticBody2D3" parent="." instance=ExtResource( 5 )] 31 | position = Vector2( 984, 300 ) 32 | scale = Vector2( 4, 10 ) 33 | 34 | [node name="StaticBody2D4" parent="." instance=ExtResource( 5 )] 35 | position = Vector2( 40, 300 ) 36 | scale = Vector2( 4, 10 ) 37 | -------------------------------------------------------------------------------- /3DWithCollisions/NetPlayer3DCollisions.gd: -------------------------------------------------------------------------------- 1 | #By Jon Chau 2 | #Note: imperfect example: desync may occur, possibly due to differing collision checks for either player 3 | extends "res://3DWithCollisions/LocalPlayer3DCollisions.gd" 4 | 5 | func input_update(input, game_state): 6 | #calculate state of object for the current frame 7 | var vect = Vector3(0, -0.05, 0) 8 | 9 | #Rect2 intersection for moving objects that can pass through 10 | for sibling in game_state: 11 | if sibling != name: 12 | if collisionMaskXZ.intersects(game_state[sibling]['collisionMaskXZ']) && collisionMaskXY.intersects(game_state[sibling]['collisionMaskXY']): 13 | updateCounter += 1 14 | # print("NetPlayer) Rect2 intersection! counter is: " + str(counter) + ", updateCounter is: " + str(updateCounter)) 15 | 16 | 17 | if input.net_input[0]: #W 18 | vect.z += 0.2 19 | 20 | if input.net_input[1]: #A 21 | vect.x += 0.2 22 | 23 | if input.net_input[2]: #S 24 | vect.z -= 0.2 25 | 26 | if input.net_input[3]: #D 27 | vect.x -= 0.2 28 | 29 | if input.net_input[4]: #SPACE 30 | updateCounter = updateCounter/2 31 | 32 | #move_and_collide for "solid" stationary objects 33 | var collision = move_and_collide(vect) 34 | if collision: 35 | vect = vect.slide(collision.normal) 36 | move_and_collide(vect) 37 | 38 | collisionMaskXY = Rect2(Vector2(translation.x - radius, translation.y - height), Vector2(radius, height) * 2) 39 | collisionMaskXZ = Rect2(Vector2(translation.x - radius, translation.z - radius), Vector2(radius, radius) * 2) 40 | -------------------------------------------------------------------------------- /2D/LocalPlayer2D.gd: -------------------------------------------------------------------------------- 1 | #By Jon Chau 2 | extends KinematicBody2D 3 | 4 | var counter = -1 #test value for checking if rollback and saving states is working properly 5 | var updateX = null 6 | var updateY = null 7 | var updateCounter = null 8 | 9 | func _ready(): 10 | updateX = position.x 11 | updateY = position.y 12 | updateCounter = counter 13 | 14 | 15 | func reset_state(game_state): 16 | if game_state.has(name): 17 | updateX = game_state[name]['x'] 18 | updateY = game_state[name]['y'] 19 | updateCounter = game_state[name]['counter'] 20 | #check if this object exists within the loaded game_state 21 | else: 22 | free() #delete from memory 23 | 24 | 25 | func frame_start(): 26 | #set update vars to current values 27 | updateX = position.x 28 | updateY = position.y 29 | updateCounter = counter 30 | 31 | 32 | func input_update(input): 33 | #calculate state of object for the current frame 34 | if input.local_input[0]: #W 35 | updateY -= 7 36 | 37 | if input.local_input[1]: #A 38 | updateX -= 7 39 | 40 | if input.local_input[2]: #S 41 | updateY += 7 42 | 43 | if input.local_input[3]: #D 44 | updateX += 7 45 | 46 | if !input.local_input[4]: #SPACE 47 | updateCounter += 1 48 | else: 49 | updateCounter = updateCounter/2 50 | 51 | 52 | func input_execute(): 53 | #execute calculated state of object for current frame 54 | set_position(Vector2(updateX, updateY)) 55 | counter = updateCounter 56 | 57 | 58 | func get_state(): 59 | #return dict of relevant state variables to be stored in Frame_States 60 | return {'x': updateX, 'y': updateY, 'counter': updateCounter} -------------------------------------------------------------------------------- /3D/LocalPlayer3D.gd: -------------------------------------------------------------------------------- 1 | #By Jon Chau 2 | extends KinematicBody 3 | 4 | var counter = -1 #test value for checking if rollback and saving states is working properly 5 | var updateX 6 | var updateY 7 | var updateZ 8 | var updateCounter 9 | 10 | func _ready(): 11 | updateX = translation.x 12 | updateY = translation.y 13 | updateZ = translation.z 14 | updateCounter = counter 15 | 16 | 17 | func reset_state(game_state): 18 | if game_state.has(name): 19 | updateX = game_state[name]['x'] 20 | updateY = game_state[name]['y'] 21 | updateZ = game_state[name]['z'] 22 | updateCounter = game_state[name]['counter'] 23 | #check if this object exists within the loaded game_state 24 | else: 25 | free() #delete from memory 26 | 27 | 28 | func frame_start(): 29 | #set update vars to current values 30 | updateX = translation.x 31 | updateY = translation.y 32 | updateZ = translation.z 33 | updateCounter = counter 34 | 35 | 36 | func input_update(input): 37 | #calculate state of object for the current frame 38 | if input.local_input[0]: #W 39 | updateZ -= 0.5 40 | 41 | if input.local_input[1]: #A 42 | updateX -= 0.5 43 | 44 | if input.local_input[2]: #S 45 | updateZ += 0.5 46 | 47 | if input.local_input[3]: #D 48 | updateX += 0.5 49 | 50 | if !input.local_input[4]: #SPACE 51 | updateCounter += 1 52 | else: 53 | updateCounter = updateCounter/2 54 | 55 | 56 | func input_execute(): 57 | #execute calculated state of object for current frame 58 | set_translation(Vector3(updateX, updateY, updateZ)) 59 | counter = updateCounter 60 | 61 | 62 | func get_state(): 63 | #return dict of relevant state variables to be stored in Frame_States 64 | return {'x': updateX, 'y': updateY, 'z': updateZ, 'counter': updateCounter} -------------------------------------------------------------------------------- /3D/Stage.tscn: -------------------------------------------------------------------------------- 1 | [gd_scene load_steps=7 format=2] 2 | 3 | [ext_resource path="res://InputControl.gd" type="Script" id=1] 4 | [ext_resource path="res://3D/Player.tscn" type="PackedScene" id=2] 5 | [ext_resource path="res://3D/LocalPlayer3D.gd" type="Script" id=3] 6 | [ext_resource path="res://3D/NetPlayer3D.gd" type="Script" id=4] 7 | [ext_resource path="res://3D/Floor.tscn" type="PackedScene" id=5] 8 | [ext_resource path="res://3D/Wall.tscn" type="PackedScene" id=6] 9 | 10 | [node name="Spatial" type="Spatial"] 11 | 12 | [node name="Camera" type="Camera" parent="."] 13 | transform = Transform( 1, 0, 0, 0, -4.37114e-008, 1, 0, -1, -4.37114e-008, 0, 20, 0 ) 14 | 15 | [node name="InputControl" type="Node" parent="."] 16 | script = ExtResource( 1 ) 17 | 18 | [node name="LocalPlayer" parent="InputControl" instance=ExtResource( 2 )] 19 | transform = Transform( 0.999999, 0, 0, 0, -4.37114e-008, -1, 0, 0.999999, -4.37114e-008, 0, 3, 7 ) 20 | collision_mask = 32769 21 | script = ExtResource( 3 ) 22 | 23 | [node name="NetPlayer" parent="InputControl" instance=ExtResource( 2 )] 24 | transform = Transform( 0.999999, 0, 0, 0, -4.37114e-008, -1, 0, 0.999999, -4.37114e-008, 0, 3, -7 ) 25 | script = ExtResource( 4 ) 26 | 27 | [node name="Floor" parent="." instance=ExtResource( 5 )] 28 | 29 | [node name="Wall" parent="." instance=ExtResource( 6 )] 30 | transform = Transform( 1, 0, 0, 0, 1, 0, 0, 0, 2, 10, 1.5, 0 ) 31 | 32 | [node name="Wall2" parent="." instance=ExtResource( 6 )] 33 | transform = Transform( 1, 0, 0, 0, 1, 0, 0, 0, 2, -10, 1.5, 0 ) 34 | 35 | [node name="Wall3" parent="." instance=ExtResource( 6 )] 36 | transform = Transform( -4.37114e-008, 0, 2, 0, 1, 0, -1, 0, -8.74228e-008, 0, 1.5, 10 ) 37 | 38 | [node name="Wall4" parent="." instance=ExtResource( 6 )] 39 | transform = Transform( -4.37114e-008, 0, 2, 0, 1, 0, -1, 0, -8.74228e-008, 0, 1.5, -10 ) 40 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # RollbackNetcodeGodot (OUTDATED) 2 | 3 | # UPDATE: New code and tutorial series available 4 | Repositories: 5 | 6 | https://github.com/JonChauG/GodotRollbackNetcode-Part-1 7 | 8 | https://github.com/JonChauG/GodotRollbackNetcode-Part-2 9 | 10 | https://github.com/JonChauG/GodotRollbackNetcode-Part-3-FINAL (Please use the repository here instead of this one) 11 | 12 | --- 13 | 14 | Tutorial Videos: 15 | 16 | Part 1: Base Game and Saving Game States - https://www.youtube.com/watch?v=AOct7C422z8 17 | 18 | Part 2: Delay-Based Netcode - https://www.youtube.com/watch?v=X55-gfqhQ_E 19 | 20 | Part 3 (Final): Rollback Netcode - https://www.youtube.com/watch?v=sg1Q_71cjd8 21 | 22 | --- 23 | Rollback netcode example for Godot. 24 | 25 | Demo video: https://www.youtube.com/watch?v=CrqZW6EoGII 26 | 27 | 28 | Many games use delay-based netcode in which games will block until they receive inputs from networked players that allow the game to proceed (to maintain synchronization between players). However, games may constantly block and delay in an imperfect network while they wait for needed inputs, making for an unpleasant experience. 29 | 30 | 31 | With rollback netcode, if the game has not yet received needed inputs from the network, the game will continue on a temporary guessed input. When the game finally receives the actual input to replace the guess, the game will resimulate the game state to as if the actual input arrived "on time". Implementations that resimulate the game state in a single frame allow for an uninterrupted transition into the current "correct" game state. The player will see a sudden and abrupt visual adjustment, but this is usually a more pleasant experience than that of delay-based netcode. 32 | 33 | 34 | In order to resimulate the game state, my implementation saves a state snapshot at every frame as a base to begin resimulation.* Given memory limitations and unpleasantness from visually extreme state corrections, it is practical to limit the amount of states saved at any given time and thus limit how far back in the past resimulation can begin from. As a result, rollback netcode (my implementation and those I've seen) blocks like delay-based netcode when the saved oldest game state has an unfulfilled guess input to be replaced by an actual input from the network. 35 | 36 | 37 | Overall, compared to delay-based netcode, rollback netcode provides a larger time window for needed packets to arrive while maintaining a good player experience. 38 | 39 | 40 | \*I save states at every frame for simplicity. I believe you only really need to save states on frames when inputs are guessed, but all player inputs should be saved on every frame until unneeded. 41 | -------------------------------------------------------------------------------- /2DWithCollisions/LocalPlayer2DCollisions.gd: -------------------------------------------------------------------------------- 1 | #By Jon Chau 2 | extends KinematicBody2D 3 | 4 | var counter = -1 #test value for checking if rollback and saving states is working properly 5 | var updateCounter = null 6 | var rectExtents = null 7 | var collisionMask = null 8 | var label = null 9 | 10 | 11 | func _ready(): 12 | updateCounter = counter 13 | label = get_node("Label") 14 | rectExtents = get_node("CollisionShape2D").shape.get_extents() #assuming constant rectangle CollisionShape2D 15 | collisionMask = Rect2(Vector2(position.x - rectExtents.x, position.y - rectExtents.y), Vector2(rectExtents.x, rectExtents.y) * 2) 16 | 17 | func reset_state(game_state): 18 | if game_state.has(name): 19 | #set x and y to state's saved position directly 20 | position.x = game_state[name]['x'] 21 | position.y = game_state[name]['y'] 22 | updateCounter = game_state[name]['counter'] 23 | collisionMask = game_state[name]['collisionMask'] 24 | #check if this object exists within the loaded game_state 25 | else: 26 | free() #delete from memory 27 | 28 | 29 | func frame_start(): 30 | #set update vars to current values 31 | updateCounter = counter 32 | collisionMask = Rect2(Vector2(position.x - rectExtents.x, position.y - rectExtents.y), Vector2(rectExtents.x, rectExtents.y) * 2) 33 | 34 | 35 | func input_update(input, game_state): 36 | #calculate state of object for the current frame 37 | var vect = Vector2(0, 0) 38 | 39 | #Rect2 intersection for moving objects that can pass through 40 | for sibling in game_state: 41 | if sibling != name: 42 | if collisionMask.intersects(game_state[sibling]['collisionMask']): 43 | updateCounter += 1 44 | # print("LocalPlayer) Rect2 intersection! counter is: " + str(counter) + ", updateCounter is: " + str(updateCounter)) 45 | 46 | if input.local_input[0]: #W 47 | vect.y -= 7 48 | 49 | if input.local_input[1]: #A 50 | vect.x -= 7 51 | 52 | if input.local_input[2]: #S 53 | vect.y += 7 54 | 55 | if input.local_input[3]: #D 56 | vect.x += 7 57 | 58 | if input.local_input[4]: #SPACE 59 | updateCounter = updateCounter/2 60 | 61 | #move_and_collide for "solid" stationary objects 62 | var collision = move_and_collide(vect) 63 | if collision: 64 | vect = vect.slide(collision.normal) 65 | move_and_collide(vect) 66 | 67 | collisionMask = Rect2(Vector2(position.x - rectExtents.x, position.y - rectExtents.y), Vector2(rectExtents.x, rectExtents.y) * 2) 68 | 69 | 70 | func input_execute(): 71 | #execute calculated state of object for current frame 72 | counter = updateCounter 73 | label.text = str(counter) 74 | 75 | 76 | 77 | func get_state(): 78 | #return dict of relevant state variables to be stored in Frame_States 79 | return {'x': position.x, 'y': position.y, 'counter': updateCounter, 'collisionMask': collisionMask} -------------------------------------------------------------------------------- /3DWithCollisions/StageCollisions.tscn: -------------------------------------------------------------------------------- 1 | [gd_scene load_steps=8 format=2] 2 | 3 | [ext_resource path="res://3DWithCollisions/Display.gd" type="Script" id=1] 4 | [ext_resource path="res://3DWithCollisions/InputControlCollisions.gd" type="Script" id=2] 5 | [ext_resource path="res://3D/Player.tscn" type="PackedScene" id=3] 6 | [ext_resource path="res://3DWithCollisions/LocalPlayer3DCollisions.gd" type="Script" id=4] 7 | [ext_resource path="res://3DWithCollisions/NetPlayer3DCollisions.gd" type="Script" id=5] 8 | [ext_resource path="res://3D/Floor.tscn" type="PackedScene" id=6] 9 | [ext_resource path="res://3D/Wall.tscn" type="PackedScene" id=7] 10 | 11 | [node name="Spatial" type="Spatial"] 12 | script = ExtResource( 1 ) 13 | 14 | [node name="Camera" type="Camera" parent="."] 15 | transform = Transform( 1, 0, 0, 0, -4.37114e-008, 1, 0, -1, -4.37114e-008, 0, 20, 0 ) 16 | 17 | [node name="InputControl" type="Node" parent="."] 18 | script = ExtResource( 2 ) 19 | 20 | [node name="LocalPlayer" parent="InputControl" instance=ExtResource( 3 )] 21 | transform = Transform( 0.999999, 0, 0, 0, -4.37113e-008, -1, 0, 0.999999, -4.37114e-008, 0, 3, 7 ) 22 | collision_layer = 0 23 | script = ExtResource( 4 ) 24 | 25 | [node name="NetPlayer" parent="InputControl" instance=ExtResource( 3 )] 26 | transform = Transform( -0.999999, -8.74227e-008, 3.82137e-015, 0, -4.37113e-008, -1, 8.74227e-008, -0.999999, 4.37114e-008, 0, 3, -7 ) 27 | collision_layer = 0 28 | script = ExtResource( 5 ) 29 | 30 | [node name="Floor" parent="." instance=ExtResource( 6 )] 31 | 32 | [node name="Wall" parent="." instance=ExtResource( 7 )] 33 | transform = Transform( 1, 0, 0, 0, 1, 0, 0, 0, 2, 10, 1.5, 0 ) 34 | 35 | [node name="Wall2" parent="." instance=ExtResource( 7 )] 36 | transform = Transform( 1, 0, 0, 0, 1, 0, 0, 0, 2, -10, 1.5, 0 ) 37 | 38 | [node name="Wall3" parent="." instance=ExtResource( 7 )] 39 | transform = Transform( -4.37114e-008, 0, 2, 0, 1, 0, -1, 0, -8.74228e-008, 0, 1.5, 10 ) 40 | 41 | [node name="Wall4" parent="." instance=ExtResource( 7 )] 42 | transform = Transform( -4.37114e-008, 0, 2, 0, 1, 0, -1, 0, -8.74228e-008, 0, 1.5, -10 ) 43 | 44 | [node name="Wall5" parent="." instance=ExtResource( 7 )] 45 | transform = Transform( -4.37114e-008, 0, 2, 0, 1, 0, -1, 0, -8.74228e-008, 0, 7, 0 ) 46 | 47 | [node name="Wall6" parent="." instance=ExtResource( 7 )] 48 | transform = Transform( 1, 0, 0, 0, 0.587785, -1.61803, 0, 0.809017, 1.17557, -7, 1.5, 4.5 ) 49 | 50 | [node name="Wall7" parent="." instance=ExtResource( 7 )] 51 | transform = Transform( 1, 0, 0, 0, 0.587785, 1.61803, 0, -0.809017, 1.17557, 7, 1.5, -4.5 ) 52 | 53 | [node name="Display" type="CanvasLayer" parent="."] 54 | 55 | [node name="LocalLabel" type="Label" parent="Display"] 56 | margin_right = 40.0 57 | margin_bottom = 36.0 58 | 59 | [node name="NetLabel" type="Label" parent="Display"] 60 | margin_right = 40.0 61 | margin_bottom = 36.0 62 | -------------------------------------------------------------------------------- /3DWithCollisions/LocalPlayer3DCollisions.gd: -------------------------------------------------------------------------------- 1 | #By Jon Chau 2 | #Note: imperfect example: desync may occur, possibly due to differing collision checks for either player 3 | extends KinematicBody 4 | 5 | var counter = -1 #test value for checking if rollback and saving states is working properly 6 | var updateCounter = null 7 | var height = null 8 | var radius = null 9 | var collisionMaskXY = null 10 | var collisionMaskXZ = null 11 | var label = null 12 | 13 | func _ready(): 14 | updateCounter = counter 15 | height = get_node("CollisionShape").shape.get_height() #assuming constant capsule CollisionShape 16 | radius = get_node("CollisionShape").shape.get_radius() #assuming constant capsule CollisionShape 17 | collisionMaskXY = Rect2(Vector2(translation.x - radius, translation.y - height), Vector2(radius, height) * 2) 18 | collisionMaskXZ = Rect2(Vector2(translation.x - radius, translation.z - radius), Vector2(radius, radius) * 2) 19 | 20 | 21 | func reset_state(game_state): 22 | if game_state.has(name): 23 | #set xyz to state's saved position directly 24 | translation.x = game_state[name]['x'] 25 | translation.y = game_state[name]['y'] 26 | translation.z = game_state[name]['z'] 27 | updateCounter = game_state[name]['counter'] 28 | collisionMaskXY = game_state[name]['collisionMaskXY'] 29 | collisionMaskXZ = game_state[name]['collisionMaskXZ'] 30 | #check if this object exists within the loaded game_state 31 | else: 32 | free() #delete from memory 33 | 34 | 35 | func frame_start(): 36 | #set update vars to current values 37 | updateCounter = counter 38 | collisionMaskXY = Rect2(Vector2(translation.x - radius, translation.y - height), Vector2(radius, height) * 2) 39 | collisionMaskXZ = Rect2(Vector2(translation.x - radius, translation.z - radius), Vector2(radius, radius) * 2) 40 | 41 | 42 | func input_update(input, game_state): 43 | #calculate state of object for the current frame 44 | var vect = Vector3(0, -0.05, 0) 45 | 46 | #Rect2 intersection for moving objects that can pass through 47 | for sibling in game_state: 48 | if sibling != name: 49 | if collisionMaskXZ.intersects(game_state[sibling]['collisionMaskXZ']) && collisionMaskXY.intersects(game_state[sibling]['collisionMaskXY']): 50 | updateCounter += 1 51 | # print("LocalPlayer) Rect2 intersection! counter is: " + str(counter) + ", updateCounter is: " + str(updateCounter)) 52 | 53 | 54 | if input.local_input[0]: #W 55 | vect.z -= 0.2 56 | 57 | if input.local_input[1]: #A 58 | vect.x -= 0.2 59 | 60 | if input.local_input[2]: #S 61 | vect.z += 0.2 62 | 63 | if input.local_input[3]: #D 64 | vect.x += 0.2 65 | 66 | if input.local_input[4]: #SPACE 67 | updateCounter = updateCounter/2 68 | 69 | #move_and_collide for "solid" stationary objects 70 | var collision = move_and_collide(vect) 71 | if collision: 72 | vect = vect.slide(collision.normal) 73 | move_and_collide(vect) 74 | 75 | collisionMaskXY = Rect2(Vector2(translation.x - radius, translation.y - height), Vector2(radius, height) * 2) 76 | collisionMaskXZ = Rect2(Vector2(translation.x - radius, translation.z - radius), Vector2(radius, radius) * 2) 77 | 78 | 79 | func input_execute(): 80 | #execute calculated state of object for current frame 81 | counter = updateCounter 82 | 83 | 84 | func get_state(): 85 | #return dict of relevant state variables to be stored in Frame_States 86 | return {'x': translation.x, 'y': translation.y, 'z': translation.z, 'counter': updateCounter, 'collisionMaskXY': collisionMaskXY, 'collisionMaskXZ': collisionMaskXZ} -------------------------------------------------------------------------------- /2DWithCollisions/InputControlCollisions.gd: -------------------------------------------------------------------------------- 1 | #By Jon Chau 2 | extends "res://InputControl.gd" 3 | 4 | func handle_input(): #get input, run rollback if necessary, implement inputs 5 | var pre_game_state = get_game_state() 6 | # print("handle_input start pre_game_state: ", pre_game_state) 7 | var actual_input = true 8 | var start_rollback = false 9 | 10 | var current_input = null 11 | var current_frame_arrival_array = [] 12 | 13 | var local_input = [false, false, false, false, false] 14 | var encoded_local_input = 0 15 | 16 | frame_start_all() #for all children, set their update vars to their current/actual values 17 | 18 | input_array_mutex.lock() 19 | #record local inputs 20 | if Input.is_key_pressed(KEY_W): 21 | local_input[0] = true 22 | encoded_local_input += 1 23 | if Input.is_key_pressed(KEY_A): 24 | local_input[1] = true 25 | encoded_local_input += 2 26 | if Input.is_key_pressed(KEY_S): 27 | local_input[2] = true 28 | encoded_local_input +=4 29 | if Input.is_key_pressed(KEY_D): 30 | local_input[3] = true 31 | encoded_local_input += 8 32 | if Input.is_key_pressed(KEY_SPACE): 33 | local_input[4] = true 34 | encoded_local_input += 16 35 | 36 | input_array[(frame_num + input_delay) % 256].local_input = local_input 37 | input_array[(frame_num + input_delay) % 256].encoded_local_input = encoded_local_input 38 | 39 | # if (false):#for testing rollback and requests (forces max rollback by only using input request system) 40 | for i in dup_send_range + 1: #send inputs for current frame as well as duplicates of past frame inputs 41 | UDPPeer.put_packet(PoolByteArray([0, (frame_num + input_delay - i) % 256, 42 | input_array[(frame_num + input_delay - i) % 256].encoded_local_input])) 43 | # print("SENT INPUT: input frame is: ", frame_num + input_delay, ", input is: ", input_array[(frame_num + input_delay) % 256].encoded_local_input) 44 | 45 | #get current input arrival boolean values for current frame & old frames eligible for rollback 46 | for i in range(0, rollback + 1): 47 | current_frame_arrival_array.push_front(input_arrival_array[frame_num - i]) #oldest frame in front 48 | 49 | input_array_mutex.unlock() 50 | 51 | input_local_saved_array_mutex.lock() 52 | input_local_saved_array[(frame_num + input_delay) % 256] = true 53 | input_local_saved_array_mutex.unlock() 54 | 55 | var current_frame_arrival = current_frame_arrival_array.pop_back() #remove current frame's arrival boolean for rollback condition comparison 56 | 57 | if current_frame_arrival_array.hash() != prev_frame_arrival_array.hash(): #if an input for an past fram has arrived (to fulfill a guess), 58 | #print("Rollback...") 59 | #iterate through all saved states until the state with the guessed input to be replaced by arrived actual input is found (rollback will begin with that state) 60 | #then, continue iterating through remaining saved states to continue rollback resimulation process 61 | var state_index = 0 #for tracking iterated element's index in state_queue 62 | for i in state_queue: #index 0 is oldest state 63 | #if an arrived input is for a past frame 64 | if (prev_frame_arrival_array[state_index] == false && current_frame_arrival_array[state_index] == true): 65 | input_array_mutex.lock() 66 | i.net_input = input_array[i.frame].net_input.duplicate() #set input in Frame_State from guess to true actual input 67 | input_array_mutex.unlock() 68 | i.actual_input = true #input has been set from guess to actual input 69 | if start_rollback == false: 70 | game_state = i.game_state #set value of game_state to old state for rollback resimulation of states/inputs 71 | reset_state_all(game_state) #reset update variables for all children to match given state ONCE 72 | start_rollback = true 73 | pre_game_state = get_game_state() 74 | update_all_with_state(input_array[i.frame], pre_game_state) #update game_state using new input 75 | #otherwise, continue simulating using currently stored input 76 | else: 77 | if start_rollback == true: 78 | pre_game_state = get_game_state() #save pre-update game_state value for Frame_State 79 | update_all_with_state(input_array[i.frame], pre_game_state) #update game_state using old (guessed or actual) input during rollback resimulation 80 | if start_rollback == true: 81 | i.game_state = pre_game_state #update Frame_States with updated game_state value. 82 | state_index += 1 83 | 84 | current_frame_arrival_array.push_back(current_frame_arrival) #reinsert current frame's arrival boolean (for next frame's prev_frame_arrival_array) 85 | current_frame_arrival_array.pop_front() #remove oldest frame's arrival boolean (needed for rollback condition comparison, but unwanted for next frame's prev_frame_arrival_array) 86 | 87 | current_input = Inputs.new() 88 | input_array_mutex.lock() 89 | #if the input for the current frame has not been received 90 | if input_arrival_array[frame_num] == false: 91 | #implement guess of empty input (can be replaced with input-guessing algorithm) 92 | # current_input.local_input = input_array[frame_num].local_input.duplicate() 93 | # input_array[frame_num].net_input = current_input.net_input 94 | 95 | #implement guess of last input used 96 | current_input.local_input = input_array[frame_num].local_input.duplicate() 97 | current_input.net_input = input_array[frame_num - 1].net_input.duplicate() 98 | input_array[frame_num].net_input = input_array[frame_num - 1].net_input.duplicate() 99 | 100 | actual_input = false 101 | else: #else (if the input for the current frame has been received), proceed with true, actual input 102 | current_input.local_input = input_array[frame_num].local_input.duplicate() 103 | current_input.net_input = input_array[frame_num].net_input.duplicate() 104 | 105 | input_arrival_array[frame_num - (rollback + 120)] = false #reset input arrival boolean for old frame 106 | input_array_mutex.unlock() 107 | 108 | input_local_saved_array_mutex.lock() 109 | input_local_saved_array[frame_num - (rollback + 120)] = false #reset viable local input boolean 110 | input_local_saved_array_mutex.unlock() 111 | 112 | if start_rollback == true: 113 | pre_game_state = get_game_state() 114 | 115 | update_all_with_state(current_input, pre_game_state) #update with current input 116 | execute_all() #implement all applied updates/inputs to all child objects 117 | 118 | #store current frame state into queue 119 | state_queue.append(Frame_State.new(current_input.local_input, current_input.net_input, frame_num, pre_game_state, actual_input)) 120 | 121 | #remove oldest state from queue if queue has exceeded size limit 122 | if len(state_queue) > rollback: 123 | state_queue.pop_front() 124 | 125 | prev_frame_arrival_array = current_frame_arrival_array #store current input arrival array for comaparisons in next frame 126 | frame_num = (frame_num + 1)%256 #increment frame_num 127 | 128 | 129 | func update_all_with_state(input, game_state): 130 | for child in get_children(): 131 | child.input_update(input, game_state) -------------------------------------------------------------------------------- /3DWithCollisions/InputControlCollisions.gd: -------------------------------------------------------------------------------- 1 | #By Jon Chau 2 | #Note: imperfect example: desync may occur, possibly due to differing collision checks for either player 3 | extends "res://InputControl.gd" 4 | 5 | func handle_input(): #get input, run rollback if necessary, implement inputs 6 | var pre_game_state = get_game_state() 7 | # print("handle_input start pre_game_state: ", pre_game_state) 8 | var actual_input = true 9 | var start_rollback = false 10 | 11 | var current_input = null 12 | var current_frame_arrival_array = [] 13 | 14 | var local_input = [false, false, false, false, false] 15 | var encoded_local_input = 0 16 | 17 | frame_start_all() #for all children, set their update vars to their current/actual values 18 | 19 | input_array_mutex.lock() 20 | #record local inputs 21 | if Input.is_key_pressed(KEY_W): 22 | local_input[0] = true 23 | encoded_local_input += 1 24 | if Input.is_key_pressed(KEY_A): 25 | local_input[1] = true 26 | encoded_local_input += 2 27 | if Input.is_key_pressed(KEY_S): 28 | local_input[2] = true 29 | encoded_local_input +=4 30 | if Input.is_key_pressed(KEY_D): 31 | local_input[3] = true 32 | encoded_local_input += 8 33 | if Input.is_key_pressed(KEY_SPACE): 34 | local_input[4] = true 35 | encoded_local_input += 16 36 | 37 | input_array[(frame_num + input_delay) % 256].local_input = local_input 38 | input_array[(frame_num + input_delay) % 256].encoded_local_input = encoded_local_input 39 | 40 | # if (false):#for testing rollback and requests (forces max rollback by only using input request system) 41 | for i in dup_send_range + 1: #send inputs for current frame as well as duplicates of past frame inputs 42 | UDPPeer.put_packet(PoolByteArray([0, (frame_num + input_delay - i) % 256, 43 | input_array[(frame_num + input_delay - i) % 256].encoded_local_input])) 44 | # print("SENT INPUT: input frame is: ", frame_num + input_delay, ", input is: ", input_array[(frame_num + input_delay) % 256].encoded_local_input) 45 | 46 | #get current input arrival boolean values for current frame & old frames eligible for rollback 47 | for i in range(0, rollback + 1): 48 | current_frame_arrival_array.push_front(input_arrival_array[frame_num - i]) #oldest frame in front 49 | 50 | input_array_mutex.unlock() 51 | 52 | input_local_saved_array_mutex.lock() 53 | input_local_saved_array[(frame_num + input_delay) % 256] = true 54 | input_local_saved_array_mutex.unlock() 55 | 56 | var current_frame_arrival = current_frame_arrival_array.pop_back() #remove current frame's arrival boolean for rollback condition comparison 57 | 58 | if current_frame_arrival_array.hash() != prev_frame_arrival_array.hash(): #if an input for an past fram has arrived (to fulfill a guess), 59 | #print("Rollback...") 60 | #iterate through all saved states until the state with the guessed input to be replaced by arrived actual input is found (rollback will begin with that state) 61 | #then, continue iterating through remaining saved states to continue rollback resimulation process 62 | var state_index = 0 #for tracking iterated element's index in state_queue 63 | for i in state_queue: #index 0 is oldest state 64 | #if an arrived input is for a past frame 65 | if (prev_frame_arrival_array[state_index] == false && current_frame_arrival_array[state_index] == true): 66 | input_array_mutex.lock() 67 | i.net_input = input_array[i.frame].net_input.duplicate() #set input in Frame_State from guess to true actual input 68 | input_array_mutex.unlock() 69 | i.actual_input = true #input has been set from guess to actual input 70 | if start_rollback == false: 71 | game_state = i.game_state #set value of game_state to old state for rollback resimulation of states/inputs 72 | reset_state_all(game_state) #reset update variables for all children to match given state ONCE 73 | start_rollback = true 74 | pre_game_state = get_game_state() 75 | update_all_with_state(input_array[i.frame], pre_game_state) #update game_state using new input 76 | #otherwise, continue simulating using currently stored input 77 | else: 78 | if start_rollback == true: 79 | pre_game_state = get_game_state() #save pre-update game_state value for Frame_State 80 | update_all_with_state(input_array[i.frame], pre_game_state) #update game_state using old (guessed or actual) input during rollback resimulation 81 | if start_rollback == true: 82 | i.game_state = pre_game_state #update Frame_States with updated game_state value. 83 | state_index += 1 84 | 85 | current_frame_arrival_array.push_back(current_frame_arrival) #reinsert current frame's arrival boolean (for next frame's prev_frame_arrival_array) 86 | current_frame_arrival_array.pop_front() #remove oldest frame's arrival boolean (needed for rollback condition comparison, but unwanted for next frame's prev_frame_arrival_array) 87 | 88 | current_input = Inputs.new() 89 | input_array_mutex.lock() 90 | #if the input for the current frame has not been received 91 | if input_arrival_array[frame_num] == false: 92 | #implement guess of empty input (can be replaced with input-guessing algorithm) 93 | # current_input.local_input = input_array[frame_num].local_input.duplicate() 94 | # input_array[frame_num].net_input = current_input.net_input 95 | 96 | #implement guess of last input used 97 | current_input.local_input = input_array[frame_num].local_input.duplicate() 98 | current_input.net_input = input_array[frame_num - 1].net_input.duplicate() 99 | input_array[frame_num].net_input = input_array[frame_num - 1].net_input.duplicate() 100 | 101 | actual_input = false 102 | else: #else (if the input for the current frame has been received), proceed with true, actual input 103 | current_input.local_input = input_array[frame_num].local_input.duplicate() 104 | current_input.net_input = input_array[frame_num].net_input.duplicate() 105 | 106 | input_arrival_array[frame_num - (rollback + 120)] = false #reset input arrival boolean for old frame 107 | input_array_mutex.unlock() 108 | 109 | input_local_saved_array_mutex.lock() 110 | input_local_saved_array[frame_num - (rollback + 120)] = false #reset viable local input boolean 111 | input_local_saved_array_mutex.unlock() 112 | 113 | if start_rollback == true: 114 | pre_game_state = get_game_state() 115 | 116 | update_all_with_state(current_input, pre_game_state) #update with current input 117 | execute_all() #implement all applied updates/inputs to all child objects 118 | 119 | #store current frame state into queue 120 | state_queue.append(Frame_State.new(current_input.local_input, current_input.net_input, frame_num, pre_game_state, actual_input)) 121 | 122 | #remove oldest state from queue if queue has exceeded size limit 123 | if len(state_queue) > rollback: 124 | state_queue.pop_front() 125 | 126 | prev_frame_arrival_array = current_frame_arrival_array #store current input arrival array for comaparisons in next frame 127 | frame_num = (frame_num + 1)%256 #increment frame_num 128 | 129 | 130 | func update_all_with_state(input, game_state): 131 | for child in get_children(): 132 | child.input_update(input, game_state) -------------------------------------------------------------------------------- /InputControl.gd: -------------------------------------------------------------------------------- 1 | #By Jon Chau 2 | extends Node 3 | 4 | #amount of input delay in frames 5 | var input_delay = 5 6 | #number of frame states to save in order to implement rollback (max amount of frames able to rollback) 7 | var rollback = 7 8 | #frame range of duplicate past input packets to send every frame (should be less than rollback in current implementation) 9 | var dup_send_range = 5 10 | 11 | var input_array = [] #array for inputs 12 | var state_queue = [] #queue for states at passed frames (for rollback) 13 | var input_arrival_array = [] #boolean array to determine if inputs for a given frame have arrived from the network 14 | var input_local_saved_array = [] #boolean array to determine if local inputs for a given frame are viable (can be sent by request) 15 | var prev_frame_arrival_array = [] #boolean array to compare input arrivals between current and previous frames 16 | var input_array_mutex = Mutex.new() 17 | var input_local_saved_array_mutex = Mutex.new() 18 | 19 | var frame_num = 0 #ranges between 0-255 per circular input array cycle (cycle is every 256 frames) 20 | 21 | var game_state = {} #holds dictionaries that track children states every frame 22 | 23 | var input_received #boolean to detect if new inputs have been received, set to true by networking thread, set to false by main when waiting on input 24 | var input_received_mutex = Mutex.new() 25 | 26 | var input_thread = null #thread to receive inputs over the network 27 | 28 | var UDPPeer = PacketPeerUDP.new() 29 | 30 | #class declarations 31 | class Inputs: 32 | #Indexing [0]: W, [1]: A, [2]: S, [3]: D, [4]: SPACE 33 | #inputs by local player for a single frame 34 | var local_input = [false, false, false, false, false] 35 | #inputs by a player over network for a single frame 36 | var net_input = [false, false, false, false, false] 37 | var encoded_local_input = 0 38 | 39 | 40 | class Frame_State: 41 | var local_input #inputs by local player for a single frame 42 | var net_input #inputs by a player over network for a single frame 43 | var frame #frame number according to 256 frame cycle number 44 | var game_state #dictionary holds the values need for tracking a game's state at a given frame. Keys are child names. 45 | var actual_input #boolean, whether the state contains guessed input (false) or actual input (true) from networked player 46 | 47 | 48 | func _init(_local_input : Array, _net_input : Array, _frame : int, _game_state : Dictionary, _actual_input : bool): 49 | self.local_input = _local_input #Array of booleans 50 | self.net_input = _net_input #Array of booleans 51 | self.frame = _frame 52 | self.game_state = _game_state #Array of dictionaries 53 | self.actual_input = _actual_input 54 | 55 | 56 | func thr_network_inputs(userdata = null): #thread function to read inputs from network 57 | while(true): 58 | var result = true 59 | UDPPeer.wait() #wait for packets to arrive 60 | while (result): 61 | result = UDPPeer.get_packet() #receive a single packet 62 | if result: 63 | match result[0]: #switch statement for header byte 64 | 0: #input received 65 | #print("INPUT RECEIVED") 66 | if result.size() == 3: #check for complete packet (no bytes lost) 67 | input_array_mutex.lock() 68 | if input_arrival_array[result[1]] == false: #if input arrival is false 69 | # print("GOOD INPUT FOR FRAME: ", result[1], ", frame_num is: ", frame_num, ", inputs is: ", result[2]) 70 | input_array[result[1]].net_input = [ 71 | bool(result[2] & 1), 72 | bool(result[2] & 2), 73 | bool(result[2] & 4), 74 | bool(result[2] & 8), 75 | bool(result[2] & 16)] 76 | input_arrival_array[result[1]] = true 77 | input_received_mutex.lock() 78 | input_received = true 79 | input_received_mutex.unlock() 80 | input_array_mutex.unlock() 81 | 82 | 1: #request for input received 83 | #print("REQUEST FOR INPUT RECEIVED") 84 | if result.size() == 3: #check for complete packet (no bytes lost) 85 | # print("RECEIVED REQUEST FOR FRAMES ", result[1], " TO ", result[2]) 86 | var frame = result[1] 87 | input_local_saved_array_mutex.lock() 88 | while (frame != result[2]): #send inputs for requested frame and newer past frames 89 | if input_local_saved_array[frame] == false: break #do not send inputs for future frames 90 | # print("requests for frame ", frame, " sent.") 91 | UDPPeer.put_packet(PoolByteArray([0, frame, input_array[frame].encoded_local_input])) 92 | #print("FULFILLING REQUEST FOR FRAME: ", frame) 93 | frame = (frame + 1)%256 94 | input_local_saved_array_mutex.unlock() 95 | 96 | 2: #game start 97 | input_received = true 98 | 99 | 3: #game end 100 | pass #add response to game end 101 | 102 | 103 | func _ready(): 104 | #initialize input array 105 | for x in range (0, 256): 106 | input_array.append(Inputs.new()) 107 | 108 | #initialize state queue 109 | for x in range (0, rollback): 110 | #empty local input, empty net input, frame 0, inital game state, treat initial empty inputs as true 111 | state_queue.append(Frame_State.new([], [], 0, get_game_state(), true)) 112 | 113 | #initialize arrived input array 114 | for i in range (0, 256): 115 | input_arrival_array.append(false) 116 | input_local_saved_array.append(false) 117 | for i in range (1, rollback + 100): 118 | prev_frame_arrival_array.append(true) 119 | input_arrival_array[-i] = true # for initialization, pretend all "previous" inputs arrived 120 | for i in range (0, input_delay): 121 | input_arrival_array[i] = true # assume empty inputs at game start input_delay window 122 | input_local_saved_array[i] = true 123 | 124 | input_received = false #network thread will set to true when a networked player is found. 125 | 126 | #set up networking thread, definition of sending/receiving addresses and ports 127 | UDPPeer.listen(240, "*") 128 | UDPPeer.set_dest_address("::1", 240) #::1 is localhost 129 | input_thread = Thread.new() 130 | input_thread.start(self, "thr_network_inputs", null, 2) 131 | 132 | while(!input_received):#search for networked player (block until networked player is found) 133 | UDPPeer.put_packet(PoolByteArray([2])) #send ready handshake to opponent 134 | # print("SENDING HANDSHAKE") 135 | 136 | 137 | func _physics_process(delta): 138 | # print("Starting relative frame: ", frame_num) 139 | input_received_mutex.lock() 140 | if (input_received): 141 | #if the oldest Frame_State is guessed, but the input_queue Input does not yet contain an actual input for the oldest Frame_State's frame 142 | if state_queue[0].actual_input == false && input_arrival_array[state_queue[0].frame] == false: 143 | input_received = false #block until actual input is received for guessed oldest Frame_State 144 | input_received_mutex.unlock() 145 | # print("SENDING REQUEST FOR FRAMES ", state_queue[0].frame, " to ", frame_num) 146 | UDPPeer.put_packet(PoolByteArray([1, state_queue[0].frame, frame_num])) #send request for needed input 147 | else: 148 | input_received_mutex.unlock() 149 | handle_input() 150 | else: 151 | input_received_mutex.unlock() 152 | # print("SENDING REQUEST FOR FRAMES ", state_queue[0].frame, " to ", frame_num) 153 | UDPPeer.put_packet(PoolByteArray([1, state_queue[0].frame, frame_num])) #send request for needed input 154 | 155 | 156 | 157 | func handle_input(): #get input, run rollback if necessary, implement inputs 158 | var pre_game_state = get_game_state() 159 | # print("handle_input start pre_game_state: ", pre_game_state) 160 | var actual_input = true 161 | var start_rollback = false 162 | 163 | var current_input = null 164 | var current_frame_arrival_array = [] 165 | 166 | var local_input = [false, false, false, false, false] 167 | var encoded_local_input = 0 168 | 169 | frame_start_all() #for all children, set their update vars to their current/actual values 170 | 171 | input_array_mutex.lock() 172 | #record local inputs 173 | if Input.is_key_pressed(KEY_W): 174 | local_input[0] = true 175 | encoded_local_input += 1 176 | if Input.is_key_pressed(KEY_A): 177 | local_input[1] = true 178 | encoded_local_input += 2 179 | if Input.is_key_pressed(KEY_S): 180 | local_input[2] = true 181 | encoded_local_input +=4 182 | if Input.is_key_pressed(KEY_D): 183 | local_input[3] = true 184 | encoded_local_input += 8 185 | if Input.is_key_pressed(KEY_SPACE): 186 | local_input[4] = true 187 | encoded_local_input += 16 188 | 189 | input_array[(frame_num + input_delay) % 256].local_input = local_input 190 | input_array[(frame_num + input_delay) % 256].encoded_local_input = encoded_local_input 191 | 192 | # if (false):#for testing rollback and requests (forces max rollback by only using input request system) 193 | for i in dup_send_range + 1: #send inputs for current frame as well as duplicates of past frame inputs 194 | UDPPeer.put_packet(PoolByteArray([0, (frame_num + input_delay - i) % 256, 195 | input_array[(frame_num + input_delay - i) % 256].encoded_local_input])) 196 | # print("SENT INPUT: input frame is: ", frame_num + input_delay, ", input is: ", input_array[(frame_num + input_delay) % 256].encoded_local_input) 197 | 198 | #get current input arrival boolean values for current frame & old frames eligible for rollback 199 | for i in range(0, rollback + 1): 200 | current_frame_arrival_array.push_front(input_arrival_array[frame_num - i]) #oldest frame in front 201 | 202 | input_array_mutex.unlock() 203 | 204 | input_local_saved_array_mutex.lock() 205 | input_local_saved_array[(frame_num + input_delay) % 256] = true 206 | input_local_saved_array_mutex.unlock() 207 | 208 | var current_frame_arrival = current_frame_arrival_array.pop_back() #remove current frame's arrival boolean for rollback condition comparison 209 | 210 | if current_frame_arrival_array.hash() != prev_frame_arrival_array.hash(): #if an old input has newly arrived (to fulfill a guess), 211 | #print("Rollback...") 212 | #iterate through all saved states until the state with the guessed input to be replaced by arrived actual input is found (rollback will begin with that state) 213 | #then, continue iterating through remaining saved states to continue rollback resimulation process 214 | var state_index = 0 #for tracking iterated element's index in state_queue 215 | for i in state_queue: #index 0 is oldest state 216 | #if an arrived input is for a past frame 217 | if (prev_frame_arrival_array[state_index] == false && current_frame_arrival_array[state_index] == true): 218 | input_array_mutex.lock() 219 | i.net_input = input_array[i.frame].net_input.duplicate() #set input in Frame_State from guess to true actual input 220 | input_array_mutex.unlock() 221 | i.actual_input = true #input has been set from guess to actual input 222 | if start_rollback == false: 223 | game_state = i.game_state #set value of game_state to old state for rollback resimulation of states/inputs 224 | reset_state_all(game_state) #reset update variables for all children to match given state ONCE 225 | start_rollback = true 226 | pre_game_state = get_game_state() 227 | update_all(input_array[i.frame]) #update game_state using new input 228 | #otherwise, continue simulating using currently stored input 229 | else: 230 | if start_rollback == true: 231 | pre_game_state = get_game_state() #save pre-update game_state value for Frame_State 232 | update_all(input_array[i.frame]) #update game_state using old (guessed or actual) input during rollback resimulation 233 | if start_rollback == true: 234 | i.game_state = pre_game_state #update Frame_States with updated game_state value. 235 | state_index += 1 236 | 237 | current_frame_arrival_array.push_back(current_frame_arrival) #reinsert current frame's arrival boolean (for next frame's prev_frame_arrival_array) 238 | current_frame_arrival_array.pop_front() #remove oldest frame's arrival boolean (needed for rollback condition comparison, but unwanted for next frame's prev_frame_arrival_array) 239 | 240 | current_input = Inputs.new() 241 | input_array_mutex.lock() 242 | #if the input for the current frame has not been received 243 | if input_arrival_array[frame_num] == false: 244 | 245 | #implement guess of empty input (can be replaced with input-guessing algorithm) 246 | current_input.local_input = input_array[frame_num].local_input.duplicate() 247 | input_array[frame_num].net_input = current_input.net_input 248 | 249 | #implement guess of last input used 250 | # current_input.local_input = input_array[frame_num].local_input.duplicate() 251 | # current_input.net_input = input_array[frame_num - 1].net_input.duplicate() 252 | # input_array[frame_num].net_input = input_array[frame_num - 1].net_input.duplicate() 253 | 254 | actual_input = false 255 | else: #else (if the input for the current frame has been received), proceed with true, actual input 256 | current_input.local_input = input_array[frame_num].local_input.duplicate() 257 | current_input.net_input = input_array[frame_num].net_input.duplicate() 258 | 259 | input_arrival_array[frame_num - (rollback + 120)] = false #reset input arrival boolean for old frame 260 | input_array_mutex.unlock() 261 | 262 | input_local_saved_array_mutex.lock() 263 | input_local_saved_array[frame_num - (rollback + 120)] = false #reset viable local input boolean 264 | input_local_saved_array_mutex.unlock() 265 | 266 | if start_rollback == true: 267 | pre_game_state = get_game_state() 268 | 269 | update_all(current_input) #update with current input 270 | execute_all() #implement all applied updates/inputs to all child objects 271 | 272 | #store current frame state into queue 273 | state_queue.append(Frame_State.new(current_input.local_input, current_input.net_input, frame_num, pre_game_state, actual_input)) 274 | 275 | #remove oldest state from queue if queue has exceeded size limit 276 | if len(state_queue) > rollback: 277 | state_queue.pop_front() 278 | 279 | prev_frame_arrival_array = current_frame_arrival_array #store current input arrival array for comaparisons in next frame 280 | frame_num = (frame_num + 1)%256 #increment frame_num 281 | 282 | 283 | func frame_start_all(): 284 | for child in get_children(): 285 | child.frame_start() 286 | 287 | 288 | func reset_state_all(game_state): 289 | for child in get_children(): 290 | child.reset_state(game_state) 291 | 292 | 293 | func update_all(input): 294 | for child in get_children(): 295 | child.input_update(input) 296 | 297 | 298 | func execute_all(): 299 | for child in get_children(): 300 | child.input_execute() 301 | 302 | 303 | func get_game_state(): 304 | var state = {} 305 | for child in get_children(): 306 | state[child.name] = child.get_state() 307 | game_state = state 308 | return game_state.duplicate(true) #deep duplicate to copy all nested dictionaries by value instead of by reference --------------------------------------------------------------------------------