Commit ce1457fd authored by Benjamin Lee's avatar Benjamin Lee 💬
Browse files

Clients can now change game settings.

parent 2bfbf3b3
use crate::game::{
Event,
GameSettings,
GetPlayer,
Input,
InterpolatedSnapshot,
......@@ -17,6 +18,7 @@ use nalgebra::Point2;
use parking_lot::Mutex;
use std::borrow::Cow;
use std::collections::{HashMap, VecDeque};
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::Arc;
use std::time::Instant;
......@@ -38,15 +40,23 @@ pub struct Game {
players: HashMap<PlayerId, StaticPlayerState>,
snapshots: VecDeque<(Snapshot, Instant)>,
round: RoundState,
settings: GameSettings,
settings_handle: Arc<SettingsHandle>,
cursor: Arc<Mutex<Point2<f32>>>,
events: Receiver<Event>,
/// Player id for this client.
player_id: PlayerId,
}
pub struct SettingsHandle {
dirty: AtomicBool,
settings: Mutex<GameSettings>,
}
pub struct GameHandle {
events: Sender<Event>,
cursor: Arc<Mutex<Point2<f32>>>,
pub settings: Arc<SettingsHandle>,
}
impl<'a, 'b> GetPlayer for &'b Player<'a> {
......@@ -128,10 +138,28 @@ impl GameHandle {
}
}
impl SettingsHandle {
pub fn dirty(&self) -> Option<GameSettings> {
if self.dirty.load(Ordering::SeqCst) {
self.dirty.store(false, Ordering::SeqCst);
let settings = self.settings.lock();
Some(*settings)
} else {
None
}
}
pub fn settings(&self) -> GameSettings {
let settings = self.settings.lock();
*settings
}
}
impl Game {
pub fn new(
players: HashMap<PlayerId, StaticPlayerState>,
snapshot: Snapshot,
settings: GameSettings,
player_id: PlayerId,
cursor: Point2<f32>,
) -> (Game, GameHandle) {
......@@ -139,6 +167,10 @@ impl Game {
snapshots.push_back((snapshot, Instant::now()));
let (events_tx, events_rx) = channel::bounded(16);
let cursor = Arc::new(Mutex::new(cursor));
let settings_handle = Arc::new(SettingsHandle {
dirty: AtomicBool::new(false),
settings: Mutex::new(settings),
});
let game = Game {
players,
snapshots,
......@@ -146,14 +178,31 @@ impl Game {
events: events_rx,
round: RoundState::default(),
player_id,
settings,
settings_handle: Arc::clone(&settings_handle),
};
let handle = GameHandle {
cursor,
events: events_tx,
settings: settings_handle,
};
(game, handle)
}
pub fn settings(&self) -> &GameSettings {
&self.settings
}
/// Modifies the game settings and flags the change to be sent to
/// the server.
pub fn set_settings(&mut self, settings: GameSettings) {
self.settings = settings;
let mut shared = self.settings_handle.settings.lock();
*shared = settings;
drop(shared);
self.settings_handle.dirty.store(true, Ordering::SeqCst);
}
/// Handles events from the server.
pub fn handle_events(&mut self) {
for event in self.events.try_iter() {
......@@ -162,6 +211,9 @@ impl Game {
info!("transitioning to round state {:?}", round);
self.round = round;
},
Event::Settings(settings) => {
self.settings = settings;
},
Event::NewPlayer {
id,
static_state,
......@@ -208,22 +260,26 @@ impl Game {
}
}
/// Removes any old snapshots that are no longer needed for
/// interpolation.
pub fn clean_old_snapshots(&mut self, time: Instant, delay: f32) {
let delayed_time = time - SNAPSHOT_RATE.mul_f64(delay.into());
while self.snapshots.len() > 1 && delayed_time > self.snapshots[1].1 {
// Yay for short circuiting &&
self.snapshots.pop_front();
}
}
/// Interpolates snapshots with delay and returns the resulting
/// set of player states.
pub fn interpolated_players(
&mut self,
&self,
time: Instant,
cursor: Point2<f32>,
delay: f32,
) -> Players<InterpolatedSnapshot> {
let delayed_time = time - SNAPSHOT_RATE.mul_f64(delay.into());
// Get rid of old snapshots.
while self.snapshots.len() > 1 && delayed_time > self.snapshots[1].1 {
// Yay for short circuiting &&
self.snapshots.pop_front();
}
let (ref old, old_time) = self.snapshots[0];
let snapshot = match self.snapshots.get(1) {
Some(&(ref new, new_time)) => {
......
......@@ -14,11 +14,6 @@ pub mod snapshot;
pub use self::snapshot::*;
pub const BALL_RADIUS: f32 = 0.15;
pub const CURSOR_RADIUS: f32 = 0.05;
const SPRING_CONSTANT: f32 = 8.0;
const BALL_START_DISTANCE: f32 = 0.3;
const BALL_START_SPEED: f32 = 1.0;
pub type PlayerId = u16;
/// Finite state machine for the round state.
......@@ -35,9 +30,33 @@ pub enum RoundState {
PostRound(f32),
}
#[derive(Copy, Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct GameSettings {
pub ball_radius: f32,
pub cursor_radius: f32,
pub spring_constant: f32,
pub ball_start_distance: f32,
pub ball_start_speed: f32,
pub bounds_radius: f32,
}
impl Default for GameSettings {
fn default() -> GameSettings {
GameSettings {
ball_radius: 0.15,
cursor_radius: 0.05,
spring_constant: 8.0,
ball_start_distance: 0.3,
ball_start_speed: 1.0,
bounds_radius: 1.0,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum Event {
RoundState(RoundState),
Settings(GameSettings),
NewPlayer {
id: PlayerId,
static_state: StaticPlayerState,
......@@ -75,36 +94,47 @@ pub struct PlayerState {
impl Ball {
/// Gets the starting state of the ball for a given starting
/// cursor position.
pub fn starting(cursor: Point2<f32>) -> Ball {
pub fn starting(cursor: Point2<f32>, settings: &GameSettings) -> Ball {
let cursor_dir = (cursor - Point2::origin()).normalize();
let mut position = cursor + cursor_dir * BALL_START_DISTANCE;
if nalgebra::distance(&position, &Point2::origin()) > 1.0 - BALL_RADIUS
let mut position = cursor + cursor_dir * settings.ball_start_distance;
let max_dist = settings.bounds_radius - settings.ball_radius;
if nalgebra::distance_squared(&position, &Point2::origin()) >
max_dist * max_dist
{
position.coords.normalize_mut();
position *= 1.0 - BALL_RADIUS;
position *= max_dist;
}
Ball {
position,
velocity: cursor_dir * BALL_START_SPEED,
velocity: cursor_dir * settings.ball_start_speed,
}
}
/// Steps the ball forward in time using a provided cursor
/// location.
pub fn tick(&mut self, dt: f32, cursor: Option<Point2<f32>>) {
pub fn tick(
&mut self,
dt: f32,
cursor: Option<Point2<f32>>,
settings: &GameSettings,
) {
if let Some(cursor) = cursor {
let displacement = self.position - cursor;
self.velocity -= SPRING_CONSTANT * displacement * dt;
self.velocity -= settings.spring_constant * displacement * dt;
}
self.position += self.velocity * dt;
}
}
/// Clamps a cursor position within bounds.
pub fn clamp_cursor(cursor: Point2<f32>) -> Point2<f32> {
pub fn clamp_cursor(
cursor: Point2<f32>,
settings: &GameSettings,
) -> Point2<f32> {
let dist_sq = (cursor - Point2::origin()).norm_squared();
if dist_sq > (1.0 - CURSOR_RADIUS) {
(1.0 - CURSOR_RADIUS) * cursor / dist_sq.sqrt()
let max_dist = settings.bounds_radius - settings.cursor_radius;
if dist_sq > max_dist * max_dist {
max_dist * cursor / dist_sq.sqrt()
} else {
cursor
}
......@@ -115,10 +145,10 @@ impl PlayerState {
///
/// The player's ball is placed a set distance further away from
/// the origin than the cursor.
pub fn new(cursor: Point2<f32>) -> PlayerState {
pub fn new(cursor: Point2<f32>, settings: &GameSettings) -> PlayerState {
PlayerState {
cursor: Some(cursor),
ball: Ball::starting(cursor),
ball: Ball::starting(cursor, settings),
}
}
......@@ -130,8 +160,8 @@ impl PlayerState {
}
/// Steps the player forward in time.
pub fn tick(&mut self, dt: f32) {
self.ball.tick(dt, self.cursor);
pub fn tick(&mut self, dt: f32, settings: &GameSettings) {
self.ball.tick(dt, self.cursor, settings);
}
/// Returns whether the player is still alive.
......@@ -150,7 +180,7 @@ pub trait GetPlayer {
fn static_state(self) -> Self::StaticState;
/// Generates a set of circles to draw this player.
fn draw(self, scale: f32) -> SmallVec<[Circle; 2]>
fn draw(self, scale: f32, settings: &GameSettings) -> SmallVec<[Circle; 2]>
where
Self: Sized + Copy,
{
......@@ -160,14 +190,14 @@ pub trait GetPlayer {
// Ball
circles.push(Circle {
center: state.ball.position * scale,
radius: BALL_RADIUS * scale,
radius: settings.ball_radius * scale,
color,
});
if let Some(cursor) = state.cursor {
// Cursor, if alive
circles.push(Circle {
center: cursor * scale,
radius: CURSOR_RADIUS * scale,
radius: settings.cursor_radius * scale,
color,
});
}
......
use crate::game::{Ball, BALL_RADIUS, CURSOR_RADIUS};
use crate::game::{Ball, GameSettings};
use nalgebra::{Point2, Vector2};
#[derive(Debug, Copy, Clone)]
......@@ -45,20 +45,18 @@ impl<V> Circle<V> {
}
/// Returns the physics circle corresponding to the boundary.
pub fn bounds() -> Circle<Static> {
Circle::inner(1.0, Point2::origin(), Static)
pub fn bounds(settings: &GameSettings) -> Circle<Static> {
Circle::inner(settings.bounds_radius, Point2::origin(), Static)
}
/// Returns the physics circle corresponding to a given cursor
/// position.
pub fn cursor(cursor: Point2<f32>) -> Circle<Static> {
Circle::outer(CURSOR_RADIUS, cursor, Static)
pub fn cursor(cursor: Point2<f32>, settings: &GameSettings) -> Circle<Static> {
Circle::outer(settings.cursor_radius, cursor, Static)
}
impl From<Ball> for Circle<Vector2<f32>> {
fn from(ball: Ball) -> Circle<Vector2<f32>> {
Circle::outer(BALL_RADIUS, ball.position, ball.velocity)
}
pub fn ball(ball: Ball, settings: &GameSettings) -> Circle<Vector2<f32>> {
Circle::outer(settings.ball_radius, ball.position, ball.velocity)
}
impl From<Circle<Vector2<f32>>> for Ball {
......
......@@ -3,6 +3,7 @@ use crate::game::{
step_dt,
Ball,
Event,
GameSettings,
GetPlayer,
PlayerId,
PlayerState,
......@@ -56,6 +57,7 @@ pub struct Player {
pub struct Game {
pub players: HashMap<PlayerId, Player>,
pub round: RoundState,
pub settings: GameSettings,
next_id: PlayerId,
}
......@@ -107,7 +109,7 @@ impl Game {
}
if !self.round.running() {
player.state.ball = Ball::starting(cursor);
player.state.ball = Ball::starting(cursor, &self.settings);
}
true
}
......@@ -135,10 +137,13 @@ impl Game {
return transition.map(Event::RoundState).into_iter();
}
// To avoid borrow issues.
let settings = &self.settings;
for dt in step_dt(dt, 1.0 / 60.0) {
// Calculate individual ball spring physics.
for player in self.players.values_mut() {
player.state.tick(dt);
player.state.tick(dt, settings);
}
// Check for collisions between balls.
......@@ -148,8 +153,10 @@ impl Game {
// This ensures every unordered pair only gets checked
// once.
if id_a < id_b {
let mut circle_a = a.state.ball.into();
let mut circle_b = b.state.ball.into();
let mut circle_a =
physics::ball(a.state.ball, settings);
let mut circle_b =
physics::ball(b.state.ball, settings);
if resolve_collision(&mut circle_a, &mut circle_b) {
collisions.push((id_a, circle_a));
collisions.push((id_b, circle_b));
......@@ -166,8 +173,11 @@ impl Game {
// Check for collisions with walls.
for (&id, player) in self.players.iter_mut() {
let alive = player.state.alive();
let mut circle = player.state.ball.into();
if resolve_collision(&mut circle, &mut physics::bounds()) {
let mut circle = physics::ball(player.state.ball, settings);
if resolve_collision(
&mut circle,
&mut physics::bounds(settings),
) {
player.state.ball = circle.into();
if alive {
info!("{} killed {}", id, id);
......@@ -181,7 +191,7 @@ impl Game {
// Check for collisions with cursor.
for (&id, player) in self.players.iter() {
if let Some(cursor) = player.state.cursor {
let circle_cursor = physics::cursor(cursor);
let circle_cursor = physics::cursor(cursor, settings);
for (&id_ball, player_ball) in self.players.iter() {
if id == id_ball {
// Don't let players kill themselves.
......@@ -189,7 +199,8 @@ impl Game {
continue;
}
let circle_ball = player_ball.state.ball.into();
let circle_ball =
physics::ball(player_ball.state.ball, settings);
if check_collision(&circle_cursor, &circle_ball) {
info!("{} killed {}", id_ball, id);
deaths.push(id);
......@@ -260,7 +271,7 @@ impl Game {
color: Lch::new(75.0, 80.0, lab_hue).into(),
};
let player = Player {
state: PlayerState::new(cursor),
state: PlayerState::new(cursor, &self.settings),
static_state: static_state.clone(),
hue,
};
......
use crate::debug::{NetworkStats, NETWORK_STATS_RATE};
use crate::game::{
client::{Game, GameHandle},
GameSettings,
Input,
};
use crate::networking::connection::{Connection, HEADER_BYTES};
......@@ -16,13 +17,13 @@ use crate::networking::{
PING_RATE,
};
use crossbeam::channel::{self, Receiver, Sender};
use log::{error, info, trace, warn};
use log::{debug, error, info, trace, warn};
use mio::net::UdpSocket;
use mio::{Event, Poll, PollOpt, Ready, Registration, SetReadiness, Token};
use mio_extras::timer::{self, Timeout, Timer};
use nalgebra::Point2;
use serde::{Deserialize, Serialize};
use std::collections::VecDeque;
use std::collections::{HashMap, VecDeque};
use std::io::{self, Cursor};
use std::net::SocketAddr;
use std::thread::{self, JoinHandle};
......@@ -48,6 +49,7 @@ pub enum ClientPacket {
/// Cursor position when connecting.
cursor: Point2<f32>,
},
Settings(GameSettings),
Input(Input),
Disconnect,
Ping,
......@@ -81,6 +83,7 @@ pub struct Client {
poll: Poll,
timeout: Timeout,
connection: Connection,
reliable: HashMap<u32, ClientPacket>,
state: ClientState,
_shutdown: Registration,
/// Marks after `shutdown` has been received, to shutdown when the
......@@ -137,6 +140,33 @@ impl Drop for ClientHandle {
}
}
impl ClientPacket {
fn reliable(&self) -> bool {
match self {
ClientPacket::Handshake {
..
} => true,
ClientPacket::Settings(_) => true,
ClientPacket::Input(_) => false,
ClientPacket::Disconnect => false,
ClientPacket::Ping => false,
ClientPacket::Pong(_) => false,
}
}
fn resend(&self, game: Option<&GameHandle>) -> bool {
match self {
ClientPacket::Handshake {
..
} => true,
ClientPacket::Settings(settings) => {
&game.as_ref().unwrap().settings.settings() == settings
},
_ => self.reliable(),
}
}
}
impl EventHandler for Client {
fn poll(&self) -> &Poll {
&self.poll
......@@ -270,6 +300,7 @@ impl Client {
poll,
timeout,
connection: Connection::default(),
reliable: HashMap::new(),
state: ClientState::Connecting {
done,
cursor,
......@@ -452,9 +483,20 @@ impl Client {
let (_, interval) = tick.next(now);
self.timer.set_timeout(interval, TimeoutState::Tick);
let packet = ClientPacket::Input(game.latest_input());
trace!("sending tick packet to server: {:?}", packet);
self.send(&packet)?;
let tick_packet = ClientPacket::Input(game.latest_input());
trace!("sending tick packet to server: {:?}", tick_packet);
// If the settings have changed, send that as well.
if let Some(settings) = game.settings.dirty() {
let settings_packet = ClientPacket::Settings(settings);
trace!(
"sending settings update packet to server: {:?}",
settings_packet
);
self.send(&settings_packet)?;
}
self.send(&tick_packet)?;
},
// We shouldn't really be sending ticks in any other state.
_ => unreachable!(),
......@@ -472,15 +514,40 @@ impl Client {
return Ok(Err(RecvError::PacketTooLarge(bytes_read)));
}
let packet = &self.recv_buffer[0..bytes_read];
let (packet, sequence, _, lost) =
let (packet, sequence, acks, lost) =
match self.connection.decode(Cursor::new(packet)) {
Ok(result) => result,
Err(err) => return Ok(Err(err)),
};
if let Some(ref mut stats) = self.stats {
stats.next.packets_lost += lost.len() as u16;
}
// Remove acked packets from the reliable packet buffer.
for ack in acks.iter() {
self.reliable.remove(&ack);
}
// Possibly resend any lost packets.
for lost in lost.into_iter() {
if let Some(packet) = self.reliable.remove(&lost) {
let game = match self.state {
ClientState::Connecting {
..
} => None,
ClientState::Connected {
ref game,
..
} => Some(game),
};
if packet.resend(game) {
debug!("resending lost packet from client: {:?}", packet);
self.send(&packet)?;
}
}
}
let transition = match self.state {
ClientState::Connecting {
ref mut done,
......@@ -489,11 +556,12 @@ impl Client {
match packet {
ServerPacket::Handshake {
players,
settings,
snapshot,
id,
} => {
let (game, game_handle) =
Game::new(players, snapshot, id, *cursor);
Game::new(players, snapshot, settings, id, *cursor);
let tick = Interval::new(TICK_RATE);
let ping = Interval::new(PING_RATE);
// Start the timer for sending input ticks and pings.
......@@ -566,6 +634,10 @@ impl Client {
self.send_queue.push_back(packet);
self.reregister_socket(true)?;
if contents.reliable() {
self.reliable.insert(sequence, contents.clone());
}
Ok(sequence)
}
}
......@@ -2,6 +2,7 @@ use crate::game::{
clamp_cursor,
server::Game,
Event,
GameSettings,
GetPlayer,
PlayerId,
RoundStateKind,
......@@ -57,6 +58,7 @@ pub enum ServerPacket {
Pong(u32),
Handshake {
id: PlayerId,
settings: GameSettings,
players: HashMap<PlayerId, StaticPlayerState>,
snapshot: Snapshot,
},
......@@ -135,6 +137,7 @@ impl ServerPacket {
} => true,
Event::RemovePlayer(_) => true,
Event::RoundState(_) => true,
Event::Settings(_) => true,
Event::Snapshot(_) => false,
}
},
......@@ -153,6 +156,11 @@ impl ServerPacket {
// changed again since it was sent.
RoundStateKind::from(round) == RoundStateKind::from(game.round)
},