use quaso::{
GameLauncher,
assets::{make_directory_database, shader::ShaderAsset},
config::Config,
context::GameContext,
game::{GameInstance, GameState, GameStateChange},
game_state_custom_event,
multiplayer::{
GameMultiplayerChange, GameNetwork,
client_server::ClientServerMultiplayer,
tcp::{TcpClientConnection, TcpServerConnection},
universal::UniversalMultiplayerGameState,
},
third_party::{
fontdue::layout::{HorizontalAlign, VerticalAlign},
raui_core::{
layout::CoordsMappingScaling,
widget::{
component::{
image_box::ImageBoxProps, interactive::navigation::NavItemActive,
text_box::TextBoxProps,
},
unit::text::{TextBoxFont, TextBoxHorizontalAlign, TextBoxVerticalAlign},
utils::Color,
},
},
raui_immediate_widgets::core::{
containers::{content_box, nav_horizontal_box},
image_box,
interactive::{ImmediateButton, button},
text_box,
},
spitfire_draw::{
context::DrawContext,
sprite::{Sprite, SpriteTexture},
text::Text,
utils::{Drawable, ShaderRef, TextureRef, Vertex},
},
spitfire_glow::{
graphics::{CameraScaling, GraphicsTarget, Shader},
renderer::GlowTextureFiltering,
},
spitfire_input::{
ArrayInputCombinator, CardinalInputCombinator, InputActionRef, InputAxisRef,
InputConsume, InputMapping, MouseButton, VirtualAction, VirtualAxis,
},
tehuti::{
channel::{ChannelId, ChannelMode},
codec::postcard::PostcardCodec,
event::{Receiver, unbounded},
peer::{Peer, PeerBuilder, PeerId, PeerInfo, PeerRoleId, TypedPeer, TypedPeerRole},
replica::{
Replica, ReplicaApplyChanges, ReplicaCollectChanges, ReplicaId, ReplicationBuffer,
},
replication::{HashReplicated, primitives::RepF32},
},
tehuti_client_server::{
authority::PureAuthority,
controller::{Controller, ControllerEvent},
puppet::{Puppet, Puppetable},
},
tehuti_timeline::time::TimeStamp,
tracing::{debug, level_filters::LevelFilter},
tracing_subscriber::{
Layer, fmt::layer, layer::SubscriberExt, registry, util::SubscriberInitExt,
},
vek::{Rgba, Vec2},
windowing::event::VirtualKeyCode,
},
};
use std::{collections::HashMap, error::Error, sync::Arc};
const ADDRESS: &str = "127.0.0.1:12345";
const PLAYER_ROLE: PeerRoleId = PeerRoleId::new(1);
const PLAYER_EVENT_CHANNEL: ChannelId = ChannelId::new(0);
const PLAYER_CHANGE_CHANNEL: ChannelId = ChannelId::new(1);
const SPEED: f32 = 100.0;
const COLOR_WHITE: Color = Color {
r: 1.0,
g: 1.0,
b: 1.0,
a: 1.0,
};
const COLOR_BLACK: Color = Color {
r: 0.0,
g: 0.0,
b: 0.0,
a: 1.0,
};
fn main() -> Result<(), Box<dyn Error>> {
registry()
.with(
layer()
.with_writer(std::io::stdout)
.with_filter(LevelFilter::DEBUG),
)
.init();
GameLauncher::new(GameInstance::new(Preloader).setup_assets(|assets| {
*assets = make_directory_database("./resources/").unwrap();
}))
.title("Netcode: Replicated Client-Server")
.config(Config::load_from_file("./resources/GameConfig.toml")?)
.run();
Ok(())
}
#[derive(Default)]
struct Preloader;
impl GameState for Preloader {
fn enter(&mut self, context: GameContext) {
context.graphics.state.color = [0.2, 0.2, 0.2, 1.0];
context.graphics.state.main_camera.screen_alignment = 0.5.into();
context.graphics.state.main_camera.scaling = CameraScaling::FitVertical(500.0);
context.gui.coords_map_scaling = CoordsMappingScaling::FitVertical(500.0);
context
.assets
.spawn(
"shader://color",
(ShaderAsset::new(
Shader::COLORED_VERTEX_2D,
Shader::PASS_FRAGMENT,
),),
)
.unwrap();
context
.assets
.spawn(
"shader://image",
(ShaderAsset::new(
Shader::TEXTURED_VERTEX_2D,
Shader::TEXTURED_FRAGMENT,
),),
)
.unwrap();
context
.assets
.spawn(
"shader://text",
(ShaderAsset::new(Shader::TEXT_VERTEX, Shader::TEXT_FRAGMENT),),
)
.unwrap();
context.assets.ensure("font://roboto.ttf").unwrap();
context.assets.ensure("texture://ferris.png").unwrap();
}
fn update(&mut self, context: GameContext, _: f32) {
if !context.assets.is_busy() {
*context.state_change = GameStateChange::Swap(Box::new(Lobby::default()));
}
}
}
#[derive(Default)]
struct Lobby {
exit: InputActionRef,
}
impl GameState for Lobby {
fn enter(&mut self, context: GameContext) {
let pointer_x = InputAxisRef::default();
let pointer_y = InputAxisRef::default();
let pointer_trigger = InputActionRef::default();
self.exit = InputActionRef::default();
context.gui.interactions.inputs.pointer_position =
ArrayInputCombinator::new([pointer_x.clone(), pointer_y.clone()]);
context.gui.interactions.inputs.pointer_trigger = pointer_trigger.clone();
context.input.push_mapping(
InputMapping::default()
.consume(InputConsume::Hit)
.axis(VirtualAxis::MousePositionX, pointer_x)
.axis(VirtualAxis::MousePositionY, pointer_y)
.action(
VirtualAction::MouseButton(MouseButton::Left),
pointer_trigger,
)
.action(
VirtualAction::KeyButton(VirtualKeyCode::Escape),
self.exit.clone(),
),
);
}
fn exit(&mut self, context: GameContext) {
context.input.pop_mapping();
}
fn update(&mut self, context: GameContext, _delta_time: f32) {
if let Some(multiplayer) = context.multiplayer::<ClientServerMultiplayer>()
&& multiplayer.is_initialized()
{
*context.state_change = GameStateChange::Swap(Box::new(State::default()));
}
}
fn fixed_update(&mut self, context: GameContext, _delta_time: f32) {
if self.exit.get().is_pressed() {
*context.state_change = GameStateChange::Pop;
}
}
fn draw_gui(&mut self, context: GameContext) {
nav_horizontal_box(NavItemActive, || {
if context.multiplayer::<ClientServerMultiplayer>().is_some() {
text_box(TextBoxProps {
text: "ESTABLISHING CONNECTION...".to_owned(),
font: TextBoxFont {
name: "roboto.ttf".to_owned(),
size: 24.0,
},
color: COLOR_WHITE,
horizontal_align: TextBoxHorizontalAlign::Center,
vertical_align: TextBoxVerticalAlign::Middle,
..Default::default()
});
} else {
let host = lobby_button("HOST");
let join = lobby_button("JOIN");
let is_server = if host.trigger_start() {
Some(true)
} else if join.trigger_start() {
Some(false)
} else {
None
};
if let Some(is_server) = is_server {
debug!(
"Starting {}...",
if is_server { "server" } else { "client" }
);
context.network.clear_connections();
let mut network = GameNetwork::new(Arc::new(
PureAuthority::peer_factory(is_server)
.unwrap()
.with_typed::<PlayerController>(),
));
if is_server {
network.add_connection(TcpServerConnection::listen(ADDRESS).unwrap());
} else {
network.add_connection(TcpClientConnection::connect(ADDRESS).unwrap());
}
if let Some(multiplayer) = ClientServerMultiplayer::new(&network) {
debug!("{} started", if is_server { "Server" } else { "Client" });
*context.network = network;
*context.multiplayer_change = GameMultiplayerChange::Set(Box::new(
multiplayer.with_ticks_per_second(30),
));
}
}
}
});
}
}
fn lobby_button(label: &str) -> ImmediateButton {
button(NavItemActive, |state| {
let (bg_color, text_color) = if state.state.selected {
(COLOR_WHITE, COLOR_BLACK)
} else {
(COLOR_BLACK, COLOR_WHITE)
};
content_box((), || {
image_box(ImageBoxProps::colored(bg_color));
text_box(TextBoxProps {
text: label.to_owned(),
font: TextBoxFont {
name: "roboto.ttf".to_owned(),
size: 24.0,
},
color: text_color,
horizontal_align: TextBoxHorizontalAlign::Center,
vertical_align: TextBoxVerticalAlign::Middle,
..Default::default()
});
});
})
}
#[derive(Default)]
struct State {
players: HashMap<PeerId, Player>,
movement: CardinalInputCombinator,
exit: InputActionRef,
}
impl GameState for State {
fn enter(&mut self, mut context: GameContext) {
let move_left = InputActionRef::default();
let move_right = InputActionRef::default();
let move_up = InputActionRef::default();
let move_down = InputActionRef::default();
self.movement = CardinalInputCombinator::new(
move_left.clone(),
move_right.clone(),
move_up.clone(),
move_down.clone(),
);
context.input.push_mapping(
InputMapping::default()
.consume(InputConsume::Hit)
.action(
VirtualAction::KeyButton(VirtualKeyCode::A),
move_left.clone(),
)
.action(
VirtualAction::KeyButton(VirtualKeyCode::D),
move_right.clone(),
)
.action(VirtualAction::KeyButton(VirtualKeyCode::W), move_up.clone())
.action(
VirtualAction::KeyButton(VirtualKeyCode::S),
move_down.clone(),
)
.action(VirtualAction::KeyButton(VirtualKeyCode::Left), move_left)
.action(VirtualAction::KeyButton(VirtualKeyCode::Right), move_right)
.action(VirtualAction::KeyButton(VirtualKeyCode::Up), move_up)
.action(VirtualAction::KeyButton(VirtualKeyCode::Down), move_down)
.action(
VirtualAction::KeyButton(VirtualKeyCode::Escape),
self.exit.clone(),
),
);
if let Some(multiplayer) = context.multiplayer_mut::<ClientServerMultiplayer>() {
multiplayer.process_lifecycle_events = true;
let _ = multiplayer.authority.create_peer(PlayerController::ROLE_ID);
} else {
*context.state_change = GameStateChange::Swap(Box::new(Lobby::default()));
}
}
fn exit(&mut self, context: GameContext) {
context.input.pop_mapping();
*context.network = GameNetwork::default();
*context.multiplayer_change = GameMultiplayerChange::Reset;
}
fn fixed_update(&mut self, context: GameContext, _delta_time: f32) {
if self.exit.get().is_pressed() {
*context.state_change = GameStateChange::Swap(Box::new(Lobby::default()));
}
}
fn draw(&mut self, context: GameContext) {
for player in self.players.values() {
if let Some(character) = &player.character {
character.draw(context.draw, context.graphics);
}
}
}
fn multiplayer_peer_added(&mut self, mut context: GameContext, peer: Peer) {
let (replica_added_sender, replica_added_receiver) = unbounded();
let (replica_removed_sender, replica_removed_receiver) = unbounded();
let mut controller = Controller::new(
peer,
PLAYER_EVENT_CHANNEL,
Some(PLAYER_CHANGE_CHANNEL),
None,
replica_added_sender,
replica_removed_sender,
)
.unwrap();
if let Some(multiplayer) = context.multiplayer_mut::<ClientServerMultiplayer>() {
let _ = controller.create_replica(&mut multiplayer.authority);
}
for player in self.players.values_mut() {
if let Some(character) = &mut player.character {
character.request_full_snapshot();
}
}
self.players.insert(
controller.info().peer_id,
Player {
controller,
character: None,
replica_added_receiver,
replica_removed_receiver,
},
);
}
fn multiplayer_peer_removed(&mut self, _context: GameContext, peer_id: PeerId) {
self.players.remove(&peer_id);
}
game_state_custom_event! {
trait(UniversalMultiplayerGameState)
}
}
impl UniversalMultiplayerGameState for State {
fn handle_inputs(&mut self, _context: GameContext, _current_tick: TimeStamp) {
let [x, y] = self.movement.get();
let movement = Vec2::new(x, y).try_normalized().unwrap_or_default() * SPEED;
for player in self.players.values_mut() {
if !player.controller.info().remote
&& let Some(character) = &mut player.character
{
character.velocity_x.0 = movement.x;
character.velocity_y.0 = movement.y;
}
}
}
fn tick(
&mut self,
_context: GameContext,
_current_tick: TimeStamp,
delta_time: f32,
_resimulating: bool,
) {
for player in self.players.values_mut() {
player.tick(delta_time);
}
}
}
struct Character {
peer_info: PeerInfo,
sprite: Sprite,
position_x: HashReplicated<RepF32>,
position_y: HashReplicated<RepF32>,
velocity_x: HashReplicated<RepF32>,
velocity_y: HashReplicated<RepF32>,
}
impl Character {
fn new(peer_info: PeerInfo, position: Vec2<f32>) -> Self {
Self {
peer_info,
sprite: Sprite::single(SpriteTexture {
sampler: "u_image".into(),
texture: TextureRef::name("ferris.png"),
filtering: GlowTextureFiltering::Linear,
})
.pivot(0.5.into())
.position(position)
.scale(0.25.into()),
position_x: HashReplicated::new(RepF32(position.x)),
position_y: HashReplicated::new(RepF32(position.y)),
velocity_x: Default::default(),
velocity_y: Default::default(),
}
}
fn tick_before(&mut self, delta_time: f32) {
self.position_x.0 += self.velocity_x.0 * delta_time;
self.position_y.0 += self.velocity_y.0 * delta_time;
}
fn tick_after(&mut self) {
self.sprite.transform.position.x = self.position_x.0;
self.sprite.transform.position.y = self.position_y.0;
}
}
impl Drawable for Character {
fn draw(&self, context: &mut DrawContext, graphics: &mut dyn GraphicsTarget<Vertex>) {
self.sprite.draw(context, graphics);
Text::new(ShaderRef::name("text"))
.text(self.peer_info.peer_id.id().to_string())
.font("roboto.ttf")
.size(20.0)
.position(Vec2::from(self.sprite.transform.position) - Vec2::new(0.0, 60.0))
.tint(if self.peer_info.remote {
Rgba::red()
} else {
Rgba::white()
})
.horizontal_align(HorizontalAlign::Center)
.vertical_align(VerticalAlign::Bottom)
.draw(context, graphics);
}
}
impl Puppetable for Character {
fn collect_changes(
&mut self,
mut collector: ReplicaCollectChanges,
full_snapshot: bool,
) -> Result<(), Box<dyn Error>> {
if full_snapshot {
HashReplicated::mark_changed(&mut self.position_x);
HashReplicated::mark_changed(&mut self.position_y);
}
collector
.scope()
.maybe_collect_replicated::<0, _, _>(&self.position_x)?;
collector
.scope()
.maybe_collect_replicated::<1, _, _>(&self.position_y)?;
collector
.scope()
.maybe_collect_replicated::<2, _, _>(&self.velocity_x)?;
collector
.scope()
.maybe_collect_replicated::<3, _, _>(&self.velocity_y)?;
Ok(())
}
fn apply_changes(&mut self, mut applicator: ReplicaApplyChanges) -> Result<(), Box<dyn Error>> {
applicator
.scope()
.maybe_apply_replicated::<0, _, _>(&mut self.position_x)?;
applicator
.scope()
.maybe_apply_replicated::<1, _, _>(&mut self.position_y)?;
applicator
.scope()
.maybe_apply_replicated::<2, _, _>(&mut self.velocity_x)?;
applicator
.scope()
.maybe_apply_replicated::<3, _, _>(&mut self.velocity_y)?;
self.sprite.transform.position.x = self.position_x.0;
self.sprite.transform.position.y = self.position_y.0;
Ok(())
}
}
struct Player {
controller: Controller,
character: Option<Puppet<Character>>,
replica_added_receiver: Receiver<Replica>,
replica_removed_receiver: Receiver<ReplicaId>,
}
impl Player {
fn tick(&mut self, delta_time: f32) {
let _ = self.controller.maintain();
for replica in self.replica_added_receiver.iter() {
self.character = Some(Puppet::new(
replica,
Character::new(*self.controller.info(), 0.0.into()),
));
}
for _ in self.replica_removed_receiver.iter() {
self.character = None;
}
if let Some(character) = &mut self.character {
character.tick_before(delta_time);
}
if let Some(puppet) = &mut self.character {
let _ = puppet.replicate();
}
if let Some(character) = &mut self.character {
character.tick_after();
}
}
}
struct PlayerController;
impl TypedPeerRole for PlayerController {
const ROLE_ID: PeerRoleId = PLAYER_ROLE;
}
impl TypedPeer for PlayerController {
fn builder(builder: PeerBuilder) -> Result<PeerBuilder, Box<dyn Error>> {
if builder.info().remote {
Ok(builder
.bind_read_write::<PostcardCodec<ControllerEvent>, ControllerEvent>(
PLAYER_EVENT_CHANNEL,
ChannelMode::ReliableOrdered,
None,
)
.bind_read::<ReplicationBuffer, ReplicationBuffer>(
PLAYER_CHANGE_CHANNEL,
ChannelMode::ReliableOrdered,
None,
))
} else {
Ok(builder
.bind_read_write::<PostcardCodec<ControllerEvent>, ControllerEvent>(
PLAYER_EVENT_CHANNEL,
ChannelMode::ReliableOrdered,
None,
)
.bind_write::<ReplicationBuffer, ReplicationBuffer>(
PLAYER_CHANGE_CHANNEL,
ChannelMode::ReliableOrdered,
None,
))
}
}
}