├── .gitignore ├── addons ├── keh_general │ └── data │ │ ├── encdecbuffer.gd │ │ └── quantize.gd └── keh_network │ ├── customproperty.gd │ ├── defaultspawner.gd │ ├── entityinfo.gd │ ├── eventinfo.gd │ ├── inputdata.gd │ ├── inputinfo.gd │ ├── network.gd │ ├── nodespawner.gd │ ├── pinginfo.gd │ ├── playerdata.gd │ ├── playernode.gd │ ├── plugin.cfg │ ├── pluginloader.gd │ ├── snapentity.gd │ ├── snapshot.gd │ └── snapshotdata.gd ├── client ├── game_client.gd ├── game_client.tscn ├── main_client.gd └── main_client.tscn ├── default_env.tres ├── icon.png ├── icon.png.import ├── project.godot ├── readme.md ├── server ├── game_server.gd ├── game_server.tscn ├── main_server.gd ├── main_server.tscn └── ui │ ├── player.gd │ └── player.tscn └── shared ├── entry.gd ├── entry.tscn ├── game.gd ├── game.tscn ├── loader.gd ├── scenes ├── character.gd ├── character.tscn └── floor.tscn └── scripts ├── gamestate.gd ├── netmeshinstance.gd └── snapcharacter.gd /.gitignore: -------------------------------------------------------------------------------- 1 | .import/ 2 | .export/ 3 | export.cfg 4 | export_presets.cfg 5 | *.translation 6 | .mono/ 7 | data_*/ -------------------------------------------------------------------------------- /addons/keh_general/data/encdecbuffer.gd: -------------------------------------------------------------------------------- 1 | ############################################################################### 2 | # Copyright (c) 2019 Yuri Sarudiansky 3 | # 4 | # Permission is hereby granted, free of charge, to any person obtaining a copy 5 | # of this software and associated documentation files (the "Software"), to deal 6 | # in the Software without restriction, including without limitation the rights 7 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | # copies of the Software, and to permit persons to whom the Software is 9 | # furnished to do so, subject to the following conditions: 10 | # 11 | # The above copyright notice and this permission notice shall be included in 12 | # all copies or substantial portions of the Software. 13 | # 14 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 20 | # THE SOFTWARE. 21 | ############################################################################### 22 | 23 | extends Reference 24 | class_name EncDecBuffer 25 | 26 | # This class is meant to simplify the task of encoding/decoding data into low 27 | # level bytes (PoolByteArray). The thing is, the idea is to remove the variant 28 | # header bytes from properties (which incorporate 4 bytes for each one). 29 | # This class deals with a sub-set of the types given by Godot and was 30 | # primarily meant to be used with the networking addon, but this can be useful 31 | # in other scenarios (like a saving system for example). 32 | # 33 | # Now, why this trouble? Variables in GDScript take more bytes than we normally 34 | # expect. Each one contains an additional set of 4 bytes representing the "header", 35 | # which is basically indicating which type is actually held in memory. Some 36 | # types may even bring further overhead and directly using them through the 37 | # network may not necessarily be the best option. 38 | # 39 | # Now there is one very special case there. Unfortunately we don't have unsigned 40 | # integers within GDScript. This brings a somewhat not so fun "limitation" to 41 | # how numbers are represented. 42 | # 43 | # The maximum positive number that can be represented with an unsigned 32 bit 44 | # integer is 4294967295. However, because GDScript only deals with signed 45 | # numbers, the limit here would be 2147483647. But we can have bigger positive 46 | # numbers in GDScript, only that behind the scenes Godot uses 64 bit integers. 47 | # In other words, if we directly send a hash number (32 bit), the result will 48 | # be that 12 bytes will be used instead of just 8 (or the desired 4 bytes). 49 | # 50 | # This class allows "unsigned integers" to be stored in the PoolByteArray 51 | # using the desired 4 bytes, provided the value stays within the boundary. 52 | # 53 | # There is another improvement that could have been done IF GDScript supported 54 | # static variables (that is, shared between instances of the object - which 55 | # would create a "proper caching" of the internal data). 56 | 57 | const CTYPE_UINT: int = 65538 58 | const MAX_UINT: int = 0xFFFFFFFF 59 | 60 | # The "uint" is probably internally using 64 bit integers. As a variant it uses a different 61 | # type header but the byte ordering must be known. This property holds where the relevant 62 | # bytes are in the variable data. 63 | var uint_start: int setget noset 64 | 65 | # Certain encodings will remove some extra bytes from the internal variant data. In order to 66 | # rebuild those variables the bytes (sequences of 0's) must be added back. The next properties 67 | # are meant to hold those "fills" as ByteArrays in order to make things easier 68 | var fill_4bytes: PoolByteArray setget noset 69 | var fill_3bytes: PoolByteArray setget noset 70 | var fill_2bytes: PoolByteArray setget noset 71 | 72 | # Cache the "property headers" so those can be rebuilt 73 | # Key = type code 74 | # Value = Another dictionary containing two fields: 75 | # header = PoolByteArray corresponding to the variant header 76 | # size = number of bytes necessary used by a property of this type 77 | var property_header: Dictionary = {} setget noset 78 | 79 | # A flag indicating if the system is running on big endian or little endian. I'm honestly 80 | # not sure if this is needed but.... 81 | var is_big: bool setget noset 82 | 83 | # The buffer to store the bytes. 84 | var buffer: PoolByteArray setget set_buffer 85 | 86 | 87 | # Buffer reading index, so "read_*()" can be used to retrieve data and avoid external code 88 | # to deal with the correct indexing within the buffer. 89 | var rindex: int 90 | 91 | func _init() -> void: 92 | # Set some internal values that will help code/decode data when byte order is important 93 | var etest: int = 0x01020304 94 | var ebytes: PoolByteArray = var2bytes(etest) 95 | is_big = ebytes[0] == 1 96 | 97 | uint_start = 8 if is_big else 4 98 | 99 | fill_4bytes.append(0) 100 | fill_4bytes.append(0) 101 | fill_4bytes.append(0) 102 | fill_4bytes.append(0) 103 | 104 | fill_3bytes.append(0) 105 | fill_3bytes.append(0) 106 | fill_3bytes.append(0) 107 | 108 | fill_2bytes.append(0) 109 | fill_2bytes.append(0) 110 | 111 | # NOTE: Strings are recovered in a different way so caching the "header" is not necessary 112 | property_header[TYPE_BOOL] = { 113 | header = PoolByteArray(var2bytes(bool(false)).subarray(0, 3)), 114 | size = 1 115 | } 116 | property_header[TYPE_INT] = { 117 | header = PoolByteArray(var2bytes(int(0)).subarray(0, 3)), 118 | size = 4 119 | } 120 | property_header[TYPE_REAL] = { 121 | header = PoolByteArray(var2bytes(float(0.0)).subarray(0, 3)), 122 | size = 4 123 | } 124 | property_header[TYPE_VECTOR2] = { 125 | header = PoolByteArray(var2bytes(Vector2()).subarray(0, 3)), 126 | size = 8 127 | } 128 | property_header[TYPE_RECT2] = { 129 | header = PoolByteArray(var2bytes(Rect2()).subarray(0, 3)), 130 | size = 16 131 | } 132 | property_header[TYPE_VECTOR3] = { 133 | header = PoolByteArray(var2bytes(Vector3()).subarray(0, 3)), 134 | size = 12 135 | } 136 | property_header[TYPE_QUAT] = { 137 | header = PoolByteArray(var2bytes(Quat()).subarray(0, 3)), 138 | size = 16 139 | } 140 | property_header[TYPE_COLOR] = { 141 | header = PoolByteArray(var2bytes(Color()).subarray(0, 3)), 142 | size = 16 143 | } 144 | property_header[CTYPE_UINT] = { 145 | header = PoolByteArray(var2bytes(int(MAX_UINT)).subarray(0, 3)), 146 | size = 4 147 | } 148 | 149 | 150 | 151 | # Obtain number of bytes used by a property of the specified type 152 | func get_field_size(ftype: int) -> int: 153 | var p: Dictionary = property_header.get(ftype) 154 | if (p): 155 | return p.size 156 | else: 157 | return 0 158 | 159 | # If true is returned then the reading index is a at a position not past 160 | # the last byte of the internal PoolByteArray 161 | func has_read_data() -> bool: 162 | return rindex < buffer.size() 163 | 164 | # Return current amount of bytes stored within the internal buffer 165 | func get_current_size() -> int: 166 | return buffer.size() 167 | 168 | 169 | # A generic function to encode the specified property into the internal 170 | # byte array. 171 | func encode_bytes(val, count: int, start: int = 4) -> PoolByteArray: 172 | return var2bytes(val).subarray(start, start + count - 1) 173 | 174 | func _rewrite_bytes(val, at: int, count: int, start: int = 4) -> void: 175 | var bts: PoolByteArray = encode_bytes(val, count, start) 176 | var idx: int = at 177 | for bt in bts: 178 | buffer.set(idx, bt) 179 | idx += 1 180 | 181 | 182 | func write_bool(val: bool) -> void: 183 | buffer.append(val) 184 | 185 | func rewrite_bool(val: bool, at: int) -> void: 186 | buffer.set(at, val) 187 | 188 | func write_int(val: int) -> void: 189 | buffer.append_array(encode_bytes(val, 4)) 190 | 191 | func rewrite_int(val: int, at: int) -> void: 192 | _rewrite_bytes(val, at, 4) 193 | 194 | 195 | func write_float(val: float) -> void: 196 | # Floats in Godot are somewhat finicky! When stored in individual variables they use 197 | # 8 bytes rather than 4! When stored in vectors they use 4 bytes. So, creating a dummy 198 | # vector just to get the correct data size and store into the buffer. Retrieving it 199 | # later into a "loose variable" will work as desired. 200 | var dummyvec: Vector2 = Vector2(val, 0) 201 | buffer.append_array(encode_bytes(dummyvec.x, 4)) 202 | 203 | func rewrite_float(val: float, at: int) -> void: 204 | var dummyvec: Vector2 = Vector2(val, 0) 205 | _rewrite_bytes(dummyvec.x, at, 4) 206 | 207 | func write_vector2(val: Vector2) -> void: 208 | buffer.append_array(encode_bytes(val, 8)) 209 | 210 | func rewrite_vector2(val: Vector2, at: int) -> void: 211 | _rewrite_bytes(val, at, 8) 212 | 213 | func write_rect2(val: Rect2) -> void: 214 | buffer.append_array(encode_bytes(val, 16)) 215 | 216 | func rewrite_rect2(val: Rect2, at: int) -> void: 217 | _rewrite_bytes(val, at, 16) 218 | 219 | func write_vector3(val: Vector3) -> void: 220 | buffer.append_array(encode_bytes(val, 12)) 221 | 222 | func rewrite_vector3(val: Vector3, at: int) -> void: 223 | _rewrite_bytes(val, at, 12) 224 | 225 | func write_quat(val: Quat) -> void: 226 | buffer.append_array(encode_bytes(val, 16)) 227 | 228 | func rewrite_quat(val: Quat, at: int) -> void: 229 | _rewrite_bytes(val, at, 16) 230 | 231 | func write_color(val: Color) -> void: 232 | buffer.append_array(encode_bytes(val, 16)) 233 | 234 | func rewrite_color(val: Color, at: int) -> void: 235 | _rewrite_bytes(val, at, 16) 236 | 237 | func write_uint(val: int) -> void: 238 | assert(val <= MAX_UINT) 239 | var bytes: PoolByteArray = var2bytes(val) 240 | if (bytes.size() == 8): 241 | buffer.append_array(bytes.subarray(4, 7)) 242 | else: 243 | buffer.append_array(bytes.subarray(uint_start, uint_start + 3)) 244 | 245 | func rewrite_uint(val: int, at: int) -> void: 246 | assert(val <= MAX_UINT) 247 | var bytes: PoolByteArray = var2bytes(val) 248 | var sidx: int = uint_start # Source index 249 | if (bytes.size() == 8): 250 | sidx = 4 251 | 252 | for i in 4: 253 | buffer.set(at + i, bytes[sidx + i]) 254 | 255 | func write_byte(val: int) -> void: 256 | assert(val <= 255 && val >= 0) 257 | buffer.append(val) 258 | 259 | func rewrite_byte(val: int, at: int) -> void: 260 | assert(val <= 255 && val >= 0) 261 | buffer.set(at, val) 262 | 263 | func write_ushort(val: int) -> void: 264 | assert(val <= 0xFFFF && val >= 0) 265 | buffer.append_array(encode_bytes(val, 2, 6 if is_big else 4)) 266 | 267 | func rewrite_ushort(val: int, at: int) -> void: 268 | assert(val <= 0xFFFF && val >= 0) 269 | _rewrite_bytes(val, at, 2, 6 if is_big else 4) 270 | 271 | 272 | func write_string(val: String) -> void: 273 | var ba: PoolByteArray = val.to_utf8() 274 | write_uint(ba.size()) 275 | buffer.append_array(ba) 276 | # NOTE: Because strings may have different sizes rewriting them is not supported 277 | 278 | 279 | # This relies on the variant so no static typing here. This is a generic 280 | # function meant to extract a property from the internal PoolByteArray 281 | func read_by_type(tp: int): 282 | var sz: int = property_header[tp].size 283 | return bytes2var(property_header[tp].header + buffer.subarray(rindex, rindex + sz - 1)) 284 | 285 | 286 | 287 | func read_bool() -> bool: 288 | var r: int = rindex 289 | rindex += 1 290 | 291 | if (is_big): 292 | return bytes2var(property_header[TYPE_BOOL].header + fill_3bytes + buffer.subarray(r, r)) 293 | else: 294 | return bytes2var(property_header[TYPE_BOOL].header + buffer.subarray(r, r) + fill_3bytes) 295 | 296 | func read_int() -> int: 297 | var ret: int = read_by_type(TYPE_INT) 298 | rindex += 4 299 | return ret 300 | 301 | func read_float() -> float: 302 | var ret: float = read_by_type(TYPE_REAL) 303 | rindex += 4 304 | return ret 305 | 306 | func read_vector2() -> Vector2: 307 | var ret: Vector2 = read_by_type(TYPE_VECTOR2) 308 | rindex += 8 309 | return ret 310 | 311 | func read_rect2() -> Rect2: 312 | var ret: Rect2 = read_by_type(TYPE_RECT2) 313 | rindex += 16 314 | return ret 315 | 316 | func read_vector3() -> Vector3: 317 | var ret: Vector3 = read_by_type(TYPE_VECTOR3) 318 | rindex += 12 319 | return ret 320 | 321 | func read_quat() -> Quat: 322 | var ret: Quat = read_by_type(TYPE_QUAT) 323 | rindex += 16 324 | return ret 325 | 326 | func read_color() -> Color: 327 | var ret: Color = read_by_type(TYPE_COLOR) 328 | rindex += 16 329 | return ret 330 | 331 | func read_uint() -> int: 332 | var r: int = rindex 333 | rindex += 4 334 | 335 | if (uint_start == 4): 336 | return bytes2var(property_header[CTYPE_UINT].header + buffer.subarray(r, r + 3) + fill_4bytes) 337 | else: 338 | return bytes2var(property_header[CTYPE_UINT].header + fill_4bytes + buffer.subarray(r, r + 3)) 339 | 340 | func read_byte() -> int: 341 | var r: int = rindex 342 | rindex += 1 343 | 344 | if (is_big): 345 | return bytes2var(property_header[TYPE_INT].header + fill_3bytes + buffer.subarray(r, r)) 346 | else: 347 | return bytes2var(property_header[TYPE_INT].header + buffer.subarray(r, r) + fill_3bytes) 348 | 349 | func read_ushort() -> int: 350 | var r: int = rindex 351 | rindex += 2 352 | 353 | if (is_big): 354 | return bytes2var(property_header[TYPE_INT].header + fill_2bytes + buffer.subarray(r, r + 1)) 355 | else: 356 | return bytes2var(property_header[TYPE_INT].header + buffer.subarray(r, r + 1) + fill_2bytes) 357 | 358 | 359 | func read_string() -> String: 360 | var s: int = read_uint() 361 | var ba: PoolByteArray = buffer.subarray(rindex, rindex + s - 1) 362 | rindex += s 363 | return ba.get_string_from_utf8() 364 | 365 | 366 | 367 | ### Setters/getters 368 | func set_buffer(b: PoolByteArray) -> void: 369 | buffer = b 370 | rindex = 0 371 | 372 | 373 | func noset(_v) -> void: 374 | pass 375 | 376 | -------------------------------------------------------------------------------- /addons/keh_general/data/quantize.gd: -------------------------------------------------------------------------------- 1 | ############################################################################### 2 | # Copyright (c) 2019-2020 Yuri Sarudiansky 3 | # 4 | # Permission is hereby granted, free of charge, to any person obtaining a copy 5 | # of this software and associated documentation files (the "Software"), to deal 6 | # in the Software without restriction, including without limitation the rights 7 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | # copies of the Software, and to permit persons to whom the Software is 9 | # furnished to do so, subject to the following conditions: 10 | # 11 | # The above copyright notice and this permission notice shall be included in 12 | # all copies or substantial portions of the Software. 13 | # 14 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 20 | # THE SOFTWARE. 21 | ############################################################################### 22 | 23 | # The floating point quantization code was adapted. The original code was taken from 24 | # the Game Engine Architecture book by Jason Gregory 25 | 26 | extends Reference 27 | class_name Quantize 28 | 29 | const ROTATION_BOUNDS: float = 0.707107 30 | 31 | # Define some masks to help pack/unpack quantized rotation quaternions into integers 32 | const MASK_A_9BIT: int = 511 # 511 = 111111111 33 | const MASK_B_9BIT: int = 511 << 9 34 | const MASK_C_9BIT: int = 511 << 18 35 | const MASK_INDEX_9BIT: int = 3 << 27 36 | const MASK_SIGNAL_9BIT: int = 1 << 30 37 | 38 | 39 | const MASK_A_10BIT: int = 1023 # 1023 = 1111111111 40 | const MASK_B_10BIT: int = 1023 << 10 41 | const MASK_C_10BIT: int = 1023 << 20 42 | const MASK_INDEX_10BIT: int = 3 << 30 43 | 44 | const MASK_A_15BIT: int = 32767 45 | const MASK_B_15BIT: int = 32767 << 15 46 | # The C component is packed into a secondary integer but having dedicated 47 | # constant may help reduce confusion when reading the code 48 | const MASK_C_15BIT: int = 32767 49 | const MASK_INDEX_15BIT: int = 3 << 30 50 | # When packing compressed quaternion data using 15 bits per component, one 51 | # bit becomes "wasted". While not entirely necessary, the system here uses 52 | # that bit to restore the signals of the original quaternion in case those 53 | # got flipped because the largest component was negative. 54 | const MASK_SIGNAL_15BIT: int = 1 << 15 55 | 56 | 57 | # Quantize a unit float (range [0..1]) into an integer of the specified number of bits) 58 | static func quantize_unit_float(val: float, numbits: int) -> int: 59 | # Number of bits cannot exceed 32 bits 60 | assert(numbits <= 32 && numbits > 0) 61 | 62 | var intervals: int = 1 << numbits 63 | var scaled: float = val * (intervals - 1) 64 | var rounded: int = int(floor(scaled + 0.5)) 65 | 66 | if (rounded > intervals - 1): 67 | rounded = intervals - 1 68 | 69 | return rounded 70 | 71 | static func restore_unit_float(quantized: int, numbits: int) -> float: 72 | assert(numbits <= 32 && numbits > 0) 73 | 74 | var intervals: int = 1 << numbits 75 | var intervalsize: float = 1.0 / (intervals - 1) 76 | var approxfloat: float = float(quantized) * intervalsize 77 | 78 | 79 | return approxfloat 80 | 81 | # Quantize a float in the range [minval..maxval] 82 | static func quantize_float(val: float, minval: float, maxval: float, numbits: int) -> int: 83 | var unitfloat: float = (val - minval) / (maxval - minval) 84 | var quantized: int = quantize_unit_float(unitfloat, numbits) 85 | 86 | return quantized 87 | 88 | # Restore float in arbitrary range 89 | static func restore_float(quantized: int, minval: float, maxval: float, numbits: int) -> float: 90 | var unitfloat: float = restore_unit_float(quantized, numbits) 91 | var val: float = minval + (unitfloat * (maxval - minval)) 92 | 93 | return val 94 | 95 | 96 | # Compress the given rotation quaternion using the specified number of bits per component 97 | # using the smallest three method. The returned dictionary contains 5 fields: 98 | # a, b, c -> the smallest three quantized components 99 | # index -> the index [0..3] of the dropped (largest) component 100 | # sig -> The "signal" of the dropped component (1.0 if >= 0, -1.0 if negative) 101 | # Note: Signal is not exactly necessary, but is provided just so if there is any desire to encode it 102 | static func compress_rotation_quat(q: Quat, numbits: int) -> Dictionary: 103 | # Unfortunately it's not possible to directly iterate through the quaternion's components 104 | # using a loop, so create a temporary array to store them 105 | var aq: Array = [q.x, q.y, q.z, q.w] 106 | var mindex: int = 0 # Index of largest component 107 | var mval: float = -1.0 # Largest component value 108 | var sig: float = 1.0 # "Signal" of the dropped component 109 | 110 | # Locate the largest component, storing its absolute value as well as the index 111 | # (0 = x, 1 = y, 2 = z and 3 = w) 112 | for i in 4: 113 | var abval: float = abs(aq[i]) 114 | 115 | if (abval > mval): 116 | mval = abval 117 | mindex = i 118 | 119 | if (aq[mindex] < 0.0): 120 | sig = -1.0 121 | 122 | # Drop the largest component 123 | aq.erase(aq[mindex]) 124 | 125 | # Now loop again through the components, quantizing them 126 | for i in 3: 127 | var fl: float = aq[i] * sig 128 | aq[i] = quantize_float(fl, -ROTATION_BOUNDS, ROTATION_BOUNDS, numbits) 129 | 130 | 131 | return { 132 | "a": aq[0], 133 | "b": aq[1], 134 | "c": aq[2], 135 | "index": mindex, 136 | "sig": 1 if (sig == 1.0) else 0, 137 | } 138 | 139 | 140 | # Restore the rotation quaternion. The quantized values must be given in a dictionary with 141 | # the same format of the one returned by the compress_rotation_quat() function. 142 | static func restore_rotation_quat(quant: Dictionary, numbits: int) -> Quat: 143 | # Take the signal (just a multiplier) 144 | var sig: float = 1.0 if quant.sig == 1 else -1.0 145 | 146 | # Restore components a, b and c 147 | var ra: float = restore_float(quant.a, -ROTATION_BOUNDS, ROTATION_BOUNDS, numbits) * sig 148 | var rb: float = restore_float(quant.b, -ROTATION_BOUNDS, ROTATION_BOUNDS, numbits) * sig 149 | var rc: float = restore_float(quant.c, -ROTATION_BOUNDS, ROTATION_BOUNDS, numbits) * sig 150 | # Restore the dropped component 151 | var dropped: float = sqrt(1.0 - ra*ra - rb*rb - rc*rc) * sig 152 | 153 | var ret: Quat = Quat() 154 | 155 | match quant.index: 156 | 0: 157 | # X was dropped 158 | ret = Quat(dropped, ra, rb, rc) 159 | 160 | 1: 161 | # Y was dropped 162 | ret = Quat(ra, dropped, rb, rc) 163 | 164 | 2: 165 | # Z was dropped 166 | ret = Quat(ra, rb, dropped, rc) 167 | 168 | 3: 169 | # W was dropped 170 | ret = Quat(ra, rb, rc, dropped) 171 | 172 | return ret 173 | 174 | 175 | 176 | # Compress the given rotation quaternion using 9 bits per component. This is a "wrapper" 177 | # function that packs the quantized value into a single integer. Because there is still 178 | # some "room" (only 29 bits of the 32 are used), the original signal of the quaternion is 179 | # also stored, meaning that it can be restored. 180 | static func compress_rquat_9bits(q: Quat) -> int: 181 | # Compress the components using the generalized Quat compression 182 | var c: Dictionary = compress_rotation_quat(q, 9) 183 | return ( ((c.sig << 30) & MASK_SIGNAL_9BIT) | 184 | ((c.index << 27) & MASK_INDEX_9BIT) | 185 | ((c.c << 18) & MASK_C_9BIT) | 186 | ((c.b << 9) & MASK_B_9BIT) | 187 | (c.a & MASK_A_9BIT) ) 188 | 189 | # Restores a quaternion that was previously quantized into a single integer using 9 bits 190 | # per component. In this case the original signal of the quaternion will be restored. 191 | static func restore_rquat_9bits(compressed: int) -> Quat: 192 | var unpacked: Dictionary = { 193 | "a": compressed & MASK_A_9BIT, 194 | "b": (compressed & MASK_B_9BIT) >> 9, 195 | "c": (compressed & MASK_C_9BIT) >> 18, 196 | "index": (compressed & MASK_INDEX_9BIT) >> 27, 197 | "sig": (compressed & MASK_SIGNAL_9BIT) >> 30, 198 | } 199 | 200 | return restore_rotation_quat(unpacked, 9) 201 | 202 | 203 | # Compress the given rotation quaternion using 10 bits per component. This is a "wrapper" 204 | # function that packs the quantized values into a single integer. Note that in this case 205 | # the restored quaternion may be entirely "flipped" as the original signal cannot be 206 | # stored within the packed integer. 207 | static func compress_rquat_10bits(q: Quat) -> int: 208 | # Compress the components using the generalized function 209 | var c: Dictionary = compress_rotation_quat(q, 10) 210 | return ( ((c.index << 30) & MASK_INDEX_10BIT) | 211 | ((c.c << 20) & MASK_C_10BIT) | 212 | ((c.b << 10) & MASK_B_10BIT) | 213 | (c.a & MASK_A_10BIT) ) 214 | 215 | # Restores a quaternion that was previously quantized into a single integer using 10 bits 216 | # per component. In this case the original signal may not be restored. 217 | static func restore_rquat_10bits(c: int) -> Quat: 218 | # Unpack the components 219 | var unpacked: Dictionary = { 220 | "a": c & MASK_A_10BIT, 221 | "b": (c & MASK_B_10BIT) >> 10, 222 | "c": (c & MASK_C_10BIT) >> 20, 223 | "index": (c & MASK_INDEX_10BIT) >> 30, 224 | "sig": 1, # Use 1.0 as multiplier because the signal cannot be restored in this case 225 | } 226 | 227 | return restore_rotation_quat(unpacked, 10) 228 | 229 | 230 | # Compress the given rotation quaternion using 15 bits per component. This is a "wrapper" 231 | # function that packs the quantized values into two intergers (using PoolIntArray). In 232 | # memory this will still use the full range of the integer values, but the second entry in 233 | # the returned array can safely discard 16 bits, which is basically the desired usage when 234 | # sending data through network. Note that in this case, using a full 32 bit + 16 bit leaves 235 | # room for a single bit, which is used to encode the original quaternion signal. 236 | static func compress_rquat_15bits(q: Quat) -> PoolIntArray: 237 | # Obtain the compressed data 238 | var c: Dictionary = compress_rotation_quat(q, 15) 239 | 240 | # Pack the first element of the array - contains index, A and B elements 241 | var packed0: int = ( ((c.index << 30) & MASK_INDEX_15BIT) | 242 | ((c.b << 15) & MASK_B_15BIT) | 243 | (c.a & MASK_A_15BIT) ) 244 | 245 | # Pack the second element of the array - contains signal and C element 246 | var packed1: int = (((c.sig & MASK_SIGNAL_15BIT) << 15) | (c.c & MASK_C_15BIT)) 247 | 248 | return PoolIntArray([packed0, packed1]) 249 | 250 | # Restores a quaternion compressed using 15 bits per component. The input must be integers 251 | # within the PoolIntArray of the compression function, in the same order for the arguments. 252 | static func restore_rquat_15bits(pack0: int, pack1: int) -> Quat: 253 | # Unpack the elements 254 | var unpacked: Dictionary = { 255 | "a": pack0 & MASK_A_15BIT, 256 | "b": (pack0 & MASK_B_15BIT) >> 15, 257 | "c": pack1 & MASK_C_15BIT, 258 | "index": (pack0 & MASK_INDEX_15BIT) >> 30, 259 | "sig": (pack1 & MASK_SIGNAL_15BIT) >> 15 260 | } 261 | 262 | return restore_rotation_quat(unpacked, 15) 263 | 264 | 265 | -------------------------------------------------------------------------------- /addons/keh_network/customproperty.gd: -------------------------------------------------------------------------------- 1 | ############################################################################### 2 | # Copyright (c) 2019 Yuri Sarudiansky 3 | # 4 | # Permission is hereby granted, free of charge, to any person obtaining a copy 5 | # of this software and associated documentation files (the "Software"), to deal 6 | # in the Software without restriction, including without limitation the rights 7 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | # copies of the Software, and to permit persons to whom the Software is 9 | # furnished to do so, subject to the following conditions: 10 | # 11 | # The above copyright notice and this permission notice shall be included in 12 | # all copies or substantial portions of the Software. 13 | # 14 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 20 | # THE SOFTWARE. 21 | ############################################################################### 22 | 23 | # This network addon provides means to create properties that are associated 24 | # with players and be automatically replicated. By default the stored values 25 | # will be sent only to the server, however it's possible to configure each 26 | # property in a way that the server will broadcast the values to other clients. 27 | # This class is meant for internal usage and normally there is no need to 28 | # directly use it. 29 | # 30 | # Update: if the stored value is of a supported type by the EncDecBuffer then 31 | # the custom property will potentially be sent with multiple others in order 32 | # to reduce the number of remote calls. Basically, when a property is changed, 33 | # if it is supported a flag will be set. Then, at the end of the update all 34 | # properties with this flag will be encoded into a single byte array 35 | 36 | 37 | extends Reference 38 | class_name NetCustomProperty 39 | 40 | # Properties through this system can be marked for automatic replication. 41 | # This enumeration configures how that will work 42 | enum ReplicationMode { 43 | None, # No replication of this property 44 | ServerOnly, # If a property is changed in a client machine, it will be sent only to the server 45 | ServerBroadcast, # Property value will be broadcast to every player through the server 46 | } 47 | 48 | # Because custom properties can be of any type, this class' property meant to hold 49 | # the actual custom value is not static typed 50 | var value setget _setval 51 | 52 | # The replication method (actually, mode) for this custom property 53 | var replicate: int = ReplicationMode.ServerOnly 54 | 55 | # This flag tells if the custom property must be synchronized or not. Normally this will be set after 56 | # changing the "value" property. 57 | var dirty: bool 58 | 59 | 60 | func _init(initial_val, repl_mode: int = ReplicationMode.ServerOnly) -> void: 61 | value = initial_val 62 | replicate = repl_mode 63 | dirty = false 64 | 65 | 66 | 67 | func encode_to(edec: EncDecBuffer, pname: String, expected_type: int) -> bool: 68 | if (!dirty): 69 | return false 70 | 71 | var cfunc: String = "" 72 | match expected_type: 73 | TYPE_BOOL: 74 | cfunc = "write_bool" 75 | TYPE_INT: 76 | cfunc = "write_int" 77 | TYPE_REAL: 78 | cfunc = "write_float" 79 | TYPE_VECTOR2: 80 | cfunc = "write_vector2" 81 | TYPE_RECT2: 82 | cfunc = "write_rect2" 83 | TYPE_VECTOR3: 84 | cfunc = "write_vector3" 85 | TYPE_QUAT: 86 | cfunc = "write_quat" 87 | TYPE_COLOR: 88 | cfunc = "write_color" 89 | TYPE_STRING: 90 | cfunc = "write_string" 91 | 92 | if (!cfunc.empty()): 93 | edec.write_string(pname) 94 | edec.call(cfunc, value) 95 | # It was encoded. Assume the data will be send to through the network, thus clean up this property. 96 | dirty = false 97 | 98 | return !cfunc.empty() 99 | 100 | 101 | func decode_from(edec: EncDecBuffer, expected_type: int, make_dirty: bool) -> bool: 102 | var cfunc: String = "" 103 | match expected_type: 104 | TYPE_BOOL: 105 | cfunc = "read_bool" 106 | TYPE_INT: 107 | cfunc = "read_int" 108 | TYPE_REAL: 109 | cfunc = "read_float" 110 | TYPE_VECTOR2: 111 | cfunc = "read_vector2" 112 | TYPE_RECT2: 113 | cfunc = "read_rect2" 114 | TYPE_VECTOR3: 115 | cfunc = "read_vector3" 116 | TYPE_QUAT: 117 | cfunc = "read_quat" 118 | TYPE_COLOR: 119 | cfunc = "read_color" 120 | TYPE_STRING: 121 | cfunc = "read_string" 122 | 123 | if (!cfunc.empty()): 124 | value = edec.call(cfunc) 125 | dirty = make_dirty 126 | 127 | return !cfunc.empty() 128 | 129 | 130 | func _setval(v) -> void: 131 | # By not doing anything if the incoming value is not different from the already stored value some bandwidth 132 | # will be saved. This happens because data is only sent if it is marked as "dirty"> 133 | if (v != value): 134 | value = v 135 | dirty = replicate != ReplicationMode.None 136 | 137 | -------------------------------------------------------------------------------- /addons/keh_network/defaultspawner.gd: -------------------------------------------------------------------------------- 1 | ############################################################################### 2 | # Copyright (c) 2019 Yuri Sarudiansky 3 | # 4 | # Permission is hereby granted, free of charge, to any person obtaining a copy 5 | # of this software and associated documentation files (the "Software"), to deal 6 | # in the Software without restriction, including without limitation the rights 7 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | # copies of the Software, and to permit persons to whom the Software is 9 | # furnished to do so, subject to the following conditions: 10 | # 11 | # The above copyright notice and this permission notice shall be included in 12 | # all copies or substantial portions of the Software. 13 | # 14 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 20 | # THE SOFTWARE. 21 | ############################################################################### 22 | 23 | # The default spawner just takes the packed scene, through its constructor, 24 | # that should be used to spawn a game node. With that in mind, each type of 25 | # node to be dynamically spawned should have a corresponding spawner. If there 26 | # are more advanced things to be done with each spawning then a different class, 27 | # derived from NetNodeSpawner, can be created. 28 | 29 | extends NetNodeSpawner 30 | class_name NetDefaultSpawner 31 | 32 | # This holds the packed scene corresponding to the node that should be spawned 33 | var _scene_class: PackedScene = null 34 | 35 | 36 | func _init(ps: PackedScene) -> void: 37 | _scene_class = ps 38 | 39 | # Function that must be overridden. This is where the actual node is instanced 40 | func spawn() -> Node: 41 | return _scene_class.instance() if _scene_class else null 42 | 43 | -------------------------------------------------------------------------------- /addons/keh_network/entityinfo.gd: -------------------------------------------------------------------------------- 1 | ############################################################################### 2 | # Copyright (c) 2019 Yuri Sarudiansky 3 | # 4 | # Permission is hereby granted, free of charge, to any person obtaining a copy 5 | # of this software and associated documentation files (the "Software"), to deal 6 | # in the Software without restriction, including without limitation the rights 7 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | # copies of the Software, and to permit persons to whom the Software is 9 | # furnished to do so, subject to the following conditions: 10 | # 11 | # The above copyright notice and this permission notice shall be included in 12 | # all copies or substantial portions of the Software. 13 | # 14 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 20 | # THE SOFTWARE. 21 | ############################################################################### 22 | 23 | # This class is meant to somewhat "describe" classes derived from SnapEntityBase, 24 | # which are used to represent game objects within the snapshots. This info 25 | # helps automate encoding and decoding of the snapshots into raw bytes that 26 | # can then be sent through the network. Each instance of this class will 27 | # describe a class and will perform some of the tasks of encoding and decoding 28 | # entities into the given buffer arrays. 29 | # The network singleton automatically handles this so normally speaking there 30 | # is no need to directly deal with objects of this class. 31 | 32 | extends Reference 33 | class_name EntityInfo 34 | 35 | 36 | # Those are just shortcuts 37 | const CTYPE_UINT: int = SnapEntityBase.CTYPE_UINT 38 | const CTYPE_USHORT: int = SnapEntityBase.CTYPE_USHORT 39 | const CTYPE_BYTE: int = SnapEntityBase.CTYPE_BYTE 40 | 41 | # Maximum amount of array elements (PoolByteArray, PoolRealArray and PoolIntArray) 42 | const MAX_ARRAY_SIZE: int = 0xFF 43 | 44 | # During the registration of snapshot entity objects, supported properties 45 | # that can be replicated through this system must have some internal data 46 | # necessary to help with the tasks. Each one will have an instance of this 47 | # inner class. 48 | class ReplicableProperty: 49 | var name: String 50 | var type: int 51 | var mask: int 52 | 53 | func _init(_name: String, _type: int, _mask: int) -> void: 54 | name = _name 55 | type = _type 56 | mask = _mask 57 | 58 | # Unfortunately must use variant (arguments) here instead of static type 59 | func compare(v1, v2) -> bool: 60 | return v1 == v2 61 | 62 | # This is a specialized replicable property for floating point numbers when a tolerance must 63 | # be used when comparing values. In this case the is_equal_approx() function is used 64 | class RPropApprox extends ReplicableProperty: 65 | func _init(_name: String, _mask: int).(_name, TYPE_REAL, _mask) -> void: 66 | pass 67 | 68 | func compare(v1: float, v2: float) -> bool: 69 | return is_equal_approx(v1, v2) 70 | 71 | # Vector2, Vector3, Quat etc offers a function to perform the is_equal_approx() on each 72 | # component. This specialized ReplicableProperty performs the comparison using that 73 | class RPropApproxi extends ReplicableProperty: 74 | func _init(_name: String, _type: int, _mask: int).(_name, _type, _mask) -> void: 75 | pass 76 | 77 | func compare(v1, v2) -> bool: 78 | return v1.is_equal_approx(v2) 79 | 80 | # Although a bunch of work to create one specialized replicable property for each one 81 | # of the types bellow, it has been done because there is no easy easy way to directly 82 | # access each component of "compound types" without using the correct names. Moreover, 83 | # instead of creating a new base for each of those, still directly use ReplicableProperty 84 | # as base in order to help with readability and **maybe** performance 85 | # Nevertheless, the specializations bellow are meant to offer custom tolerance values 86 | # to compare floating point values 87 | class RPropTolFloat extends ReplicableProperty: 88 | var tolerance: float 89 | func _init(_name: String, _mask: int, _tolerance: float).(_name, TYPE_REAL, _mask) -> void: 90 | tolerance = _tolerance 91 | 92 | func compare(v1: float, v2: float) -> bool: 93 | return (abs(v1 - v2) < tolerance) 94 | 95 | class RPropTolVec2 extends ReplicableProperty: 96 | var tolerance: float 97 | func _init(_name: String, _mask: int, _tolerance: float).(_name, TYPE_VECTOR2, _mask) -> void: 98 | tolerance = _tolerance 99 | 100 | func compare(v1: Vector2, v2: Vector2) -> bool: 101 | return (abs(v1.x - v2.x) < tolerance && 102 | abs(v1.y - v2.y) < tolerance) 103 | 104 | class RPropTolRec2 extends ReplicableProperty: 105 | var tolerance: float 106 | func _init(_name: String, _mask: int, _tolerance: float).(_name, TYPE_RECT2, _mask) -> void: 107 | tolerance = _tolerance 108 | 109 | func compare(v1: Rect2, v2: Rect2) -> bool: 110 | return (abs(v1.position.x - v2.position.x) < tolerance && 111 | abs(v1.position.y - v2.position.y) < tolerance && 112 | abs(v1.size.x - v2.size.x) < tolerance && 113 | abs(v1.size.y - v2.size.y) < tolerance) 114 | 115 | class RPropTolQuat extends ReplicableProperty: 116 | var tolerance: float 117 | func _init(_name: String, _mask: int, _tolerance: float).(_name, TYPE_QUAT, _mask) -> void: 118 | tolerance = _tolerance 119 | 120 | func compare(v1: Quat, v2: Quat) -> bool: 121 | return (abs(v1.x - v2.x) < tolerance && 122 | abs(v1.y - v2.y) < tolerance && 123 | abs(v1.z - v2.z) < tolerance && 124 | abs(v1.w - v2.w) < tolerance) 125 | 126 | class RPropTolVec3 extends ReplicableProperty: 127 | var tolerance: float 128 | func _init(_name: String, _mask: int, _tolerance: float).(_name, TYPE_VECTOR3, _mask) -> void: 129 | tolerance = _tolerance 130 | 131 | func compare(v1: Vector3, v2: Vector3) -> bool: 132 | return (abs(v1.x - v2.x) < tolerance && 133 | abs(v1.y - v2.y) < tolerance && 134 | abs(v1.z - v2.z) < tolerance) 135 | 136 | class RPropTolColor extends ReplicableProperty: 137 | var tolerance: float 138 | func _init(_name: String, _mask: int, _tolerance: float).(_name, TYPE_COLOR, _mask) -> void: 139 | tolerance = _tolerance 140 | 141 | func compare(v1: Color, v2: Color) -> bool: 142 | return (abs(v1.r - v2.r) < tolerance && 143 | abs(v1.g - v2.g) < tolerance && 144 | abs(v1.b - v2.b) < tolerance && 145 | abs(v1.a - v2.a) < tolerance) 146 | 147 | 148 | # Storing registered spawners could be done through dictionaries however 149 | # that did result in errors when trying to reference non existing spawners. 150 | # Namely, assigning "nill to a variable of type dictionary". Because of that, 151 | # using this inner class to hold the registered spawner data 152 | class SpawnerData: 153 | var spawner: NetNodeSpawner 154 | var parent: Node 155 | var extra_setup: FuncRef 156 | 157 | func _init(s: NetNodeSpawner, p: Node, es: FuncRef) -> void: 158 | spawner = s 159 | parent = p 160 | extra_setup = es 161 | 162 | # This inner class is meant to make things slightly easier to deal with instead of using 163 | # "dictionary as struct" contained in yet another dictionary. More specifically, this is 164 | # meant to keep track of game nodes and any other extra data associated with that specific 165 | # node. 166 | class GameEntity: 167 | # The game node, mostly likely a visual representation of this entity 168 | var node: Node 169 | # Useful only on clients, keep track of how many frames were used during prediction for 170 | # this entity. This can be used later to re-simulate entities that don't require any 171 | # input data when correction is applied. 172 | var predcount: int 173 | 174 | func _init(n: Node, pc: int) -> void: 175 | node = n 176 | predcount = pc 177 | 178 | 179 | 180 | # The entity type name is hashed into this property 181 | var _name_hash: int 182 | # Resource that is used in order to create instances of the object described by 183 | # this entity info. 184 | var _resource: Resource 185 | # The replicable properties list. Each entry in this array is an instance of the 186 | # inner class ReplicableProperty 187 | var replicable: Array 188 | # This is held mostly to help with debugging 189 | var _namestr: String 190 | # Snap entity objects may disable the class_hash and this info is cashed in this 191 | # property to make things simpler to deal with when encoding/decoding data. 192 | var _has_chash: bool = true 193 | 194 | # If this string is not empty after instantiating this class, then there was an error, 195 | # with details stored in the property 196 | var error: String 197 | 198 | # When encoding delta snapshot, the change mask has to be added before the entity 199 | # itself. This variable holds how many bytes (1, 2 or 4) are used for this information 200 | # within the raw data for this entity type. Yes, this sort of limit the number of 201 | # properties per entity to only 30/31 (id takes one spot and if not disabled the 202 | # class_hash takes another)). 203 | var _cmask_size: int 204 | 205 | # Key = unique entity ID 206 | # Value instance of the inner GameEntity class: 207 | var _entity: Dictionary 208 | 209 | 210 | # Key = class_hash - yes, this sort of force the creation of multiple spawners, even if the 211 | # actual class_hash only points to inner properties of the spawned node. This design decision 212 | # greatly simplifies the automatic snapshot system. 213 | # Value = Instances of the SpawnerData inner class 214 | var _spawner_data: Dictionary = {} 215 | 216 | 217 | func _init(cname: String, rpath: String) -> void: 218 | replicable = [] 219 | # Verifies if the resource contains replicable properties and if implements 220 | # the required apply_state(node) function. 221 | _check_properties(cname, rpath) 222 | 223 | if replicable.size() <= 8: 224 | _cmask_size = 1 225 | elif replicable.size() <= 16: 226 | _cmask_size = 2 227 | else: 228 | _cmask_size = 4 229 | 230 | 231 | # Spawners (classes derived from NetNodeSpawner) are necessary in order to automate the 232 | # node spawning during the synchronization. The class_hash becomes the ID key to the 233 | # spawner. The parent is where the new node will be attached to when it get instanced. 234 | # The esetup is an optional function reference that will be called when a new node is 235 | # spawned in order to perform extra setup. That function will receive the node itself 236 | # as only argument. 237 | func register_spawner(chash: int, spawner: NetNodeSpawner, parent: Node, esetup: FuncRef) -> void: 238 | assert(spawner) 239 | assert(parent) 240 | 241 | _spawner_data[chash] = SpawnerData.new(spawner, parent, esetup) 242 | 243 | 244 | # Creates an instance of the entity described by this object. 245 | func create_instance(uid: int, chash: int) -> SnapEntityBase: 246 | assert(_resource) 247 | return _resource.new(uid, chash) 248 | 249 | 250 | # Creates a clone of the specified entity. 251 | func clone_entity(entity: SnapEntityBase) -> SnapEntityBase: 252 | var ret: SnapEntityBase = create_instance(0, 0) 253 | 254 | for repl in replicable: 255 | ret.set(repl.name, entity.get(repl.name)) 256 | 257 | return ret 258 | 259 | 260 | # Just to give a different access to the change mask size property. 261 | func get_change_mask_size() -> int: 262 | return _cmask_size 263 | 264 | 265 | # Compare two entities described by this instance and return a value that 266 | # can be used as a change mask. In other words, a non zero value means the 267 | # two given entities are in different states. 268 | func calculate_change_mask(e1: SnapEntityBase, e2: SnapEntityBase) -> int: 269 | assert(typeof(e1) == typeof(e2)) 270 | 271 | # The change mask 272 | var cmask: int = 0 273 | 274 | for p in replicable: 275 | if (!p.compare(e1.get(p.name), e2.get(p.name))): 276 | cmask |= p.mask 277 | 278 | return cmask 279 | 280 | 281 | # Fully encode the given snapshot entity object into the specified byte buffer 282 | func encode_full_entity(entity: SnapEntityBase, into: EncDecBuffer) -> void: 283 | # Ensure id is encoded first 284 | into.write_uint(entity.id) 285 | # Next, if the class_hash has not been disabled, encode it first 286 | if (_has_chash): 287 | into.write_uint(entity.class_hash) 288 | 289 | for repl in replicable: 290 | if (repl.name != "id" && repl.name != "class_hash"): 291 | _property_writer(repl, entity, into) 292 | 293 | 294 | # Given the raw byte array, decode an entity from it and return the instance 295 | # with the properties set. This assumes the reading index is at the desired 296 | # position 297 | func decode_full_entity(from: EncDecBuffer) -> SnapEntityBase: 298 | # Read the unique ID 299 | var uid: int = from.read_uint() 300 | # If the class_hash has not been disabled, read it 301 | var chash: int = from.read_uint() if _has_chash else 0 302 | 303 | var ret: SnapEntityBase = create_instance(uid, chash) 304 | 305 | # Read/decode each one of the replicable properties 306 | for repl in replicable: 307 | if (repl.name != "id" && repl.name != "class_hash"): 308 | _property_reader(repl, from, ret) 309 | 310 | return ret 311 | 312 | 313 | func encode_delta_entity(uid: int, entity: SnapEntityBase, cmask: int, into: EncDecBuffer) -> void: 314 | # Write the entity ID first 315 | into.write_uint(uid) 316 | 317 | # First write the change mask - using the cached amount of bits for it 318 | # In this case, the minimum amount under the limitations of raw data manipulation 319 | # that can be done with GDScript 320 | match _cmask_size: 321 | 1: 322 | into.write_byte(cmask) 323 | 324 | 2: 325 | into.write_ushort(cmask) 326 | 327 | 4: 328 | into.write_uint(cmask) 329 | 330 | _: 331 | # If here something very bad is happening in the code 332 | assert(false) 333 | 334 | if (cmask == 0): 335 | # Avoid needless loop iteration 336 | return 337 | 338 | for repl in replicable: 339 | # Not ignoring class hash. Although it's not supposed to be changed, 340 | # new entities will have the corresponding bit mask set. 341 | # ID was already encoded above, before the change mask 342 | if (repl.name == "id"): 343 | continue 344 | 345 | if (repl.mask & cmask): 346 | # This is a changed property, so encode it 347 | _property_writer(repl, entity, into) 348 | 349 | 350 | func get_full_change_mask() -> int: 351 | match _cmask_size: 352 | 1: 353 | return 0xFF 354 | 2: 355 | return 0xFFFF 356 | 4: 357 | return 0xFFFFFFFF 358 | return 0 359 | 360 | 361 | # This is a helper to extract the change mask from the EncDecBuffer. See the decode_delta_entity 362 | # comment to understand why the extraction is here instead of being part of the decoding 363 | func extract_change_mask(from: EncDecBuffer) -> int: 364 | match _cmask_size: 365 | 1: 366 | return from.read_byte() 367 | 368 | 2: 369 | return from.read_ushort() 370 | 371 | 4: 372 | return from.read_uint() 373 | 374 | # If failing here, then something is not working in the code 375 | assert(false) 376 | return -1 377 | 378 | 379 | func decode_delta_entity(from: EncDecBuffer) -> Dictionary: 380 | # Decode entity ID 381 | var uid: int = from.read_uint() 382 | # Decode change mask 383 | var cmask: int = extract_change_mask(from) 384 | 385 | # Observation here: The returned entity is meant to contain only the changed data. The rest 386 | # that does not match in the change mask will be left with default values. 387 | var entity: SnapEntityBase = create_instance(uid, 0) 388 | 389 | # Avoid replicable looping if the change mask is 0, as this entity is marked for removal 390 | # and does not contain any encoded data 391 | if (cmask > 0): 392 | for repl in replicable: 393 | if (repl.name != "id" && repl.mask & cmask): 394 | _property_reader(repl, from, entity) 395 | 396 | return { 397 | "entity": entity, 398 | "cmask": cmask, 399 | } 400 | 401 | 402 | func match_delta(changed: SnapEntityBase, source: SnapEntityBase, cmask: int) -> void: 403 | # changed is meant to have the old values copied into it as it's the entity to be 404 | # added into the new snapshot data 405 | for repl in replicable: 406 | # Only take from old value if the replicable proprety is not marked as changed 407 | if (!(repl.mask & cmask)): 408 | changed.set(repl.name, source.get(repl.name)) 409 | 410 | 411 | 412 | # Retrieve a game node given its unique ID. 413 | func get_game_node(uid: int) -> Node: 414 | var ge: GameEntity = _entity.get(uid) 415 | if (ge): 416 | return ge.node 417 | 418 | return null 419 | 420 | 421 | 422 | # Perform full cleanup of the internal container that is used to manage the 423 | # game nodes. 424 | func clear_nodes() -> void: 425 | for uid in _entity: 426 | if (!_entity[uid].node.is_queued_for_deletion()): 427 | _entity[uid].node.queue_free() 428 | 429 | _entity.clear() 430 | 431 | 432 | func spawn_node(uid: int, chash: int) -> Node: 433 | var ret: Node = null 434 | 435 | var sdata: SpawnerData = _spawner_data.get(chash) 436 | if (sdata): 437 | ret = sdata.spawner.spawn() 438 | 439 | ret.set_meta("uid", uid) 440 | ret.set_meta("chash", chash) 441 | 442 | _entity[uid] = GameEntity.new(ret, 0) 443 | sdata.parent.add_child(ret) 444 | 445 | if (sdata.extra_setup && sdata.extra_setup.is_valid()): 446 | sdata.extra_setup.call_func(ret) 447 | 448 | else: 449 | var w: String = "Could not retrieve spawner for entity %s with unique ID %d." 450 | push_warning(w % [_namestr, uid]) 451 | 452 | return ret 453 | 454 | 455 | func despawn_node(uid: int) -> void: 456 | var ge: GameEntity = _entity.get(uid) 457 | if (ge): 458 | if (!ge.node.is_queued_for_deletion()): 459 | ge.node.queue_free() 460 | 461 | # warning-ignore:return_value_discarded 462 | _entity.erase(uid) 463 | 464 | 465 | # Game object nodes added through the editor that are meant to be replicated 466 | # must be registered within the network system. This function performs this 467 | func add_pre_spawned(uid: int, node: Node) -> void: 468 | _entity[uid] = GameEntity.new(node, 0) 469 | 470 | 471 | # This will check the specified resource and if there are any replicable 472 | # properties finalize the initialization of this object. 473 | func _check_properties(cname: String, rpath: String) -> void: 474 | _resource = load(rpath) 475 | 476 | if (!_resource): 477 | return 478 | 479 | # Unfortunately it's necessary to create an instance of the class in order 480 | # to traverse its properties and methods. Since this is a dummy object 481 | # uid and class_hash are irrelevant 482 | var obj: SnapEntityBase = create_instance(0, 0) 483 | 484 | if (!obj): 485 | _resource = null 486 | error = "Unable to create dummy instance of the class %s (%s)" 487 | error = error % [cname, rpath] 488 | return 489 | 490 | if (!obj.has_method("apply_state")): 491 | _resource = null 492 | error = "Method apply_state(Node) is not implemented." 493 | return 494 | 495 | var mask: int = 1 496 | var plist: Array = obj.get_property_list() 497 | var min_size: int = 2 # Assume class_hash is not disabled 498 | 499 | for p in plist: 500 | if (p.usage & PROPERTY_USAGE_SCRIPT_VARIABLE): 501 | # If this property is the class_hash and the meta "class_hash" is set 502 | # to 0, then skip it as it means it's desired to be disabled 503 | if (p.name == "class_hash" && obj.get_meta("class_hash") == 0): 504 | _has_chash = false 505 | min_size -= 1 506 | continue 507 | 508 | var tp: int = p.type 509 | var rprop: ReplicableProperty = _build_replicable_prop(p.name, tp, mask, obj) 510 | 511 | if (rprop): 512 | # Push the replicable property into the internal container 513 | replicable.push_back(rprop) 514 | # Advance the mask 515 | mask *= 2 516 | 517 | 518 | # After iterating through available properties, verify if there is at least 519 | # one replicable property besides "id" and "class_hash" taking into account 520 | # the fact that class_hash may be disabled). 521 | if (replicable.size() <= min_size): 522 | _resource = null 523 | error = "There are no defined (supported) replicable properties." 524 | return 525 | 526 | _name_hash = cname.hash() 527 | _namestr = cname 528 | _entity = {} 529 | 530 | 531 | # Based on the given instance of ReplicableProperty, reads a property from the 532 | # byte buffer into an instance of the snapshot entity object. 533 | func _property_reader(repl: ReplicableProperty, from: EncDecBuffer, into: SnapEntityBase) -> void: 534 | match repl.type: 535 | TYPE_BOOL: 536 | into.set(repl.name, from.read_bool()) 537 | TYPE_INT: 538 | into.set(repl.name, from.read_int()) 539 | TYPE_REAL: 540 | into.set(repl.name, from.read_float()) 541 | TYPE_VECTOR2: 542 | into.set(repl.name, from.read_vector2()) 543 | TYPE_RECT2: 544 | into.set(repl.name, from.read_rect2()) 545 | TYPE_QUAT: 546 | into.set(repl.name, from.read_quat()) 547 | TYPE_COLOR: 548 | into.set(repl.name, from.read_color()) 549 | TYPE_VECTOR3: 550 | into.set(repl.name, from.read_vector3()) 551 | CTYPE_UINT: 552 | into.set(repl.name, from.read_uint()) 553 | CTYPE_BYTE: 554 | into.set(repl.name, from.read_byte()) 555 | CTYPE_USHORT: 556 | into.set(repl.name, from.read_ushort()) 557 | TYPE_STRING: 558 | into.set(repl.name, from.read_string()) 559 | TYPE_RAW_ARRAY: 560 | var s: int = from.read_byte() 561 | var a: PoolByteArray = PoolByteArray() 562 | for _i in s: 563 | a.append(from.read_byte()) 564 | into.set(repl.name, a) 565 | TYPE_INT_ARRAY: 566 | var s: int = from.read_byte() 567 | var a: PoolIntArray = PoolIntArray() 568 | for _i in s: 569 | a.append(from.read_int()) 570 | into.set(repl.name, a) 571 | TYPE_REAL_ARRAY: 572 | var s: int = from.read_byte() 573 | var a: PoolRealArray = PoolRealArray() 574 | for _i in s: 575 | a.append(from.read_float()) 576 | into.set(repl.name, a) 577 | 578 | # Based on the given instance of ReplicableProperty, writes a property from the 579 | # instance of snapshot entity object into the specified byte array 580 | func _property_writer(repl: ReplicableProperty, entity: SnapEntityBase, into: EncDecBuffer) -> void: 581 | # Relying on the variant feature so no static typing here 582 | var val = entity.get(repl.name) 583 | 584 | match repl.type: 585 | TYPE_BOOL: 586 | into.write_bool(val) 587 | TYPE_INT: 588 | into.write_int(val) 589 | TYPE_REAL: 590 | into.write_float(val) 591 | TYPE_VECTOR2: 592 | into.write_vector2(val) 593 | TYPE_RECT2: 594 | into.write_rect2(val) 595 | TYPE_QUAT: 596 | into.write_quat(val) 597 | TYPE_COLOR: 598 | into.write_color(val) 599 | TYPE_VECTOR3: 600 | into.write_vector3(val) 601 | CTYPE_UINT: 602 | into.write_uint(val) 603 | CTYPE_BYTE: 604 | into.write_byte(val) 605 | CTYPE_USHORT: 606 | into.write_ushort(val) 607 | TYPE_STRING: 608 | into.write_string(val) 609 | TYPE_RAW_ARRAY: 610 | # This is, in theory, PoolByteArray 611 | assert(val is PoolByteArray) 612 | # Ensure amount of elements can be encoded within a single byte 613 | assert(val.size() <= MAX_ARRAY_SIZE) 614 | # First write number of bytes 615 | into.write_byte(val.size()) 616 | # Then the actual bytes 617 | for b in val: 618 | into.write_byte(b) 619 | TYPE_INT_ARRAY: 620 | # This is, in theory, PoolIntArray 621 | assert(val is PoolIntArray) 622 | # Ensure amount of elements can be encoded within a single byte 623 | assert(val.size() <= MAX_ARRAY_SIZE) 624 | # First write number of ints 625 | into.write_byte(val.size()) 626 | # Then the integers 627 | for i in val: 628 | into.write_int(i) 629 | TYPE_REAL_ARRAY: 630 | # This is, in theory, PoolRealArray 631 | assert(val is PoolRealArray) 632 | # Ensure amount of elements can be encoded within a single byte 633 | assert(val.size() <= MAX_ARRAY_SIZE) 634 | # First write number of floats 635 | into.write_uint(val.size()) 636 | # Then the floats 637 | for f in val: 638 | into.write_float(f) 639 | 640 | 641 | 642 | func _build_replicable_prop(name: String, tp: int, mask: int, obj: Object) -> ReplicableProperty: 643 | var ret: ReplicableProperty = null 644 | 645 | match tp: 646 | TYPE_INT: 647 | # Check if this integer is meant to be used with a different size in bytes 648 | # when encoding/decoding into a byte array 649 | if (obj.has_meta(name)): 650 | var mval: int = obj.get_meta(name) 651 | match mval: 652 | CTYPE_UINT, CTYPE_BYTE, CTYPE_USHORT: 653 | tp = mval 654 | 655 | # Use the regular ReplicableProperty because integers don't need tolerance to compare them 656 | ret = ReplicableProperty.new(name, tp, mask) 657 | 658 | TYPE_REAL, TYPE_VECTOR2, TYPE_RECT2, TYPE_QUAT, TYPE_VECTOR3, TYPE_COLOR: 659 | # In here, any specialized comparison will be left for later in this function 660 | if (!obj.has_meta(name)): 661 | # This property does not require any special comparison method, so just 662 | # use the default replicable property class 663 | ret = ReplicableProperty.new(name, tp, mask) 664 | 665 | TYPE_BOOL, TYPE_STRING, TYPE_RAW_ARRAY, TYPE_INT_ARRAY, TYPE_REAL_ARRAY: 666 | ret = ReplicableProperty.new(name, tp, mask) 667 | 668 | _: 669 | # This is not a supported type. Bail so the test bellow can be done 670 | return null 671 | 672 | if (!ret): 673 | # If here, the type is supported but it does require one of the specialized ReplicableProperty 674 | # because a tolerance is required when comparing values. What this means is, the test for the 675 | # existance of the meta has already been done 676 | var tol: float = obj.get_meta(name) 677 | 678 | if (tol <= 0.0): 679 | # No custom tolerance is requested so use the is_equal_approx() function 680 | if (tp == TYPE_REAL): 681 | ret = RPropApprox.new(name, mask) 682 | else: 683 | ret = RPropApproxi.new(name, tp, mask) 684 | 685 | else: 686 | # A custom tolerance is required. This means the specific type must be known in order to 687 | # create the correct replicable property 688 | match (tp): 689 | TYPE_REAL: 690 | ret = RPropTolFloat.new(name, mask, tol) 691 | TYPE_VECTOR2: 692 | ret = RPropTolVec2.new(name, mask, tol) 693 | TYPE_RECT2: 694 | ret = RPropTolRec2.new(name, mask, tol) 695 | TYPE_QUAT: 696 | ret = RPropTolQuat.new(name, mask, tol) 697 | TYPE_VECTOR3: 698 | ret = RPropTolVec3.new(name, mask, tol) 699 | TYPE_COLOR: 700 | ret = RPropTolColor.new(name, mask, tol) 701 | 702 | return ret 703 | 704 | 705 | func update_pred_count(delta: int) -> void: 706 | for uid in _entity: 707 | _entity[uid].predcount = int(max(_entity[uid].predcount + delta, 0)) 708 | 709 | 710 | func get_pred_count(uid: int) -> int: 711 | var ge: GameEntity = _entity.get(uid) 712 | if (ge): 713 | return ge.predcount 714 | 715 | return 0 716 | -------------------------------------------------------------------------------- /addons/keh_network/eventinfo.gd: -------------------------------------------------------------------------------- 1 | ############################################################################### 2 | # Copyright (c) 2019 Yuri Sarudiansky 3 | # 4 | # Permission is hereby granted, free of charge, to any person obtaining a copy 5 | # of this software and associated documentation files (the "Software"), to deal 6 | # in the Software without restriction, including without limitation the rights 7 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | # copies of the Software, and to permit persons to whom the Software is 9 | # furnished to do so, subject to the following conditions: 10 | # 11 | # The above copyright notice and this permission notice shall be included in 12 | # all copies or substantial portions of the Software. 13 | # 14 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 20 | # THE SOFTWARE. 21 | ############################################################################### 22 | 23 | # This class is meant to hold information about an event type that can be 24 | # replicated through the network. It also performs the task of encoding and 25 | # decoding the events of the described type. 26 | 27 | extends Reference 28 | class_name NetEventInfo 29 | 30 | # Those are just shortcuts 31 | const CTYPE_UINT: int = SnapEntityBase.CTYPE_UINT 32 | const CTYPE_USHORT: int = SnapEntityBase.CTYPE_USHORT 33 | const CTYPE_BYTE: int = SnapEntityBase.CTYPE_BYTE 34 | 35 | # Type ID of events described by this 36 | var _type_id: int 37 | 38 | # This array holds the types of the parameters expected by events this type 39 | var _param_types: Array 40 | 41 | # Functions held here will be called as soon as events are decoded. Each entry is 42 | # a dictionary with the following fields: 43 | # - obj: Object instance holding the function to be called 44 | # - funcname: Function name to be called 45 | var _evt_handlers: Array 46 | 47 | 48 | func _init(id: int, pt: Array) -> void: 49 | # Event type ID must fit in 16 bits 50 | assert(id >= 0 && id < 0xFFFF) 51 | # If the next assert fails, then an unsupported parameter type is in the array 52 | assert(check_types(pt)) 53 | 54 | _type_id = id 55 | _param_types = pt 56 | 57 | 58 | func attach_handler(obj: Object, fname: String) -> void: 59 | _evt_handlers.push_back({"obj": obj, "funcname": fname}) 60 | 61 | 62 | func clear_handlers() -> void: 63 | _evt_handlers.clear() 64 | 65 | 66 | func encode(into: EncDecBuffer, params: Array) -> void: 67 | # Parameters must match the expected parameter list 68 | assert(params.size() == _param_types.size()) 69 | 70 | # Write the parameters 71 | var idx: int = 0 72 | for pt in _param_types: 73 | match pt: 74 | TYPE_BOOL: 75 | into.write_bool(params[idx]) 76 | TYPE_INT: 77 | into.write_int(params[idx]) 78 | TYPE_REAL: 79 | into.write_float(params[idx]) 80 | TYPE_VECTOR2: 81 | into.write_vector2(params[idx]) 82 | TYPE_RECT2: 83 | into.write_rect2(params[idx]) 84 | TYPE_QUAT: 85 | into.write_quat(params[idx]) 86 | TYPE_COLOR: 87 | into.write_color(params[idx]) 88 | TYPE_VECTOR3: 89 | into.write_vector3(params[idx]) 90 | CTYPE_UINT: 91 | into.write_uint(params[idx]) 92 | CTYPE_BYTE: 93 | into.write_byte(params[idx]) 94 | CTYPE_USHORT: 95 | into.write_ushort(params[idx]) 96 | 97 | idx += 1 98 | 99 | 100 | func decode(from: EncDecBuffer) -> void: 101 | # At this point, "from" should already have gone past the type ID of this event 102 | var params: Array = [] 103 | 104 | for pt in _param_types: 105 | match pt: 106 | TYPE_BOOL: 107 | params.push_back(from.read_bool()) 108 | TYPE_INT: 109 | params.push_back(from.read_int()) 110 | TYPE_REAL: 111 | params.push_back(from.read_float()) 112 | TYPE_VECTOR2: 113 | params.push_back(from.read_vector2()) 114 | TYPE_RECT2: 115 | params.push_back(from.read_rect2()) 116 | TYPE_QUAT: 117 | params.push_back(from.read_quat()) 118 | TYPE_COLOR: 119 | params.push_back(from.read_color()) 120 | TYPE_VECTOR3: 121 | params.push_back(from.read_vector3()) 122 | CTYPE_UINT: 123 | params.push_back(from.read_uint()) 124 | CTYPE_BYTE: 125 | params.push_back(from.read_byte()) 126 | CTYPE_USHORT: 127 | params.push_back(from.read_ushort()) 128 | 129 | call_handlers(params) 130 | 131 | 132 | func call_handlers(params: Array) -> void: 133 | var toerase: Array = [] 134 | for eh in _evt_handlers: 135 | if (eh.obj): 136 | eh.obj.callv(eh.funcname, params) 137 | 138 | else: 139 | toerase.push_back(eh) 140 | 141 | for te in toerase: 142 | _evt_handlers.erase(te) 143 | 144 | 145 | 146 | # This is meant to be called within the assert, in other words only on non release builds 147 | func check_types(ptypes: Array) -> bool: 148 | for pt in ptypes: 149 | match pt: 150 | TYPE_BOOL, TYPE_INT, TYPE_REAL, TYPE_VECTOR2, TYPE_RECT2, TYPE_QUAT, TYPE_COLOR, TYPE_VECTOR3,\ 151 | CTYPE_UINT, CTYPE_BYTE, CTYPE_USHORT: 152 | pass 153 | _: 154 | return false 155 | return true 156 | -------------------------------------------------------------------------------- /addons/keh_network/inputdata.gd: -------------------------------------------------------------------------------- 1 | ############################################################################### 2 | # Copyright (c) 2019 Yuri Sarudiansky 3 | # 4 | # Permission is hereby granted, free of charge, to any person obtaining a copy 5 | # of this software and associated documentation files (the "Software"), to deal 6 | # in the Software without restriction, including without limitation the rights 7 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | # copies of the Software, and to permit persons to whom the Software is 9 | # furnished to do so, subject to the following conditions: 10 | # 11 | # The above copyright notice and this permission notice shall be included in 12 | # all copies or substantial portions of the Software. 13 | # 14 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 20 | # THE SOFTWARE. 21 | ############################################################################### 22 | 23 | # This is meant to be a "lightweight data object". Basically, when input 24 | # is gathered through the network input system, an object of this class 25 | # will be generated. When encoding data, it will be retrieved from one 26 | # object of this class. When decoding, an object of this class will be 27 | # generated. 28 | # Instead of using the normal input polling, when this kind of data becomes 29 | # necessary it should be requested from the network object, which will then 30 | # provide an object of this class. 31 | 32 | extends Reference 33 | class_name InputData 34 | 35 | var _vec2: Dictionary = {} 36 | var _vec3: Dictionary = {} 37 | var _analog: Dictionary = {} 38 | var _action: Dictionary = {} 39 | var _has_input: bool = false 40 | var signature: int = 0 41 | 42 | func _init(s: int) -> void: 43 | signature = s 44 | 45 | func has_input() -> bool: 46 | return _has_input 47 | 48 | func get_custom_vec2(name: String) -> Vector2: 49 | return _vec2.get(name, Vector2()) 50 | 51 | func set_custom_vec2(name: String, val: Vector2) -> void: 52 | _vec2[name] = val 53 | _has_input = (val.x != 0.0 || val.y != 0.0 || _has_input) 54 | 55 | func get_custom_vec3(name: String) -> Vector3: 56 | return _vec3.get(name, Vector3()) 57 | 58 | func set_custom_vec3(name: String, val: Vector3) -> void: 59 | _vec3[name] = val 60 | _has_input = (val.x != 0.0 || val.y != 0.0 || val.z != 0.0 || _has_input) 61 | 62 | func set_custom_bool(name: String, val: bool) -> void: 63 | _action[name] = val 64 | _has_input = (val || _has_input) 65 | 66 | 67 | func get_mouse_relative() -> Vector2: 68 | return get_custom_vec2("relative") 69 | 70 | func set_mouse_relative(mr: Vector2) -> void: 71 | set_custom_vec2("relative", mr) 72 | 73 | func get_mouse_speed() -> Vector2: 74 | return get_custom_vec2("speed") 75 | 76 | func set_mouse_speed(ms: Vector2) -> void: 77 | set_custom_vec2("speed", ms) 78 | 79 | func get_analog(map: String) -> float: 80 | return _analog.get(map, 0.0) 81 | 82 | func set_analog(map: String, val: float) -> void: 83 | _analog[map] = val 84 | _has_input = (val != 0.0 || _has_input) 85 | 86 | func is_pressed(map: String) -> bool: 87 | return _action.get(map, false) 88 | 89 | func get_custom_bool(map: String) -> bool: 90 | return _action.get(map, false) 91 | 92 | 93 | func set_pressed(map: String, p: bool) -> void: 94 | _action[map] = p 95 | _has_input = (p || _has_input) 96 | 97 | -------------------------------------------------------------------------------- /addons/keh_network/inputinfo.gd: -------------------------------------------------------------------------------- 1 | ############################################################################### 2 | # Copyright (c) 2019 Yuri Sarudiansky 3 | # 4 | # Permission is hereby granted, free of charge, to any person obtaining a copy 5 | # of this software and associated documentation files (the "Software"), to deal 6 | # in the Software without restriction, including without limitation the rights 7 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | # copies of the Software, and to permit persons to whom the Software is 9 | # furnished to do so, subject to the following conditions: 10 | # 11 | # The above copyright notice and this permission notice shall be included in 12 | # all copies or substantial portions of the Software. 13 | # 14 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 20 | # THE SOFTWARE. 21 | ############################################################################### 22 | 23 | # This class is meant to hold information regarding which input data 24 | # (based on the project settings input mappings) should be encoded/decoded 25 | # when dealing with the network. 26 | # This is meant for internal usage and normally speaking should not be 27 | # directly used by external code. 28 | 29 | extends Reference 30 | class_name NetInputInfo 31 | 32 | 33 | ### Those are options retrieved from the ProjectSettings 34 | var _use_mouse_relative: bool = false 35 | var _use_mouse_speed: bool = false 36 | # If this is true then analog input data will be quantized with precision of 8 bits 37 | # Error margin should be more than acceptable for this kind of data 38 | var _quantize_analog: bool = false 39 | 40 | 41 | # The following dictionaries are meant to hold the list of input data meant to be 42 | # encoded/decoded, thus replicated through the network. Each dictionary corresponds to 43 | # a supported data type (analog, boolean, vector2 and vector3). Analog and boolean are 44 | # the only ones to be automatically retrieved by polling the input device state, based 45 | # on the input map settings and the registered input names. 46 | # Within those dictionaries each entry, keyed by the input name, will be another dictionary 47 | # with the following fields: 48 | # mask: a bit mask corresponding to the entry when building the "change mask" 49 | # custom: true if the input entry corresponds to a custom data. 50 | 51 | var _analog_list: Dictionary = {} 52 | var _bool_list: Dictionary = {} 53 | var _vec2_list: Dictionary = {} 54 | var _vec3_list: Dictionary = {} 55 | 56 | # As soon as a custom input data type is registered this flag will be set to true. 57 | # This is mostly to help issue warnings when custom input are required but the function 58 | # reference necessary to generate the data is not set 59 | var _has_custom_data: bool = false 60 | 61 | 62 | var _print_debug: bool = false 63 | 64 | 65 | func _init() -> void: 66 | # Obtain the options from ProjectSettings 67 | if (ProjectSettings.has_setting("keh_addons/network/use_input_mouse_relative")): 68 | _use_mouse_relative = ProjectSettings.get_setting("keh_addons/network/use_input_mouse_relative") 69 | 70 | if (ProjectSettings.has_setting("keh_addons/network/use_input_mouse_speed")): 71 | _use_mouse_speed = ProjectSettings.get_setting("keh_addons/network/use_input_mouse_speed") 72 | 73 | if (ProjectSettings.has_setting("keh_addons/network/quantize_analog_input")): 74 | _quantize_analog = ProjectSettings.get_setting("keh_addons/network/quantize_analog_input") 75 | 76 | if (ProjectSettings.has_setting("keh_addons/network/print_debug_info")): 77 | _print_debug = ProjectSettings.get_setting("keh_addons/network/print_debug_info") 78 | 79 | 80 | func has_custom_data() -> bool: 81 | return _has_custom_data 82 | 83 | 84 | # A 'generic' internal function meant to create the correct entry within the various 85 | # input list containers. 86 | func _register_data(container: Dictionary, n: String, c: bool) -> void: 87 | if (!container.has(n)): 88 | container[n] = { 89 | "mask": 1 << container.size(), 90 | "custom": c, 91 | } 92 | _has_custom_data = _has_custom_data || c 93 | 94 | 95 | # Register either a boolean or analog input data. 96 | func register_action(map: String, is_analog: bool, custom: bool) -> void: 97 | if (_print_debug): 98 | print_debug("Registering%snetwork input '%s' | analog: %s" % [" custom " if custom else " ", map, is_analog]) 99 | if (is_analog): 100 | _register_data(_analog_list, map, custom) 101 | else: 102 | _register_data(_bool_list, map, custom) 103 | 104 | # Register vector2 data, which is necessarily custom data 105 | func register_vec2(map: String) -> void: 106 | if (_print_debug): 107 | print_debug("Registering custom vector2 network input data %s" % map) 108 | _register_data(_vec2_list, map, true) 109 | 110 | # Register vector3 data, which is necessarily custom data 111 | func register_vec3(map: String) -> void: 112 | if (_print_debug): 113 | print_debug("Registering custom vector3 network input data %s" % map) 114 | _register_data(_vec3_list, map, true) 115 | 116 | 117 | # Reset all previous input registration. 118 | func reset_actions() -> void: 119 | _analog_list.clear() 120 | _bool_list.clear() 121 | _vec2_list.clear() 122 | _vec3_list.clear() 123 | _has_custom_data = false 124 | 125 | 126 | # Allow overriding the project setting related to the mouse relative 127 | func set_use_mouse_relative(use: bool) -> void: 128 | _use_mouse_relative = use 129 | 130 | # Allow overriding the project setting related to the mouse speed 131 | func set_use_mouse_speed(use: bool) -> void: 132 | _use_mouse_speed = use 133 | 134 | 135 | func use_mouse_relative() -> bool: 136 | return _use_mouse_relative 137 | 138 | func use_mouse_speed() -> bool: 139 | return _use_mouse_speed 140 | 141 | 142 | func make_empty() -> InputData: 143 | var ret: InputData = InputData.new(0) 144 | 145 | if (_use_mouse_relative): 146 | ret.set_mouse_relative(Vector2()) 147 | if (_use_mouse_speed): 148 | ret.set_mouse_speed(Vector2()) 149 | 150 | for a in _analog_list: 151 | ret.set_analog(a, 0.0) 152 | 153 | for b in _bool_list: 154 | ret.set_pressed(b, false) 155 | 156 | for v in _vec2_list: 157 | ret.set_custom_vec2(v, Vector2()) 158 | 159 | for v in _vec3_list: 160 | ret.set_custom_vec3(v, Vector3()) 161 | 162 | return ret 163 | 164 | # When encoded, change masks use a variable number of bytes depending on the 165 | # amount of registered input data of the specific type. So, if there are less 166 | # than 9 analog actions, the change mask will use a single byte. This requires 167 | # some checking before (re)writing data. This helper internal function is meant 168 | # to perform the correct writing of the mask. 169 | # This function assumes the proper check of the size > 0 has already been done 170 | func _write_mask(m: int, size: int, buffer: EncDecBuffer, at: int = -1) -> void: 171 | if (size <= 8): 172 | if (at < 0): 173 | buffer.write_byte(m) 174 | else: 175 | buffer.rewrite_byte(m, at) 176 | 177 | elif (size <= 16): 178 | if (at < 0): 179 | buffer.write_ushort(m) 180 | else: 181 | buffer.rewrite_ushort(m, at) 182 | 183 | else: 184 | if (at < 0): 185 | buffer.write_uint(m) 186 | else: 187 | buffer.rewrite_uint(m, at) 188 | 189 | # And this helper internal function is meant to perform the reading of encoded 190 | # change masks. 191 | func _read_mask(size: int, buffer: EncDecBuffer) -> int: 192 | if (size <= 8): 193 | return buffer.read_byte() 194 | elif (size <= 16): 195 | return buffer.read_ushort() 196 | 197 | return buffer.read_uint() 198 | 199 | 200 | 201 | # Given an EncDecBuffer and an InputData, encode the input into the buffer 202 | func encode_to(encdec: EncDecBuffer, input: InputData) -> void: 203 | # Encode the signature of the input object - Integer, set as uint - 4 bytes 204 | encdec.write_uint(input.signature) 205 | 206 | # Encode the flag indicating if this object has input or not 207 | encdec.write_bool(input.has_input()) 208 | 209 | # If there is input, encode it 210 | if (input.has_input()): 211 | # Encode mouse relative if it's enabled - Vector2 - 8 bytes 212 | if (_use_mouse_relative): 213 | encdec.write_vector2(input.get_mouse_relative()) 214 | 215 | # Encode mouse speed if it's enabled - Vector2 - 8 bytes 216 | if (_use_mouse_speed): 217 | encdec.write_vector2(input.get_mouse_speed()) 218 | 219 | # Encode analog data, if there is at least one registered 220 | if (_analog_list.size() > 0): 221 | var windex: int = encdec.get_current_size() 222 | var cmask: int = 0 223 | _write_mask(cmask, _analog_list.size(), encdec) 224 | 225 | for a in _analog_list: 226 | var fval: float = input.get_analog(a) 227 | if (fval != 0): 228 | cmask |= _analog_list[a].mask 229 | # Since this analog input is not zero, encode it 230 | if (_quantize_analog): 231 | # Quantization is enabled, so use it 232 | var quant: int = Quantize.quantize_float(fval, 0.0, 1.0, 8) 233 | encdec.write_byte(quant) 234 | else: 235 | # Encode normaly 236 | encdec.write_float(fval) 237 | 238 | # All relevant analogs have been encoded. If something changed the 239 | # mask must be updated within the encoded byte array. The correct index 240 | # is stored in the windex variable 241 | if (cmask != 0): 242 | _write_mask(cmask, _analog_list.size(), encdec, windex) 243 | 244 | # Encode boolean data 245 | if (_bool_list.size() > 0): 246 | var mask: int = 0 247 | for m in _bool_list: 248 | if (input.is_pressed(m)): 249 | mask |= _bool_list[m].mask 250 | 251 | _write_mask(mask, _bool_list.size(), encdec) 252 | 253 | # Encode custom vector2 data 254 | if (_vec2_list.size() > 0): 255 | var windex: int = encdec.get_current_size() 256 | var mask: int = 0 257 | _write_mask(mask, _vec2_list.size(), encdec) 258 | 259 | for v in _vec2_list: 260 | var vec2: Vector2 = input.get_custom_vec2(v) 261 | if (vec2.x != 0.0 || vec2.y != 0.0): 262 | mask |= _vec2_list[v].mask 263 | encdec.write_vector2(vec2) 264 | 265 | if (mask != 0): 266 | _write_mask(mask, _vec2_list.size(), encdec, windex) 267 | 268 | # Encode custom vector3 data 269 | if (_vec3_list.size() > 0): 270 | var windex: int = encdec.get_current_size() 271 | var mask: int = 0 272 | _write_mask(mask, _vec3_list.size(), encdec) 273 | 274 | for v in _vec3_list: 275 | var vec3: Vector3 = input.get_custom_vec3(v) 276 | if (vec3.x != 0.0 || vec3.y != 0.0 || vec3.z != 0.0): 277 | mask |= _vec3_list[v].mask 278 | encdec.write_vector3(vec3) 279 | 280 | if (mask != 0): 281 | _write_mask(mask, _vec3_list.size(), encdec, windex) 282 | 283 | 284 | 285 | # Use the received EncDecBuffer to decode data into an InputData object, which will 286 | # be returned. This assumes the buffer is in the correct reading position 287 | func decode_from(encdec: EncDecBuffer) -> InputData: 288 | var ret: InputData = InputData.new(0) 289 | 290 | # Decode the signature 291 | ret.signature = encdec.read_uint() 292 | # Decode the _has_input flag 293 | var has_input: bool = encdec.read_bool() 294 | 295 | if (has_input): 296 | # Decode mouse relative data if it's enabled 297 | if (_use_mouse_relative): 298 | ret.set_mouse_relative(encdec.read_vector2()) 299 | 300 | # Decode mouse speed data if it's enabled 301 | if (_use_mouse_speed): 302 | ret.set_mouse_speed(encdec.read_vector2()) 303 | 304 | 305 | # Decode analog data 306 | if (_analog_list.size() > 0): 307 | # First the change mask, indicating which of the analog inputs were 308 | # encoded 309 | var cmask: int = _read_mask(_analog_list.size(), encdec) 310 | for a in _analog_list: 311 | if (cmask & _analog_list[a].mask): 312 | if (_quantize_analog): 313 | # Analog quantization is enabled, so extract the quantized value first 314 | var quantized: int = encdec.read_byte() 315 | # Then restore the float 316 | ret.set_analog(a, Quantize.restore_float(quantized, 0.0, 1.0, 8)) 317 | else: 318 | # No quantization used, so directly take the float 319 | ret.set_analog(a, encdec.read_float()) 320 | 321 | # Decode boolean data 322 | if (_bool_list.size() > 0): 323 | var mask: int = _read_mask(_bool_list.size(), encdec) 324 | for b in _bool_list: 325 | ret.set_pressed(b, (mask & _bool_list[b].mask)) 326 | 327 | # Decode vector2 data 328 | if (_vec2_list.size() > 0): 329 | var mask: int = _read_mask(_vec2_list.size(), encdec) 330 | for v in _vec2_list: 331 | if (mask & _vec2_list[v].mask): 332 | ret.set_custom_vec2(v, encdec.read_vector2()) 333 | 334 | # Decode vector3 data 335 | if (_vec3_list.size() > 0): 336 | var mask: int = _read_mask(_vec3_list.size(), encdec) 337 | for v in _vec3_list: 338 | if (mask & _vec3_list[v].mask): 339 | ret.set_custom_vec3(v, encdec.read_vector3()) 340 | 341 | return ret 342 | 343 | -------------------------------------------------------------------------------- /addons/keh_network/nodespawner.gd: -------------------------------------------------------------------------------- 1 | ############################################################################### 2 | # Copyright (c) 2019 Yuri Sarudiansky 3 | # 4 | # Permission is hereby granted, free of charge, to any person obtaining a copy 5 | # of this software and associated documentation files (the "Software"), to deal 6 | # in the Software without restriction, including without limitation the rights 7 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | # copies of the Software, and to permit persons to whom the Software is 9 | # furnished to do so, subject to the following conditions: 10 | # 11 | # The above copyright notice and this permission notice shall be included in 12 | # all copies or substantial portions of the Software. 13 | # 14 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 20 | # THE SOFTWARE. 21 | ############################################################################### 22 | 23 | # This is just a base class. For an example of spawner check the defaultspawner.gd 24 | # or any of the spawners in the demo project. 25 | 26 | extends Reference 27 | class_name NetNodeSpawner 28 | 29 | 30 | func spawn() -> Node: 31 | # A class derived from NetNodeSpawner must implement the 'spawn() -> Node' method 32 | assert(false) 33 | return null 34 | 35 | -------------------------------------------------------------------------------- /addons/keh_network/pinginfo.gd: -------------------------------------------------------------------------------- 1 | ############################################################################### 2 | # Copyright (c) 2019 Yuri Sarudiansky 3 | # 4 | # Permission is hereby granted, free of charge, to any person obtaining a copy 5 | # of this software and associated documentation files (the "Software"), to deal 6 | # in the Software without restriction, including without limitation the rights 7 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | # copies of the Software, and to permit persons to whom the Software is 9 | # furnished to do so, subject to the following conditions: 10 | # 11 | # The above copyright notice and this permission notice shall be included in 12 | # all copies or substantial portions of the Software. 13 | # 14 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 20 | # THE SOFTWARE. 21 | ############################################################################### 22 | 23 | # Initializing the ping system requires quite a bit of code. So adding that 24 | # into a class. 25 | # This is meant for internal addon usage so normally there is no need to 26 | # directly access objects of this class. 27 | 28 | extends Reference 29 | class_name NetPingInfo 30 | 31 | const PING_INTERVAL: float = 1.0 # Wait one second between ping requests 32 | const PING_TIMEOUT: float = 5.0 # Wait five seconds before considering a ping request as lost 33 | 34 | var interval: Timer # Timer to control interval between ping requests 35 | var timeout: Timer # Timer to control ping timeouts 36 | var signature: int # Signature of the ping request 37 | var lost_packets: int # Number of packets considered lost 38 | var last_ping: float # Last measured ping, in milliseconds 39 | var parent: Node # Node to hold the timers and remote functions. It should be a NetPlayerNode 40 | 41 | func _init(pid: int, node: Node) -> void: 42 | signature = 0 43 | lost_packets = 0 44 | last_ping = 0.0 45 | parent = node 46 | 47 | # Initialize the timers 48 | interval = Timer.new() 49 | interval.wait_time = PING_INTERVAL 50 | interval.process_mode = Timer.TIMER_PROCESS_IDLE 51 | interval.set_name("net_ping_interval") 52 | # warning-ignore:return_value_discarded 53 | interval.connect("timeout", self, "_request_ping", [pid]) 54 | 55 | timeout = Timer.new() 56 | timeout.wait_time = PING_TIMEOUT 57 | timeout.process_mode = Timer.TIMER_PROCESS_IDLE 58 | timeout.set_name("net_ping_timeout") 59 | # warning-ignore:return_value_discarded 60 | timeout.connect("timeout", self, "_on_ping_timeout", [pid]) 61 | 62 | # Timers must be added into the tree otherwise they aren't updated 63 | parent.add_child(interval) 64 | parent.add_child(timeout) 65 | 66 | # Make sure the timeout is stopped while the interval is running 67 | timeout.stop() 68 | interval.start() 69 | 70 | 71 | func _request_ping(pid: int) -> void: 72 | signature += 1 73 | interval.stop() 74 | timeout.start() 75 | parent.rpc_unreliable_id(pid, "_client_ping", signature, last_ping) 76 | 77 | func _on_ping_timeout(pid: int) -> void: 78 | # The last ping request has timed out. No answer received, so assume the packet has been lost 79 | lost_packets += 1 80 | # Request a new ping - no need to wait through interval since there was already 5 seconds 81 | # from the previous request 82 | _request_ping(pid) 83 | 84 | func calculate_and_restart(sig: int) -> float: 85 | var ret: float = -1.0 86 | if (signature == sig): 87 | # Obtain the amount of time and convert to milliseconds 88 | last_ping = (PING_TIMEOUT - timeout.time_left) * 1000 89 | ret = last_ping 90 | timeout.stop() 91 | interval.start() 92 | 93 | return ret 94 | -------------------------------------------------------------------------------- /addons/keh_network/playerdata.gd: -------------------------------------------------------------------------------- 1 | ############################################################################### 2 | # Copyright (c) 2019 Yuri Sarudiansky 3 | # 4 | # Permission is hereby granted, free of charge, to any person obtaining a copy 5 | # of this software and associated documentation files (the "Software"), to deal 6 | # in the Software without restriction, including without limitation the rights 7 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | # copies of the Software, and to permit persons to whom the Software is 9 | # furnished to do so, subject to the following conditions: 10 | # 11 | # The above copyright notice and this permission notice shall be included in 12 | # all copies or substantial portions of the Software. 13 | # 14 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 20 | # THE SOFTWARE. 21 | ############################################################################### 22 | 23 | # Meant to make things easier to manage player data by holding containers 24 | # and providing access function to manipulate those. 25 | # This is not meant to be manually instanced but direct access to an object 26 | # of this class is very helpful specially when player list is required. To 27 | # that end, it should be pretty simple to do so using the instance that is 28 | # automatically created within the network singleton (network.player_data). 29 | 30 | extends Reference 31 | class_name NetPlayerData 32 | 33 | # This input info will be given to each instance of NetPlayerNode so proper input processing 34 | # can be done, specially encoding/decoding 35 | var input_info: NetInputInfo 36 | 37 | var local_player: NetPlayerNode 38 | 39 | # Map from network ID to node 40 | var remote_player: Dictionary 41 | 42 | # Holds the list of registered custom properties. When a player node is created, a copy of 43 | # this dictionary must be attached into that node. Note that it must be a copy and not a 44 | # reference since the custom properties are potentially different on each player. 45 | # This also have a secondary use: since it will always hold the initial value it will serve to 46 | # determine the expected value type when encoding and decoding. 47 | var custom_property: Dictionary 48 | 49 | # The FuncRefs that are used within each NetPlayerNode 50 | var ping_signaler: FuncRef setget set_ping_signaler 51 | var cprop_signaler: FuncRef setget set_cprop_signaler 52 | var cprop_broadcaster: FuncRef setget set_cprop_broadcaster 53 | 54 | func _init() -> void: 55 | input_info = NetInputInfo.new() 56 | local_player = NetPlayerNode.new(input_info, true) 57 | remote_player = {} 58 | custom_property = {} 59 | 60 | # Create an instance of a NetPlayerNode while also adding the necessary 61 | # internal data to the created object. 62 | func create_player(id: int) -> NetPlayerNode: 63 | var np: NetPlayerNode = NetPlayerNode.new(input_info) 64 | np.set_network_id(id) 65 | np.ping_signaler = ping_signaler 66 | np.custom_prop_signaler = cprop_signaler 67 | np.custom_prop_broadcast_requester = cprop_broadcaster 68 | 69 | # Add the registered custom properties 70 | for pname in custom_property: 71 | np._add_custom_property(pname, custom_property[pname]) 72 | 73 | return np 74 | 75 | # Add a player node to the internal container. Effectively registers a player. 76 | # This is automatically called by the internal system and there is no need 77 | # to deal with this from game code. 78 | func add_remote(np: NetPlayerNode) -> void: 79 | remote_player[np.net_id] = np 80 | 81 | 82 | # Cleanup the internal player node container 83 | func clear_remote() -> void: 84 | for p in remote_player: 85 | remote_player[p].queue_free() 86 | 87 | remote_player.clear() 88 | 89 | 90 | # Retrieve the NetPlayerNode corresponding to the specified player ID 91 | func get_pnode(pid: int) -> NetPlayerNode: 92 | if (local_player.net_id == pid): 93 | return local_player 94 | return remote_player.get(pid) 95 | 96 | 97 | # Retrieve the number of players, including the local one. 98 | func get_player_count() -> int: 99 | # Local player plus remote peers 100 | return 1 + remote_player.size() 101 | 102 | 103 | # Add (register) a custom player property 104 | func add_custom_property(pname: String, default_value, replicate: int = NetCustomProperty.ReplicationMode.ServerOnly) -> void: 105 | var prop: NetCustomProperty = NetCustomProperty.new(default_value, replicate) 106 | custom_property[pname] = prop 107 | 108 | # The player node function will create a new custom property object so no need to worry about references 109 | # interfering with different player nodes. Nevertheless, ensure the local player gets this custom property 110 | local_player._add_custom_property(pname, prop) 111 | 112 | # And ensure the remote players also have this custom property 113 | for pid in remote_player: 114 | remote_player[pid]._add_custom_property(pname, prop) 115 | 116 | 117 | ### Setters/getters 118 | func set_ping_signaler(ps: FuncRef) -> void: 119 | ping_signaler = ps 120 | local_player.ping_signaler = ps 121 | 122 | func set_cprop_signaler(cps: FuncRef) -> void: 123 | cprop_signaler = cps 124 | local_player.custom_prop_signaler = cps 125 | 126 | func set_cprop_broadcaster(cpb: FuncRef) -> void: 127 | cprop_broadcaster = cpb 128 | local_player.custom_prop_broadcast_requester = cpb 129 | -------------------------------------------------------------------------------- /addons/keh_network/playernode.gd: -------------------------------------------------------------------------------- 1 | ############################################################################### 2 | # Copyright (c) 2019 Yuri Sarudiansky 3 | # 4 | # Permission is hereby granted, free of charge, to any person obtaining a copy 5 | # of this software and associated documentation files (the "Software"), to deal 6 | # in the Software without restriction, including without limitation the rights 7 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | # copies of the Software, and to permit persons to whom the Software is 9 | # furnished to do so, subject to the following conditions: 10 | # 11 | # The above copyright notice and this permission notice shall be included in 12 | # all copies or substantial portions of the Software. 13 | # 14 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 20 | # THE SOFTWARE. 21 | ############################################################################### 22 | 23 | # Holds necessary data for each connected player. Because this is a node 24 | # it will be part of the tree hierarchy, more specifically as a child of 25 | # the network singleton. 26 | # Besides holding data, it's also where the input data is retrieved and 27 | # replicated to. 28 | # Note that while it's OK to directly access objects of this class, manually 29 | # creating then is absolutely unnecessary. 30 | 31 | 32 | extends Node 33 | class_name NetPlayerNode 34 | 35 | var net_id: int = 1 setget set_network_id 36 | 37 | # The input cache is used in two different ways, depending on which machine 38 | # it's running and which player this node corresponds to. 39 | # Running on server: 40 | # - If this node corresponds to the local player (the server), then the 41 | # cache does nothing because there is no need to validate its data. 42 | # - If this node corresponds to a client then the cache will hold the 43 | # received input data, which will be retrieved from it when iterating 44 | # the game state. At that moment the input is removed from the buffer 45 | # and a secondary container holds information that maps from the 46 | # snapshot signature to the used input signature. With this information 47 | # when encoding snapshot data it's possible to attach to it the 48 | # signature of the input data used to simulate the game. Another thing 49 | # to keep in mind is that incoming input data may be out of order or 50 | # duplicated. To that end it's a lot simpler to deal with a dictionary 51 | # to hold the objects. 52 | # Running on client: 53 | # - If this node corresponds to a remote player, does nothing because a 54 | # client knows nothing about other clients nor the server in regards 55 | # to input data. 56 | # - If this node corresponds to the local player then the cache must 57 | # hold a sequential (ascending) set of input objects that are mostly 58 | # meant for validation when snapshot data arrives. When validating, 59 | # any input older than the one just checked must be removed from the 60 | # container, thus keeping this data in ascending order makes things 61 | # a lot easier. In this case it's better to have an array rather than 62 | # dictionary to hold this data. 63 | # With all this information in mind, this inner class is meant to make 64 | # things a bit easier to deal with those differences. 65 | class InputCache: 66 | # Key = input signature | value = instance of InputData 67 | # This is meant for the server 68 | var sbuffer: Dictionary 69 | # The container used by the client. It is necessary so non acknowledged 70 | # input data must be sent again. 71 | var cbuffer: Array 72 | # Keep track of the last input signature used by this machine 73 | var last_sig: int 74 | # This will be used only on the server and each entry is keyed by the snapshot 75 | # signature, holding the input signature as value. 76 | var snapinpu: Dictionary 77 | # Count the number of 0-input snapshots that weren't acknowledged by the client 78 | # If this value is bigger than 0 then the server will send the newest full 79 | # snapshot within its history rather than calculating delta snapshot 80 | var no_input_count: int 81 | # Holds the signature of the last acknowledged snapshot signature. This will be 82 | # used as reference to cleanup older data. 83 | var last_ack_snap: int 84 | 85 | func _init() -> void: 86 | sbuffer = {} 87 | cbuffer = [] 88 | last_sig = 0 89 | snapinpu = {} 90 | no_input_count = 0 91 | last_ack_snap = 0 92 | 93 | # Creates the association of snapshot signature with input for the corresponding 94 | # player. This will automatically take care of the "no input count" 95 | func associate(snap_sig: int, isig: int) -> void: 96 | snapinpu[snap_sig] = isig 97 | if (isig == 0): 98 | no_input_count += 1 99 | 100 | # Acknowledges the specified snapshot signature by removing it from the snapinpu 101 | # container. It automatically updates the "no_input_count" property by subtracting 102 | # from it if the given snapshot didn't use any input 103 | func acknowledge(snap_sig: int) -> void: 104 | # Depending on how the synchronization occurred, it's possible some older data 105 | # didn't get a direct acknowledgement but those are now irrelevant. So, the 106 | # cleanup must be performed through a loop that goes from the last acknowledged 107 | # up to the one specified. During this loop the "no_input_count" must be 108 | # correctly updated 109 | for sig in range(last_ack_snap + 1, snap_sig + 1): 110 | var isig: int = snapinpu.get(sig, -1) 111 | if (isig > -1): 112 | # warning-ignore:return_value_discarded 113 | snapinpu.erase(sig) 114 | if (isig == 0): 115 | no_input_count -= 1 116 | 117 | last_ack_snap = snap_sig 118 | 119 | 120 | # The input cache 121 | var _input_cache: InputCache 122 | 123 | # Used to encode/decode input data 124 | var _edec_input: EncDecBuffer 125 | 126 | # Server will only send snapshot data to this client when this flag is 127 | # set to true. 128 | var _is_ready: bool = false setget set_ready 129 | 130 | # Input info, necessary to properly deal with input 131 | var _input_info: NetInputInfo 132 | 133 | # This flag is meant to help identify if this node corresponds to the local player or not 134 | # and is relevant mostly on the server. 135 | var _is_local: bool 136 | 137 | # The ping/pong system 138 | var _ping: NetPingInfo 139 | 140 | # Custom data system. Entries here: 141 | # Key = custom property name 142 | # Value = instance of NetCustomProperty 143 | var _custom_data: Dictionary 144 | 145 | # Counts how many custom properties have been changed and not replicated yet. 146 | var _custom_prop_dirty_count: int = 0 147 | 148 | # These vectors will be used to cache mouse data from the _input function 149 | # Obviously those will only be used on the local machine 150 | #var _mposition: Vector2 = Vector2() # Should mouse position be used? 151 | var _mrelative: Vector2 = Vector2() 152 | var _mspeed: Vector2 = Vector2() 153 | 154 | # When set to false it will disable polling input for local player 155 | var _local_input_enabled: bool = true 156 | 157 | ### Options retrieved from project settings 158 | var _broadcast_ping: bool = false 159 | 160 | # "Signaler" for new ping values 161 | var ping_signaler: FuncRef 162 | # "Signaler" for custom property changes 163 | var custom_prop_signaler: FuncRef 164 | # The FuncRef pointing to the function that will request the server to actually 165 | # broadcast the specified custom property 166 | var custom_prop_broadcast_requester: FuncRef 167 | 168 | 169 | func _init(input_info: NetInputInfo, is_local: bool = false) -> void: 170 | _is_local = is_local 171 | _input_info = input_info 172 | _input_cache = InputCache.new() 173 | _edec_input = EncDecBuffer.new() 174 | _ping = null 175 | _custom_data = {} 176 | 177 | if (ProjectSettings.has_setting("keh_addons/network/broadcast_measured_ping")): 178 | _broadcast_ping = ProjectSettings.get_setting("keh_addons/network/broadcast_measured_ping") 179 | 180 | 181 | func _ready() -> void: 182 | set_process_input(_is_local) 183 | 184 | 185 | 186 | func _input(evt: InputEvent) -> void: 187 | if (evt is InputEventMouseMotion): 188 | #_mposition = evt.position # Should mouse position be used? 189 | # Accumulate mouse relative so behavior is more consistent when VSync is toggled 190 | _mrelative += evt.relative 191 | _mspeed = evt.speed 192 | 193 | 194 | func _poll_input() -> InputData: 195 | assert(_is_local) 196 | 197 | _input_cache.last_sig += 1 198 | var retval: InputData = InputData.new(_input_cache.last_sig) 199 | 200 | if (_input_info.use_mouse_relative() && _local_input_enabled): 201 | retval.set_mouse_relative(_mrelative) 202 | # Must reset the cache otherwise motion will still be sent even if there is none 203 | _mrelative = Vector2() 204 | 205 | if (_input_info.use_mouse_speed() && _local_input_enabled): 206 | retval.set_mouse_speed(_mspeed) 207 | _mspeed = Vector2() 208 | 209 | # Gather the analog data 210 | for a in _input_info._analog_list: 211 | if (!_input_info._analog_list[a].custom && _local_input_enabled): 212 | retval.set_analog(a, Input.get_action_strength(a)) 213 | else: 214 | # Assume this analog data is "neutral". Doing this to ensure the data 215 | # is present on the returned object 216 | retval.set_analog(a, 0.0) 217 | 218 | for b in _input_info._bool_list: 219 | if (!_input_info._bool_list[b].custom && _local_input_enabled): 220 | retval.set_pressed(b, Input.is_action_pressed(b)) 221 | else: 222 | # Assume this custom boolean is not pressed. Doing this to ensure the 223 | # data is present on the returned object. 224 | retval.set_pressed(b, false) 225 | 226 | return retval 227 | 228 | 229 | func reset_data() -> void: 230 | _input_cache.sbuffer = {} 231 | _input_cache.cbuffer = [] 232 | _input_cache.last_sig = 0 233 | _input_cache.snapinpu = {} 234 | _input_cache.no_input_count = 0 235 | _input_cache.last_ack_snap = 0 236 | 237 | 238 | 239 | # The server uses this to know if the client is ready to receive snapshot data. 240 | func is_ready() -> bool: 241 | return _is_ready 242 | 243 | 244 | # This must be called only on client machines belonging to the local player. All of the cached 245 | # input data will be encoded and sent to the server. 246 | func _dispatch_input_data() -> void: 247 | # If this assert is failing then the function is being called on authority machine 248 | assert(get_tree().has_network_peer() && !get_tree().is_network_server()) 249 | assert(_is_local) 250 | 251 | # NOTE: Should this check amount of input data and do nothing if it's 0? 252 | 253 | # Prepare the encdecbuffer to encode input data 254 | _edec_input.buffer = PoolByteArray() 255 | 256 | # Encode buffer size - two bytes should give plenty of packet loss time 257 | _edec_input.write_ushort(_input_cache.cbuffer.size()) 258 | 259 | # Now encode each input object in the buffer 260 | for input in _input_cache.cbuffer: 261 | _input_info.encode_to(_edec_input, input) 262 | 263 | # Send the encoded data to the server - this should go directly to the 264 | # correct player node within the server 265 | rpc_unreliable_id(1, "server_receive_input", _edec_input.buffer) 266 | 267 | 268 | # Obtain input data. If running on the local machine the state will be polled. 269 | # If on a client (still local machine) then the data will be sent to the server. 270 | # If on server (but not local) the data will be retrieved from the cache/buffer. 271 | func get_input(snap_sig: int) -> InputData: 272 | # This will be used for a few tests within this function 273 | var is_authority: bool = !get_tree().has_network_peer() || get_tree().is_network_server() 274 | 275 | # This function should be called only by the authority or local machine 276 | assert(_is_local || is_authority) 277 | 278 | var retval: InputData 279 | if (_is_local): 280 | retval = _poll_input() 281 | 282 | if (!is_authority): 283 | # Local machine but on a client. This means, input data must be sent to the server 284 | # First, cache the new input object 285 | _input_cache.cbuffer.push_back(retval) 286 | 287 | # Input will be sent to the server when the snapshot is finished just so it give a 288 | # chance for custom input to be correctly set before dispatching 289 | 290 | else: 291 | # In theory if here it's authority machine as the assert above should 292 | # break if not local machine and not on authority. Asserts are removed 293 | # from release builds and that assert in this function is mostly to *try* 294 | # to catch errors early. Anyway, checking here again just to make sure 295 | if (!is_authority): 296 | return null 297 | 298 | # Running on the server but for a client. Must retrieve the data from 299 | # the input cache 300 | if (_input_cache.sbuffer.size() > 0): 301 | retval = _input_cache.sbuffer.get(_input_cache.last_sig + 1) 302 | if (retval): 303 | # There is a valid input object in the cache, so update the last 304 | # used signature 305 | _input_cache.last_sig += 1 306 | # The object that will be used is not needed within the cache anymore 307 | # so remove it 308 | # warning-ignore:return_value_discarded 309 | _input_cache.sbuffer.erase(_input_cache.last_sig) 310 | 311 | if (!retval): 312 | retval = _input_info.make_empty() 313 | 314 | # Later, given the snapshot obtain the input signature from the snapinpu dictionary 315 | _input_cache.associate(snap_sig, retval.signature) 316 | 317 | return retval 318 | 319 | 320 | 321 | # This should already be the correct player within the hierarchy node. 322 | remote func server_receive_input(encoded: PoolByteArray) -> void: 323 | assert(get_tree().is_network_server()) 324 | 325 | _edec_input.buffer = encoded 326 | # Decode the amount of input objects 327 | var count: int = _edec_input.read_ushort() 328 | 329 | # Decode each one of the objects 330 | for _i in count: 331 | var input: InputData = _input_info.decode_from(_edec_input) 332 | 333 | # Cache this if it's newer than the last input signature 334 | if (input.signature > _input_cache.last_sig): 335 | _input_cache.sbuffer[input.signature] = input 336 | 337 | 338 | # Retrieve the signature of the last input data used on this machine 339 | func get_last_input_signature() -> int: 340 | return _input_cache.last_sig 341 | 342 | # Given the snapshot signature, return the input signature that was used. This will 343 | # be "valid" only on servers on a node corresponding to a client 344 | func get_used_input_in_snap(sig: int) -> int: 345 | assert(get_tree().has_network_peer() && get_tree().is_network_server()) 346 | 347 | var ret: int = _input_cache.snapinpu.get(sig, 0) 348 | 349 | return ret 350 | 351 | 352 | # Returns the signature of the last acknowledged snapshot 353 | func get_last_acked_snap_sig() -> int: 354 | return _input_cache.last_ack_snap 355 | 356 | 357 | # Returns the amount of non acknowledged snapshots, which will be used by the 358 | # server to determine if full snapshot data must be sent or not 359 | func get_non_acked_snap_count() -> int: 360 | return _input_cache.snapinpu.size() 361 | 362 | # Tells if there is any non acknowledged snapshot that didn't use any input from the 363 | # client corresponding to this node. This is another condition that will be used to 364 | # determine which data will be sent to this client 365 | func has_snap_with_no_input() -> bool: 366 | return _input_cache.no_input_count > 0 367 | 368 | # This function is meant to be run on clients but not called remotely. 369 | # It removes from the cache all the input objects older and equal to the specified signature 370 | func client_acknowledge_input(sig: int) -> void: 371 | assert(get_tree().has_network_peer() && !get_tree().is_network_server()) 372 | 373 | while (_input_cache.cbuffer.size() > 0 && _input_cache.cbuffer.front().signature <= sig): 374 | _input_cache.cbuffer.pop_front() 375 | 376 | # Retrieve the list of cached input objects, which corresponds to non acknowledged input data. 377 | func get_cached_input_list() -> Array: 378 | return _input_cache.cbuffer 379 | 380 | 381 | # This function is meant to be run on servers but not called remotely. 382 | # Basically, when a client receives snapshot data, an answer must be given specifying 383 | # the signature of the newest received. With this, internal cleanup can be performed 384 | # and then later only the relevant data can be sent to the client 385 | func server_acknowledge_snapshot(sig: int) -> void: 386 | assert(get_tree().has_network_peer() && get_tree().is_network_server()) 387 | 388 | _input_cache.acknowledge(sig) 389 | 390 | 391 | ### Ping/Pong system 392 | func start_ping() -> void: 393 | _ping = NetPingInfo.new(net_id, self) 394 | 395 | # When the interval timer expires, a function will be called and that function will 396 | # remote call this, which is meant to be run only on client machines 397 | remote func _client_ping(sig: int, last_ping: float) -> void: 398 | # Answer back to the server 399 | rpc_unreliable_id(1, "_server_pong", sig) 400 | if (sig > 1): 401 | ping_signaler.call_func(net_id, last_ping) 402 | 403 | remote func _server_pong(sig: int) -> void: 404 | # Bail if not the server - this should be an error though 405 | if (!get_tree().is_network_server()): 406 | return 407 | 408 | # The RPC call "arrives" at the node corresponding to the player that "called" it. 409 | # This means that "net_id" holds the correct network ID of the client. 410 | 411 | var measured: float = _ping.calculate_and_restart(sig) 412 | if (measured >= 0.0): 413 | if (_broadcast_ping): 414 | for pid in get_tree().get_network_connected_peers(): 415 | if (pid != net_id): 416 | rpc_unreliable_id(pid, "_client_ping_broadcast", measured) 417 | 418 | # The server must get a signal with the measured value 419 | ping_signaler.call_func(net_id, measured) 420 | 421 | # If the broadcast ping option is enabled then the server will call this function on 422 | # each client in order to give the measured ping value and allow other clients to 423 | # display somewhere the player's latency values 424 | remote func _client_ping_broadcast(value: float) -> void: 425 | # When this is called it will run on the player node corresponding to the correct 426 | # player, meaning that the "net_id" property holds the correct player's network ID 427 | ping_signaler.call_func(net_id, value) 428 | 429 | 430 | ### Custom property system - This relies on the variant feature, specially when dealing 431 | ### with the property values. So, no static typing for those 432 | func _add_custom_property(pname: String, prop: NetCustomProperty) -> void: 433 | _custom_data[pname] = NetCustomProperty.new(prop.value, prop.replicate) 434 | 435 | func has_dirty_custom_prop() -> bool: 436 | return _custom_prop_dirty_count > 0 437 | 438 | # Encode the "dirty" supported custom properties into the given EncDecBuffer. If a non supported property is 439 | # found then it will be directly sent through the "_check_replication()" function. 440 | # the prop_ref argument here is the the dicitonary that holds the list of properties with their initial values, 441 | # which are then used to determine the expected value type. 442 | # Retruns true if at least one of the "dirty properties" is supported by the EncDecBuffer. 443 | func _encode_custom_props(edec: EncDecBuffer, prop_ref: Dictionary) -> bool: 444 | if (!has_dirty_custom_prop()): 445 | return false 446 | 447 | var is_authority: bool = (!get_tree().has_network_peer() || get_tree().is_network_server()) 448 | 449 | edec.buffer = PoolByteArray() 450 | edec.write_uint(net_id) 451 | 452 | # Yes, this limits to 255 custom properties that can be encoded into a single packet. 453 | # This should be way more than enough! Regardless, the writing loop will end at 255 encoded properties. 454 | # On the next loop iteration the remaining dirty properties will be encoded. 455 | edec.write_byte(0) 456 | var encoded_props: int = 0 457 | 458 | for pname in _custom_data: 459 | var prop: NetCustomProperty = _custom_data[pname] 460 | if (prop.replicate == NetCustomProperty.ReplicationMode.ServerOnly && is_authority): 461 | # This property is meant to be "server only" and code is running on the server. Ensure the prop is 462 | # not dirty and don't encode it. 463 | prop.dirty = false 464 | continue 465 | 466 | # In here it doesn't matter if the code is running on srever or client. The property has to be checked 467 | # and if dirty it must be sent through network regardless. 468 | if (prop.encode_to(edec, pname, typeof(prop_ref[pname].value))): 469 | # The property was encoded - that means, it is of supported type AND is dirty. Update the encoded 470 | # counter. 471 | encoded_props += 1 472 | elif (prop.dirty): 473 | # This property is dirty but it couldn't be encoded. Most likely because it's is not supported by the 474 | # EncDecBuffer. Because of that, individually send this property 475 | _check_replication(pname, prop) 476 | 477 | if (encoded_props == 255): 478 | # Do not allow encoding go past 255 properties. Yet, with this system any property that still need 479 | # to be synchronized will be dispatched at a later moment 480 | break 481 | 482 | if (encoded_props > 0): 483 | # Rewrite the custom property header which is basically the number of encoded properties. 484 | edec.rewrite_byte(encoded_props, 4) 485 | 486 | return encoded_props > 0 487 | 488 | 489 | func _decode_custom_props(from: EncDecBuffer, prop_ref: Dictionary, isauthority: bool) -> void: 490 | var ecount: int = from.read_byte() 491 | 492 | for _i in ecount: 493 | var pname: String = from.read_string() 494 | var pref: NetCustomProperty = prop_ref.get(pname, null) 495 | if (!pref): 496 | return 497 | 498 | var prop: NetCustomProperty = _custom_data[pname] 499 | if (!prop.decode_from(from, typeof(pref.value), isauthority)): 500 | return 501 | else: 502 | # Allow the "core" of the networking system to emit a signal indicating that a custom property has 503 | # been changed through synchronization 504 | custom_prop_signaler.call_func(net_id, pname, prop.value) 505 | 506 | if (prop.dirty): 507 | _custom_prop_dirty_count += 1 508 | 509 | 510 | 511 | func _check_replication(pname: String, prop: NetCustomProperty) -> void: 512 | var is_authority: bool = (!get_tree().has_network_peer() || get_tree().is_network_server()) 513 | match prop.replicate: 514 | NetCustomProperty.ReplicationMode.ServerOnly: 515 | # This property is meant to be given only to the server so if already there nothing 516 | # to be done. Otherwise, directly send the property to the server, which will be 517 | # automaticaly given to the correct node 518 | if (!is_authority): 519 | rpc_id(1, "_rem_set_custom_property", pname, prop.value) 520 | 521 | NetCustomProperty.ReplicationMode.ServerBroadcast: 522 | # This property must be broadcast through the server. So if running there directly 523 | # use the rpc() function, othewise use the FuncRef to request the server to do the 524 | # broadcasting 525 | if (is_authority): 526 | rpc("_rem_set_custom_property", pname, prop.value) 527 | 528 | else: 529 | custom_prop_broadcast_requester.call_func(pname, prop.value) 530 | 531 | prop.dirty = false 532 | 533 | 534 | 535 | func set_custom_property(pname: String, value) -> void: 536 | assert(_custom_data.has(pname)) 537 | 538 | var prop: NetCustomProperty = _custom_data[pname] 539 | # This line automatically marks the property as "dirty" if necessary. Dirty properties will be synchronized 540 | # at a later moment. 541 | prop.value = value 542 | if (prop.dirty): 543 | _custom_prop_dirty_count += 1 544 | 545 | 546 | 547 | # This is used to retrieve the value of a custom property 548 | func get_custom_property(pname: String, defval = null): 549 | var prop: NetCustomProperty = _custom_data.get(pname, null) 550 | return prop.value if prop else defval 551 | 552 | 553 | 554 | # This function is meant to be called by (and in) the server in order to send all properties 555 | # set to "ServerBroadcast" to the specified player 556 | func sync_custom_with(pid: int) -> void: 557 | if (!get_tree().is_network_server()): 558 | return 559 | 560 | for pname in _custom_data: 561 | var prop: NetCustomProperty = _custom_data[pname] 562 | 563 | if (prop.replicate == NetCustomProperty.ReplicationMode.ServerBroadcast): 564 | rpc_id(pid, "_rem_set_custom_property", pname, prop.value) 565 | 566 | 567 | # This is meant to set the custom property but by using remote calls. This should be 568 | # automatically called based on the replication setting. One thing to note is that 569 | # this will be called using the reliable channel 570 | remote func _rem_set_custom_property(pname: String, val) -> void: 571 | assert(_custom_data.has(pname)) 572 | _custom_data[pname].value = val 573 | 574 | # This allows the "core" of the networking system to emit a signal indicating that 575 | # a custom property has been changed through remote call 576 | custom_prop_signaler.call_func(net_id, pname, val) 577 | 578 | 579 | ### Setters/Getters 580 | func set_network_id(id: int) -> void: 581 | net_id = id 582 | set_name("player_%d" % net_id) 583 | 584 | func set_ready(r: bool) -> void: 585 | _is_ready = r 586 | 587 | -------------------------------------------------------------------------------- /addons/keh_network/plugin.cfg: -------------------------------------------------------------------------------- 1 | [plugin] 2 | 3 | name="Networking Wrapper" 4 | description="A wrapper around the high level networking API, which sort of automates game synchronization through snapshot interpolation. 5 | This plugin requires: 6 | - keh_general/data/encdecbuffer.gd" 7 | author="Yuri Sarudiansky" 8 | version="1.0" 9 | script="pluginloader.gd" 10 | -------------------------------------------------------------------------------- /addons/keh_network/pluginloader.gd: -------------------------------------------------------------------------------- 1 | ############################################################################### 2 | # Copyright (c) 2019 Yuri Sarudiansky 3 | # 4 | # Permission is hereby granted, free of charge, to any person obtaining a copy 5 | # of this software and associated documentation files (the "Software"), to deal 6 | # in the Software without restriction, including without limitation the rights 7 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | # copies of the Software, and to permit persons to whom the Software is 9 | # furnished to do so, subject to the following conditions: 10 | # 11 | # The above copyright notice and this permission notice shall be included in 12 | # all copies or substantial portions of the Software. 13 | # 14 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 20 | # THE SOFTWARE. 21 | ############################################################################### 22 | 23 | tool 24 | extends EditorPlugin 25 | 26 | const base_path: String = "keh_addons/network/" 27 | 28 | var _extra_settings: Array = [] 29 | 30 | 31 | # This will be called by the engine whenever the plugin is activated 32 | func enable_plugin() -> void: 33 | # Automatically add the network class as a singleton (autoload) 34 | add_autoload_singleton("network", "res://addons/keh_network/network.gd") 35 | 36 | # This will be called by the engine whenever the plugin is deactivated 37 | func disable_plugin() -> void: 38 | # Remove the network class from the singleton (autoload) list. But must check if it exists 39 | # AutoLoad scripts are added into the ProjectSettings under "autoload/[singleton_name]" path 40 | if (ProjectSettings.has_setting("autoload/network")): 41 | remove_autoload_singleton("network") 42 | 43 | 44 | func _enter_tree(): 45 | # Add project settings if they are not present 46 | var compr: Dictionary = { 47 | "hint": PROPERTY_HINT_ENUM, 48 | "hint_string": "None, RangeCoder, FastLZ, ZLib, ZSTD" 49 | } 50 | 51 | _reg_setting("compression", TYPE_INT, NetworkedMultiplayerENet.COMPRESS_RANGE_CODER, compr) 52 | _reg_setting("use_websocket", TYPE_BOOL, false) 53 | _reg_setting("max_snapshot_history", TYPE_INT, 120) 54 | _reg_setting("max_client_snapshot_history", TYPE_INT, 60) 55 | _reg_setting("full_snapshot_threshold", TYPE_INT, 12) 56 | _reg_setting("broadcast_measured_ping", TYPE_BOOL, true) 57 | _reg_setting("use_input_mouse_relative", TYPE_BOOL, false) 58 | _reg_setting("use_input_mouse_speed", TYPE_BOOL, false) 59 | _reg_setting("quantize_analog_input", TYPE_BOOL, false) 60 | _reg_setting("print_debug_info", TYPE_BOOL, false) 61 | 62 | 63 | 64 | func _exit_tree(): 65 | # Remove the additional project settings - those will remain on the ProjectSettings window until 66 | # the editor is restarted 67 | for es in _extra_settings: 68 | ProjectSettings.clear(es) 69 | 70 | _extra_settings.clear() 71 | 72 | 73 | # def_val is relying on the variant, thus no static typing 74 | func _reg_setting(sname: String, type: int, def_val, info: Dictionary = {}) -> void: 75 | var fpath: String = base_path + sname 76 | if (!ProjectSettings.has_setting(fpath)): 77 | ProjectSettings.set(fpath, def_val) 78 | 79 | _extra_settings.append(fpath) 80 | 81 | # Those must be done regardless if the setting existed before or not, otherwise the ProjectSettings window 82 | # will not work correctly (yeah, the default value as well as the hints must be provided) 83 | ProjectSettings.set_initial_value(fpath, def_val) 84 | 85 | var propinfo: Dictionary = { 86 | "name": fpath, 87 | "type": type 88 | } 89 | if (info.has("hint")): 90 | propinfo["hint"] = info.hint 91 | if (info.has("hint_string")): 92 | propinfo["hint_string"] = info.hint_string 93 | 94 | ProjectSettings.add_property_info(propinfo) 95 | 96 | -------------------------------------------------------------------------------- /addons/keh_network/snapentity.gd: -------------------------------------------------------------------------------- 1 | ############################################################################### 2 | # Copyright (c) 2019 Yuri Sarudiansky 3 | # 4 | # Permission is hereby granted, free of charge, to any person obtaining a copy 5 | # of this software and associated documentation files (the "Software"), to deal 6 | # in the Software without restriction, including without limitation the rights 7 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | # copies of the Software, and to permit persons to whom the Software is 9 | # furnished to do so, subject to the following conditions: 10 | # 11 | # The above copyright notice and this permission notice shall be included in 12 | # all copies or substantial portions of the Software. 13 | # 14 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 20 | # THE SOFTWARE. 21 | ############################################################################### 22 | 23 | # Objects derived from this class are meant to represent the state of the 24 | # game nodes within the high level snapshots. Basically, properties within 25 | # those classes are meant to hold the data that will be replicated through 26 | # the network system. Note that not all property types are supported, only 27 | # the following ones: 28 | # - bool 29 | # - int 30 | # - float 31 | # - Vector2 32 | # - Rect2 33 | # - Quat 34 | # - Color 35 | # - Vector3 36 | # Another thing to keep in mind is the fact that derived classes *must* 37 | # implement the "apply_state(node)" function, which is basically the may 38 | # way the replication system will take snapshot state and apply into the 39 | # game nodes. 40 | # At the end of this file there is a template that can be copied and 41 | # pasted into new files to create new entity types. In the demo project 42 | # there are also some examples of classes that can be used. 43 | 44 | extends Reference 45 | class_name SnapEntityBase 46 | 47 | const CTYPE_UINT: int = 65538 48 | const CTYPE_BYTE: int = 131074 49 | const CTYPE_USHORT: int = 196610 50 | 51 | 52 | # A unique ID is necessary in order to correctly find the node within the game 53 | # world that is associated with this entity data. 54 | # Note that the uniqueness is a requirement within the entity type and not necessarily 55 | # across the entire game session. 56 | var id: int 57 | 58 | # In order to properly spawn game objects the packed scene is necessary information and 59 | # some times the correct one must be replicated. Instead of sending a string (which 60 | # is not supported by the automatic replication system), a "name" is hashed and that 61 | # value is replicated (which adds another 4 bytes). This name is the "category" when 62 | # registering the spawners within the snapshot data object. Still, sometimes this value 63 | # is not necessary and it can be completely ignored if, at the _init() of the derived 64 | # class, a call to set_meta("class_hash", 0) is added. 65 | # Yes, this means you can't create a field named no_class_hash and set it to be unsigned. 66 | var class_hash: int 67 | 68 | 69 | 70 | func _init(uid: int, chash: int) -> void: 71 | id = uid 72 | class_hash = chash 73 | 74 | # Both id and class_hash are meant to be replicated (encoded/decoded) as unsigned integers 75 | # of 32 bits. So, set both of them to be used as such by just creating two meta values 76 | # with the property names and the "EntityDescription.CTYPE_UINT" 77 | set_meta("id", CTYPE_UINT) 78 | set_meta("class_hash", CTYPE_UINT) 79 | 80 | 81 | 82 | ############################################################################ 83 | ### Copy the code bellow into a derived entity type to have a "template" ### 84 | ############################################################################ 85 | 86 | #extends SnapEntityBase 87 | #class_name TheEntityTypeNameClass 88 | # 89 | #func _init(uid: int, chash: int).(uid, chash) -> void: 90 | # pass # Initialize the entity data 91 | # 92 | #func apply_state(to_node: Node) -> void: 93 | # pass # Apply the properties of this entity into the node 94 | 95 | -------------------------------------------------------------------------------- /addons/keh_network/snapshot.gd: -------------------------------------------------------------------------------- 1 | ############################################################################### 2 | # Copyright (c) 2019 Yuri Sarudiansky 3 | # 4 | # Permission is hereby granted, free of charge, to any person obtaining a copy 5 | # of this software and associated documentation files (the "Software"), to deal 6 | # in the Software without restriction, including without limitation the rights 7 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | # copies of the Software, and to permit persons to whom the Software is 9 | # furnished to do so, subject to the following conditions: 10 | # 11 | # The above copyright notice and this permission notice shall be included in 12 | # all copies or substantial portions of the Software. 13 | # 14 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 20 | # THE SOFTWARE. 21 | ############################################################################### 22 | 23 | # This is the high level snapshot representation. Normally there 24 | # is no need to directly create or even access instances of this 25 | # class, since the network system provides a lot of interface to 26 | # automatically deal with that. 27 | 28 | 29 | extends Reference 30 | class_name NetSnapshot 31 | 32 | # Signature can also be called frame or even timestamp. In any case, this is 33 | # just an incrementing number to help identify the snapshots 34 | var signature: int 35 | 36 | # Signature of the input data used when generating this snapshot. On the 37 | # authority machine this may not make much difference but is used on clients 38 | # to compare with incoming snapshot data 39 | var input_sig: int 40 | 41 | # Key = (snap) entity hashed name | Value = Dictionary 42 | # Inner Dictionary, Key = entity unique ID | Value = instance of SnapEntityBase 43 | var _entity_data: Dictionary 44 | 45 | 46 | func _init(sig: int) -> void: 47 | signature = sig 48 | input_sig = 0 49 | _entity_data = {} 50 | 51 | func add_type(nhash: int) -> void: 52 | _entity_data[nhash] = {} 53 | 54 | func get_entity_count(nhash: int) -> int: 55 | assert(_entity_data.has(nhash)) 56 | return _entity_data[nhash].size() 57 | 58 | func add_entity(nhash: int, entity: SnapEntityBase) -> void: 59 | assert(_entity_data.has(nhash)) 60 | _entity_data[nhash][entity.id] = entity 61 | 62 | func remove_entity(nhash: int, uid: int) -> void: 63 | assert(_entity_data.has(nhash)) 64 | if (_entity_data[nhash].has(uid)): 65 | _entity_data[nhash].erase(uid) 66 | 67 | func get_entity(nhash: int, uid: int) -> SnapEntityBase: 68 | assert(_entity_data.has(nhash)) 69 | return _entity_data[nhash].get(uid) 70 | 71 | 72 | func build_tracker() -> Dictionary: 73 | var ret: Dictionary = {} 74 | 75 | for ehash in _entity_data: 76 | ret[ehash] = _entity_data[ehash].keys() 77 | 78 | return ret 79 | 80 | -------------------------------------------------------------------------------- /addons/keh_network/snapshotdata.gd: -------------------------------------------------------------------------------- 1 | ############################################################################### 2 | # Copyright (c) 2019 Yuri Sarudiansky 3 | # 4 | # Permission is hereby granted, free of charge, to any person obtaining a copy 5 | # of this software and associated documentation files (the "Software"), to deal 6 | # in the Software without restriction, including without limitation the rights 7 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | # copies of the Software, and to permit persons to whom the Software is 9 | # furnished to do so, subject to the following conditions: 10 | # 11 | # The above copyright notice and this permission notice shall be included in 12 | # all copies or substantial portions of the Software. 13 | # 14 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 20 | # THE SOFTWARE. 21 | ############################################################################### 22 | 23 | # Only a single instance of this class is necessary and is automatically 24 | # done within the network singleton. 25 | # This class is meant to manage the instances of the high level snapshot 26 | # objects. 27 | 28 | extends Reference 29 | class_name NetSnapshotData 30 | 31 | 32 | # Each entry in this dictionary is an instance of the EntityInfo, keyed by the 33 | # hashed name of the entity class derived from SnapEntityBase (snapentity.gd) 34 | var _entity_info: Dictionary = {} 35 | 36 | # This dictionary is a helper container that takes the resource name (normally 37 | # directly the class_name) and points to the hashed name, which will then help 38 | # get into the desired entry within _entity_info 39 | var _entity_name: Dictionary = {} 40 | 41 | # Holds the history of snapshots 42 | var _history: Array = [] 43 | 44 | # This object will hold the most recent snapshot data received from the server. 45 | # When delta snapshot is received, a reference must be used in order to rebuild 46 | # the full snapshot, which will be exactly the contents of this object. 47 | var _server_state: NetSnapshot = null 48 | 49 | 50 | 51 | func _init() -> void: 52 | register_entity_types() 53 | 54 | 55 | # This function gather the list of available (script) classes and for each entry 56 | # corresponding to a class derived from SnapEntityBase will then be further analyzed. 57 | # In that case, provided it contains any valid replicable property and implements 58 | # the necessary methods, it will be automatically registered within the valid 59 | # classes that can be represented within the snapshots. 60 | func register_entity_types() -> void: 61 | # Each entry in the obtained array is a dictionary containing the following fields: 62 | # base, class, language, path 63 | var clist: Array = ProjectSettings.get_setting("_global_script_classes") if ProjectSettings.has_setting("_global_script_classes") else [] 64 | 65 | var pdebug: bool = ProjectSettings.get_setting("keh_addons/network/print_debug_info") if ProjectSettings.has_setting("keh_addons/network/print_debug_info") else false 66 | 67 | for c in clist: 68 | # Only interested in classes derived from SnapEntityBase 69 | if (c.base == "SnapEntityBase"): 70 | var edata: EntityInfo = EntityInfo.new(c.class, c.path) 71 | 72 | if (edata.error.length() > 0): 73 | var msg: String = "Skipping registration of class %s (%d). Reason: %s" 74 | push_warning(msg % [c.class, edata._name_hash, edata.error]) 75 | 76 | else: 77 | if (pdebug): 78 | print_debug("Registering snapshot object type ", c.class, " with hash ", edata._name_hash) 79 | 80 | # This is the actual registration of the object type 81 | _entity_info[edata._name_hash] = edata 82 | # And this will help obtain the necessary data given the class' resource 83 | _entity_name[edata._resource] = { 84 | "name": c.class, 85 | "hash": edata._name_hash, 86 | } 87 | 88 | 89 | 90 | # Spawners are the main mean to create game nodes in association with the various 91 | # classes derived from SnapEntityBase 92 | func register_spawner(eclass: Resource, chash: int, spawner: NetNodeSpawner, parent: Node, esetup: FuncRef = null) -> void: 93 | # Using this assert to ensure that if a function reference for extra setup is given, it's at 94 | # least valid. Using assert because it becomes removed from release builds 95 | assert(!esetup || esetup.is_valid()) 96 | 97 | var ename: Dictionary = _entity_name.get(eclass) 98 | if (!ename): 99 | var e: String = "Trying to register spawner associated with snapshot entity class defined in %s, which is not registered." 100 | push_error(e % eclass.resource_path) 101 | return 102 | 103 | var einfo: EntityInfo = _entity_info.get(ename.hash) 104 | einfo.register_spawner(chash, spawner, parent, esetup) 105 | 106 | 107 | # Spawn a game node using a registered spawner 108 | func spawn_node(eclass: Resource, uid: int, chash: int) -> Node: 109 | var ret: Node = null 110 | 111 | var ename: Dictionary = _entity_name.get(eclass) 112 | if (ename): 113 | var einfo: EntityInfo = _entity_info.get(ename.hash) 114 | ret = einfo.spawn_node(uid, chash) 115 | 116 | return ret 117 | 118 | # Internally used to retrieve the EntityInfo instance associated with specified snapshot entity class 119 | func _get_entity_info(snapres: Resource) -> EntityInfo: 120 | var ename: Dictionary = _entity_name.get(snapres) 121 | if (ename): 122 | return _entity_info.get(ename.hash) 123 | 124 | return null 125 | 126 | # Retrieve a game node given its unique ID and associated snapshot entity class 127 | func get_game_node(uid: int, snapres: Resource) -> Node: 128 | var ret: Node = null 129 | var einfo: EntityInfo = _get_entity_info(snapres) 130 | if (einfo): 131 | ret = einfo.get_game_node(uid) 132 | 133 | return ret 134 | 135 | 136 | # Retrieve the prediction count for the specified entity 137 | func get_prediction_count(uid: int, snapres: Resource) -> int: 138 | var ret: int = 0 139 | var einfo: EntityInfo = _get_entity_info(snapres) 140 | if (einfo): 141 | ret = einfo.get_pred_count(uid) 142 | 143 | return ret 144 | 145 | 146 | 147 | # Despawn a node from the game 148 | func despawn_node(eclass: Resource, uid: int) -> void: 149 | var ename: Dictionary = _entity_name.get(eclass) 150 | if (ename): 151 | var einfo: EntityInfo = _entity_info.get(ename.hash) 152 | einfo.despawn_node(uid) 153 | 154 | # Adds a "pre-spawned" node into the internal node management so it can be 155 | # properly handled (located) by the replication system. 156 | func add_pre_spawned_node(eclass: Resource, uid: int, node: Node) -> void: 157 | var ename: Dictionary = _entity_name.get(eclass) 158 | if (ename): 159 | var einfo: EntityInfo = _entity_info.get(ename.hash) 160 | einfo.add_pre_spawned(uid, node) 161 | 162 | 163 | # Locate the snapshot given its signature and return it, null if not found 164 | func get_snapshot(signature: int) -> NetSnapshot: 165 | for s in _history: 166 | if (s.signature == signature): 167 | return s 168 | 169 | return null 170 | 171 | func get_snapshot_by_input(isig: int) -> NetSnapshot: 172 | for s in _history: 173 | if (s.input_sig == isig): 174 | return s 175 | 176 | return null 177 | 178 | 179 | func reset() -> void: 180 | for ehash in _entity_info: 181 | _entity_info[ehash].clear_nodes() 182 | 183 | _server_state = null 184 | _history.clear() 185 | 186 | 187 | # Encode the provided snapshot into the given EncDecBuffer, "attaching" the given 188 | # input signature as part of the data. This function encodes the entire snapshot 189 | func encode_full(snap: NetSnapshot, into: EncDecBuffer, isig: int) -> void: 190 | # Encode signature of the snapshot 191 | into.write_uint(snap.signature) 192 | 193 | # Encode the input signature 194 | into.write_uint(isig) 195 | 196 | # Iterate through the valid entity types 197 | for ehash in _entity_info: 198 | # First bandwidth optimization -> don't encode entity type + quantity 199 | # if the quantity is 0. 200 | var ecount: int = snap.get_entity_count(ehash) 201 | if (ecount == 0): 202 | continue 203 | 204 | # There is at least one entity to be encoded, so obtain the description 205 | # object to help with the entities 206 | var einfo: EntityInfo = _entity_info[ehash] 207 | 208 | # Encode the entity hash ID 209 | into.write_uint(ehash) 210 | # Encode the amount of entities of this type 211 | into.write_uint(ecount) 212 | 213 | # Encode the entities of this type 214 | for uid in snap._entity_data[ehash]: 215 | # Get the entity in order to encode the properties 216 | var entity: SnapEntityBase = snap.get_entity(ehash, uid) 217 | 218 | einfo.encode_full_entity(entity, into) 219 | 220 | 221 | # Decode the snapshot data from the provided EncDecBuffer, returning an instance of 222 | # NetSnapshot. 223 | func decode_full(from: EncDecBuffer) -> NetSnapshot: 224 | # Decode the signature of the snapshot 225 | var sig: int = from.read_uint() 226 | # Decode the input signature 227 | var isig: int = from.read_uint() 228 | 229 | # This function is called only on clients, where verified snapshots are removed 230 | # from its history. This means that if this snapshot is older than the first 231 | # in the container then it can be discarded. However the matching system here 232 | # uses the input signature and not the snapshot signature (which is used to 233 | # acknowledge data to the server). 234 | if (isig > 0 && _history.size() > 0 && isig < _history.front().input_sig): 235 | return null 236 | 237 | var retval: NetSnapshot = NetSnapshot.new(sig) 238 | 239 | # "Attach" input signature into the snapshot 240 | retval.input_sig = isig 241 | 242 | # The snapshot checking algorithm requires that each entity type has its 243 | # entry within the snapshot data, so add them 244 | for ehash in _entity_info: 245 | retval.add_type(ehash) 246 | 247 | 248 | # Decode the entities 249 | while from.has_read_data(): 250 | # Read the entity type ID 251 | var ehash: int = from.read_uint() 252 | 253 | var einfo: EntityInfo = _entity_info.get(ehash) 254 | 255 | if (!einfo): 256 | var e: String = "While decoding full snapshot data, got an entity type hash %d which doesn't map to any valid registered entity type." 257 | push_error(e % ehash) 258 | return null 259 | 260 | # Take number of entities of this type 261 | var count: int = from.read_uint() 262 | 263 | for _i in count: 264 | var entity: SnapEntityBase = einfo.decode_full_entity(from) 265 | if (entity): 266 | retval.add_entity(ehash, entity) 267 | 268 | return retval 269 | 270 | 271 | func encode_delta(snap: NetSnapshot, oldsnap: NetSnapshot, into: EncDecBuffer, isig: int) -> void: 272 | # Scan oldsnap comparing to snap. Encode only the changes. Removed entities must 273 | # be explicitly encoded with a "change mask = 0" 274 | # Scanning will iterate through entities in the snap object. The same entity will 275 | # be retrieved from oldsnap. If not found, then assume iterated entity is new. 276 | # A list must be used to keep track of entities that are in the older snapshot but 277 | # not on the newer one, indicating removed game object. 278 | 279 | # Encode snapshot signature 280 | into.write_uint(snap.signature) 281 | 282 | # Encode input signature 283 | into.write_uint(isig) 284 | 285 | # Encode a flag indicating if there is any change at all in this snapshot - assume there isn't 286 | into.write_bool(false) 287 | # But not for the actual flag here. It's easier to change this to true 288 | var has_data: bool = false 289 | 290 | # During entity scanning, entries from this container will be removed. After the loop, 291 | # any remaining entries are entities removed from the game 292 | var tracker: Dictionary = oldsnap.build_tracker() 293 | 294 | # Iterate through the valid entity types 295 | for ehash in _entity_info: 296 | # Get entity count in the new snapshot 297 | var necount: int = snap.get_entity_count(ehash) 298 | # Get entity count in the old snapshot 299 | var oecount: int = oldsnap.get_entity_count(ehash) 300 | 301 | # Don't encode entity type + quantity if both are 0 302 | if (necount == 0 && oecount == 0): 303 | continue 304 | 305 | # At least one of the snapshots contains entities of this type. Assume every 306 | # single entity has been changed 307 | var ccount: int = necount 308 | 309 | var einfo: EntityInfo = _entity_info[ehash] 310 | 311 | 312 | # NOTE: Postponing encoding of typehash plus change count to a moment where it is 313 | # sure there is at least one changed entity of this type. Originally trie to do 314 | # things normally and remove the relevant bytes from the buffer array but it didn't 315 | # work very well. 316 | # This flag is used to tell if the typehash plus change count has been encoded or 317 | # not, just to prevent multiple encodings of this data 318 | var written_type_header: bool = false 319 | 320 | # Get the writing position of the entity count as most likely it will be updated 321 | var countpos: int = into.get_current_size() + 4 322 | 323 | # Check the entities 324 | for uid in snap._entity_data[ehash]: 325 | # Retrive old state of this entity - obviously if it exists (if not this will be null) 326 | var eold: SnapEntityBase = oldsnap.get_entity(ehash, uid) 327 | # Retrieve new state of this entity - it should exist as the iteration is based on the 328 | # new snapshot. 329 | var enew: SnapEntityBase = snap.get_entity(ehash, uid) 330 | 331 | # Assume the entity is new 332 | var cmask: int = einfo.get_full_change_mask() 333 | 334 | if (eold && enew): 335 | # Ok, entity exist on both snapshots so it's not new. Calculate the "real" change mask 336 | cmask = einfo.calculate_change_mask(eold, enew) 337 | # Remove this from the tracker so it isn't considered as a removed entity 338 | tracker[ehash].erase(uid) 339 | 340 | 341 | if (cmask != 0): 342 | if (!written_type_header): 343 | # Write the entity type has ID. 344 | into.write_uint(ehash) 345 | # And the change counter 346 | into.write_uint(ccount) 347 | # Prevent rewriting the information 348 | written_type_header = true 349 | 350 | # This entity requires encoding 351 | einfo.encode_delta_entity(uid, enew, cmask, into) 352 | has_data = true 353 | 354 | else: 355 | # The entity was not changed. Update the change counter 356 | ccount -= 1 357 | 358 | # Check the tracker for entities that are in the old snapshot but not in the new one. 359 | # In other words, entities that were removed from the game world 360 | # Those must be encoded with a change mask set to 0, which will indicate remove entities 361 | # when being decoded. 362 | for uid in tracker[ehash]: 363 | if (!written_type_header): 364 | into.write_uint(ehash) 365 | into.write_uint(ccount) 366 | written_type_header = true 367 | 368 | einfo.encode_delta_entity(uid, null, 0, into) 369 | ccount += 1 370 | 371 | if (ccount > 0): 372 | into.rewrite_uint(ccount, countpos) 373 | 374 | # Everything iterated through. Check if there is anything at all 375 | if (has_data): 376 | into.rewrite_bool(true, 8) 377 | 378 | 379 | # In here the "old snapshot" is not needed because it is basically a property in this 380 | # class (_server_state) 381 | func decode_delta(from: EncDecBuffer) -> NetSnapshot: 382 | # Decode snapshot signature 383 | var snapsig: int = from.read_uint() 384 | # Decode input signature 385 | var isig: int = from.read_uint() 386 | 387 | # This check is explained in the decode_full() function 388 | if (isig > 0 && _history.size() > 0 && isig < _history.front().input_sig): 389 | return null 390 | 391 | var retval: NetSnapshot = NetSnapshot.new(snapsig) 392 | 393 | retval.input_sig = isig 394 | 395 | # The snapshot checking algorithm requires that each entity type has its 396 | # entry within the snapshot data, so add them 397 | for ehash in _entity_info: 398 | retval.add_type(ehash) 399 | 400 | # Check if the flag indicating if there is any data at all 401 | var has_data: bool = from.read_bool() 402 | 403 | # This will be used to track unchaged entities. Basically, when an entity is decoded, 404 | # the corresponding entry will be removed from this data. After that, remaining entries 405 | # here are indicating entities that didn't change and must be copied into the new snapshot 406 | var tracker: Dictionary = _server_state.build_tracker() 407 | 408 | if (has_data): 409 | # Decode the entities 410 | while from.has_read_data(): 411 | # Read entity type ID 412 | var ehash: int = from.read_uint() 413 | var einfo: EntityInfo = _entity_info.get(ehash) 414 | 415 | if (!einfo): 416 | var e: String = "While decoding delta snapshot data, got an entity type hash %d which doesn't map to any valid registered entity type." 417 | push_error(e % ehash) 418 | return null 419 | 420 | # Take number of encoded entities of this type 421 | var count: int = from.read_uint() 422 | 423 | # Decode them 424 | for _i in count: 425 | var edata: Dictionary = einfo.decode_delta_entity(from) 426 | 427 | var oldent: SnapEntityBase = _server_state.get_entity(ehash, edata.entity.id) 428 | 429 | if (oldent): 430 | # The entity exists in the old state. Check if it's not marked for removal 431 | if (edata.cmask > 0): 432 | # It isn't. So, "match" the delta to make the data correct (that is, take unchanged) 433 | # data from the old state and apply into the new one. 434 | einfo.match_delta(edata.entity, oldent, edata.cmask) 435 | # Add the changed entity into the return value 436 | retval.add_entity(ehash, edata.entity) 437 | 438 | # This is a changed entity, so remove it from the tracker 439 | tracker[ehash].erase(edata.entity.id) 440 | 441 | else: 442 | # Entity is not in the old state. Add the decoded data into the return value in the 443 | # hopes it is holding the entire correct data (this can be checked by comparing the cmask though) 444 | # Change mask can be 0 in this case, when the acknowledgement still didn't arrive theren when 445 | # server dispatched a new data set. 446 | if (edata.cmask > 0): 447 | retval.add_entity(ehash, edata.entity) 448 | 449 | # Check the tracker now 450 | for ehash in tracker: 451 | var einfo: EntityInfo = _entity_info.get(ehash) 452 | for uid in tracker[ehash]: 453 | var entity: SnapEntityBase = _server_state.get_entity(ehash, uid) 454 | retval.add_entity(ehash, einfo.clone_entity(entity)) 455 | 456 | return retval 457 | 458 | 459 | 460 | 461 | # This function is meant to be run on clients but not called remotely. The objective here 462 | # is to take the provided snapshot, which contains server's data, locate the internal 463 | # corresponding snapshot and compare them. Any differences are to be considered as errors 464 | # in the client's prediction and must be corrected using the server's data. 465 | # Corresponding snapshot means, primarily, the snapshot with the same input signatures. 466 | # However, it's possible the server will send snapshot without any client's input. This will 467 | # always be the case at the very beginning of the client's session, when there is no 468 | # server data to initiate the local simulation. Still, if there is enough data loss 469 | # during the synchronization then the sever will have to send snapshot data without any 470 | # client's input. 471 | # That said, the overall tasks here: 472 | # - Snapshot contains input -> locate the snapshot in the history with the same input 473 | # signature and use that one to compare, removing any older (or equal) from the history. 474 | # - Snapshot does not contain input -> take the last snapshot in the history to use for 475 | # comparison and don't remove anything from the history. 476 | # During the comparison, any difference must be corrected by applying the server state into 477 | # all snapshots in the local history. 478 | # On errors the ideal path is to locally re-simulate the game using cached input data 479 | # just so no input is missed on small errors. Since this is not possible with Godot then 480 | # just apply the corrected state into the corresponding nodes and hope the interpolation 481 | # will make things look "less glitchy" 482 | func client_check_snapshot(snap: NetSnapshot) -> void: 483 | var local: NetSnapshot = null 484 | var popcount: int = 0 485 | if (snap.input_sig > 0): 486 | 487 | # Locate the local snapshot with corresponding input signature. Remove it and all 488 | # older than that from the internal history 489 | while (_history.size() > 0 && _history.front().input_sig <= snap.input_sig): 490 | local = _history.pop_front() 491 | popcount += 1 492 | 493 | if (local.input_sig != snap.input_sig): 494 | _update_prediction_count(-popcount) 495 | # This should not occur! 496 | return 497 | 498 | else: 499 | local = _history.front() if _history.size() > 0 else null 500 | 501 | 502 | if (!local): 503 | _update_prediction_count(-popcount) 504 | # This should not occur! 505 | return 506 | 507 | _server_state = snap 508 | 509 | for ehash in _entity_info: 510 | var einfo: EntityInfo = _entity_info[ehash] 511 | 512 | # The entity type *must* exist on both ends 513 | assert(local._entity_data.has(ehash)) 514 | assert(snap._entity_data.has(ehash)) 515 | 516 | # Retrieve the list of entities on the local snapshot. This will be used to 517 | # track ones that are locally present but not on the server's data. 518 | var local_entity: Array = local._entity_data[ehash].keys() 519 | 520 | # Iterate through entities of the server's snapshot 521 | for uid in snap._entity_data[ehash]: 522 | var rentity: SnapEntityBase = snap.get_entity(ehash, uid) 523 | var lentity: SnapEntityBase = local.get_entity(ehash, uid) 524 | var node: Node = null 525 | 526 | if (rentity && lentity): 527 | # Entity exists on both ends. First update the local_entity array because 528 | # it's meant to hold entities that are present only in the local machine 529 | local_entity.erase(uid) 530 | 531 | # And now check if there is any difference 532 | var cmask: int = einfo.calculate_change_mask(rentity, lentity) 533 | 534 | if (cmask > 0): 535 | # There is at least one property with different values. So, it must be corrected. 536 | node = einfo.get_game_node(uid) 537 | 538 | else: 539 | # Entity exists only on the server's data. If necessary spawn the game node. 540 | var n: Node = einfo.get_game_node(uid) 541 | if (!n): 542 | node = einfo.spawn_node(uid, rentity.class_hash) 543 | 544 | 545 | if (node): 546 | # If here, then it's necessary to apply the server's state into the node 547 | rentity.apply_state(node) 548 | 549 | # "Propagate" the server's data into every snapshot in the local history 550 | for s in _history: 551 | s.add_entity(ehash, einfo.clone_entity(rentity)) 552 | 553 | # Now check the entities that are in the local snapshot but not on the 554 | # remote one. The local ones must be removed from the game. 555 | for uid in local_entity: 556 | despawn_node(einfo._resource, uid) 557 | 558 | # All entities have been verified. Now update the prediction count 559 | _update_prediction_count(-popcount) 560 | 561 | 562 | func _add_to_history(snap: NetSnapshot) -> void: 563 | _history.push_back(snap) 564 | 565 | pass 566 | 567 | func _check_history_size(max_size: int, has_authority: bool) -> void: 568 | var popped: int = 0 569 | 570 | while (_history.size() > max_size): 571 | _history.pop_front() 572 | popped += 1 573 | 574 | if (!has_authority): 575 | _update_prediction_count(1 - popped) 576 | 577 | # Internally used, this updates the prediction count of each entity 578 | func _update_prediction_count(delta: int) -> void: 579 | for ehash in _entity_info: 580 | var einfo: EntityInfo = _entity_info[ehash] 581 | einfo.update_pred_count(delta) 582 | -------------------------------------------------------------------------------- /client/game_client.gd: -------------------------------------------------------------------------------- 1 | # This script is reference material to a written tutorial found on my web page (http://kehomsforge.com) 2 | 3 | extends Node 4 | 5 | var dc_message: String = "Disconnected from the server." 6 | 7 | func _ready() -> void: 8 | # warning-ignore:return_value_discarded 9 | loader.network.connect("kicked", self, "_on_kicked") 10 | # warning-ignore:return_value_discarded 11 | loader.network.connect("disconnected", self, "_on_disconnected") 12 | 13 | # Notify the server that this client is ready to receive snapshot data 14 | loader.network.notify_ready() 15 | 16 | 17 | func _exit_tree() -> void: 18 | loader.network.reset_system() 19 | # No harm if already disconnected 20 | loader.network.disconnect_from_server() 21 | 22 | 23 | func _physics_process(_dt: float) -> void: 24 | # Request initialization of the snapshot for this physics frame 25 | loader.network.init_snapshot() 26 | 27 | 28 | func _on_kicked(reason: String) -> void: 29 | dc_message = reason 30 | 31 | 32 | func _on_disconnected() -> void: 33 | set_pause_mode(Node.PAUSE_MODE_STOP) 34 | $ui/dc_dialog.dialog_text = dc_message 35 | $ui/dc_dialog.popup_centered() 36 | 37 | 38 | 39 | func _on_dc_dialog_confirmed() -> void: 40 | # warning-ignore:return_value_discarded 41 | get_tree().change_scene("res://client/main_client.tscn") 42 | -------------------------------------------------------------------------------- /client/game_client.tscn: -------------------------------------------------------------------------------- 1 | [gd_scene load_steps=3 format=2] 2 | 3 | [ext_resource path="res://shared/game.tscn" type="PackedScene" id=1] 4 | [ext_resource path="res://client/game_client.gd" type="Script" id=2] 5 | 6 | [node name="game_center" type="Node"] 7 | script = ExtResource( 2 ) 8 | 9 | [node name="ui" type="CanvasLayer" parent="."] 10 | 11 | [node name="dc_dialog" type="AcceptDialog" parent="ui"] 12 | margin_right = 83.0 13 | margin_bottom = 58.0 14 | 15 | [node name="game" parent="." instance=ExtResource( 1 )] 16 | 17 | [node name="Camera" type="Camera" parent="game"] 18 | transform = Transform( 1, 0, 0, 0, 0.917685, 0.397308, 0, -0.397308, 0.917685, 0, 5.75469, 25.2017 ) 19 | [connection signal="confirmed" from="ui/dc_dialog" to="." method="_on_dc_dialog_confirmed"] 20 | -------------------------------------------------------------------------------- /client/main_client.gd: -------------------------------------------------------------------------------- 1 | # This script is reference material to a written tutorial found on my web page (http://kehomsforge.com) 2 | 3 | extends Node2D 4 | 5 | func _ready() -> void: 6 | # warning-ignore:return_value_discarded 7 | loader.network.connect("join_fail", self, "_on_join_failed") 8 | # warning-ignore:return_value_discarded 9 | loader.network.connect("join_accepted", self, "_on_join_accepted") 10 | 11 | 12 | func _on_join_failed() -> void: 13 | $ui/err_dialog.popup_centered() 14 | 15 | 16 | func _on_join_accepted() -> void: 17 | # warning-ignore:return_value_discarded 18 | get_tree().change_scene("res://client/game_client.tscn") 19 | 20 | 21 | func _on_bt_join_pressed() -> void: 22 | var port: int = 1234 23 | if (!$ui/panel/txt_port.text.empty() && $ui/panel/txt_port.text.is_valid_integer()): 24 | port = $ui/panel/txt_port.text.to_int() 25 | 26 | var ip: String = $ui/panel/txt_ip.text 27 | if (ip.empty()): 28 | return 29 | 30 | loader.network.join_server(ip, port) 31 | -------------------------------------------------------------------------------- /client/main_client.tscn: -------------------------------------------------------------------------------- 1 | [gd_scene load_steps=2 format=2] 2 | 3 | [ext_resource path="res://client/main_client.gd" type="Script" id=1] 4 | 5 | [node name="main_menu" type="Node2D"] 6 | script = ExtResource( 1 ) 7 | 8 | [node name="ui" type="CanvasLayer" parent="."] 9 | 10 | [node name="panel" type="Panel" parent="ui"] 11 | anchor_left = 0.5 12 | anchor_top = 0.5 13 | anchor_right = 0.5 14 | anchor_bottom = 0.5 15 | margin_left = -96.0 16 | margin_top = -56.0 17 | margin_right = 96.0 18 | margin_bottom = 56.0 19 | __meta__ = { 20 | "_edit_use_anchors_": false 21 | } 22 | 23 | [node name="lbl_ip" type="Label" parent="ui/panel"] 24 | margin_left = 16.0 25 | margin_top = 16.0 26 | margin_right = 32.0 27 | margin_bottom = 40.0 28 | text = "IP" 29 | valign = 1 30 | 31 | [node name="txt_ip" type="LineEdit" parent="ui/panel"] 32 | margin_left = 72.0 33 | margin_top = 16.0 34 | margin_right = 176.0 35 | margin_bottom = 40.0 36 | text = "127.0.0.1" 37 | 38 | [node name="lbl_port" type="Label" parent="ui/panel"] 39 | margin_left = 16.0 40 | margin_top = 48.0 41 | margin_right = 56.0 42 | margin_bottom = 72.0 43 | text = "Port" 44 | valign = 1 45 | 46 | [node name="txt_port" type="LineEdit" parent="ui/panel"] 47 | margin_left = 72.0 48 | margin_top = 48.0 49 | margin_right = 130.0 50 | margin_bottom = 72.0 51 | text = "1234" 52 | 53 | [node name="bt_join" type="Button" parent="ui/panel"] 54 | margin_left = 56.0 55 | margin_top = 80.0 56 | margin_right = 136.0 57 | margin_bottom = 100.0 58 | text = "Join" 59 | 60 | [node name="err_dialog" type="AcceptDialog" parent="ui"] 61 | margin_right = 83.0 62 | margin_bottom = 58.0 63 | popup_exclusive = true 64 | dialog_text = "Failed to connect to server." 65 | [connection signal="pressed" from="ui/panel/bt_join" to="." method="_on_bt_join_pressed"] 66 | -------------------------------------------------------------------------------- /default_env.tres: -------------------------------------------------------------------------------- 1 | [gd_resource type="Environment" load_steps=2 format=2] 2 | 3 | [sub_resource type="ProceduralSky" id=1] 4 | 5 | [resource] 6 | background_mode = 2 7 | background_sky = SubResource( 1 ) 8 | -------------------------------------------------------------------------------- /icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kehom/gdDedicatedServer/5e5cac5858b13308065716fae995cbf3d38ebd6d/icon.png -------------------------------------------------------------------------------- /icon.png.import: -------------------------------------------------------------------------------- 1 | [remap] 2 | 3 | importer="texture" 4 | type="StreamTexture" 5 | path="res://.import/icon.png-487276ed1e3a0c39cad0279d744ee560.stex" 6 | metadata={ 7 | "vram_texture": false 8 | } 9 | 10 | [deps] 11 | 12 | source_file="res://icon.png" 13 | dest_files=[ "res://.import/icon.png-487276ed1e3a0c39cad0279d744ee560.stex" ] 14 | 15 | [params] 16 | 17 | compress/mode=0 18 | compress/lossy_quality=0.7 19 | compress/hdr_mode=0 20 | compress/bptc_ldr=0 21 | compress/normal_map=0 22 | flags/repeat=0 23 | flags/filter=true 24 | flags/mipmaps=false 25 | flags/anisotropic=false 26 | flags/srgb=2 27 | process/fix_alpha_border=true 28 | process/premult_alpha=false 29 | process/HDR_as_SRGB=false 30 | process/invert_color=false 31 | stream=false 32 | size_limit=0 33 | detect_3d=true 34 | svg/scale=1.0 35 | -------------------------------------------------------------------------------- /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 | "base": "Reference", 13 | "class": "EncDecBuffer", 14 | "language": "GDScript", 15 | "path": "res://addons/keh_general/data/encdecbuffer.gd" 16 | }, { 17 | "base": "Reference", 18 | "class": "EntityInfo", 19 | "language": "GDScript", 20 | "path": "res://addons/keh_network/entityinfo.gd" 21 | }, { 22 | "base": "Reference", 23 | "class": "InputData", 24 | "language": "GDScript", 25 | "path": "res://addons/keh_network/inputdata.gd" 26 | }, { 27 | "base": "Reference", 28 | "class": "NetCustomProperty", 29 | "language": "GDScript", 30 | "path": "res://addons/keh_network/customproperty.gd" 31 | }, { 32 | "base": "NetNodeSpawner", 33 | "class": "NetDefaultSpawner", 34 | "language": "GDScript", 35 | "path": "res://addons/keh_network/defaultspawner.gd" 36 | }, { 37 | "base": "Reference", 38 | "class": "NetEventInfo", 39 | "language": "GDScript", 40 | "path": "res://addons/keh_network/eventinfo.gd" 41 | }, { 42 | "base": "Reference", 43 | "class": "NetInputInfo", 44 | "language": "GDScript", 45 | "path": "res://addons/keh_network/inputinfo.gd" 46 | }, { 47 | "base": "Spatial", 48 | "class": "NetMeshInstance", 49 | "language": "GDScript", 50 | "path": "res://shared/scripts/netmeshinstance.gd" 51 | }, { 52 | "base": "Reference", 53 | "class": "NetNodeSpawner", 54 | "language": "GDScript", 55 | "path": "res://addons/keh_network/nodespawner.gd" 56 | }, { 57 | "base": "Reference", 58 | "class": "NetPingInfo", 59 | "language": "GDScript", 60 | "path": "res://addons/keh_network/pinginfo.gd" 61 | }, { 62 | "base": "Reference", 63 | "class": "NetPlayerData", 64 | "language": "GDScript", 65 | "path": "res://addons/keh_network/playerdata.gd" 66 | }, { 67 | "base": "Node", 68 | "class": "NetPlayerNode", 69 | "language": "GDScript", 70 | "path": "res://addons/keh_network/playernode.gd" 71 | }, { 72 | "base": "Reference", 73 | "class": "NetSnapshot", 74 | "language": "GDScript", 75 | "path": "res://addons/keh_network/snapshot.gd" 76 | }, { 77 | "base": "Reference", 78 | "class": "NetSnapshotData", 79 | "language": "GDScript", 80 | "path": "res://addons/keh_network/snapshotdata.gd" 81 | }, { 82 | "base": "Node", 83 | "class": "Network", 84 | "language": "GDScript", 85 | "path": "res://addons/keh_network/network.gd" 86 | }, { 87 | "base": "Reference", 88 | "class": "Quantize", 89 | "language": "GDScript", 90 | "path": "res://addons/keh_general/data/quantize.gd" 91 | }, { 92 | "base": "SnapEntityBase", 93 | "class": "SnapCharacter", 94 | "language": "GDScript", 95 | "path": "res://shared/scripts/snapcharacter.gd" 96 | }, { 97 | "base": "Reference", 98 | "class": "SnapEntityBase", 99 | "language": "GDScript", 100 | "path": "res://addons/keh_network/snapentity.gd" 101 | } ] 102 | _global_script_class_icons={ 103 | "EncDecBuffer": "", 104 | "EntityInfo": "", 105 | "InputData": "", 106 | "NetCustomProperty": "", 107 | "NetDefaultSpawner": "", 108 | "NetEventInfo": "", 109 | "NetInputInfo": "", 110 | "NetMeshInstance": "", 111 | "NetNodeSpawner": "", 112 | "NetPingInfo": "", 113 | "NetPlayerData": "", 114 | "NetPlayerNode": "", 115 | "NetSnapshot": "", 116 | "NetSnapshotData": "", 117 | "Network": "", 118 | "Quantize": "", 119 | "SnapCharacter": "", 120 | "SnapEntityBase": "" 121 | } 122 | 123 | [application] 124 | 125 | config/name="TUT - Dedicated Server" 126 | run/main_scene="res://shared/entry.tscn" 127 | config/icon="res://icon.png" 128 | 129 | [autoload] 130 | 131 | loader="*res://shared/loader.gd" 132 | 133 | [editor_plugins] 134 | 135 | enabled=PoolStringArray( "keh_network" ) 136 | 137 | [input] 138 | 139 | move_forward={ 140 | "deadzone": 0.5, 141 | "events": [ Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":0,"alt":false,"shift":false,"control":false,"meta":false,"command":false,"pressed":false,"scancode":87,"unicode":0,"echo":false,"script":null) 142 | ] 143 | } 144 | move_backward={ 145 | "deadzone": 0.5, 146 | "events": [ Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":0,"alt":false,"shift":false,"control":false,"meta":false,"command":false,"pressed":false,"scancode":83,"unicode":0,"echo":false,"script":null) 147 | ] 148 | } 149 | move_left={ 150 | "deadzone": 0.5, 151 | "events": [ Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":0,"alt":false,"shift":false,"control":false,"meta":false,"command":false,"pressed":false,"scancode":65,"unicode":0,"echo":false,"script":null) 152 | ] 153 | } 154 | move_right={ 155 | "deadzone": 0.5, 156 | "events": [ Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":0,"alt":false,"shift":false,"control":false,"meta":false,"command":false,"pressed":false,"scancode":68,"unicode":0,"echo":false,"script":null) 157 | ] 158 | } 159 | jump={ 160 | "deadzone": 0.5, 161 | "events": [ Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":0,"alt":false,"shift":false,"control":false,"meta":false,"command":false,"pressed":false,"scancode":32,"unicode":0,"echo":false,"script":null) 162 | ] 163 | } 164 | 165 | [keh_addons] 166 | 167 | network/print_debug_info=true 168 | 169 | [rendering] 170 | 171 | environment/default_environment="res://default_env.tres" 172 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | ## Godot Dedicated Server Tutorial 2 | 3 | The code on this repository is reference material to a written tutorial found on my web page, [www.kehomsforge.com](http://www.kehomsforge.com/tutorials/single/gdDedicatedServer). 4 | 5 | Note that this project uses my networking addon (which is included within this repository). This addon itself is part of a pack, which can be found on [this repository](https://github.com/Kehom/GodotAddonPack). 6 | 7 | Update (Nov 19, 2020): There is now a new (and better) method of not instantiating unnecessary nodes within the dedicated server route. The tutorial has been updated with the new code. The old method is still available in a different branch (target-gd3.2_old). -------------------------------------------------------------------------------- /server/game_server.gd: -------------------------------------------------------------------------------- 1 | # This script is reference material to a written tutorial found on my web page (http://kehomsforge.com) 2 | 3 | extends Node 4 | 5 | # Preaload the UI Player scene, which will be instantiated whenever a player joins the game 6 | const uiplayer_scene: PackedScene = preload("res://server/ui/player.tscn") 7 | 8 | # Key = player ID 9 | # Value = whatever data we need associated with the player. 10 | var player_data: Dictionary = {} 11 | 12 | 13 | func _ready() -> void: 14 | # warning-ignore:return_value_discarded 15 | loader.network.connect("player_added", self, "_on_player_added") 16 | # warning-ignore:return_value_discarded 17 | loader.network.connect("player_removed", self, "_on_player_removed") 18 | 19 | 20 | func _exit_tree() -> void: 21 | loader.network.reset_system() 22 | 23 | 24 | func _physics_process(_dt: float) -> void: 25 | # Request initialization of the snapshot for this physics frame 26 | loader.network.init_snapshot() 27 | 28 | 29 | func _on_bt_quit_pressed() -> void: 30 | # Close the server 31 | loader.network.close_server() 32 | # Then quit the app 33 | get_tree().quit() 34 | 35 | 36 | func _on_bt_close_pressed() -> void: 37 | # Close the server 38 | loader.network.close_server() 39 | # Return to the main menu 40 | # warning-ignore:return_value_discarded 41 | get_tree().change_scene("res://server/main_server.tscn") 42 | 43 | 44 | func _on_player_added(pid: int) -> void: 45 | # Create instance of the UI element 46 | var uielement: Node = uiplayer_scene.instance() 47 | uielement.set_data(pid) 48 | 49 | # Attach it into the vbox 50 | $ui/panel/scroll/vbox.add_child(uielement) 51 | 52 | # Spawn the character node 53 | var cnode: KinematicBody = loader.network.snapshot_data.spawn_node(SnapCharacter, pid, 0) 54 | # Obtain the index of the spawn point 55 | var index: int = loader.network.player_data.get_player_count() - 1 56 | # Place the character at the spawn point 57 | cnode.global_transform.origin = $game.get_spawn_position(index) 58 | 59 | # Cache the node 60 | player_data[pid] = { 61 | "ui_element": uielement, 62 | } 63 | 64 | 65 | func _on_player_removed(pid: int) -> void: 66 | # Obtain the player data 67 | var pdata: Dictionary = player_data.get(pid, {}) 68 | 69 | # Bail if the obtained entry does not exist (IE.: it's empty) 70 | if (pdata.empty()): 71 | return 72 | 73 | # "ui_element" should point to the node within the vbox container. So, queue free it 74 | pdata.ui_element.queue_free() 75 | # Then erase from the cached data 76 | # warning-ignore:return_value_discarded 77 | player_data.erase(pid) 78 | 79 | # De-spawn the player character 80 | loader.network.snapshot_data.despawn_node(SnapCharacter, pid) 81 | -------------------------------------------------------------------------------- /server/game_server.tscn: -------------------------------------------------------------------------------- 1 | [gd_scene load_steps=3 format=2] 2 | 3 | [ext_resource path="res://shared/game.tscn" type="PackedScene" id=1] 4 | [ext_resource path="res://server/game_server.gd" type="Script" id=2] 5 | 6 | [node name="game_center" type="Node"] 7 | script = ExtResource( 2 ) 8 | 9 | [node name="ui" type="CanvasLayer" parent="."] 10 | 11 | [node name="panel" type="Panel" parent="ui"] 12 | anchor_bottom = 1.0 13 | margin_right = 208.0 14 | __meta__ = { 15 | "_edit_use_anchors_": false 16 | } 17 | 18 | [node name="bt_quit" type="Button" parent="ui/panel"] 19 | margin_left = 16.0 20 | margin_top = 16.0 21 | margin_right = 96.0 22 | margin_bottom = 36.0 23 | text = "Quit" 24 | __meta__ = { 25 | "_edit_use_anchors_": false 26 | } 27 | 28 | [node name="bt_close" type="Button" parent="ui/panel"] 29 | margin_left = 112.0 30 | margin_top = 16.0 31 | margin_right = 192.0 32 | margin_bottom = 36.0 33 | text = "Close" 34 | 35 | [node name="scroll" type="ScrollContainer" parent="ui/panel"] 36 | anchor_right = 1.0 37 | anchor_bottom = 1.0 38 | margin_left = 16.0 39 | margin_top = 48.0 40 | margin_right = -16.0 41 | margin_bottom = -16.0 42 | 43 | [node name="vbox" type="VBoxContainer" parent="ui/panel/scroll"] 44 | margin_right = 176.0 45 | margin_bottom = 536.0 46 | size_flags_horizontal = 3 47 | size_flags_vertical = 3 48 | 49 | [node name="game" parent="." instance=ExtResource( 1 )] 50 | [connection signal="pressed" from="ui/panel/bt_quit" to="." method="_on_bt_quit_pressed"] 51 | [connection signal="pressed" from="ui/panel/bt_close" to="." method="_on_bt_close_pressed"] 52 | -------------------------------------------------------------------------------- /server/main_server.gd: -------------------------------------------------------------------------------- 1 | # This script is reference material to a written tutorial found on my web page (http://kehomsforge.com) 2 | 3 | extends Node2D 4 | 5 | func _ready() -> void: 6 | # Connect functions to the events given by the networking system 7 | # warning-ignore:return_value_discarded 8 | loader.network.connect("server_created", self, "_on_server_created") 9 | # warning-ignore:return_value_discarded 10 | loader.network.connect("server_creation_failed", self, "_on_server_creation_failed") 11 | 12 | 13 | func _on_server_created() -> void: 14 | # warning-ignore:return_value_discarded 15 | get_tree().change_scene("res://server/game_server.tscn") 16 | 17 | 18 | func _on_server_creation_failed() -> void: 19 | $ui/err_dialog.popup_centered() 20 | 21 | 22 | 23 | func _on_bt_start_pressed() -> void: 24 | # This is the default value, which will be used in case the specified one in the line edit is invalid 25 | var port: int = 1234 26 | if (!$ui/panel/txt_port.text.empty() && $ui/panel/txt_port.text.is_valid_integer()): 27 | port = $ui/panel/txt_port.text.to_int() 28 | 29 | # Same thing for maximum amount of players 30 | var mplayers: int = 6 31 | if (!$ui/panel/txt_maxplayers.text.empty() && $ui/panel/txt_maxplayers.text.is_valid_integer()): 32 | mplayers = $ui/panel/txt_maxplayers.text.to_int() 33 | 34 | # Try to create the server 35 | loader.network.create_server(port, "Awesome Server Name", mplayers) 36 | -------------------------------------------------------------------------------- /server/main_server.tscn: -------------------------------------------------------------------------------- 1 | [gd_scene load_steps=2 format=2] 2 | 3 | [ext_resource path="res://server/main_server.gd" type="Script" id=1] 4 | 5 | [node name="main_menu" type="Node2D"] 6 | script = ExtResource( 1 ) 7 | 8 | [node name="ui" type="CanvasLayer" parent="."] 9 | 10 | [node name="panel" type="Panel" parent="ui"] 11 | anchor_left = 0.5 12 | anchor_top = 0.5 13 | anchor_right = 0.5 14 | anchor_bottom = 0.5 15 | margin_left = -92.0 16 | margin_top = -60.0 17 | margin_right = 92.0 18 | margin_bottom = 60.0 19 | __meta__ = { 20 | "_edit_use_anchors_": false 21 | } 22 | 23 | [node name="bt_start" type="Button" parent="ui/panel"] 24 | margin_left = 16.0 25 | margin_top = 16.0 26 | margin_right = 168.0 27 | margin_bottom = 36.0 28 | text = "Start Server" 29 | __meta__ = { 30 | "_edit_use_anchors_": false 31 | } 32 | 33 | [node name="lbl_port" type="Label" parent="ui/panel"] 34 | margin_left = 16.0 35 | margin_top = 48.0 36 | margin_right = 42.0 37 | margin_bottom = 72.0 38 | text = "Port" 39 | valign = 1 40 | 41 | [node name="txt_port" type="LineEdit" parent="ui/panel"] 42 | margin_left = 112.0 43 | margin_top = 48.0 44 | margin_right = 170.0 45 | margin_bottom = 72.0 46 | text = "1234" 47 | 48 | [node name="lbl_maxplayers" type="Label" parent="ui/panel"] 49 | margin_left = 16.0 50 | margin_top = 80.0 51 | margin_right = 92.0 52 | margin_bottom = 104.0 53 | text = "Max Players" 54 | valign = 1 55 | 56 | [node name="txt_maxplayers" type="LineEdit" parent="ui/panel"] 57 | margin_left = 112.0 58 | margin_top = 80.0 59 | margin_right = 170.0 60 | margin_bottom = 104.0 61 | text = "6" 62 | 63 | [node name="err_dialog" type="AcceptDialog" parent="ui"] 64 | margin_right = 83.0 65 | margin_bottom = 58.0 66 | popup_exclusive = true 67 | dialog_text = "Failed to create server" 68 | [connection signal="pressed" from="ui/panel/bt_start" to="." method="_on_bt_start_pressed"] 69 | -------------------------------------------------------------------------------- /server/ui/player.gd: -------------------------------------------------------------------------------- 1 | # This script is reference material to a written tutorial found on my web page (http://kehomsforge.com) 2 | 3 | extends Control 4 | 5 | var pid: int = 0 6 | 7 | func set_data(id: int) -> void: 8 | pid = id 9 | $lbl_id.text = str(id) 10 | 11 | 12 | func _on_bt_kick_pressed() -> void: 13 | if (pid != 0): 14 | # Bellow we could retrieve the message from a line edit instead of hard coding it. 15 | loader.network.kick_player(pid, "You have been kicked") 16 | -------------------------------------------------------------------------------- /server/ui/player.tscn: -------------------------------------------------------------------------------- 1 | [gd_scene load_steps=2 format=2] 2 | 3 | [ext_resource path="res://server/ui/player.gd" type="Script" id=1] 4 | 5 | [node name="player" type="Control"] 6 | margin_right = 128.0 7 | margin_bottom = 48.0 8 | rect_min_size = Vector2( 0, 48 ) 9 | script = ExtResource( 1 ) 10 | __meta__ = { 11 | "_edit_use_anchors_": false 12 | } 13 | 14 | [node name="lbl_id" type="Label" parent="."] 15 | margin_right = 40.0 16 | margin_bottom = 14.0 17 | text = "[player_id]" 18 | 19 | [node name="bt_kick" type="Button" parent="."] 20 | margin_top = 24.0 21 | margin_right = 128.0 22 | margin_bottom = 44.0 23 | text = "Kick" 24 | __meta__ = { 25 | "_edit_use_anchors_": false 26 | } 27 | [connection signal="pressed" from="bt_kick" to="." method="_on_bt_kick_pressed"] 28 | -------------------------------------------------------------------------------- /shared/entry.gd: -------------------------------------------------------------------------------- 1 | # This script is reference material to a written tutorial found on my web page (http://kehomsforge.com) 2 | 3 | extends Node 4 | 5 | func _ready() -> void: 6 | # By default open the client main menu 7 | var spath: String = "res://client/main_client.tscn" 8 | if ("--server" in OS.get_cmdline_args() || OS.has_feature("dedicated_server")): 9 | # Requesting to open in server mode, so change the string to it 10 | spath = "res://server/main_server.tscn" 11 | # Cache that we are indeed in server mode 12 | loader.is_dedicated_server = true 13 | #loader.network.set_dedicated_server_mode(true) 14 | loader.network.call_deferred("set_dedicated_server_mode", true) 15 | 16 | # And transition into the appropriate scene 17 | # warning-ignore:return_value_discarded 18 | get_tree().change_scene(spath) 19 | -------------------------------------------------------------------------------- /shared/entry.tscn: -------------------------------------------------------------------------------- 1 | [gd_scene load_steps=2 format=2] 2 | 3 | [ext_resource path="res://shared/entry.gd" type="Script" id=1] 4 | 5 | [node name="entry" type="Node"] 6 | script = ExtResource( 1 ) 7 | -------------------------------------------------------------------------------- /shared/game.gd: -------------------------------------------------------------------------------- 1 | # This script is reference material to a written tutorial found on my web page (http://kehomsforge.com) 2 | 3 | extends Spatial 4 | 5 | 6 | func _ready() -> void: 7 | # The default spawner require the scene, so load it 8 | var charscene: PackedScene = load("res://shared/scenes/character.tscn") 9 | # Register network entity spawner for the characters 10 | loader.network.snapshot_data.register_spawner(SnapCharacter, 0, NetDefaultSpawner.new(charscene), self) 11 | 12 | 13 | 14 | func get_spawn_position(index: int) -> Vector3: 15 | # Just some boundary check. If there is any intention to change the number of spawn points during development, 16 | # perhaps it would be a better idea to put those nodes into a group and use the size within this check 17 | if (index < 0 || index > 7): 18 | return Vector3() 19 | 20 | # Locate the spawn point node given its index 21 | var p: Position3D = get_node("spawn_points/" + str(index)) 22 | # Provide this node's global position 23 | return p.global_transform.origin 24 | -------------------------------------------------------------------------------- /shared/game.tscn: -------------------------------------------------------------------------------- 1 | [gd_scene load_steps=3 format=2] 2 | 3 | [ext_resource path="res://shared/game.gd" type="Script" id=1] 4 | [ext_resource path="res://shared/scenes/floor.tscn" type="PackedScene" id=2] 5 | 6 | [node name="game" type="Spatial"] 7 | script = ExtResource( 1 ) 8 | 9 | [node name="ground" type="Spatial" parent="."] 10 | 11 | [node name="floor1" parent="ground" instance=ExtResource( 2 )] 12 | transform = Transform( 1, 0, 0, 0, 1, 0, 0, 0, 1, -15, 0, -15 ) 13 | 14 | [node name="floor2" parent="ground" instance=ExtResource( 2 )] 15 | transform = Transform( 1, 0, 0, 0, 1, 0, 0, 0, 1, -5, 0, -15 ) 16 | 17 | [node name="floor3" parent="ground" instance=ExtResource( 2 )] 18 | transform = Transform( 1, 0, 0, 0, 1, 0, 0, 0, 1, 5, 0, -15 ) 19 | 20 | [node name="floor4" parent="ground" instance=ExtResource( 2 )] 21 | transform = Transform( 1, 0, 0, 0, 1, 0, 0, 0, 1, 15, 0, -15 ) 22 | 23 | [node name="floor5" parent="ground" instance=ExtResource( 2 )] 24 | transform = Transform( 1, 0, 0, 0, 1, 0, 0, 0, 1, -15, 0, -5 ) 25 | 26 | [node name="floor6" parent="ground" instance=ExtResource( 2 )] 27 | transform = Transform( 1, 0, 0, 0, 1, 0, 0, 0, 1, -5, 0, -5 ) 28 | 29 | [node name="floor7" parent="ground" instance=ExtResource( 2 )] 30 | transform = Transform( 1, 0, 0, 0, 1, 0, 0, 0, 1, 5, 0, -5 ) 31 | 32 | [node name="floor8" parent="ground" instance=ExtResource( 2 )] 33 | transform = Transform( 1, 0, 0, 0, 1, 0, 0, 0, 1, 15, 0, -5 ) 34 | 35 | [node name="floor9" parent="ground" instance=ExtResource( 2 )] 36 | transform = Transform( 1, 0, 0, 0, 1, 0, 0, 0, 1, -15, 0, 5 ) 37 | 38 | [node name="floor10" parent="ground" instance=ExtResource( 2 )] 39 | transform = Transform( 1, 0, 0, 0, 1, 0, 0, 0, 1, -5, 0, 5 ) 40 | 41 | [node name="floor11" parent="ground" instance=ExtResource( 2 )] 42 | transform = Transform( 1, 0, 0, 0, 1, 0, 0, 0, 1, 5, 0, 5 ) 43 | 44 | [node name="floor12" parent="ground" instance=ExtResource( 2 )] 45 | transform = Transform( 1, 0, 0, 0, 1, 0, 0, 0, 1, 15, 0, 5 ) 46 | 47 | [node name="floor13" parent="ground" instance=ExtResource( 2 )] 48 | transform = Transform( 1, 0, 0, 0, 1, 0, 0, 0, 1, -15, 0, 15 ) 49 | 50 | [node name="floor14" parent="ground" instance=ExtResource( 2 )] 51 | transform = Transform( 1, 0, 0, 0, 1, 0, 0, 0, 1, -5, 0, 15 ) 52 | 53 | [node name="floor15" parent="ground" instance=ExtResource( 2 )] 54 | transform = Transform( 1, 0, 0, 0, 1, 0, 0, 0, 1, 5, 0, 15 ) 55 | 56 | [node name="floor16" parent="ground" instance=ExtResource( 2 )] 57 | transform = Transform( 1, 0, 0, 0, 1, 0, 0, 0, 1, 15, 0, 15 ) 58 | 59 | [node name="spawn_points" type="Spatial" parent="."] 60 | 61 | [node name="0" type="Position3D" parent="spawn_points"] 62 | transform = Transform( 1, 0, 0, 0, 1, 0, 0, 0, 1, -15.7442, 1.97472, 10.1523 ) 63 | 64 | [node name="1" type="Position3D" parent="spawn_points"] 65 | transform = Transform( 1, 0, 0, 0, 1, 0, 0, 0, 1, -12.7442, 1.97472, 10.1523 ) 66 | 67 | [node name="2" type="Position3D" parent="spawn_points"] 68 | transform = Transform( 1, 0, 0, 0, 1, 0, 0, 0, 1, -6.74416, 1.97472, 10.1523 ) 69 | 70 | [node name="3" type="Position3D" parent="spawn_points"] 71 | transform = Transform( 1, 0, 0, 0, 1, 0, 0, 0, 1, -3.74416, 1.97472, 10.1523 ) 72 | 73 | [node name="4" type="Position3D" parent="spawn_points"] 74 | transform = Transform( 1, 0, 0, 0, 1, 0, 0, 0, 1, 2.25584, 1.97472, 10.1523 ) 75 | 76 | [node name="5" type="Position3D" parent="spawn_points"] 77 | transform = Transform( 1, 0, 0, 0, 1, 0, 0, 0, 1, 6.25584, 1.97472, 10.1523 ) 78 | 79 | [node name="6" type="Position3D" parent="spawn_points"] 80 | transform = Transform( 1, 0, 0, 0, 1, 0, 0, 0, 1, 12.2558, 1.97472, 10.1523 ) 81 | 82 | [node name="7" type="Position3D" parent="spawn_points"] 83 | transform = Transform( 1, 0, 0, 0, 1, 0, 0, 0, 1, 16.2558, 1.97472, 10.1523 ) 84 | -------------------------------------------------------------------------------- /shared/loader.gd: -------------------------------------------------------------------------------- 1 | # This script is reference material to a written tutorial found on my web page (http://kehomsforge.com) 2 | 3 | extends Node 4 | 5 | # This is the network addon. 6 | var network: Node = null 7 | 8 | # The gamestate autoload. 9 | var gamestate: Node = null 10 | 11 | # Chache if we are in server or client mode, assuming client 12 | var is_dedicated_server: bool = false 13 | 14 | func _ready() -> void: 15 | # If in standalone mode (that is, exported binary), load the actual resource data pack 16 | if (OS.has_feature("standalone")): 17 | # Load the resource pack file named data.pck. The second argument is set to false in order to avoid 18 | # any existing resource to be overwritten by the contents of this pack. 19 | # NOTE: ideally this should check the return value and because this basically contains the core of the 20 | # game logic, if failed then a message box should be displayed and then quit the app/game 21 | # warning-ignore:return_value_discarded 22 | ProjectSettings.load_resource_pack("data.pck", false) 23 | 24 | var root: Node = get_tree().get_root() 25 | 26 | var netclass: Script = load("res://addons/keh_network/network.gd") 27 | network = netclass.new() 28 | 29 | var gstateclass: Script = load("res://shared/scripts/gamestate.gd") 30 | gamestate = gstateclass.new() 31 | 32 | # At this point the tree is still being built, so defer the calls to add children to the root. 33 | root.call_deferred("add_child", network) 34 | root.call_deferred("add_child", gamestate) 35 | 36 | 37 | -------------------------------------------------------------------------------- /shared/scenes/character.gd: -------------------------------------------------------------------------------- 1 | # This script is reference material to a written tutorial found on my web page (http://kehomsforge.com) 2 | 3 | tool 4 | extends KinematicBody 5 | 6 | # Gravity constant 7 | const GRAVITY: float = 9.81 8 | 9 | # Determines the speed in which the character will move 10 | export var movement_speed: float = 9.0 11 | 12 | # Initial vertical velocity when jumping 13 | export var vert_v0: float = 5.4 14 | 15 | # If true, character can jump 16 | var _can_jump: bool = false 17 | 18 | # Accumulate vertical acceleration 19 | var _vert_vel: float = 0.0 20 | 21 | # Cache incoming server state. It includes a flag telling if the data is actually a correction or not 22 | var _correction: Dictionary = { "has_correction": false } 23 | 24 | # Cache the entity unique ID in here 25 | var _uid: int = 0 26 | 27 | 28 | func _ready() -> void: 29 | # If in editor simply disable processing as it's not needed here 30 | if (Engine.is_editor_hint()): 31 | set_physics_process(false) 32 | 33 | if (has_meta("uid")): 34 | _uid = get_meta("uid") 35 | 36 | # if (loader.network.is_id_local(_uid)): 37 | # Create the camera 38 | # var cam: Camera = Camera.new() 39 | # Attach to the node hierarchy 40 | # $camera_pos.add_child(cam) 41 | # Ensure it is the active one 42 | # cam.current = true 43 | 44 | func _physics_process(dt: float) -> void: 45 | # Check if there is any correction to be done 46 | if (_correction.has_correction): 47 | # Yes, there is. Apply it 48 | global_transform.origin = _correction.position 49 | global_transform.basis = Basis(_correction.orientation) 50 | _vert_vel = _correction.vert_velocity 51 | # Reset the flag so correction doesn't occur when not needed 52 | _correction.has_correction = false 53 | 54 | # Replay input objects within internal history if this character belongs to local player 55 | if (loader.network.is_id_local(_uid)): 56 | var input_list: Array = loader.network.player_data.local_player.get_cached_input_list() 57 | for i in input_list: 58 | _handle_input(dt, i) 59 | loader.network.correct_in_snapshot(generate_snap_object(), i) 60 | 61 | # Handle input 62 | _handle_input(dt, loader.network.get_input(_uid)) 63 | 64 | # Snapshot current state 65 | loader.network.snapshot_entity(generate_snap_object()) 66 | 67 | # InputData is a class type defined within the Network addon. 68 | func _handle_input(dt: float, input: InputData) -> void: 69 | if (!input): 70 | return 71 | 72 | var jump_pressed: bool = input.is_pressed("jump") 73 | var vel: Vector3 = Vector3() 74 | var aim: Basis = global_transform.basis 75 | 76 | if (input.is_pressed("move_forward")): 77 | vel -= aim.z 78 | if (input.is_pressed("move_backward")): 79 | vel += aim.z 80 | if (input.is_pressed("move_left")): 81 | vel -= aim.x 82 | if (input.is_pressed("move_right")): 83 | vel += aim.x 84 | 85 | # Normalize the horizontal movement and apply movement speed 86 | vel = vel.normalized() * movement_speed 87 | 88 | # Check jump 89 | vel.y = _vert_vel 90 | if (_can_jump && jump_pressed): 91 | vel.y = vert_v0 92 | _can_jump = false 93 | 94 | # Integrate gravity. Yes, this is not a good integrator but it should be OK for this demo 95 | vel.y -= (GRAVITY * dt) 96 | 97 | # Move. Giving a bunch of arguments with their default values in order to reach the last argument, which 98 | # must be false in order for collisions to properly work with rigid bodies. 99 | vel = move_and_slide(vel, Vector3.UP, false, 4, 0.785398, false) 100 | _vert_vel = vel.y 101 | 102 | # Cache the "can jump" situation so it can be used at a more intuitive location within this code. 103 | _can_jump = is_on_floor() && !jump_pressed 104 | 105 | 106 | func apply_state(state: Dictionary) -> void: 107 | # Take the server state and cache it 108 | _correction["position"] = state.position 109 | _correction["orientation"] = state.orientation 110 | _correction["vert_velocity"] = state.vert_velocity 111 | # And set the flag indicating that a correction is necessary 112 | _correction["has_correction"] = true 113 | 114 | 115 | func generate_snap_object() -> SnapCharacter: 116 | # Second argument, class hash, is required but we don't use it, so setting to 0 117 | var snap: SnapCharacter = SnapCharacter.new(_uid, 0) 118 | 119 | # Transfer the character state into the object 120 | snap.position = global_transform.origin 121 | snap.orientation = global_transform.basis.get_rotation_quat() 122 | snap.vert_velocity = _vert_vel 123 | 124 | return snap 125 | -------------------------------------------------------------------------------- /shared/scenes/character.tscn: -------------------------------------------------------------------------------- 1 | [gd_scene load_steps=5 format=2] 2 | 3 | [ext_resource path="res://shared/scripts/netmeshinstance.gd" type="Script" id=1] 4 | [ext_resource path="res://shared/scenes/character.gd" type="Script" id=2] 5 | 6 | [sub_resource type="CapsuleMesh" id=1] 7 | 8 | [sub_resource type="CapsuleShape" id=2] 9 | 10 | [node name="character" type="KinematicBody"] 11 | script = ExtResource( 2 ) 12 | 13 | [node name="mesh" type="Spatial" parent="."] 14 | transform = Transform( 1, 0, 0, 0, -4.37114e-08, -1, 0, 1, -4.37114e-08, 0, 0, 0 ) 15 | script = ExtResource( 1 ) 16 | mesh = SubResource( 1 ) 17 | 18 | [node name="collision" type="CollisionShape" parent="."] 19 | transform = Transform( 1, 0, 0, 0, -1.62921e-07, -1, 0, 1, -1.62921e-07, 0, 0, 0 ) 20 | shape = SubResource( 2 ) 21 | 22 | [node name="camera_pos" type="Position3D" parent="."] 23 | transform = Transform( 1, 0, 0, 0, 0.939693, 0.34202, 0, -0.34202, 0.939693, 0, 3.7, 5.2 ) 24 | -------------------------------------------------------------------------------- /shared/scenes/floor.tscn: -------------------------------------------------------------------------------- 1 | [gd_scene load_steps=5 format=2] 2 | 3 | [ext_resource path="res://shared/scripts/netmeshinstance.gd" type="Script" id=1] 4 | 5 | [sub_resource type="BoxShape" id=1] 6 | extents = Vector3( 5, 0.1, 5 ) 7 | 8 | [sub_resource type="SpatialMaterial" id=2] 9 | albedo_color = Color( 0.0784314, 0.45098, 0.0745098, 1 ) 10 | 11 | [sub_resource type="CubeMesh" id=3] 12 | material = SubResource( 2 ) 13 | size = Vector3( 10, 0.2, 10 ) 14 | 15 | [node name="floor" type="StaticBody"] 16 | 17 | [node name="collision" type="CollisionShape" parent="."] 18 | shape = SubResource( 1 ) 19 | 20 | [node name="mesh" type="Spatial" parent="."] 21 | script = ExtResource( 1 ) 22 | mesh = SubResource( 3 ) 23 | -------------------------------------------------------------------------------- /shared/scripts/gamestate.gd: -------------------------------------------------------------------------------- 1 | # This script is reference material to a written tutorial found on my web page (http://kehomsforge.com) 2 | 3 | extends Node 4 | 5 | func _ready() -> void: 6 | # Register input within the network system 7 | loader.network.register_action("move_forward", false) 8 | loader.network.register_action("move_backward", false) 9 | loader.network.register_action("move_left", false) 10 | loader.network.register_action("move_right", false) 11 | loader.network.register_action("jump", false) 12 | -------------------------------------------------------------------------------- /shared/scripts/netmeshinstance.gd: -------------------------------------------------------------------------------- 1 | tool 2 | extends Spatial 3 | class_name NetMeshInstance 4 | 5 | # Expose the mesh property - this will be applied to the MeshInstance node if it's created. Any other property of the 6 | # MeshInstance can be exposed like this 7 | export var mesh: Mesh = null setget set_mesh 8 | 9 | # Store the InstanceMesh node here 10 | var _mesh_node: MeshInstance = null 11 | 12 | 13 | func _ready() -> void: 14 | if (Engine.is_editor_hint() || !loader.is_dedicated_server): 15 | _mesh_node = MeshInstance.new() 16 | _mesh_node.set_name("mesh") 17 | add_child(_mesh_node) 18 | # This is necessary for pre-created nodes (level design for example) 19 | _mesh_node.mesh = mesh 20 | 21 | 22 | func set_mesh(m: Mesh) -> void: 23 | mesh = m 24 | call_deferred("_check_mesh") 25 | 26 | 27 | func _check_mesh() -> void: 28 | if (_mesh_node): 29 | _mesh_node.mesh = mesh 30 | -------------------------------------------------------------------------------- /shared/scripts/snapcharacter.gd: -------------------------------------------------------------------------------- 1 | # This script is reference material to a written tutorial found on my web page (http://kehomsforge.com) 2 | 3 | extends SnapEntityBase 4 | class_name SnapCharacter 5 | 6 | var position: Vector3 7 | var orientation: Quat 8 | var vert_velocity: float 9 | 10 | func _init(id: int, chash: int = 0).(id, chash) -> void: 11 | position = Vector3() 12 | orientation = Quat() 13 | vert_velocity = 0 14 | 15 | func apply_state(n: Node) -> void: 16 | if (n is KinematicBody && n.has_method("apply_state")): 17 | n.apply_state({ 18 | "position": position, 19 | "orientation": orientation, 20 | "vert_velocity": vert_velocity, 21 | }) 22 | --------------------------------------------------------------------------------