all the things and even documentation... <_<

This commit is contained in:
dani 2023-07-20 04:39:34 +00:00
commit a370ec7fd0
19 changed files with 526 additions and 0 deletions

2
.gitignore vendored Normal file
View File

@ -0,0 +1,2 @@
/target
/Cargo.lock

8
.idea/.gitignore vendored Normal file
View File

@ -0,0 +1,8 @@
# Default ignored files
/shelf/
/workspace.xml
# Editor-based HTTP Client requests
/httpRequests/
# Datasource local storage ignored files
/dataSources/
/dataSources.local.xml

12
.idea/gsa.iml Normal file
View File

@ -0,0 +1,12 @@
<?xml version="1.0" encoding="UTF-8"?>
<module type="CPP_MODULE" version="4">
<component name="NewModuleRootManager">
<content url="file://$MODULE_DIR$">
<sourceFolder url="file://$MODULE_DIR$/examples" isTestSource="false" />
<sourceFolder url="file://$MODULE_DIR$/src" isTestSource="false" />
<excludeFolder url="file://$MODULE_DIR$/target" />
</content>
<orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" />
</component>
</module>

View File

@ -0,0 +1,10 @@
<component name="InspectionProjectProfileManager">
<profile version="1.0">
<option name="myName" value="Project Default" />
<inspection_tool class="SpellCheckingInspection" enabled="false" level="TYPO" enabled_by_default="false">
<option name="processCode" value="true" />
<option name="processLiterals" value="true" />
<option name="processComments" value="true" />
</inspection_tool>
</profile>
</component>

8
.idea/modules.xml Normal file
View File

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectModuleManager">
<modules>
<module fileurl="file://$PROJECT_DIR$/.idea/gsa.iml" filepath="$PROJECT_DIR$/.idea/gsa.iml" />
</modules>
</component>
</project>

6
.idea/vcs.xml Normal file
View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="" vcs="Git" />
</component>
</project>

25
Cargo.toml Normal file
View File

@ -0,0 +1,25 @@
[package]
name = "gsa"
version = "0.1.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
lazy_static = "1.4.0"
glam = "0.24.0"
ascii = "1.1.0"
gilrs = "0.10.2"
[dependencies.skunk2d]
path = "../skunk2d"
[profile.release-dani]
inherits = "release"
opt-level = "z"
lto = true
codegen-units = 1
strip = true
panic = "abort"

23
examples/basic.rs Normal file
View File

@ -0,0 +1,23 @@
use glam::IVec2;
use gsa::{run, Gsa};
struct Game {}
fn init(gsa: &mut Gsa) -> Game {
gsa.sprites[0].tile = 0x0300;
gsa.sprites[1].tile = 0x0200;
gsa.maps[0].tiles[0][0] = 0x0300;
gsa.maps[1].half_tile = true;
gsa.write_string(1, IVec2::ONE, "Hello world nyaa~");
Game {}
}
fn update(_game: &mut Game, gsa: &mut Gsa) {
gsa.sprites[0].pos.x = (gsa.sprites[0].pos.x + 1) % 300;
gsa.sprites[1].pos += gsa.input.dir;
if gsa.input.pressed.face_down {
gsa.sprites[1].tile += 1;
}
}
run!(init, update);

BIN
examples/gfx.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

47
src/buttons.rs Normal file
View File

@ -0,0 +1,47 @@
/// State of all GSA buttons
#[derive(Default, Copy, Clone)]
pub struct Buttons {
/// UP on dpad
pub dpad_up: bool,
/// DOWN on dpad
pub dpad_down: bool,
/// LEFT on dpad
pub dpad_left: bool,
/// RIGHT on dpad
pub dpad_right: bool,
/// X on nintendo, Y on microsoft, TRIANGLE on sony
pub face_up: bool,
/// B on nintendo, A on microsoft, CROSS on sony
pub face_down: bool,
/// Y on nintendo, X on microsoft, SQUARE on sony
pub face_left: bool,
/// A on nintendo, B on microsoft, CIRCLE on sony
pub face_right: bool,
/// left shoulder button
pub l: bool,
/// right shoulder button
pub r: bool,
/// start button
pub start: bool,
/// select button
pub select: bool,
}
impl Buttons {
pub(crate) fn pressed(old: &Buttons, new: &Buttons) -> Buttons {
Buttons {
dpad_up: !old.dpad_up && new.dpad_up,
dpad_down: !old.dpad_down && new.dpad_down,
dpad_left: !old.dpad_left && new.dpad_left,
dpad_right: !old.dpad_right && new.dpad_right,
face_up: !old.face_up && new.face_up,
face_down: !old.face_down && new.face_down,
face_left: !old.face_left && new.face_left,
face_right: !old.face_right && new.face_right,
l: !old.l && new.l,
r: !old.r && new.r,
start: !old.start && new.start,
select: !old.select && new.select,
}
}
}

