Files
Cinderwild/scripts/net/creature_net.gd

265 lines
9.8 KiB
GDScript

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 BearScript = preload("res://scripts/creatures/bear.gd")
const CrowScript = preload("res://scripts/creatures/crow.gd")
const GuardScript = preload("res://scripts/creatures/guard.gd")
const StagScript = preload("res://scripts/creatures/old_stag.gd") # (rears and lowers its antlers) # (falls apart in its own way)
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<peer>" -> 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", "bear", "crow", "fox", "guard", "old_stag", "spirit_wolf"]
const STATE_IDS := ["idle", "wander", "flee", "charge", "dead", "rest", "alert", "stalk", "lunge", "retreat", "home", "defend", "bash",
"warn", "raid", "eat", "fly", "fly_off", "peck", "sneak", "carry", "circle", "telegraph", "stunned", "summon", "stomp"]
## 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(roundi(c.hp * 10.0), 0, 65535)) # (tenths: up to 6553, the Old Stag has 900)
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) / 10.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:
# (Kinds with their own look: the bear rears up, a crow flaps.)
var script: Script = {"bear": BearScript, "crow": CrowScript, "guard": GuardScript, "old_stag": StagScript}.get(kind, Creature)
c = script.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()