1
Fork 0

Compare commits

...

10 commits

8 changed files with 236 additions and 88 deletions

4
Cargo.lock generated
View file

@ -58,7 +58,7 @@ checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
[[package]] [[package]]
name = "client" name = "client"
version = "0.1.0" version = "1.0.0"
dependencies = [ dependencies = [
"gilrs", "gilrs",
"minifb", "minifb",
@ -609,7 +609,7 @@ checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184"
[[package]] [[package]]
name = "pirates" name = "pirates"
version = "0.1.0" version = "1.0.0"
dependencies = [ dependencies = [
"libm", "libm",
"nalgebra", "nalgebra",

View file

@ -4,8 +4,12 @@ members = [
"client", "client",
] ]
[profile.release]
lto = true
opt-level = 3
[profile.minsized] [profile.minsized]
inherits = "release" inherits = "release"
lto = true lto = true
opt-level = 'z' opt-level = 's'
strip = true strip = true

View file

@ -2,15 +2,32 @@
my [entry](https://js13kgames.com/entries/simple-sailing-simulator) to js13kgames 2023 my [entry](https://js13kgames.com/entries/simple-sailing-simulator) to js13kgames 2023
![Gameplay screenshot, a pixelated sailboat exits a well-protected bay](https://github.com/Speedy6451/simplesailing/assets/37423245/16b60975-08f3-4f62-b0df-f78ba95454f5)
> Brave the north wind and search for York, or simply explore. > Brave the north wind and search for York, or simply explore.
### Controls ### Controls
+/-: zoom |keyboard | controller | action
A: rudder left |---|---|---
D: rudder right |`A`|`D-Left`|rudder left
|`D`|`D-Right`|rudder right
||Left Stick X|rudder
|`+`|`D-Up`|zoom in
|`-`|`D-Down`|zoom out
||Right Stick Y|zoom
|`E`|`B`|raise sails
|`Q`|`A`|lower sails
||Left Stick Y + Left Bumper|control sails
|arrow keys|Right Stick + Right Bumper|pan camera
|`R`|`Y`|reset sailboat
|`/`|`X`|reset camera
|Esc||quit
Your sailboat travels fastest going perpendicular to the wind. Your sailboat travels fastest going perpendicular to the wind.
#### Installation
download the [latest release](https://github.com/Speedy6451/simplesailing/releases/latest)
#### Building from source #### Building from source

View file

@ -1,6 +1,6 @@
[package] [package]
name = "client" name = "client"
version = "0.1.0" version = "1.0.0"
edition = "2021" edition = "2021"
description = "A game about wind" description = "A game about wind"

View file

@ -1,8 +1,10 @@
use std::time::SystemTime;
use minifb::{Key, ScaleMode, Window, WindowOptions, Scale}; use minifb::{Key, ScaleMode, Window, WindowOptions, Scale};
extern crate pirates; extern crate pirates;
use pirates::{WIDTH, HEIGHT}; use pirates::{WIDTH, HEIGHT, Input};
#[cfg(feature = "gilrs")] #[cfg(feature = "gilrs")]
use gilrs::{Axis, Gilrs, Button}; use gilrs::{Axis, Gilrs, Button};
use pirates::Input::*;
fn main() { fn main() {
#[cfg(feature = "gilrs")] #[cfg(feature = "gilrs")]
@ -26,17 +28,17 @@ fn main() {
let mut buffer: Vec<u32> = Vec::with_capacity(WIDTH * HEIGHT); let mut buffer: Vec<u32> = Vec::with_capacity(WIDTH * HEIGHT);
fn keyboard_input(key: u8) { fn keyboard_input(key: Input) {
unsafe { unsafe {
pirates::KEYCODE[0] = key; pirates::KEYCODE[0] = key as u8;
pirates::keyboard_input(); pirates::keyboard_input();
} }
} }
fn analog_input(chan: u8, val: f32) { fn analog_input(chan: Input, val: f32) {
let val = (val * 127.0) as i8; let val = (val * 127.0) as i8;
unsafe { unsafe {
pirates::KEYCODE[0] = chan; pirates::KEYCODE[0] = chan as u8;
pirates::KEYCODE[1] = (val + 127) as u8; pirates::KEYCODE[1] = (val + 127) as u8;
pirates::keyboard_input(); pirates::keyboard_input();
} }
@ -45,9 +47,16 @@ fn main() {
#[cfg(feature = "gamepad")] #[cfg(feature = "gamepad")]
let mut gamepad_handle = None; let mut gamepad_handle = None;
let mut frame_start: SystemTime = SystemTime::now();
while window.is_open() && !window.is_key_down(Key::Escape) { while window.is_open() && !window.is_key_down(Key::Escape) {
let last_frame = frame_start.elapsed().unwrap().as_micros();
frame_start = SystemTime::now();
buffer.clear(); buffer.clear();
unsafe { unsafe {
pirates::LAST_FRAME_TIME = last_frame as f32 / 1000.0;
pirates::frame_entry(); pirates::frame_entry();
for pix in pirates::BUFFER { for pix in pirates::BUFFER {
// AABBGGRR to 00RRGGBB // AABBGGRR to 00RRGGBB
@ -66,43 +75,64 @@ fn main() {
#[cfg(feature = "gamepad")] #[cfg(feature = "gamepad")]
if let Some(gamepad) = gamepad_handle.map(|h| gilrs.gamepad(h)) { if let Some(gamepad) = gamepad_handle.map(|h| gilrs.gamepad(h)) {
gamepad.axis_data(Axis::LeftStickX).map(|axis| { gamepad.axis_data(Axis::LeftStickX).map(|axis| {
analog_input(1, axis.value()); analog_input(AxisRudder, axis.value());
}); });
if gamepad.is_pressed(Button::LeftTrigger) {
gamepad.axis_data(Axis::LeftStickY).map(|axis| {
analog_input(AxisSail, axis.value());
});
}
if gamepad.is_pressed(Button::South) {
keyboard_input(RaiseSail)
}
if gamepad.is_pressed(Button::East) {
keyboard_input(LowerSail)
}
if gamepad.is_pressed(Button::RightTrigger) { if gamepad.is_pressed(Button::RightTrigger) {
gamepad.axis_data(Axis::RightStickY).map(|axis| { gamepad.axis_data(Axis::RightStickY).map(|axis| {
analog_input(3, dbg!(axis.value())); analog_input(AxisPanY, axis.value());
}); });
gamepad.axis_data(Axis::RightStickX).map(|axis| { gamepad.axis_data(Axis::RightStickX).map(|axis| {
analog_input(4, dbg!(axis.value())); analog_input(AxisPanX, axis.value());
}); });
} else { } else {
gamepad.axis_data(Axis::RightStickY).map(|axis| { gamepad.axis_data(Axis::RightStickY).map(|axis| {
analog_input(0, dbg!(axis.value())); analog_input(AxisZoom, axis.value());
}); });
} }
if gamepad.is_pressed(Button::West) {
keyboard_input(ResetCamera)
}
if gamepad.is_pressed(Button::North) {
keyboard_input(ResetBoat)
}
if gamepad.is_pressed(Button::DPadLeft) { if gamepad.is_pressed(Button::DPadLeft) {
keyboard_input(65) keyboard_input(PanLeft)
} }
if gamepad.is_pressed(Button::DPadRight) { if gamepad.is_pressed(Button::DPadRight) {
keyboard_input(68) keyboard_input(PanRight)
} }
if gamepad.is_pressed(Button::DPadUp) { if gamepad.is_pressed(Button::DPadUp) {
keyboard_input(61) keyboard_input(ZoomIn)
} }
if gamepad.is_pressed(Button::DPadDown) { if gamepad.is_pressed(Button::DPadDown) {
keyboard_input(173) keyboard_input(ZoomOut)
} }
} }
window.get_keys().iter().for_each(|key| match key { window.get_keys().iter().for_each(|key| match key {
Key::A => keyboard_input(65), Key::A => keyboard_input(RudderLeft),
Key::D => keyboard_input(68), Key::D => keyboard_input(RudderRight),
Key::Equal => keyboard_input(61), Key::Equal => keyboard_input(ZoomIn),
Key::Minus => keyboard_input(173), Key::Minus => keyboard_input(ZoomOut),
Key::Up => keyboard_input(38), Key::Up => keyboard_input(PanUp),
Key::Down => keyboard_input(40), Key::Down => keyboard_input(PanDown),
Key::Left => keyboard_input(37), Key::Left => keyboard_input(PanLeft),
Key::Right => keyboard_input(39), Key::Right => keyboard_input(PanRight),
Key::R => keyboard_input(ResetBoat),
Key::Slash => keyboard_input(ResetCamera),
Key::E => keyboard_input(RaiseSail),
Key::Q => keyboard_input(LowerSail),
_ => (), _ => (),
}); });

View file

@ -4,8 +4,20 @@ var memory;
var exports; var exports;
const width = 160; const width = 160;
const height = 144; const height = 144;
const TIME_GAIN = 0.1; // game is unplayable at full speed with wasm refresh rates
function blit_frame() { function blit_frame() {
// this is required when using an allocator from wasm as there is
// no way to update the internal pointer when linear memory shifts
image = new ImageData(
new Uint8ClampedArray(
memory.buffer,
exports.BUFFER.value,
4 * width * height,
),
width,
);
ctx.putImageData(image, 0, 0); ctx.putImageData(image, 0, 0);
} }
@ -47,21 +59,27 @@ async function init() {
document.getElementById("body").onkeydown=keyboard_callback; document.getElementById("body").onkeydown=keyboard_callback;
memory = instance.exports.memory memory = instance.exports.memory
const buffer_address = instance.exports.BUFFER.value;
image = new ImageData(
new Uint8ClampedArray(
memory.buffer,
buffer_address,
4 * width * height,
),
width,
);
instance.exports.frame_entry(); instance.exports.frame_entry();
ctx.textBaseline = 'top' ctx.textBaseline = 'top'
ctx.textAlign = 'left'; ctx.textAlign = 'left';
const render = () => { var last;
var elapsed;
const render = (time) => {
if(!last) { elapsed = 0; }
else {
elapsed = time-last;
}
last=time;
if(!elapsed) {
elapsed = 0.0;
}
const FRAME_TIME = new Float32Array(exports.memory.buffer, exports.LAST_FRAME_TIME, 1);
FRAME_TIME[0] = elapsed * TIME_GAIN;
instance.exports.frame_entry(); instance.exports.frame_entry();
requestAnimationFrame(render); requestAnimationFrame(render);

View file

@ -1,16 +1,12 @@
[package] [package]
name = "pirates" name = "pirates"
version = "0.1.0" version = "1.0.0"
edition = "2021" edition = "2021"
[features] [features]
default = ["wasm"] default = ["wasm"]
wasm = ["wee_alloc"] wasm = ["wee_alloc"]
[profile.release]
lto = true
opt-level = 's'
[lib] [lib]
crate-type = ["lib", "cdylib"] crate-type = ["lib", "cdylib"]

View file

@ -23,8 +23,6 @@ use spin::Mutex;
#[cfg(feature = "rayon")] #[cfg(feature = "rayon")]
use rayon::prelude::*; use rayon::prelude::*;
use crate::noise::lerp;
mod sampler; mod sampler;
mod noise; mod noise;
@ -47,6 +45,9 @@ fn draw_text(text: &str, x: i32, y: i32, size: u8) {
pub const WIDTH: usize = 160; pub const WIDTH: usize = 160;
pub const HEIGHT: usize = 144; pub const HEIGHT: usize = 144;
#[no_mangle]
pub static mut LAST_FRAME_TIME: f32 = 0.0;
#[no_mangle] #[no_mangle]
pub static mut BUFFER: [u32; WIDTH * HEIGHT] = [0; WIDTH * HEIGHT]; pub static mut BUFFER: [u32; WIDTH * HEIGHT] = [0; WIDTH * HEIGHT];
@ -77,7 +78,12 @@ static MAP: [u8; MAP_WIDTH * MAP_HEIGHT] = [ // should deflate to smaller than b
static CAMERA: Mutex<[f32; 3]> = Mutex::new([0.0, 0.0, 0.18]); static CAMERA: Mutex<[f32; 3]> = Mutex::new([0.0, 0.0, 0.18]);
static BOAT: Mutex<Boat> = Mutex::new(Boat { x: 0.0, y: 0.0, theta: 0.0, vel: 0.0 }); static BOAT: Mutex<Boat> = Mutex::new(Boat {
x: 0.0, y: 0.0,
theta: 0.0,
vel: 0.0,
sail: 1.0,
});
#[no_mangle] #[no_mangle]
pub unsafe extern fn keyboard_input() { pub unsafe extern fn keyboard_input() {
@ -105,50 +111,71 @@ fn render_frame(buffer: &mut [u32; WIDTH*HEIGHT]) {
let mut camera = CAMERA.lock(); let mut camera = CAMERA.lock();
let mut boat = BOAT.lock(); let mut boat = BOAT.lock();
//camera[0] += 1.0; let mut gain = unsafe { // very much an approximation of constant-velocity animation
LAST_FRAME_TIME / (1000.0 / 20.0) // normalize to 20fps, cap simulation at 10fps
};
while let Some(key) = INPUTS.lock().pop() { while let Some(key) = INPUTS.lock().pop() {
match key[0] { use Input::*;
38 => camera[1] -= 10.0*camera[2], // up let input = (key[0] as u8).try_into().ok();
40 => camera[1] += 10.0*camera[2], // down if input.is_none() { continue; }
37 => camera[0] -= 10.0*camera[2], // left match input.unwrap() {
39 => camera[0] += 10.0*camera[2], // right PanUp => camera[1] -= gain*10.0*camera[2], // up
61 => camera[2] *= 0.9, // + PanDown => camera[1] += gain*10.0*camera[2], // down
173 => camera[2] *= 1.1, // - PanLeft => camera[0] -= gain*10.0*camera[2], // left
65 => boat.theta -= 10.0, // A PanRight => camera[0] += gain*10.0*camera[2], // right
68 => boat.theta += 10.0, // D ResetCamera => {
0 => camera[2] *= 1.0 - (key[1] as f32 - 127.0) * 0.0004, // analog zoom *camera = [
1 => boat.theta += (key[1] as f32 - 127.0) * 0.031, // analog rudder noise::lerp(camera[0], 0.00, (gain * 0.25).min(1.0)),
3 => camera[1] -= (key[1] as f32 - 127.0) * 0.004, // pan[y] noise::lerp(camera[1], 0.00, (gain * 0.25).min(1.0)),
4 => camera[0] += (key[1] as f32 - 127.0) * 0.004, // pan[x] noise::lerp(camera[2], 0.18, (gain * 0.25).min(1.0)),
_ => {} ]
}, // reset camera (/)
ResetBoat => boat.set_pos(Vector2::zeros()), // reset boat (r)
ZoomIn => camera[2] *= 1.0 - 0.1*gain, // +
ZoomOut => camera[2] *= 1.0 + 0.1*gain, // -
RudderLeft => boat.theta -= gain*10.0, // A
RudderRight => boat.theta += gain*10.0, // D
AxisZoom => camera[2] *= 1.0 - (key[1] as f32 - 127.0) * 0.0004 * gain, // analog zoom
AxisRudder => boat.theta += gain * (key[1] as f32 - 127.0) * 0.062, // analog rudder
AxisPanY => camera[1] -= gain * (key[1] as f32 - 127.0) * 0.1 * camera[2], // pan[y]
AxisPanX => camera[0] += gain * (key[1] as f32 - 127.0) * 0.1 * camera[2], // pan[x]
AxisSail => boat.sail += gain * (key[1] as f32 - 127.0) * 0.0013, // sail
RaiseSail => boat.sail += gain * 0.062, // E
LowerSail => boat.sail -= gain * 0.062, // Q
} }
} }
boat.sail = boat.sail.clamp(0.0, 1.5);
let wind = 0.0/RAD_TO_DEG; let wind = 0.0/RAD_TO_DEG;
let vel = boat.get_velocity(wind);
let camera_vec = Vector2::new(camera[0],camera[1]); let step = 1.0; // 50ms
let boat_pos = boat.get_pos(); while gain > 0.0 { // when draw fps is low, physics will still run at a minimum of 20fps
gain -= step;
let gain = gain + step;
let vel = boat.get_velocity(wind);
let depth = -sample_world(boat_pos+HALF, rand); let depth = -sample_world(boat.get_pos()+HALF, rand);
if depth < -0.04 { if depth < -0.04 {
boat.vel = 0.0; boat.vel = 0.0;
} else if depth < 0.0 { } else if depth < 0.0 {
boat.vel *= (1.0 - depth) * 0.25; boat.vel *= 1.0 + (depth * gain * 30.2).min(0.0);
} }
if depth > -0.04 { if depth > -0.04 {
boat.go_smooth(-vel * 0.42); boat.vel = noise::lerp(boat.vel, vel * 0.82, 0.13 * gain);
boat.go(gain);
}
} }
// draw sea // draw sea
const HALF: Vector2<f32> = Vector2::new(WIDTH as f32 / 2.0, HEIGHT as f32 / 2.0); const HALF: Vector2<f32> = Vector2::new(WIDTH as f32 / 2.0, HEIGHT as f32 / 2.0);
let camera_vec = Vector2::new(camera[0],camera[1]);
#[cfg(feature = "rayon")] #[cfg(feature = "rayon")]
let mut buffer_iter = buffer.par_iter_mut(); let buffer_iter = buffer.par_iter_mut();
#[cfg(not(feature = "rayon"))] #[cfg(not(feature = "rayon"))]
let mut buffer_iter = buffer.iter_mut(); let buffer_iter = buffer.iter_mut();
buffer_iter.enumerate().for_each(|pix| { buffer_iter.enumerate().for_each(|pix| {
let y = pix.0 / WIDTH; let y = pix.0 / WIDTH;
@ -157,7 +184,7 @@ fn render_frame(buffer: &mut [u32; WIDTH*HEIGHT]) {
point -= HALF; point -= HALF;
point *= camera[2]; point *= camera[2];
point += HALF; point += HALF;
let n = sample_world(point+camera_vec+boat_pos, rand); let n = sample_world(point+camera_vec+boat.get_pos(), rand);
*pix.1 = *pix.1 =
if n > 0.1 { if n > 0.1 {
let n = (n+0.1) * 300.0; let n = (n+0.1) * 300.0;
@ -237,8 +264,7 @@ fn draw_tri(color: u32, buffer: &mut [u32; WIDTH*HEIGHT], p1: Vector2<f32>, p2:
} }
fn sample_world(point: Vector2<f32>, rand: PerlinBuf) -> f32 { fn sample_world(point: Vector2<f32>, rand: PerlinBuf) -> f32 {
let offset = Vector2::new(64480.0, 7870.0); let offset = Vector2::new(64492.0, 7892.0);
//240.0,240.0
let point = point + offset; let point = point + offset;
let mut n = 0.0; let mut n = 0.0;
n += (sampler::sample_map_inter(point / 64.0, &MAP)-0.5)* 0.6; n += (sampler::sample_map_inter(point / 64.0, &MAP)-0.5)* 0.6;
@ -257,6 +283,8 @@ struct Boat {
y: f32, y: f32,
theta: f32, theta: f32,
vel: f32, vel: f32,
/// sail height 0-1
sail: f32,
} }
impl Boat { impl Boat {
@ -270,26 +298,81 @@ impl Boat {
} }
fn get_intensity(self: &Self, wind_direction: f32) -> f32 { fn get_intensity(self: &Self, wind_direction: f32) -> f32 {
libm::sinf((self.theta-wind_direction)/RAD_TO_DEG) libm::sinf((self.theta-wind_direction)/RAD_TO_DEG) * self.sail
} }
fn get_velocity(self: &Self, wind_direction: f32) -> f32 { fn get_velocity(self: &Self, wind_direction: f32) -> f32 {
libm::fabsf(self.get_intensity(wind_direction)) libm::fabsf(self.get_intensity(wind_direction))
} }
fn go_smooth(self: &mut Self, vel: f32) { fn go(self: &mut Self, gain: f32) {
self.vel = noise::lerp(self.vel, vel, 0.09);
self.go(self.vel);
}
fn go(self: &mut Self, velocity: f32) {
let cos = libm::cosf((self.theta+ 45.0)/RAD_TO_DEG); let cos = libm::cosf((self.theta+ 45.0)/RAD_TO_DEG);
let sin = libm::sinf((self.theta+ 45.0)/RAD_TO_DEG); let sin = libm::sinf((self.theta+ 45.0)/RAD_TO_DEG);
let unit = Vector2::new( let unit = Vector2::new(
cos - sin, cos - sin,
sin + cos); sin + cos);
self.set_pos(self.get_pos() + unit * velocity); self.set_pos(self.get_pos() + unit * -self.vel * gain);
} }
} }
impl TryFrom<u8> for Input {
type Error = &'static str;
fn try_from(input: u8) -> Result<Self, Self::Error> {
use Input::*;
match input {
38 => Ok(PanUp),
40 => Ok(PanDown),
37 => Ok(PanLeft),
39 => Ok(PanRight),
61 => Ok(ZoomIn),
173 => Ok(ZoomOut),
65 => Ok(RudderLeft),
68 => Ok(RudderRight),
69 => Ok(RaiseSail),
81 => Ok(LowerSail),
82 => Ok(ResetBoat),
191 => Ok(ResetCamera),
0 => Ok(AxisZoom),
1 => Ok(AxisRudder),
3 => Ok(AxisPanY),
4 => Ok(AxisPanX),
5 => Ok(AxisSail),
_ => Err("unmapped")
}
}
}
#[repr(u8)]
pub enum Input {
/// Up Arrow
PanUp = 38,
/// Down Arrow
PanDown = 40,
/// Left Arrow
PanLeft = 37,
/// Right Arrow
PanRight = 39,
/// +
ZoomIn = 61,
/// -
ZoomOut = 173,
/// A
RudderLeft = 65,
/// D
RudderRight = 68,
/// "/"
ResetCamera = 191,
/// R
ResetBoat = 82,
/// E
RaiseSail = 69,
/// Q
LowerSail = 81,
AxisZoom = 0,
AxisRudder = 1,
AxisPanY = 3,
AxisPanX = 4,
AxisSail = 5,
}