56
src/gsa.rs Normal file
View File

@ -0,0 +1,56 @@
use crate::input::Input;
use crate::rgb::Rgb;
use crate::sprite::Sprite;
use crate::tilemap::Tilemap;
use crate::{MAX_SPRITES, MAX_TILEMAPS, TILEMAP_MAX_SIZE};
use ascii::{AsciiChar, AsciiStr};
use glam::IVec2;
/// Complete state of GSA
pub struct Gsa {
/// Sprites available
pub sprites: [Sprite; MAX_SPRITES],
/// Palette used to draw graphics, initially loaded from gfx.gif
pub palette: [Rgb; 256],
/// Tilemap layers available
pub maps: [Tilemap; MAX_TILEMAPS],
/// Currently selected font
///
/// Chosen as half-size tile index, extends 16x16 half tiles in x and y
pub font: u16,
/// Current input state
pub input: Input,
}
impl Gsa {
/// Clears all tiles of given map to 0
pub fn clear_map(&mut self, map: usize) {
self.fill_map(map, 0);
}
/// Sets all tiles of map to val
pub fn fill_map(&mut self, map: usize, val: u16) {
for x in 0..TILEMAP_MAX_SIZE {
for y in 0..TILEMAP_MAX_SIZE {
self.maps[map].tiles[x][y] = val;
}
}
}
/// Write given string on given map, at given position, with font [Gsa::font]
pub fn write_string(&mut self, map: usize, pos: IVec2, str: &str) {
let str = AsciiStr::from_ascii(str).unwrap();
for (i, ch) in str.into_iter().enumerate() {
self.maps[map].tiles[pos.x as usize + i][pos.y as usize] = self.get_char_tile(*ch);
}
}
fn get_char_tile(&self, ch: AsciiChar) -> u16 {
let ch = ch as u16;
self.font + (ch % 0x10) + (ch / 0x10) * 0x100
}
}

18
src/input.rs Normal file
View File

@ -0,0 +1,18 @@
use crate::buttons::Buttons;
use glam::IVec2;
/// Input State
///
/// GSA pretends all input is a snes-like gamepad
/// d-pad, 4 face buttons, l and r shoulder buttons, start and select
#[derive(Default)]
pub struct Input {
/// Buttons that are currentle held down
pub down: Buttons,
/// Buttons that have been pressed in the current frame
pub pressed: Buttons,
/// Directional vector (-1 to 1) from current dpad state
pub dir: IVec2,
}

47
src/lib.rs Normal file
View File

@ -0,0 +1,47 @@
#![deny(missing_docs)]
//! # Game Skunk Advance
//! Game development library modelled after an imaginary console
//!
//! ## Specs
//! - Resolution: 304x176 (19x11 tiles)
//! - Colors: 256 (indexed out of a possible 24-bit)
//! - Tilesize: 16x16 (or 8x8 for half-tiles)
//! - Tileset: 65536 tiles, indexed via 0xYYXX
//! - Sprites: 256 of size 16x16 (pondering allowing larger sprites)
//! - Tilemaps: 4 of size 1024x1024, scrollable
//!
//! ## Features not yet implemented
//! - Sound (no samples
//! - Synth
//! - Speech
//! - Savegames
//! - Helpers
//! - Gamepad text keyboard input
//! - Menus
mod buttons;
mod gsa;
mod input;
mod rgb;
mod run;
mod sprite;
mod state;
mod tilemap;
pub use crate::buttons::Buttons;
pub use crate::gsa::Gsa;
pub use crate::input::Input;
pub use crate::rgb::Rgb;
pub use crate::run::run;
pub use crate::sprite::Sprite;
pub use crate::tilemap::Tilemap;
/// Amount of sprites in [Gsa::sprites]
pub const MAX_SPRITES: usize = 0xff;
/// X and y dimensions of maps in [Gsa::maps]
pub const TILEMAP_MAX_SIZE: usize = 1024;
/// Amount of tile maps in [Gsa::maps]
pub const MAX_TILEMAPS: usize = 4;

