extends Node ## Autoload "CreatureNet": the wildlife on a server (M2.5, ## docs/MULTIPLAYER_PLAN.md ยง5). ## ## The server's world runs the animals and the wolf pack (world.gd in server ## mode), so everyone sees the same rabbit in the same place, and a pack ## hunts whoever it likes. Ten times a second it sends everyone: ## - every animal and wolf, alive or a carcass: id, kind, where, facing, ## what it's doing, its speed and health; ## - every fishing float: whose it is, where, and whether a fish is biting. ## Players' games show these as puppets (creature.gd, bobber.gd `puppet`): ## they animate and can be hit or butchered (Actions sends that to the ## server), but decide nothing. Arrows and spears in flight are sent once ## when they're loosed and flown on each screen for show; only the server's ## copy hits anything. Howls are sent too. ## ## Snapshots are unreliable (a lost one is replaced 0.1 s later). Something ## missing from the snapshots for a second (despawned, butchered) goes. const Creature = preload("res://scripts/creatures/creature.gd") const Bobber = preload("res://scripts/fishing/bobber.gd") const Projectile = preload("res://scripts/creatures/projectile.gd") const WolfPack = preload("res://scripts/creatures/wolf_pack.gd") const SEND_INTERVAL := 0.1 const FORGET_AFTER := 1.0 # Server. var _next_cid := 1 var _by_cid: Dictionary = {} ## cid -> creature (server) var _send_left := 0.0 # Client. var world: Node3D var puppets: Dictionary = {} ## cid -> creature.gd puppet var floats: Dictionary = {} ## owner peer -> bobber.gd puppet var _seen: Dictionary = {} ## cid or "f" -> last time seen (s) var _root: Node3D var _howl_player: AudioStreamPlayer3D var projectiles_seen := 0 ## tests read these var howls_heard := 0 # --- server ---------------------------------------------------------------------- func _server_world() -> Node3D: return WorldNet.world if Net.is_server() and WorldNet.world != null and is_instance_valid(WorldNet.world) else null ## The server's creature with id `cid`, alive or a carcass. func creature(cid: int) -> Node3D: var c: Node3D = _by_cid.get(cid) return c if c != null and is_instance_valid(c) and not c.is_queued_for_deletion() else null func _process(delta: float) -> void: var w := _server_world() if w != null: _server_tick(w, delta) elif world != null: _client_tick() func _server_tick(w: Node3D, delta: float) -> void: if not w.launched.is_connected(_on_launched): w.launched.connect(_on_launched) _send_left -= delta if _send_left > 0.0: return _send_left = SEND_INTERVAL var all: Array = [] _by_cid.clear() for group in ["creature", "carcass"]: for c in w.get_tree().get_nodes_in_group(group): if not c.has_meta("cid"): c.set_meta("cid", _next_cid) _next_cid += 1 _by_cid[int(c.get_meta("cid"))] = c all.append(c) var fl: Array = [] for p in w.bobbers: var b: Node3D = w.bobbers[p] if is_instance_valid(b) and is_instance_valid(p): fl.append([int(p.peer), b.global_position, b.state]) if all.is_empty() and fl.is_empty(): return # Packed tight and split, so no packet goes over the MTU (a lost # fragment loses the whole unreliable packet). var i := 0 while i < maxi(all.size(), 1): var chunk := PackedByteArray() chunk.resize(mini(PER_PACKET, all.size() - i) * ENTRY) var o := 0 for c in all.slice(i, i + PER_PACKET): _encode(chunk, o, c) o += ENTRY for peer in WorldNet._ready_peers: _s2c_life.rpc_id(peer, chunk, fl if i == 0 else []) i += PER_PACKET const PER_PACKET := 40 const ENTRY := 23 const KIND_IDS := ["rabbit", "deer", "boar", "wolf", "alpha"] const STATE_IDS := ["idle", "wander", "flee", "charge", "dead", "rest", "alert", "stalk", "lunge", "retreat", "home", "defend", "bash"] ## One creature in 23 bytes: id, kind, state, position, facing, speed, health. static func _encode(buf: PackedByteArray, o: int, c: Node3D) -> void: buf.encode_u32(o, int(c.get_meta("cid"))) buf.encode_u8(o + 4, maxi(KIND_IDS.find(c.kind), 0)) buf.encode_u8(o + 5, maxi(STATE_IDS.find(c.state), 0)) var p: Vector3 = c.global_position buf.encode_float(o + 6, p.x) buf.encode_float(o + 10, p.y) buf.encode_float(o + 14, p.z) buf.encode_u16(o + 18, int(fposmod(c.yaw, TAU) / TAU * 65535.0)) buf.encode_u8(o + 20, clampi(int(c.speed * 20.0), 0, 255)) buf.encode_u16(o + 21, clampi(int(c.hp * 100.0), 0, 65535)) static func _decode(buf: PackedByteArray, o: int) -> Array: return [buf.decode_u32(o), KIND_IDS[mini(buf.decode_u8(o + 4), KIND_IDS.size() - 1)], Vector3(buf.decode_float(o + 6), buf.decode_float(o + 10), buf.decode_float(o + 14)), buf.decode_u16(o + 18) / 65535.0 * TAU, STATE_IDS[mini(buf.decode_u8(o + 5), STATE_IDS.size() - 1)], buf.decode_u8(o + 20) / 20.0, buf.decode_u16(o + 21) / 100.0] func _on_launched(id: String, origin: Vector3, vel: Vector3, shooter: Node3D) -> void: var by := int(shooter.peer) if shooter != null and is_instance_valid(shooter) and shooter.get("peer") != null else 0 for peer in WorldNet._ready_peers: _s2c_launched.rpc_id(peer, id, origin, vel, by) ## The pack howls (wolf_pack.gd): everyone hears it, from where it is. func howl(at: Vector3) -> void: for peer in WorldNet._ready_peers: _s2c_howl.rpc_id(peer, at) # --- client ---------------------------------------------------------------------- func join_world(w: Node3D) -> void: world = w puppets.clear() floats.clear() _seen.clear() projectiles_seen = 0 howls_heard = 0 _root = Node3D.new() _root.name = "NetCreatures" w.add_child(_root) func leave_world() -> void: if not Net.is_server(): world = null puppets.clear() floats.clear() _root = null static func _now() -> float: return Time.get_ticks_msec() / 1000.0 func _alive_world() -> bool: return world != null and is_instance_valid(world) and _root != null and is_instance_valid(_root) ## Anything the server has stopped sending goes. func _client_tick() -> void: if not _alive_world(): return var now := _now() for cid in puppets.keys(): if now - float(_seen.get(cid, 0.0)) > FORGET_AFTER: var c: Variant = puppets[cid] puppets.erase(cid) if is_instance_valid(c): (c as Node).queue_free() for peer in floats.keys(): if now - float(_seen.get("f%d" % peer, 0.0)) > FORGET_AFTER * 0.6: _drop_float(peer) func _drop_float(peer: int) -> void: var b: Variant = floats.get(peer) # (may already be freed: untyped) floats.erase(peer) if is_instance_valid(b): (b as Node).queue_free() if peer == Net.local_peer and world != null and is_instance_valid(world): world.bobber = null @rpc("authority", "call_remote", "unreliable_ordered") func _s2c_life(packed: PackedByteArray, fl: Array) -> void: if not _alive_world(): return var now := _now() var list: Array = [] for o in range(0, packed.size() - ENTRY + 1, ENTRY): list.append(_decode(packed, o)) for e in list: var cid := int(e[0]) var kind := String(e[1]) if not Creature.KINDS.has(kind): continue var got: Variant = puppets.get(cid) var c: Node3D = got if is_instance_valid(got) else null if c == null: c = Creature.new() c.puppet = true c.setup(kind, world, e[2], cid) c.set_meta("cid", cid) c.yaw = float(e[3]) _root.add_child(c) c.global_position = e[2] c.rotation.y = c.yaw puppets[cid] = c c.net_update(e[2], float(e[3]), String(e[4]), float(e[5]), float(e[6])) _seen[cid] = now for f in fl: _float_update(int(f[0]), f[1], String(f[2])) _seen["f%d" % int(f[0])] = now func _float_update(peer: int, pos: Vector3, state: String) -> void: var got: Variant = floats.get(peer) var b: Node3D = got if is_instance_valid(got) else null if b == null: b = Bobber.new() b.puppet = true b.setup(world, world.player, pos, Vector3.ZERO, 0) _root.add_child(b) floats[peer] = b if peer == Net.local_peer: world.bobber = b # hands.gd: the prompt, the line from our rod b.global_position = pos b.state = state if peer != Net.local_peer: # Someone else's line: from about where their rod would be. var av: Node3D = world.net_sync.avatars.get(peer) if world.net_sync != null else null if av != null and is_instance_valid(av): b.tip = av.global_position + Vector3(0, 1.5, 0) + (-av.global_basis.z) * 0.8 else: b.tip = pos + Vector3(0, 0.1, 0) @rpc("authority", "call_remote", "reliable") func _s2c_launched(id: String, origin: Vector3, vel: Vector3, by: int) -> void: if not _alive_world(): return projectiles_seen += 1 var pr: Node3D = Projectile.new() pr.visual = true pr.setup(id, world, origin, vel, 0.0, world.player if by == Net.local_peer else null) _root.add_child(pr) @rpc("authority", "call_remote", "reliable") func _s2c_howl(at: Vector3) -> void: if not _alive_world(): return howls_heard += 1 if DisplayServer.get_name() == "headless": return if _howl_player == null or not is_instance_valid(_howl_player): _howl_player = AudioStreamPlayer3D.new() _howl_player.stream = WolfPack.howl_stream() _howl_player.unit_size = 30.0 _howl_player.max_distance = 400.0 _howl_player.volume_db = 2.0 _root.add_child(_howl_player) _howl_player.global_position = at _howl_player.pitch_scale = randf_range(0.9, 1.1) _howl_player.play()