10
src/rgb.rs Normal file
View File

@ -0,0 +1,10 @@
/// RGB Color
#[derive(Copy, Clone)]
pub struct Rgb {
/// Red component 0-255
pub r: u8,
/// Green component 0-255
pub g: u8,
/// Blue component 0-255
pub b: u8,
}

57
src/run.rs Normal file
View File

@ -0,0 +1,57 @@
use crate::state::State;
use crate::{Gsa, Rgb, Sprite, MAX_SPRITES};
use gilrs::Gilrs;
use skunk2d::Image8;
/// Creates main function, includes gfx.gif, and calls run
///
/// Pass the following two functions to it:
/// - fn init(gsa: &mut Gsa) -> TGame
/// - fn update(game: &mut Game, gsa: &mut Gsa)
///
/// TGame can be any type, usually a struct that contains your game's state
#[macro_export]
macro_rules! run {
($init: ident, $update: ident) => {
fn main() {
run($init, $update, include_bytes!("gfx.gif"));
}
};
}
/// This is called by [run!]
pub fn run<TGame: 'static>(
init_fn: fn(gsa: &mut Gsa) -> TGame,
update_fn: fn(game: &mut TGame, gsa: &mut Gsa),
image_data: &[u8],
) {
let tileset = Image8::load_data(image_data);
let pal = Image8::load_data_palette(image_data);
let mut palette = [Rgb { r: 0, g: 0, b: 0 }; 256];
for i in 0..pal.len() / 3 {
palette[i] = Rgb {
r: pal[i * 3 + 2],
g: pal[i * 3 + 1],
b: pal[i * 3],
};
}
let mut gsa = Gsa {
sprites: [Sprite::default(); MAX_SPRITES],
maps: Default::default(),
palette,
font: 0x1010,
input: Default::default(),
};
let game = init_fn(&mut gsa);
let state = State::<TGame> {
gsa,
game,
tileset,
update_fn,
first: true,
gilrs: Gilrs::new().unwrap(),
};
skunk2d::run_with::<Image8, State<TGame>>(304, 176, 60, state);
}

17
src/sprite.rs Normal file
View File

@ -0,0 +1,17 @@
use glam::IVec2;
use std::fmt::{Display, Formatter};
/// Sprite which will be displayed on screen, unless tile=0
#[derive(Default, Copy, Clone)]
pub struct Sprite {
/// Position on screen
pub pos: IVec2,
/// Tile index
pub tile: u16,
}
impl Display for Sprite {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
write!(f, "[pos: {}, tile: {}]", self.pos, self.tile)
}
}

146
src/state.rs Normal file
View File

@ -0,0 +1,146 @@
use crate::buttons::Buttons;
use crate::{Gsa, TILEMAP_MAX_SIZE};
use gilrs::{Button, EventType, Gilrs};
use glam::IVec2;
use skunk2d::{Event, IRect, Image8, WindowState};
use std::rc::Rc;
pub struct State<TGame> {
pub(crate) gsa: Gsa,
pub(crate) game: TGame,
pub(crate) update_fn: fn(game: &mut TGame, gsa: &mut Gsa),
pub(crate) tileset: Rc<Image8>,
pub(crate) first: bool,
pub(crate) gilrs: Gilrs,
}
impl<TGame> skunk2d::Game<Image8> for State<TGame> {
fn update(&mut self, window_state: &mut WindowState) {
if self.first {
window_state.toggle_fullscreen();
self.first = false;
}
let mut new_buttons = self.gsa.input.down;
while let Some(event) = self.gilrs.next_event() {
match event {
gilrs::Event {
event: EventType::ButtonPressed(button, _),
..
} => match button {
Button::South => new_buttons.face_down = true,
Button::East => new_buttons.face_left = true,
Button::North => new_buttons.face_up = true,
Button::West => new_buttons.face_right = true,
Button::LeftTrigger | Button::LeftTrigger2 => new_buttons.l = true,
Button::RightTrigger | Button::RightTrigger2 => new_buttons.r = true,
Button::Select => new_buttons.select = true,
Button::Start => new_buttons.start = true,
Button::DPadUp => new_buttons.dpad_up = true,
Button::DPadDown => new_buttons.dpad_down = true,
Button::DPadLeft => new_buttons.dpad_left = true,
Button::DPadRight => new_buttons.dpad_right = true,
_ => {}
},
gilrs::Event {
event: EventType::ButtonReleased(button, _),
..
} => match button {
Button::South => new_buttons.face_down = false,
Button::East => new_buttons.face_left = false,
Button::North => new_buttons.face_up = false,
Button::West => new_buttons.face_right = false,
Button::LeftTrigger | Button::LeftTrigger2 => new_buttons.l = false,
Button::RightTrigger | Button::RightTrigger2 => new_buttons.r = false,
Button::Select => new_buttons.select = false,
Button::Start => new_buttons.start = false,
Button::DPadUp => new_buttons.dpad_up = false,
Button::DPadDown => new_buttons.dpad_down = false,
Button::DPadLeft => new_buttons.dpad_left = false,
Button::DPadRight => new_buttons.dpad_right = false,
_ => {}
},
_ => (),
}
}
self.gsa.input.pressed = Buttons::pressed(&self.gsa.input.down, &new_buttons);
self.gsa.input.down = new_buttons;
self.gsa.input.dir = IVec2 {
x: if self.gsa.input.down.dpad_left { -1 } else { 0 }
+ if self.gsa.input.down.dpad_right { 1 } else { 0 },
y: if self.gsa.input.down.dpad_up { -1 } else { 0 }
+ if self.gsa.input.down.dpad_down { 1 } else { 0 },
};
//todo: don't if not updated... how check? <_<
for i in 0..=255 {
window_state.set_palette(
i,
self.gsa.palette[i as usize].r,
self.gsa.palette[i as usize].g,
self.gsa.palette[i as usize].b,
);
}
(self.update_fn)(&mut self.game, &mut self.gsa);
}
fn on_event(&mut self, _window_state: &mut WindowState, _event: Event) {}
fn draw(&self, target: &mut Image8) {
target.clear();
for map in &self.gsa.maps {
let tcmult = if map.half_tile { 2 } else { 1 };
let tilesize = if map.half_tile { 8 } else { 16 };
let mut startx = map.scroll.x / tilesize;
let mut starty = map.scroll.y / tilesize;
let endx = (TILEMAP_MAX_SIZE as i32).min(startx + 20 * tcmult);
let endy = (TILEMAP_MAX_SIZE as i32).min(starty + 12 * tcmult);
startx = 0.max(startx);
starty = 0.max(starty);
for x in startx..endx {
for y in starty..endy {
let tile = map.tiles[x as usize][y as usize];
if tile > 0 {
let ty = tile / 0x100;
let tx = tile % 0x100;
target.draw_image_partial(
IVec2 {
x: x * tilesize - map.scroll.x,
y: y * tilesize - map.scroll.y,
},
&self.tileset,
IRect {
pos: IVec2 {
x: tx as i32 * tilesize,
y: ty as i32 * tilesize,
},
size: IVec2 {
x: tilesize,
y: tilesize,
},
},
)
}
}
}
}
for sprite in &self.gsa.sprites {
if sprite.tile > 0 {
let ty = sprite.tile / 0x100;
let tx = sprite.tile % 0x100;
target.draw_image_partial(
sprite.pos,
&self.tileset,
IRect {
pos: IVec2 {
x: tx as i32 * 16,
y: ty as i32 * 16,
},
size: IVec2 { x: 16, y: 16 },
},
);
}
}
}
}

22
src/tilemap.rs Normal file
View File

@ -0,0 +1,22 @@
use crate::TILEMAP_MAX_SIZE;
use glam::IVec2;
/// Tilemap which will be rendered on screen
pub struct Tilemap {
/// Tiles in idx, accessible via \[x\]\[y\], x and y 0..[TILEMAP_MAX_SIZE]
pub tiles: Box<[[u16; TILEMAP_MAX_SIZE]; TILEMAP_MAX_SIZE]>,
/// Camera scroll (negative draw offset) for rendering
pub scroll: IVec2,
/// Are tiles indices half-tile indices?
pub half_tile: bool,
}
impl Default for Tilemap {
fn default() -> Self {
Self {
tiles: [[0u16; TILEMAP_MAX_SIZE]; TILEMAP_MAX_SIZE].into(),
scroll: IVec2::ZERO,
half_tile: false,
}
}
}

12
todo.md Normal file
View File

@ -0,0 +1,12 @@
# TODO
- Input (mapped minimal controller)
- Gamepad doooone :)
- Keyboard still gotta
- Sound
- Something synth'y would be nice...
- Speech synthesis...? :)
- Savegames
- Helpers
- Menu
- Fake Keyboard Text Input