Repository: PacktPublishing/Game-Development-with-Rust-and-WebAssembly
Branch: main
Commit: e7122da5435a
Files: 24
Total size: 82.8 KB
Directory structure:
gitextract_9erurgo8/
├── .github/
│ └── workflows/
│ └── build.yml
├── .gitignore
├── .rustfmt
├── .tool-versions
├── Cargo.toml
├── LICENSE
├── README.md
├── js/
│ └── index.js
├── package.json
├── rust-toolchain.toml
├── rustfmt.toml
├── src/
│ ├── browser.rs
│ ├── engine.rs
│ ├── game.rs
│ ├── lib.rs
│ ├── segments.rs
│ ├── sound.rs
│ └── test_browser.rs
├── static/
│ ├── index.html
│ ├── rhb.json
│ ├── styles.css
│ └── tiles.json
├── tests/
│ └── app.rs
└── webpack.config.js
================================================
FILE CONTENTS
================================================
================================================
FILE: .github/workflows/build.yml
================================================
on: [push]
name: build
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions-rs/toolchain@v1
with:
toolchain: 1.57.0
target: wasm32-unknown-unknown
override: true
components: clippy
- uses: actions-rs/install@v0.1
with:
crate: wasm-pack
version: 0.9.1
use-tool-cache: true
- name: Annotate commit with clippy warnings
uses: actions-rs/clippy-check@v1
with:
token: ${{ secrets.GITHUB_TOKEN }}
- uses: actions/setup-node@v2
with:
node-version: '16.13.0'
- run: npm install
- run: npm test
- run: npm run build
- name: Deploy to Netlify
uses: nwtgck/actions-netlify@v1.2
with:
publish-dir: './dist'
production-branch: main
github-token: ${{ secrets.GITHUB_TOKEN }}
deploy-message: "Deploy from GitHub Actions"
enable-pull-request-comment: true
enable-commit-comment: true
overwrites-pull-request-comment: true
env:
NETLIFY_AUTH_TOKEN: ${{ secrets.NETLIFY_AUTH_TOKEN }}
NETLIFY_SITE_ID: ${{ secrets.NETLIFY_SITE_ID }}
timeout-minutes: 1
================================================
FILE: .gitignore
================================================
node_modules
/dist
/target
/pkg
/wasm-pack.log
# Local Netlify folder
.netlify
.DS_Store
================================================
FILE: .rustfmt
================================================
edition = "2018"
================================================
FILE: .tool-versions
================================================
nodejs 16.13.0
================================================
FILE: Cargo.toml
================================================
# You must change these to your own details.
[package]
name = "rust-webpack-template"
description = "Walk the Dog - the game for the Rust Games with WebAssembly book"
version = "0.1.0"
authors = ["Eric Smith <paytonrules@gmail.com>"]
categories = ["wasm"]
readme = "README.md"
edition = "2021"
[lib]
crate-type = ["cdylib"]
[profile.release]
# This makes the compiled code faster and smaller, but it makes compiling slower,
# so it's only enabled in release mode.
lto = true
[features]
# If you uncomment this line, it will enable `wee_alloc`:
#default = ["wee_alloc"]
[dependencies]
# The `wasm-bindgen` crate provides the bare minimum functionality needed
# to interact with JavaScript.
wasm-bindgen = { version = "0.2.78", features = ["serde-serialize"] }
console_error_panic_hook = "0.1.7"
rand = "0.8.4"
getrandom = { version = "0.2.3", features = ["js"] }
futures = "0.3.17"
wasm-bindgen-futures = "0.4.28"
serde = {version = "1.0.131", features = ["derive"] }
anyhow = "1.0.51"
async-trait = "0.1.52"
js-sys = "0.3.55"
# The `web-sys` crate allows you to interact with the various browser APIs,
# like the DOM.
[dependencies.web-sys]
version = "0.3.55"
features = ["console",
"Window",
"Document",
"HtmlCanvasElement",
"CanvasRenderingContext2d",
"Element",
"HtmlImageElement",
"Response",
"Performance",
"KeyboardEvent",
"AudioContext",
"AudioBuffer",
"AudioBufferSourceNode",
"AudioDestinationNode",
"AudioBufferOptions",
]
# These crates are used for running unit tests.
[dev-dependencies]
wasm-bindgen-test = "0.3.28"
================================================
FILE: LICENSE
================================================
MIT License
Copyright (c) 2021 Packt
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
================================================
FILE: README.md
================================================
# Game Development with Rust and WebAssembly
Game Development with Rust and WebAssembly, published by Packt
<a href="https://www.packtpub.com/product/game-development-with-rust-and-webassembly/9781801070973"><img src="https://static.packt-cdn.com/products/9781801070973/cover/smaller" alt="Game Development with Rust and WebAssembly" height="256px" align="right"></a>
This is the code repository for [Game Development with Rust and WebAssembly](https://www.packtpub.com/product/game-development-with-rust-and-webassembly/9781801070973), published by Packt.
**Learn how to run Rust on the web while building a game**
## What is this book about?
The Rust programming language has held the most-loved technology ranking on Stack Overflow for 6 years running, while JavaScript has been the most-used programming language for 9 years straight as it runs on every web browser. Now, thanks to WebAssembly (or Wasm), you can use the language you love on the platform that's everywhere.
This book covers the following exciting features:
* Build and deploy a Rust application to the web using WebAssembly
* Use wasm-bindgen and the Canvas API to draw real-time graphics
* Write a game loop and take keyboard input for dynamic action
* Explore collision detection and create a dynamic character that can jump on and off platforms and fall down holes
* Manage animations using state machines
* Generate levels procedurally for an endless runner
* Load and display sprites and sprite sheets for animations
If you feel this book is for you, get your [copy](https://www.amazon.com/dp/1801070970) today!
<a href="https://www.packtpub.com/?utm_source=github&utm_medium=banner&utm_campaign=GitHubBanner"><img src="https://raw.githubusercontent.com/PacktPublishing/GitHub/master/GitHub.png"
alt="https://www.packtpub.com/" border="5" /></a>
## Instructions and Navigation
You're currently looking the main branch of this repository, which represents the "completed" state of this book. I say completed because development on this branch is ongoing - specifically the challenges cited in the book are being implemented here. If you want to see the end state of any chapter those are stored as tags, such as https://github.com/PacktPublishing/Game-Development-with-Rust-and-WebAssembly/tree/chapter_1.
**Following is what you need for this book:**
This game development book is for developers interested in Rust who want to create and deploy 2D games to the web. Game developers looking to build a game on the web platform using WebAssembly without C++ programming or web developers who want to explore WebAssembly along with JavaScript web will also find this book useful. The book will also help Rust developers who want to move from the server side to the client side by familiarizing them with the WebAssembly toolchain. Basic knowledge of Rust programming is assumed.
With the following software and hardware list you can run all code files present in the book (Chapter 1-11).
### Software and Hardware List
| Chapter | Software required | version | OS required |
|----------|--------------------------------------------|---------|-------------|
| (1 - 11) | Rust Toolchains via Rustup | 1.57.0 | Any OS |
| (1 - 11) | NodeJS | 16.13.0 | Any OS |
| (1 - 11) | Rust Compile target wasm32-unknown-unknown | NA | NA |
I use https://asdf-vm.com to install Node and a .tool-versions file is present but you don't have to. Instructions for creating a new project are found in the book (chapter 1) but the project can also be setup by cloning this repository and running the commands for building and running. Speaking of that:
### Running this App
#### Installation
`npm install` Will install the Node dependencies (primarily WebPack). Don't worry you don't have to think about those much.
#### Running in debug
`npm start` Will compile the application to Wasm and start a server, running it at localhost:8080 by default. This will also ensure `wasm-pack` is setup and running and run `cargo build`.
#### Building for release
`npm run build` Creates a release build and puts it in the `dist` directory.
#### Running Tests
`npm run test`
You can use a lot of the `cargo` commands as well - but those do not go through the process of bundling up the built assembly for distribution.
#### Deployment
This branch is setup for continuous deployment with GitHub Actions, as is the tag for chapter_10. Something to keep in mind when forking the repository. The current production version of this game can be found at:
https://rust-games-webassembly.netlify.app
## Challenges
At the end of the book (Further Resources and What's Next?) there are six challenges for you, the reader. I'll be completing them on and off [my stream](www.twitch.tv/paytonrules), and making a note here when they are complete.
Challenge #6:
- Challenge #6 was completed two ways. I displayed the score via the canvas's render text function, as well as via the DOM. There are two branches with the solutions: [add-score](https://github.com/PacktPublishing/Game-Development-with-Rust-and-WebAssembly/tree/add-score) and [add-score-html](https://github.com/PacktPublishing/Game-Development-with-Rust-and-WebAssembly/tree/add-score-via-html). The HTML version looks a lot better, and is also implemented in the main branch.
## More Information
We also provide a PDF file that has color images of the screenshots/diagrams used in this book. [Click here to download it](https://static.packt-cdn.com/downloads/9781801070973_ColorImages.pdf).
The Code in Action videos for this book can be viewed at https://bit.ly/3uxXl4W.
### Related products <Other books you may enjoy>
* Creative Projects for Rust Programmers [[Packt]](https://www.packtpub.com/product/creative-projects-for-rust-programmers/9781789346220) [[Amazon]](https://www.amazon.com/Creative-Projects-Rust-Programmers-WebAssembly/dp/1789346223)
* Rust Web Programming [[Packt]](https://www.packtpub.com/product/rust-web-programming/9781800560819) [[Amazon]](https://www.amazon.com/Rust-Web-Programming-hands-programming-dp-1800560818/dp/1800560818/ref=mt_other?_encoding=UTF8&me=&qid=)
## Get to Know the Author
**Eric Smith** is a software crafter with over 20 years of software development experience. Since 2005, he's worked at 8th Light, where he consults for companies big and small by delivering software, mentoring developers, and coaching teams. He's a frequent speaker at conferences speaking on topics such as educating developers and test-driven development, and holds a master's degree in video game development from DePaul University. Eric wrote much of the code for this book live on his Twitch stream. When he's not at the computer, you can find Eric running obstacle races and traveling with his family.
### Download a free PDF
<i>If you have already purchased a print or Kindle version of this book, you can get a DRM-free PDF version at no cost.<br>Simply click on the link to claim your free PDF.</i>
<p align="center"> <a href="https://packt.link/free-ebook/9781801070973">https://packt.link/free-ebook/9781801070973 </a> </p>
================================================
FILE: js/index.js
================================================
import("../pkg/index.js").catch(console.error);
================================================
FILE: package.json
================================================
{
"author": "You <you@example.com>",
"name": "rust-webpack-template",
"version": "0.1.0",
"scripts": {
"build": "rimraf dist pkg && webpack",
"start": "rimraf dist pkg && webpack-dev-server --open -d --host 0.0.0.0",
"test": "cargo test && wasm-pack test --headless --chrome"
},
"devDependencies": {
"@wasm-tool/wasm-pack-plugin": "^1.1.0",
"copy-webpack-plugin": "^5.0.3",
"rimraf": "^3.0.0",
"webpack": "^4.42.0",
"webpack-cli": "^3.3.3",
"webpack-dev-server": "^3.7.1"
},
"dependencies": {
"netlify-cli": "^8.0.1"
}
}
================================================
FILE: rust-toolchain.toml
================================================
[toolchain]
channel = "1.57.0"
targets = ["wasm32-unknown-unknown"]
================================================
FILE: rustfmt.toml
================================================
edition = "2018"
================================================
FILE: src/browser.rs
================================================
use anyhow::{anyhow, Result};
use js_sys::ArrayBuffer;
use std::future::Future;
use wasm_bindgen::{
closure::WasmClosure, closure::WasmClosureFnOnce, prelude::Closure, JsCast, JsValue,
};
use wasm_bindgen_futures::JsFuture;
use web_sys::{
CanvasRenderingContext2d, Document, Element, HtmlCanvasElement, HtmlElement, HtmlImageElement,
Response, Window,
};
// Straight taken from https://rustwasm.github.io/book/game-of-life/debugging.html
macro_rules! log {
( $( $t:tt )* ) => {
web_sys::console::log_1(&format!( $( $t )* ).into());
}
}
macro_rules! error {
( $( $t:tt )* ) => {
web_sys::console::error_1(&format!( $( $t )* ).into());
}
}
pub fn window() -> Result<Window> {
web_sys::window().ok_or_else(|| anyhow!("No Window Found"))
}
pub fn document() -> Result<Document> {
window()?
.document()
.ok_or_else(|| anyhow!("No Document Found"))
}
pub fn canvas() -> Result<HtmlCanvasElement> {
document()?
.get_element_by_id("canvas")
.ok_or_else(|| anyhow!("No Canvas Element found with ID 'canvas'"))?
.dyn_into::<web_sys::HtmlCanvasElement>()
.map_err(|element| anyhow!("Error converting {:#?} to HtmlCanvasElement", element))
}
pub fn context() -> Result<CanvasRenderingContext2d> {
canvas()?
.get_context("2d")
.map_err(|js_value| anyhow!("Error getting 2d context {:#?}", js_value))?
.ok_or_else(|| anyhow!("No 2d context found"))?
.dyn_into::<web_sys::CanvasRenderingContext2d>()
.map_err(|element| {
anyhow!(
"Error converting {:#?} to CanvasRenderingContext2d",
element
)
})
}
pub fn spawn_local<F>(future: F)
where
F: Future<Output = ()> + 'static,
{
wasm_bindgen_futures::spawn_local(future);
}
pub async fn fetch_with_str(resource: &str) -> Result<JsValue> {
JsFuture::from(window()?.fetch_with_str(resource))
.await
.map_err(|err| anyhow!("error fetching {:#?}", err))
}
pub async fn fetch_response(resource: &str) -> Result<Response> {
fetch_with_str(resource)
.await?
.dyn_into()
.map_err(|err| anyhow!("error converting fetch to Response {:#?}", err))
}
pub async fn fetch_json(json_path: &str) -> Result<JsValue> {
let resp = fetch_response(json_path).await?;
JsFuture::from(
resp.json()
.map_err(|err| anyhow!("Could not get JSON from response {:#?}", err))?,
)
.await
.map_err(|err| anyhow!("error fetching JSON {:#?}", err))
}
pub async fn fetch_array_buffer(resource: &str) -> Result<ArrayBuffer> {
let array_buffer = fetch_response(resource)
.await?
.array_buffer()
.map_err(|err| anyhow!("Error loading array buffer {:#?}", err))?;
JsFuture::from(array_buffer)
.await
.map_err(|err| anyhow!("Error converting array buffer into a future {:#?}", err))?
.dyn_into()
.map_err(|err| anyhow!("Error converting raw JSValue to ArrayBuffer {:#?}", err))
}
pub fn new_image() -> Result<HtmlImageElement> {
HtmlImageElement::new().map_err(|err| anyhow!("Could not create HtmlImageElement: {:#?}", err))
}
pub type LoopClosure = Closure<dyn FnMut(f64)>;
pub fn create_raf_closure(f: impl FnMut(f64) + 'static) -> LoopClosure {
closure_wrap(Box::new(f))
}
pub fn request_animation_frame(callback: &LoopClosure) -> Result<i32> {
window()?
.request_animation_frame(callback.as_ref().unchecked_ref())
.map_err(|err| anyhow!("Cannot request animation frame {:#?}", err))
}
pub fn closure_once<F, A, R>(fn_once: F) -> Closure<F::FnMut>
where
F: 'static + WasmClosureFnOnce<A, R>,
{
Closure::once(fn_once)
}
pub fn closure_wrap<T: WasmClosure + ?Sized>(data: Box<T>) -> Closure<T> {
Closure::wrap(data)
}
pub fn now() -> Result<f64> {
Ok(window()?
.performance()
.ok_or_else(|| anyhow!("Performance object not found"))?
.now())
}
pub fn draw_ui(html: &str) -> Result<()> {
find_ui()?
.insert_adjacent_html("afterbegin", html)
.map_err(|err| anyhow!("Could not insert html {:#?}", err))
}
pub fn hide_ui() -> Result<()> {
let ui = find_ui()?;
if let Some(child) = ui.first_child() {
ui.remove_child(&child)
.map(|_removed_child| ())
.map_err(|err| anyhow!("Failed to remove child {:#?}", err))
.and_then(|_unit| {
canvas()?
.focus()
.map_err(|err| anyhow!("Could not set focus to canvas! {:#?}", err))
})
} else {
Ok(())
}
}
fn find_ui() -> Result<Element> {
document().and_then(|doc| {
doc.get_element_by_id("ui")
.ok_or_else(|| anyhow!("UI element not found"))
})
}
pub fn find_html_element_by_id(id: &str) -> Result<HtmlElement> {
document()
.and_then(|doc| {
doc.get_element_by_id(id)
.ok_or_else(|| anyhow!("Element with id {} not found", id))
})
.and_then(|element| {
element
.dyn_into::<HtmlElement>()
.map_err(|err| anyhow!("Could not cast into HtmlElement {:#?}", err))
})
}
#[cfg(test)]
mod tests {
use super::*;
use wasm_bindgen_test::wasm_bindgen_test;
wasm_bindgen_test::wasm_bindgen_test_configure!(run_in_browser);
#[wasm_bindgen_test]
async fn test_error_loading_json() {
let json = fetch_json("not_there.json").await;
assert_eq!(json.is_err(), true);
}
}
================================================
FILE: src/engine.rs
================================================
use crate::browser::{self, LoopClosure};
use crate::sound;
use anyhow::{anyhow, Result};
use async_trait::async_trait;
use futures::channel::{
mpsc::{unbounded, UnboundedReceiver},
oneshot::channel,
};
use serde::Deserialize;
use std::{cell::RefCell, collections::HashMap, rc::Rc, sync::Mutex};
use wasm_bindgen::{prelude::Closure, JsCast, JsValue};
use web_sys::AudioContext;
use web_sys::{AudioBuffer, HtmlElement};
use web_sys::{CanvasRenderingContext2d, HtmlImageElement};
#[derive(Deserialize, Clone)]
pub struct SheetRect {
pub x: i16,
pub y: i16,
pub w: i16,
pub h: i16,
}
#[derive(Deserialize, Clone)]
#[serde(rename_all = "camelCase")]
pub struct Cell {
pub frame: SheetRect,
pub sprite_source_size: SheetRect,
}
#[derive(Deserialize, Clone)]
pub struct Sheet {
pub frames: HashMap<String, Cell>,
}
#[derive(Clone, Copy, Default)]
pub struct Point {
pub x: i16,
pub y: i16,
}
#[derive(Default)]
pub struct Rect {
pub position: Point,
pub width: i16,
pub height: i16,
}
impl Rect {
pub const fn new(position: Point, width: i16, height: i16) -> Self {
Rect {
position,
width,
height,
}
}
pub const fn new_from_x_y(x: i16, y: i16, width: i16, height: i16) -> Self {
Rect::new(Point { x, y }, width, height)
}
pub fn intersects(&self, rect: &Rect) -> bool {
self.x() < rect.right()
&& self.right() > rect.x()
&& self.y() < rect.bottom()
&& self.bottom() > rect.y()
}
pub fn right(&self) -> i16 {
self.x() + self.width
}
pub fn bottom(&self) -> i16 {
self.y() + self.height
}
pub fn set_x(&mut self, x: i16) {
self.position.x = x
}
pub fn x(&self) -> i16 {
self.position.x
}
pub fn y(&self) -> i16 {
self.position.y
}
}
pub struct Renderer {
context: CanvasRenderingContext2d,
}
impl Renderer {
pub fn clear(&self, rect: &Rect) {
self.context.clear_rect(
rect.x().into(),
rect.y().into(),
rect.width.into(),
rect.height.into(),
);
}
pub fn draw_image(&self, image: &HtmlImageElement, frame: &Rect, destination: &Rect) {
self.context
.draw_image_with_html_image_element_and_sw_and_sh_and_dx_and_dy_and_dw_and_dh(
image,
frame.x().into(),
frame.y().into(),
frame.width.into(),
frame.height.into(),
destination.x().into(),
destination.y().into(),
destination.width.into(),
destination.height.into(),
)
.expect("Drawing is throwing exceptions! Unrecoverable error.");
}
pub fn draw_entire_image(&self, image: &HtmlImageElement, position: &Point) {
self.context
.draw_image_with_html_image_element(image, position.x.into(), position.y.into())
.expect("Drawing is throwing exceptions! Unrecoverable error.");
}
#[allow(dead_code)]
pub fn draw_rect(&self, bounding_box: &Rect) {
self.context.set_stroke_style(&JsValue::from_str("#FF0000"));
self.context.begin_path();
self.context.rect(
bounding_box.x().into(),
bounding_box.y().into(),
bounding_box.width.into(),
bounding_box.height.into(),
);
self.context.stroke();
}
#[allow(dead_code)]
pub fn draw_text(&self, text: &str, location: &Point) -> Result<()> {
self.context.set_font("16pt Ken Future");
self.context
.fill_text(text, location.x.into(), location.y.into())
.map_err(|err| anyhow!("Error filling text {:#?}", err))?;
Ok(())
}
}
pub async fn load_image(source: &str) -> Result<HtmlImageElement> {
let image = browser::new_image()?;
let (complete_tx, complete_rx) = channel::<Result<()>>();
let success_tx = Rc::new(Mutex::new(Some(complete_tx)));
let error_tx = Rc::clone(&success_tx);
let success_callback = browser::closure_once(move || {
if let Some(success_tx) = success_tx.lock().ok().and_then(|mut opt| opt.take()) {
if let Err(err) = success_tx.send(Ok(())) {
error!("Could not send successful image loaded message! {:#?}", err);
}
}
});
let error_callback: Closure<dyn FnMut(JsValue)> = browser::closure_once(move |err| {
if let Some(error_tx) = error_tx.lock().ok().and_then(|mut opt| opt.take()) {
if let Err(err) = error_tx.send(Err(anyhow!("Error Loading Image: {:#?}", err))) {
error!("Could not send error message on loading image! {:#?}", err);
}
}
});
image.set_onload(Some(success_callback.as_ref().unchecked_ref()));
image.set_onerror(Some(error_callback.as_ref().unchecked_ref()));
image.set_src(source);
complete_rx.await??;
Ok(image)
}
#[async_trait(?Send)]
pub trait Game {
async fn initialize(&self) -> Result<Box<dyn Game>>;
fn update(&mut self, keystate: &KeyState);
fn draw(&self, renderer: &Renderer);
}
// Sixty Frames per second, converted to a frame length in milliseconds
const FRAME_SIZE: f32 = 1.0 / 60.0 * 1000.0;
pub struct GameLoop {
last_frame: f64,
accumulated_delta: f32,
}
type SharedLoopClosure = Rc<RefCell<Option<LoopClosure>>>;
impl GameLoop {
pub async fn start(game: impl Game) -> Result<()> {
let mut keyevent_receiver = prepare_input()?;
let mut game = game.initialize().await?;
let mut game_loop = GameLoop {
last_frame: browser::now()?,
accumulated_delta: 0.0,
};
let renderer = Renderer {
context: browser::context()?,
};
let f: SharedLoopClosure = Rc::new(RefCell::new(None));
let g = f.clone();
let mut keystate = KeyState::new();
*g.borrow_mut() = Some(browser::create_raf_closure(move |perf: f64| {
process_input(&mut keystate, &mut keyevent_receiver);
let frame_time = perf - game_loop.last_frame;
game_loop.accumulated_delta += frame_time as f32;
while game_loop.accumulated_delta > FRAME_SIZE {
game.update(&keystate);
game_loop.accumulated_delta -= FRAME_SIZE;
}
game_loop.last_frame = perf;
game.draw(&renderer);
if cfg!(debug_assertions) {
unsafe {
draw_frame_rate(&renderer, frame_time);
}
}
browser::request_animation_frame(f.borrow().as_ref().unwrap()).unwrap();
}));
browser::request_animation_frame(
g.borrow()
.as_ref()
.ok_or_else(|| anyhow!("GameLoop: Loop is None"))?,
)?;
Ok(())
}
}
unsafe fn draw_frame_rate(renderer: &Renderer, frame_time: f64) {
static mut FRAMES_COUNTED: i32 = 0;
static mut TOTAL_FRAME_TIME: f64 = 0.0;
static mut FRAME_RATE: i32 = 0;
FRAMES_COUNTED += 1;
TOTAL_FRAME_TIME += frame_time;
if TOTAL_FRAME_TIME > 1000.0 {
FRAME_RATE = FRAMES_COUNTED;
TOTAL_FRAME_TIME = 0.0;
FRAMES_COUNTED = 0;
}
if let Err(err) = renderer.draw_text(
&format!("Frame Rate {}", FRAME_RATE),
&Point { x: 400, y: 100 },
) {
error!("Could not draw text {:#?}", err);
};
}
#[derive(Debug)]
pub struct KeyState {
pressed_keys: HashMap<String, web_sys::KeyboardEvent>,
}
impl KeyState {
fn new() -> Self {
KeyState {
pressed_keys: HashMap::new(),
}
}
pub fn is_pressed(&self, code: &str) -> bool {
self.pressed_keys.contains_key(code)
}
fn set_pressed(&mut self, code: &str, event: web_sys::KeyboardEvent) {
self.pressed_keys.insert(code.into(), event);
}
fn set_released(&mut self, code: &str) {
self.pressed_keys.remove(code);
}
}
enum KeyPress {
KeyUp(web_sys::KeyboardEvent),
KeyDown(web_sys::KeyboardEvent),
}
fn process_input(state: &mut KeyState, keyevent_receiver: &mut UnboundedReceiver<KeyPress>) {
loop {
match keyevent_receiver.try_next() {
Ok(None) => break,
Err(_err) => break,
Ok(Some(evt)) => match evt {
KeyPress::KeyUp(evt) => state.set_released(&evt.code()),
KeyPress::KeyDown(evt) => state.set_pressed(&evt.code(), evt),
},
};
}
}
fn prepare_input() -> Result<UnboundedReceiver<KeyPress>> {
let (keydown_sender, keyevent_receiver) = unbounded();
let keydown_sender = Rc::new(RefCell::new(keydown_sender));
let keyup_sender = Rc::clone(&keydown_sender);
let onkeydown = browser::closure_wrap(Box::new(move |keycode: web_sys::KeyboardEvent| {
if let Err(err) = keydown_sender
.borrow_mut()
.start_send(KeyPress::KeyDown(keycode))
{
error!("Could not send keyDown message {:#?}", err);
}
}) as Box<dyn FnMut(web_sys::KeyboardEvent)>);
let onkeyup = browser::closure_wrap(Box::new(move |keycode: web_sys::KeyboardEvent| {
if let Err(err) = keyup_sender
.borrow_mut()
.start_send(KeyPress::KeyUp(keycode))
{
error!("Could not send keyUp message {:#?}", err);
}
}) as Box<dyn FnMut(web_sys::KeyboardEvent)>);
browser::canvas()?.set_onkeydown(Some(onkeydown.as_ref().unchecked_ref()));
browser::canvas()?.set_onkeyup(Some(onkeyup.as_ref().unchecked_ref()));
onkeydown.forget();
onkeyup.forget();
Ok(keyevent_receiver)
}
pub fn add_click_handler(elem: HtmlElement) -> UnboundedReceiver<()> {
let (mut click_sender, click_receiver) = unbounded();
let on_click = browser::closure_wrap(Box::new(move || {
if let Err(err) = click_sender.start_send(()) {
error!("Could not send click message {:#?}", err);
}
}) as Box<dyn FnMut()>);
elem.set_onclick(Some(on_click.as_ref().unchecked_ref()));
on_click.forget();
click_receiver
}
pub struct Image {
element: HtmlImageElement,
bounding_box: Rect,
}
impl Image {
pub fn new(element: HtmlImageElement, position: Point) -> Self {
let bounding_box = Rect::new(position, element.width() as i16, element.height() as i16);
Self {
element,
bounding_box,
}
}
pub fn draw(&self, renderer: &Renderer) {
renderer.draw_entire_image(&self.element, &self.bounding_box.position)
}
pub fn bounding_box(&self) -> &Rect {
&self.bounding_box
}
pub fn move_horizontally(&mut self, distance: i16) {
self.set_x(self.bounding_box.x() + distance);
}
pub fn set_x(&mut self, x: i16) {
self.bounding_box.set_x(x);
}
pub fn right(&self) -> i16 {
self.bounding_box.right()
}
}
pub struct SpriteSheet {
sheet: Sheet,
image: HtmlImageElement,
}
impl SpriteSheet {
pub fn new(sheet: Sheet, image: HtmlImageElement) -> Self {
SpriteSheet { sheet, image }
}
pub fn cell(&self, name: &str) -> Option<&Cell> {
self.sheet.frames.get(name)
}
pub fn draw(&self, renderer: &Renderer, source: &Rect, destination: &Rect) {
renderer.draw_image(&self.image, source, destination);
}
}
#[derive(Clone)]
pub struct Audio {
context: AudioContext,
}
#[derive(Clone)]
pub struct Sound {
pub buffer: AudioBuffer,
}
impl Audio {
pub fn new() -> Result<Self> {
Ok(Audio {
context: sound::create_audio_context()?,
})
}
pub async fn load_sound(&self, filename: &str) -> Result<Sound> {
let array_buffer = browser::fetch_array_buffer(filename).await?;
let audio_buffer = sound::decode_audio_data(&self.context, &array_buffer).await?;
Ok(Sound {
buffer: audio_buffer,
})
}
pub fn play_sound(&self, sound: &Sound) -> Result<()> {
sound::play_sound(&self.context, &sound.buffer, sound::LOOPING::No)
}
pub fn play_looping_sound(&self, sound: &Sound) -> Result<()> {
sound::play_sound(&self.context, &sound.buffer, sound::LOOPING::Yes)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn two_rects_that_intersect_on_the_left() {
let rect1 = Rect {
position: Point { x: 10, y: 10 },
height: 100,
width: 100,
};
let rect2 = Rect {
position: Point { x: 0, y: 10 },
height: 100,
width: 100,
};
assert_eq!(rect2.intersects(&rect1), true);
}
}
================================================
FILE: src/game.rs
================================================
use std::rc::Rc;
use anyhow::{anyhow, Result};
use async_trait::async_trait;
use futures::channel::mpsc::UnboundedReceiver;
use rand::prelude::*;
use web_sys::HtmlImageElement;
use self::red_hat_boy_states::*;
use crate::{
browser,
engine::{
self, Audio, Cell, Game, Image, KeyState, Point, Rect, Renderer, Sheet, Sound, SpriteSheet,
},
segments::*,
};
const HEIGHT: i16 = 600;
const TIMELINE_MINIMUM: i16 = 1000;
const OBSTACLE_BUFFER: i16 = 20;
pub struct WalkTheDog {
machine: Option<WalkTheDogStateMachine>,
}
impl WalkTheDog {
pub fn new() -> Self {
WalkTheDog { machine: None }
}
}
enum WalkTheDogStateMachine {
Ready(WalkTheDogState<Ready>),
Walking(WalkTheDogState<Walking>),
GameOver(WalkTheDogState<GameOver>),
}
impl WalkTheDogStateMachine {
fn new(walk: Walk) -> Self {
WalkTheDogStateMachine::Ready(WalkTheDogState::new(walk))
}
fn update(self, keystate: &KeyState) -> Self {
match self {
WalkTheDogStateMachine::Ready(state) => state.update(keystate).into(),
WalkTheDogStateMachine::Walking(state) => state.update(keystate).into(),
WalkTheDogStateMachine::GameOver(state) => state.update().into(),
}
}
fn draw(&self, renderer: &Renderer) {
match self {
WalkTheDogStateMachine::Ready(state) => state.draw(renderer),
WalkTheDogStateMachine::Walking(state) => state.draw(renderer),
WalkTheDogStateMachine::GameOver(state) => state.draw(renderer),
};
}
}
struct WalkTheDogState<T> {
_state: T,
walk: Walk,
}
impl<T> WalkTheDogState<T> {
fn draw(&self, renderer: &Renderer) {
self.walk.draw(renderer);
}
}
struct Ready;
impl WalkTheDogState<Ready> {
fn new(walk: Walk) -> WalkTheDogState<Ready> {
browser::draw_ui("<p id='score'>0</>").unwrap();
WalkTheDogState {
_state: Ready,
walk,
}
}
fn run_right(&mut self) {
self.walk.boy.run_right();
}
fn start_running(mut self) -> WalkTheDogState<Walking> {
self.run_right();
WalkTheDogState {
_state: Walking,
walk: self.walk,
}
}
fn update(mut self, keystate: &KeyState) -> ReadyEndState {
self.walk.boy.update();
if keystate.is_pressed("ArrowRight") {
ReadyEndState::Complete(self.start_running())
} else {
ReadyEndState::Continue(self)
}
}
}
enum ReadyEndState {
Complete(WalkTheDogState<Walking>),
Continue(WalkTheDogState<Ready>),
}
impl From<ReadyEndState> for WalkTheDogStateMachine {
fn from(state: ReadyEndState) -> Self {
match state {
ReadyEndState::Complete(walking) => walking.into(),
ReadyEndState::Continue(ready) => ready.into(),
}
}
}
struct Walking;
impl WalkTheDogState<Walking> {
fn end_game(self) -> WalkTheDogState<GameOver> {
let receiver = browser::draw_ui("<button id='new_game'>New Game</button>")
.and_then(|_unit| browser::find_html_element_by_id("new_game"))
.map(engine::add_click_handler)
.unwrap();
WalkTheDogState {
_state: GameOver {
new_game_event: receiver,
},
walk: self.walk,
}
}
fn update(mut self, keystate: &KeyState) -> WalkingEndState {
if keystate.is_pressed("Space") {
self.walk.boy.jump();
}
if keystate.is_pressed("ArrowDown") {
self.walk.boy.slide();
}
self.walk.boy.update();
let walking_speed = self.walk.velocity();
let [first_background, second_background] = &mut self.walk.backgrounds;
first_background.move_horizontally(walking_speed);
second_background.move_horizontally(walking_speed);
if first_background.right() < 0 {
first_background.set_x(second_background.right());
}
if second_background.right() < 0 {
second_background.set_x(first_background.right());
}
self.walk.obstacles.retain(|obstacle| obstacle.right() > 0);
self.walk.obstacles.iter_mut().for_each(|obstacle| {
obstacle.move_horizontally(walking_speed);
obstacle.check_intersection(&mut self.walk.boy);
});
if self.walk.timeline < TIMELINE_MINIMUM {
self.walk.generate_next_segment();
} else {
self.walk.timeline += walking_speed;
}
self.walk.score += 1;
if self.walk.knocked_out() {
WalkingEndState::Complete(self.end_game())
} else {
WalkingEndState::Continue(self)
}
}
}
enum WalkingEndState {
Continue(WalkTheDogState<Walking>),
Complete(WalkTheDogState<GameOver>),
}
impl From<WalkingEndState> for WalkTheDogStateMachine {
fn from(state: WalkingEndState) -> Self {
match state {
WalkingEndState::Continue(walking) => walking.into(),
WalkingEndState::Complete(game_over) => game_over.into(),
}
}
}
struct GameOver {
new_game_event: UnboundedReceiver<()>,
}
impl WalkTheDogState<GameOver> {
fn update(mut self) -> GameOverEndState {
if self._state.new_game_pressed() {
GameOverEndState::Complete(self.new_game())
} else {
GameOverEndState::Continue(self)
}
}
fn new_game(self) -> WalkTheDogState<Ready> {
if let Err(err) = browser::hide_ui() {
error!("Error hiding the browser {:#?}", err);
}
WalkTheDogState {
_state: Ready,
walk: Walk::reset(self.walk),
}
}
}
enum GameOverEndState {
Continue(WalkTheDogState<GameOver>),
Complete(WalkTheDogState<Ready>),
}
impl From<GameOverEndState> for WalkTheDogStateMachine {
fn from(state: GameOverEndState) -> Self {
match state {
GameOverEndState::Continue(game_over) => game_over.into(),
GameOverEndState::Complete(ready) => ready.into(),
}
}
}
impl GameOver {
fn new_game_pressed(&mut self) -> bool {
matches!(self.new_game_event.try_next(), Ok(Some(())))
}
}
impl From<WalkTheDogState<Walking>> for WalkTheDogStateMachine {
fn from(state: WalkTheDogState<Walking>) -> Self {
WalkTheDogStateMachine::Walking(state)
}
}
impl From<WalkTheDogState<GameOver>> for WalkTheDogStateMachine {
fn from(state: WalkTheDogState<GameOver>) -> Self {
WalkTheDogStateMachine::GameOver(state)
}
}
impl From<WalkTheDogState<Ready>> for WalkTheDogStateMachine {
fn from(state: WalkTheDogState<Ready>) -> Self {
WalkTheDogStateMachine::Ready(state)
}
}
pub trait Obstacle {
fn check_intersection(&self, boy: &mut RedHatBoy);
fn draw(&self, renderer: &Renderer);
fn move_horizontally(&mut self, x: i16);
fn right(&self) -> i16;
}
pub struct Platform {
sheet: Rc<SpriteSheet>,
sprites: Vec<Cell>,
bounding_boxes: Vec<Rect>,
position: Point,
}
impl Platform {
pub fn new(
sheet: Rc<SpriteSheet>,
position: Point,
sprite_names: &[&str],
bounding_boxes: &[Rect],
) -> Self {
let sprites = sprite_names
.iter()
.filter_map(|sprite_name| sheet.cell(sprite_name).cloned())
.collect();
let bounding_boxes = bounding_boxes
.iter()
.map(|bounding_box| {
Rect::new_from_x_y(
bounding_box.x() + position.x,
bounding_box.y() + position.y,
bounding_box.width,
bounding_box.height,
)
})
.collect();
Platform {
sheet,
position,
sprites,
bounding_boxes,
}
}
fn bounding_boxes(&self) -> &Vec<Rect> {
&self.bounding_boxes
}
}
impl Obstacle for Platform {
fn check_intersection(&self, boy: &mut RedHatBoy) {
if let Some(box_to_land_on) = self
.bounding_boxes()
.iter()
.find(|&bounding_box| boy.bounding_box().intersects(bounding_box))
{
if boy.velocity_y() > 0 && boy.pos_y() < self.position.y {
boy.land_on(box_to_land_on.y());
} else {
boy.knock_out();
}
}
}
fn draw(&self, renderer: &Renderer) {
let mut x = 0;
self.sprites.iter().for_each(|sprite| {
self.sheet.draw(
renderer,
&Rect::new_from_x_y(
sprite.frame.x,
sprite.frame.y,
sprite.frame.w,
sprite.frame.h,
),
// Just use position and the standard widths in the tileset
&Rect::new_from_x_y(
self.position.x + x,
self.position.y,
sprite.frame.w,
sprite.frame.h,
),
);
x += sprite.frame.w;
});
}
fn move_horizontally(&mut self, x: i16) {
self.position.x += x;
self.bounding_boxes.iter_mut().for_each(|bounding_box| {
bounding_box.set_x(bounding_box.position.x + x);
})
}
fn right(&self) -> i16 {
self.bounding_boxes()
.last()
.unwrap_or(&Rect::default())
.right()
}
}
pub struct RedHatBoy {
state_machine: RedHatBoyStateMachine,
sprite_sheet: Sheet,
image: HtmlImageElement,
}
impl RedHatBoy {
fn new(sprite_sheet: Sheet, image: HtmlImageElement, audio: Audio, sound: Sound) -> Self {
RedHatBoy {
state_machine: RedHatBoyStateMachine::Idle(RedHatBoyState::new(audio, sound)),
sprite_sheet,
image,
}
}
fn reset(boy: Self) -> Self {
RedHatBoy::new(
boy.sprite_sheet,
boy.image,
boy.state_machine.context().audio.clone(),
boy.state_machine.context().jump_sound.clone(),
)
}
fn run_right(&mut self) {
self.state_machine = self.state_machine.clone().transition(Event::Run);
}
fn slide(&mut self) {
self.state_machine = self.state_machine.clone().transition(Event::Slide);
}
fn jump(&mut self) {
self.state_machine = self.state_machine.clone().transition(Event::Jump);
}
fn knock_out(&mut self) {
self.state_machine = self.state_machine.clone().transition(Event::KnockOut);
}
fn land_on(&mut self, position: i16) {
self.state_machine = self.state_machine.clone().transition(Event::Land(position));
}
fn update(&mut self) {
self.state_machine = self.state_machine.clone().update();
}
fn knocked_out(&self) -> bool {
self.state_machine.knocked_out()
}
fn pos_y(&self) -> i16 {
self.state_machine.context().position.y
}
fn velocity_y(&self) -> i16 {
self.state_machine.context().velocity.y
}
fn walking_speed(&self) -> i16 {
self.state_machine.context().velocity.x
}
fn frame_name(&self) -> String {
format!(
"{} ({}).png",
self.state_machine.frame_name(),
(self.state_machine.context().frame / 3) + 1
)
}
fn current_sprite(&self) -> Option<&Cell> {
self.sprite_sheet.frames.get(&self.frame_name())
}
fn bounding_box(&self) -> Rect {
const X_OFFSET: i16 = 18;
const Y_OFFSET: i16 = 14;
const WIDTH_OFFSET: i16 = 28;
Rect::new_from_x_y(
self.destination_box().x() + X_OFFSET,
self.destination_box().y() + Y_OFFSET,
self.destination_box().width - WIDTH_OFFSET,
self.destination_box().height - Y_OFFSET,
)
}
fn destination_box(&self) -> Rect {
let sprite = self.current_sprite().expect("Cell not found");
Rect::new_from_x_y(
self.state_machine.context().position.x + sprite.sprite_source_size.x,
self.state_machine.context().position.y + sprite.sprite_source_size.y,
sprite.frame.w,
sprite.frame.h,
)
}
fn draw(&self, renderer: &Renderer) {
let sprite = self.current_sprite().expect("Cell not found");
renderer.draw_image(
&self.image,
&Rect::new_from_x_y(
sprite.frame.x,
sprite.frame.y,
sprite.frame.w,
sprite.frame.h,
),
&self.destination_box(),
);
}
}
#[derive(Clone)]
enum RedHatBoyStateMachine {
Idle(RedHatBoyState<Idle>),
Running(RedHatBoyState<Running>),
Sliding(RedHatBoyState<Sliding>),
Jumping(RedHatBoyState<Jumping>),
Falling(RedHatBoyState<Falling>),
KnockedOut(RedHatBoyState<KnockedOut>),
}
pub enum Event {
Run,
Jump,
Slide,
KnockOut,
Land(i16),
Update,
}
impl RedHatBoyStateMachine {
fn transition(self, event: Event) -> Self {
match (self.clone(), event) {
(RedHatBoyStateMachine::Idle(state), Event::Run) => state.run().into(),
(RedHatBoyStateMachine::Running(state), Event::Jump) => state.jump().into(),
(RedHatBoyStateMachine::Running(state), Event::Slide) => state.slide().into(),
(RedHatBoyStateMachine::Running(state), Event::KnockOut) => state.knock_out().into(),
(RedHatBoyStateMachine::Running(state), Event::Land(position)) => {
state.land_on(position).into()
}
(RedHatBoyStateMachine::Jumping(state), Event::Land(position)) => {
state.land_on(position).into()
}
(RedHatBoyStateMachine::Jumping(state), Event::KnockOut) => state.knock_out().into(),
(RedHatBoyStateMachine::Sliding(state), Event::KnockOut) => state.knock_out().into(),
(RedHatBoyStateMachine::Sliding(state), Event::Land(position)) => {
state.land_on(position).into()
}
(RedHatBoyStateMachine::Idle(state), Event::Update) => state.update().into(),
(RedHatBoyStateMachine::Running(state), Event::Update) => state.update().into(),
(RedHatBoyStateMachine::Jumping(state), Event::Update) => state.update().into(),
(RedHatBoyStateMachine::Sliding(state), Event::Update) => state.update().into(),
(RedHatBoyStateMachine::Falling(state), Event::Update) => state.update().into(),
_ => self,
}
}
fn frame_name(&self) -> &str {
match self {
RedHatBoyStateMachine::Idle(state) => state.frame_name(),
RedHatBoyStateMachine::Running(state) => state.frame_name(),
RedHatBoyStateMachine::Jumping(state) => state.frame_name(),
RedHatBoyStateMachine::Sliding(state) => state.frame_name(),
RedHatBoyStateMachine::Falling(state) => state.frame_name(),
RedHatBoyStateMachine::KnockedOut(state) => state.frame_name(),
}
}
fn context(&self) -> &RedHatBoyContext {
match self {
RedHatBoyStateMachine::Idle(state) => state.context(),
RedHatBoyStateMachine::Running(state) => state.context(),
RedHatBoyStateMachine::Jumping(state) => state.context(),
RedHatBoyStateMachine::Sliding(state) => state.context(),
RedHatBoyStateMachine::Falling(state) => state.context(),
RedHatBoyStateMachine::KnockedOut(state) => state.context(),
}
}
fn knocked_out(&self) -> bool {
matches!(self, RedHatBoyStateMachine::KnockedOut(_))
}
fn update(self) -> Self {
self.transition(Event::Update)
}
}
impl From<RedHatBoyState<Idle>> for RedHatBoyStateMachine {
fn from(state: RedHatBoyState<Idle>) -> Self {
RedHatBoyStateMachine::Idle(state)
}
}
impl From<RedHatBoyState<Running>> for RedHatBoyStateMachine {
fn from(state: RedHatBoyState<Running>) -> Self {
RedHatBoyStateMachine::Running(state)
}
}
impl From<RedHatBoyState<Sliding>> for RedHatBoyStateMachine {
fn from(state: RedHatBoyState<Sliding>) -> Self {
RedHatBoyStateMachine::Sliding(state)
}
}
impl From<RedHatBoyState<Jumping>> for RedHatBoyStateMachine {
fn from(state: RedHatBoyState<Jumping>) -> Self {
RedHatBoyStateMachine::Jumping(state)
}
}
impl From<RedHatBoyState<Falling>> for RedHatBoyStateMachine {
fn from(state: RedHatBoyState<Falling>) -> Self {
RedHatBoyStateMachine::Falling(state)
}
}
impl From<RedHatBoyState<KnockedOut>> for RedHatBoyStateMachine {
fn from(state: RedHatBoyState<KnockedOut>) -> Self {
RedHatBoyStateMachine::KnockedOut(state)
}
}
impl From<SlidingEndState> for RedHatBoyStateMachine {
fn from(state: SlidingEndState) -> Self {
match state {
SlidingEndState::Sliding(sliding) => sliding.into(),
SlidingEndState::Running(running) => running.into(),
}
}
}
impl From<JumpingEndState> for RedHatBoyStateMachine {
fn from(state: JumpingEndState) -> Self {
match state {
JumpingEndState::Jumping(jumping) => jumping.into(),
JumpingEndState::Landing(landing) => landing.into(),
}
}
}
impl From<FallingEndState> for RedHatBoyStateMachine {
fn from(state: FallingEndState) -> Self {
match state {
FallingEndState::Falling(falling) => falling.into(),
FallingEndState::KnockedOut(knocked_out) => knocked_out.into(),
}
}
}
mod red_hat_boy_states {
use super::{Audio, Sound, HEIGHT};
use crate::engine::Point;
const FLOOR: i16 = 479;
const PLAYER_HEIGHT: i16 = HEIGHT - FLOOR;
const RUNNING_SPEED: i16 = 4;
const STARTING_POINT: i16 = -20;
const IDLE_FRAMES: u8 = 29;
const RUNNING_FRAMES: u8 = 23;
const JUMPING_FRAMES: u8 = 35;
const SLIDING_FRAMES: u8 = 14;
const FALLING_FRAMES: u8 = 29;
const IDLE_FRAME_NAME: &str = "Idle";
const RUN_FRAME_NAME: &str = "Run";
const SLIDING_FRAME_NAME: &str = "Slide";
const JUMPING_FRAME_NAME: &str = "Jump";
const FALLING_FRAME_NAME: &str = "Dead";
const JUMP_SPEED: i16 = -25;
const GRAVITY: i16 = 1;
const TERMINAL_VELOCITY: i16 = 20;
#[derive(Clone)]
pub struct RedHatBoyState<S> {
context: RedHatBoyContext,
_state: S,
}
impl<S> RedHatBoyState<S> {
pub fn context(&self) -> &RedHatBoyContext {
&self.context
}
fn update_context(&mut self, frames: u8) {
self.context = self.context.clone().update(frames);
}
}
#[derive(Copy, Clone)]
pub struct Idle;
impl RedHatBoyState<Idle> {
pub fn new(audio: Audio, jump_sound: Sound) -> Self {
RedHatBoyState {
context: RedHatBoyContext {
frame: 0,
position: Point {
x: STARTING_POINT,
y: FLOOR,
},
velocity: Point { x: 0, y: 0 },
audio,
jump_sound,
},
_state: Idle {},
}
}
pub fn frame_name(&self) -> &str {
IDLE_FRAME_NAME
}
pub fn update(mut self) -> RedHatBoyState<Idle> {
self.update_context(IDLE_FRAMES);
self
}
pub fn run(self) -> RedHatBoyState<Running> {
RedHatBoyState {
context: self.context.reset_frame().run_right(),
_state: Running {},
}
}
}
#[derive(Copy, Clone)]
pub struct Running;
impl RedHatBoyState<Running> {
pub fn frame_name(&self) -> &str {
RUN_FRAME_NAME
}
pub fn update(mut self) -> RedHatBoyState<Running> {
self.update_context(RUNNING_FRAMES);
self
}
pub fn jump(self) -> RedHatBoyState<Jumping> {
RedHatBoyState {
context: self
.context
.reset_frame()
.set_vertical_velocity(JUMP_SPEED)
.play_jump_sound(),
_state: Jumping {},
}
}
pub fn slide(self) -> RedHatBoyState<Sliding> {
RedHatBoyState {
context: self.context.reset_frame(),
_state: Sliding {},
}
}
pub fn knock_out(self) -> RedHatBoyState<Falling> {
RedHatBoyState {
context: self.context.reset_frame().stop(),
_state: Falling {},
}
}
pub fn land_on(self, position: i16) -> RedHatBoyState<Running> {
RedHatBoyState {
context: self.context.set_on(position),
_state: Running {},
}
}
}
#[derive(Copy, Clone)]
pub struct Jumping;
pub enum JumpingEndState {
Jumping(RedHatBoyState<Jumping>),
Landing(RedHatBoyState<Running>),
}
impl RedHatBoyState<Jumping> {
pub fn frame_name(&self) -> &str {
JUMPING_FRAME_NAME
}
pub fn knock_out(self) -> RedHatBoyState<Falling> {
RedHatBoyState {
context: self.context.reset_frame().stop(),
_state: Falling {},
}
}
pub fn update(mut self) -> JumpingEndState {
self.update_context(JUMPING_FRAMES);
if self.context.position.y >= FLOOR {
JumpingEndState::Landing(self.land_on(HEIGHT))
} else {
JumpingEndState::Jumping(self)
}
}
pub fn land_on(self, position: i16) -> RedHatBoyState<Running> {
RedHatBoyState {
context: self.context.reset_frame().set_on(position),
_state: Running,
}
}
}
#[derive(Copy, Clone)]
pub struct Sliding;
pub enum SlidingEndState {
Sliding(RedHatBoyState<Sliding>),
Running(RedHatBoyState<Running>),
}
impl RedHatBoyState<Sliding> {
pub fn frame_name(&self) -> &str {
SLIDING_FRAME_NAME
}
pub fn stand(self) -> RedHatBoyState<Running> {
RedHatBoyState {
context: self.context.reset_frame(),
_state: Running {},
}
}
pub fn knock_out(self) -> RedHatBoyState<Falling> {
RedHatBoyState {
context: self.context.reset_frame().stop(),
_state: Falling {},
}
}
pub fn update(mut self) -> SlidingEndState {
self.update_context(SLIDING_FRAMES);
if self.context.frame >= SLIDING_FRAMES {
SlidingEndState::Running(self.stand())
} else {
SlidingEndState::Sliding(self)
}
}
pub fn land_on(self, position: i16) -> RedHatBoyState<Sliding> {
RedHatBoyState {
context: self.context.set_on(position),
_state: Sliding {},
}
}
}
#[derive(Copy, Clone)]
pub struct Falling;
impl RedHatBoyState<Falling> {
pub fn frame_name(&self) -> &str {
FALLING_FRAME_NAME
}
pub fn knock_out(self) -> RedHatBoyState<KnockedOut> {
RedHatBoyState {
context: self.context,
_state: KnockedOut {},
}
}
pub fn update(mut self) -> FallingEndState {
self.update_context(FALLING_FRAMES);
if self.context.frame >= FALLING_FRAMES {
FallingEndState::KnockedOut(self.knock_out())
} else {
FallingEndState::Falling(self)
}
}
}
pub enum FallingEndState {
KnockedOut(RedHatBoyState<KnockedOut>),
Falling(RedHatBoyState<Falling>),
}
#[derive(Copy, Clone)]
pub struct KnockedOut;
impl RedHatBoyState<KnockedOut> {
pub fn frame_name(&self) -> &str {
FALLING_FRAME_NAME
}
}
#[derive(Clone)]
pub struct RedHatBoyContext {
pub frame: u8,
pub position: Point,
pub velocity: Point,
pub audio: Audio,
pub jump_sound: Sound,
}
impl RedHatBoyContext {
pub fn update(mut self, frame_count: u8) -> Self {
if self.velocity.y < TERMINAL_VELOCITY {
self.velocity.y += GRAVITY;
}
if self.frame < frame_count {
self.frame += 1;
} else {
self.frame = 0;
}
self.position.y += self.velocity.y;
if self.position.y > FLOOR {
self.position.y = FLOOR;
}
self
}
fn reset_frame(mut self) -> Self {
self.frame = 0;
self
}
fn set_vertical_velocity(mut self, y: i16) -> Self {
self.velocity.y = y;
self
}
fn run_right(mut self) -> Self {
self.velocity.x += RUNNING_SPEED;
self
}
fn stop(mut self) -> Self {
self.velocity.x = 0;
self.velocity.y = 0;
self
}
fn set_on(mut self, position: i16) -> Self {
let position = position - PLAYER_HEIGHT;
self.position.y = position;
self
}
fn play_jump_sound(self) -> Self {
if let Err(err) = self.audio.play_sound(&self.jump_sound) {
log!("Error playing jump sound {:#?}", err);
}
self
}
}
}
pub struct Walk {
obstacle_sheet: Rc<SpriteSheet>,
stone: HtmlImageElement,
boy: RedHatBoy,
backgrounds: [Image; 2],
obstacles: Vec<Box<dyn Obstacle>>,
timeline: i16,
score: u16,
}
impl Walk {
fn knocked_out(&self) -> bool {
self.boy.knocked_out()
}
fn reset(walk: Self) -> Self {
let starting_obstacles =
stone_and_platform(walk.stone.clone(), walk.obstacle_sheet.clone(), 0);
let timeline = rightmost(&starting_obstacles);
Walk {
boy: RedHatBoy::reset(walk.boy),
backgrounds: walk.backgrounds,
obstacles: starting_obstacles,
obstacle_sheet: walk.obstacle_sheet,
stone: walk.stone,
score: 0,
timeline,
}
}
fn draw(&self, renderer: &Renderer) {
self.backgrounds.iter().for_each(|background| {
background.draw(renderer);
});
self.boy.draw(renderer);
self.obstacles.iter().for_each(|obstacle| {
obstacle.draw(renderer);
});
browser::find_html_element_by_id("score")
.map(|element| element.set_inner_html(&format!("Score: {}", self.score)))
.unwrap();
}
fn velocity(&self) -> i16 {
-self.boy.walking_speed()
}
fn generate_next_segment(&mut self) {
let mut rng = thread_rng();
let next_segment = rng.gen_range(0..2);
let mut next_obstacles = match next_segment {
0 => stone_and_platform(
self.stone.clone(),
self.obstacle_sheet.clone(),
self.timeline + OBSTACLE_BUFFER,
),
1 => platform_and_stone(
self.stone.clone(),
self.obstacle_sheet.clone(),
self.timeline + OBSTACLE_BUFFER,
),
_ => vec![],
};
self.timeline = rightmost(&next_obstacles);
self.obstacles.append(&mut next_obstacles);
}
}
pub struct Barrier {
image: Image,
}
impl Barrier {
pub fn new(image: Image) -> Self {
Barrier { image }
}
}
impl Obstacle for Barrier {
fn check_intersection(&self, boy: &mut RedHatBoy) {
if boy.bounding_box().intersects(self.image.bounding_box()) {
boy.knock_out()
}
}
fn draw(&self, renderer: &Renderer) {
self.image.draw(renderer);
}
fn move_horizontally(&mut self, x: i16) {
self.image.move_horizontally(x);
}
fn right(&self) -> i16 {
self.image.right()
}
}
#[async_trait(?Send)]
impl Game for WalkTheDog {
async fn initialize(&self) -> Result<Box<dyn Game>> {
match self.machine {
None => {
let sheet = browser::fetch_json("rhb.json")
.await?
.into_serde::<Sheet>()?;
let background = engine::load_image("BG.png").await?;
let stone = engine::load_image("Stone.png").await?;
let tiles = browser::fetch_json("tiles.json").await?;
let sprite_sheet = Rc::new(SpriteSheet::new(
tiles.into_serde::<Sheet>()?,
engine::load_image("tiles.png").await?,
));
let audio = Audio::new()?;
let sound = audio.load_sound("SFX_Jump_23.mp3").await?;
let background_music = audio.load_sound("background_song.mp3").await?;
audio.play_looping_sound(&background_music)?;
let rhb = RedHatBoy::new(sheet, engine::load_image("rhb.png").await?, audio, sound);
let background_width = background.width() as i16;
let starting_obstacles = stone_and_platform(stone.clone(), sprite_sheet.clone(), 0);
let timeline = rightmost(&starting_obstacles);
let machine = WalkTheDogStateMachine::new(Walk {
boy: rhb,
backgrounds: [
Image::new(background.clone(), Point { x: 0, y: 0 }),
Image::new(
background,
Point {
x: background_width,
y: 0,
},
),
],
obstacles: starting_obstacles,
obstacle_sheet: sprite_sheet,
score: 0,
stone,
timeline,
});
Ok(Box::new(WalkTheDog {
machine: Some(machine),
}))
}
Some(_) => Err(anyhow!("Error: Game is already initialized!")),
}
}
fn update(&mut self, keystate: &KeyState) {
if let Some(machine) = self.machine.take() {
self.machine.replace(machine.update(keystate));
}
assert!(self.machine.is_some());
}
fn draw(&self, renderer: &Renderer) {
renderer.clear(&Rect::new(Point { x: 0, y: 0 }, 600, HEIGHT));
if let Some(machine) = &self.machine {
machine.draw(renderer);
}
}
}
fn rightmost(obstacle_list: &[Box<dyn Obstacle>]) -> i16 {
obstacle_list
.iter()
.map(|obstacle| obstacle.right())
.max_by(|x, y| x.cmp(y))
.unwrap_or(0)
}
#[cfg(test)]
mod tests {
use super::*;
use futures::channel::mpsc::unbounded;
use std::collections::HashMap;
use web_sys::{AudioBuffer, AudioBufferOptions};
use wasm_bindgen_test::wasm_bindgen_test;
wasm_bindgen_test::wasm_bindgen_test_configure!(run_in_browser);
#[wasm_bindgen_test]
fn test_transition_from_game_over_to_new_game() {
let (_, receiver) = unbounded();
let image = HtmlImageElement::new().unwrap();
let audio = Audio::new().unwrap();
let options = AudioBufferOptions::new(1, 3000.0);
let sound = Sound {
buffer: AudioBuffer::new(&options).unwrap(),
};
let rhb = RedHatBoy::new(
Sheet {
frames: HashMap::new(),
},
image.clone(),
audio,
sound,
);
let sprite_sheet = SpriteSheet::new(
Sheet {
frames: HashMap::new(),
},
image.clone(),
);
let walk = Walk {
boy: rhb,
backgrounds: [
Image::new(image.clone(), Point { x: 0, y: 0 }),
Image::new(image.clone(), Point { x: 0, y: 0 }),
],
obstacles: vec![],
obstacle_sheet: Rc::new(sprite_sheet),
stone: image.clone(),
score: 0,
timeline: 0,
};
let document = browser::document().unwrap();
document
.body()
.unwrap()
.insert_adjacent_html("afterbegin", "<div id='ui'></div>")
.unwrap();
browser::draw_ui("<p>This is the UI</p>").unwrap();
let state = WalkTheDogState {
_state: GameOver {
new_game_event: receiver,
},
walk: walk,
};
state.new_game();
let ui = browser::find_html_element_by_id("ui").unwrap();
assert_eq!(ui.child_element_count(), 0);
}
}
================================================
FILE: src/lib.rs
================================================
#[macro_use]
mod browser;
mod engine;
mod game;
mod segments;
mod sound;
use engine::GameLoop;
use game::WalkTheDog;
use wasm_bindgen::prelude::*;
// This is like the `main` function, except for JavaScript.
#[wasm_bindgen(start)]
pub fn main_js() -> Result<(), JsValue> {
console_error_panic_hook::set_once();
browser::spawn_local(async move {
let game = WalkTheDog::new();
GameLoop::start(game)
.await
.expect("Could not start game loop");
});
Ok(())
}
================================================
FILE: src/segments.rs
================================================
use std::rc::Rc;
use web_sys::HtmlImageElement;
use crate::engine::{Image, Point, Rect, SpriteSheet};
use crate::game::{Barrier, Obstacle, Platform};
const LOW_PLATFORM: i16 = 420;
const HIGH_PLATFORM: i16 = 375;
const FIRST_PLATFORM: i16 = 370;
const STONE_ON_GROUND: i16 = 546;
const FLOATING_PLATFORM_SPRITES: [&str; 3] = ["13.png", "14.png", "15.png"];
const PLATFORM_WIDTH: i16 = 384;
const PLATFORM_HEIGHT: i16 = 93;
const PLATFORM_EDGE_WIDTH: i16 = 60;
const PLATFORM_EDGE_HEIGHT: i16 = 54;
const FLOATING_PLATFORM_BOUNDING_BOXES: [Rect; 3] = [
Rect::new_from_x_y(0, 0, PLATFORM_EDGE_WIDTH, PLATFORM_EDGE_HEIGHT),
Rect::new_from_x_y(
PLATFORM_EDGE_WIDTH,
0,
PLATFORM_WIDTH - (PLATFORM_EDGE_WIDTH * 2),
PLATFORM_HEIGHT,
),
Rect::new_from_x_y(
PLATFORM_WIDTH - PLATFORM_EDGE_WIDTH,
0,
PLATFORM_EDGE_WIDTH,
PLATFORM_EDGE_HEIGHT,
),
];
fn create_floating_platform(sprite_sheet: Rc<SpriteSheet>, position: Point) -> Platform {
Platform::new(
sprite_sheet,
position,
&FLOATING_PLATFORM_SPRITES,
&FLOATING_PLATFORM_BOUNDING_BOXES,
)
}
pub fn stone_and_platform(
stone: HtmlImageElement,
sprite_sheet: Rc<SpriteSheet>,
offset_x: i16,
) -> Vec<Box<dyn Obstacle>> {
const INITIAL_STONE_OFFSET: i16 = 150;
vec![
Box::new(Barrier::new(Image::new(
stone,
Point {
x: offset_x + INITIAL_STONE_OFFSET,
y: STONE_ON_GROUND,
},
))),
Box::new(create_floating_platform(
sprite_sheet,
Point {
x: offset_x + FIRST_PLATFORM,
y: LOW_PLATFORM,
},
)),
]
}
pub fn platform_and_stone(
stone: HtmlImageElement,
sprite_sheet: Rc<SpriteSheet>,
offset_x: i16,
) -> Vec<Box<dyn Obstacle>> {
const INITIAL_STONE_OFFSET: i16 = 400;
const INITIAL_PLATFORM_OFFSET: i16 = 200;
vec![
Box::new(Barrier::new(Image::new(
stone,
Point {
x: offset_x + INITIAL_STONE_OFFSET,
y: STONE_ON_GROUND,
},
))),
Box::new(create_floating_platform(
sprite_sheet,
Point {
x: offset_x + INITIAL_PLATFORM_OFFSET,
y: HIGH_PLATFORM,
},
)),
]
}
================================================
FILE: src/sound.rs
================================================
use anyhow::{anyhow, Result};
use js_sys::ArrayBuffer;
use wasm_bindgen::JsCast;
use wasm_bindgen_futures::JsFuture;
use web_sys::{AudioBuffer, AudioBufferSourceNode, AudioContext, AudioDestinationNode, AudioNode};
pub fn create_audio_context() -> Result<AudioContext> {
AudioContext::new().map_err(|err| anyhow!("Could not create audio context: {:#?}", err))
}
fn create_buffer_source(ctx: &AudioContext) -> Result<AudioBufferSourceNode> {
ctx.create_buffer_source()
.map_err(|err| anyhow!("Error creating buffer source {:#?}", err))
}
fn connect_with_audio_node(
buffer_source: &AudioBufferSourceNode,
destination: &AudioDestinationNode,
) -> Result<AudioNode> {
buffer_source
.connect_with_audio_node(destination)
.map_err(|err| anyhow!("Error connecting audio source to destination {:#?}", err))
}
fn create_track_source(ctx: &AudioContext, buffer: &AudioBuffer) -> Result<AudioBufferSourceNode> {
let track_source = create_buffer_source(ctx)?;
track_source.set_buffer(Some(buffer));
connect_with_audio_node(&track_source, &ctx.destination())?;
Ok(track_source)
}
pub enum LOOPING {
No,
Yes,
}
pub fn play_sound(ctx: &AudioContext, buffer: &AudioBuffer, looping: LOOPING) -> Result<()> {
let track_source = create_track_source(ctx, buffer)?;
if matches!(looping, LOOPING::Yes) {
track_source.set_loop(true);
}
track_source
.start()
.map_err(|err| anyhow!("Could not start sound! {:#?}", err))
}
pub async fn decode_audio_data(
ctx: &AudioContext,
array_buffer: &ArrayBuffer,
) -> Result<AudioBuffer> {
JsFuture::from(
ctx.decode_audio_data(array_buffer)
.map_err(|err| anyhow!("Could not decode audio from array buffer {:#?}", err))?,
)
.await
.map_err(|err| anyhow!("Could not convert promise to future {:#?}", err))?
.dyn_into()
.map_err(|err| anyhow!("Could not cast into AudioBuffer {:#?}", err))
}
================================================
FILE: src/test_browser.rs
================================================
================================================
FILE: static/index.html
================================================
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>My Rust + Webpack project!</title>
<link rel="stylesheet" href="styles.css" type="text/css" media=
"screen">
<link rel="preload" as="image" href="Button.svg">
<link rel="preload" as="font" href=
"kenney_future_narrow-webfont.woff2">
</head>
<body>
<div id="ui"></div>
<canvas id="canvas" style="outline: none" tabindex="0" height=
"600" width="600">
Your browser does not support the Canvas.
</canvas>
<script src="index.js"></script>
</body>
</html>
================================================
FILE: static/rhb.json
================================================
{"frames": {
"Dead (1).png":
{
"frame": {"x":0,"y":0,"w":71,"h":115},
"rotated": false,
"trimmed": true,
"spriteSourceSize": {"x":58,"y":8,"w":71,"h":115},
"sourceSize": {"w":160,"h":136}
},
"Dead (2).png":
{
"frame": {"x":117,"y":0,"w":87,"h":114},
"rotated": false,
"trimmed": true,
"spriteSourceSize": {"x":45,"y":9,"w":87,"h":114},
"sourceSize": {"w":160,"h":136}
},
"Dead (3).png":
{
"frame": {"x":234,"y":0,"w":97,"h":106},
"rotated": false,
"trimmed": true,
"spriteSourceSize": {"x":35,"y":18,"w":97,"h":106},
"sourceSize": {"w":160,"h":136}
},
"Dead (4).png":
{
"frame": {"x":351,"y":0,"w":105,"h":91},
"rotated": false,
"trimmed": true,
"spriteSourceSize": {"x":22,"y":32,"w":105,"h":91},
"sourceSize": {"w":160,"h":136}
},
"Dead (5).png":
{
"frame": {"x":468,"y":0,"w":107,"h":83},
"rotated": false,
"trimmed": true,
"spriteSourceSize": {"x":19,"y":45,"w":107,"h":83},
"sourceSize": {"w":160,"h":136}
},
"Dead (6).png":
{
"frame": {"x":585,"y":0,"w":107,"h":70},
"rotated": false,
"trimmed": true,
"spriteSourceSize": {"x":17,"y":58,"w":107,"h":70},
"sourceSize": {"w":160,"h":136}
},
"Dead (7).png":
{
"frame": {"x":702,"y":0,"w":109,"h":67},
"rotated": false,
"trimmed": true,
"spriteSourceSize": {"x":15,"y":59,"w":109,"h":67},
"sourceSize": {"w":160,"h":136}
},
"Dead (8).png":
{
"frame": {"x":819,"y":0,"w":110,"h":68},
"rotated": false,
"trimmed": true,
"spriteSourceSize": {"x":13,"y":61,"w":110,"h":68},
"sourceSize": {"w":160,"h":136}
},
"Dead (9).png":
{
"frame": {"x":936,"y":0,"w":115,"h":68},
"rotated": false,
"trimmed": true,
"spriteSourceSize": {"x":13,"y":61,"w":115,"h":68},
"sourceSize": {"w":160,"h":136}
},
"Dead (10).png":
{
"frame": {"x":1053,"y":0,"w":117,"h":68},
"rotated": false,
"trimmed": true,
"spriteSourceSize": {"x":13,"y":61,"w":117,"h":68},
"sourceSize": {"w":160,"h":136}
},
"Hurt (1).png":
{
"frame": {"x":1170,"y":0,"w":71,"h":115},
"rotated": false,
"trimmed": true,
"spriteSourceSize": {"x":58,"y":8,"w":71,"h":115},
"sourceSize": {"w":160,"h":136}
},
"Hurt (2).png":
{
"frame": {"x":1287,"y":0,"w":69,"h":112},
"rotated": false,
"trimmed": true,
"spriteSourceSize": {"x":67,"y":11,"w":69,"h":112},
"sourceSize": {"w":160,"h":136}
},
"Hurt (3).png":
{
"frame": {"x":1404,"y":0,"w":64,"h":103},
"rotated": false,
"trimmed": true,
"spriteSourceSize": {"x":78,"y":17,"w":64,"h":103},
"sourceSize": {"w":160,"h":136}
},
"Hurt (4).png":
{
"frame": {"x":1521,"y":0,"w":63,"h":102},
"rotated": false,
"trimmed": true,
"spriteSourceSize": {"x":79,"y":18,"w":63,"h":102},
"sourceSize": {"w":160,"h":136}
},
"Hurt (5).png":
{
"frame": {"x":1638,"y":0,"w":64,"h":102},
"rotated": false,
"trimmed": true,
"spriteSourceSize": {"x":79,"y":18,"w":64,"h":102},
"sourceSize": {"w":160,"h":136}
},
"Hurt (6).png":
{
"frame": {"x":1755,"y":0,"w":64,"h":101},
"rotated": false,
"trimmed": true,
"spriteSourceSize": {"x":79,"y":19,"w":64,"h":101},
"sourceSize": {"w":160,"h":136}
},
"Hurt (7).png":
{
"frame": {"x":1872,"y":0,"w":65,"h":101},
"rotated": false,
"trimmed": true,
"spriteSourceSize": {"x":79,"y":19,"w":65,"h":101},
"sourceSize": {"w":160,"h":136}
},
"Hurt (8).png":
{
"frame": {"x":0,"y":122,"w":68,"h":111},
"rotated": false,
"trimmed": true,
"spriteSourceSize": {"x":69,"y":12,"w":68,"h":111},
"sourceSize": {"w":160,"h":136}
},
"Idle (1).png":
{
"frame": {"x":117,"y":122,"w":71,"h":115},
"rotated": false,
"trimmed": true,
"spriteSourceSize": {"x":58,"y":8,"w":71,"h":115},
"sourceSize": {"w":160,"h":136}
},
"Idle (2).png":
{
"frame": {"x":234,"y":122,"w":71,"h":115},
"rotated": false,
"trimmed": true,
"spriteSourceSize": {"x":58,"y":8,"w":71,"h":115},
"sourceSize": {"w":160,"h":136}
},
"Idle (3).png":
{
"frame": {"x":351,"y":122,"w":70,"h":114},
"rotated": false,
"trimmed": true,
"spriteSourceSize": {"x":59,"y":9,"w":70,"h":114},
"sourceSize": {"w":160,"h":136}
},
"Idle (4).png":
{
"frame": {"x":468,"y":122,"w":70,"h":114},
"rotated": false,
"trimmed": true,
"spriteSourceSize": {"x":59,"y":9,"w":70,"h":114},
"sourceSize": {"w":160,"h":136}
},
"Idle (5).png":
{
"frame": {"x":585,"y":122,"w":70,"h":113},
"rotated": false,
"trimmed": true,
"spriteSourceSize": {"x":59,"y":10,"w":70,"h":113},
"sourceSize": {"w":160,"h":136}
},
"Idle (6).png":
{
"frame": {"x":702,"y":122,"w":71,"h":113},
"rotated": false,
"trimmed": true,
"spriteSourceSize": {"x":59,"y":10,"w":71,"h":113},
"sourceSize": {"w":160,"h":136}
},
"Idle (7).png":
{
"frame": {"x":819,"y":122,"w":71,"h":113},
"rotated": false,
"trimmed": true,
"spriteSourceSize": {"x":59,"y":10,"w":71,"h":113},
"sourceSize": {"w":160,"h":136}
},
"Idle (8).png":
{
"frame": {"x":936,"y":122,"w":70,"h":113},
"rotated": false,
"trimmed": true,
"spriteSourceSize": {"x":59,"y":10,"w":70,"h":113},
"sourceSize": {"w":160,"h":136}
},
"Idle (9).png":
{
"frame": {"x":1053,"y":122,"w":70,"h":114},
"rotated": false,
"trimmed": true,
"spriteSourceSize": {"x":59,"y":9,"w":70,"h":114},
"sourceSize": {"w":160,"h":136}
},
"Idle (10).png":
{
"frame": {"x":1170,"y":122,"w":70,"h":114},
"rotated": false,
"trimmed": true,
"spriteSourceSize": {"x":59,"y":9,"w":70,"h":114},
"sourceSize": {"w":160,"h":136}
},
"Jump (1).png":
{
"frame": {"x":1287,"y":122,"w":71,"h":115},
"rotated": false,
"trimmed": true,
"spriteSourceSize": {"x":58,"y":8,"w":71,"h":115},
"sourceSize": {"w":160,"h":136}
},
"Jump (2).png":
{
"frame": {"x":1404,"y":122,"w":70,"h":110},
"rotated": false,
"trimmed": true,
"spriteSourceSize": {"x":69,"y":13,"w":70,"h":110},
"sourceSize": {"w":160,"h":136}
},
"Jump (3).png":
{
"frame": {"x":1521,"y":122,"w":69,"h":109},
"rotated": false,
"trimmed": true,
"spriteSourceSize": {"x":72,"y":14,"w":69,"h":109},
"sourceSize": {"w":160,"h":136}
},
"Jump (4).png":
{
"frame": {"x":1638,"y":122,"w":70,"h":119},
"rotated": false,
"trimmed": true,
"spriteSourceSize": {"x":58,"y":3,"w":70,"h":119},
"sourceSize": {"w":160,"h":136}
},
"Jump (5).png":
{
"frame": {"x":1755,"y":122,"w":71,"h":119},
"rotated": false,
"trimmed": true,
"spriteSourceSize": {"x":58,"y":3,"w":71,"h":119},
"sourceSize": {"w":160,"h":136}
},
"Jump (6).png":
{
"frame": {"x":1872,"y":122,"w":70,"h":119},
"rotated": false,
"trimmed": true,
"spriteSourceSize": {"x":59,"y":3,"w":70,"h":119},
"sourceSize": {"w":160,"h":136}
},
"Jump (7).png":
{
"frame": {"x":0,"y":244,"w":70,"h":119},
"rotated": false,
"trimmed": true,
"spriteSourceSize": {"x":59,"y":3,"w":70,"h":119},
"sourceSize": {"w":160,"h":136}
},
"Jump (8).png":
{
"frame": {"x":117,"y":244,"w":71,"h":119},
"rotated": false,
"trimmed": true,
"spriteSourceSize": {"x":58,"y":3,"w":71,"h":119},
"sourceSize": {"w":160,"h":136}
},
"Jump (9).png":
{
"frame": {"x":234,"y":244,"w":70,"h":119},
"rotated": false,
"trimmed": true,
"spriteSourceSize": {"x":58,"y":3,"w":70,"h":119},
"sourceSize": {"w":160,"h":136}
},
"Jump (10).png":
{
"frame": {"x":351,"y":244,"w":69,"h":114},
"rotated": false,
"trimmed": true,
"spriteSourceSize": {"x":64,"y":6,"w":69,"h":114},
"sourceSize": {"w":160,"h":136}
},
"Jump (11).png":
{
"frame": {"x":468,"y":244,"w":73,"h":109},
"rotated": false,
"trimmed": true,
"spriteSourceSize": {"x":64,"y":11,"w":73,"h":109},
"sourceSize": {"w":160,"h":136}
},
"Jump (12).png":
{
"frame": {"x":585,"y":244,"w":68,"h":111},
"rotated": false,
"trimmed": true,
"spriteSourceSize": {"x":67,"y":11,"w":68,"h":111},
"sourceSize": {"w":160,"h":136}
},
"Run (1).png":
{
"frame": {"x":702,"y":244,"w":71,"h":115},
"rotated": false,
"trimmed": true,
"spriteSourceSize": {"x":58,"y":8,"w":71,"h":115},
"sourceSize": {"w":160,"h":136}
},
"Run (2).png":
{
"frame": {"x":819,"y":244,"w":75,"h":122},
"rotated": false,
"trimmed": true,
"spriteSourceSize": {"x":55,"y":5,"w":75,"h":122},
"sourceSize": {"w":160,"h":136}
},
"Run (3).png":
{
"frame": {"x":936,"y":244,"w":75,"h":117},
"rotated": false,
"trimmed": true,
"spriteSourceSize": {"x":56,"y":4,"w":75,"h":117},
"sourceSize": {"w":160,"h":136}
},
"Run (4).png":
{
"frame": {"x":1053,"y":244,"w":71,"h":113},
"rotated": false,
"trimmed": true,
"spriteSourceSize": {"x":57,"y":7,"w":71,"h":113},
"sourceSize": {"w":160,"h":136}
},
"Run (5).png":
{
"frame": {"x":1170,"y":244,"w":71,"h":115},
"rotated": false,
"trimmed": true,
"spriteSourceSize": {"x":58,"y":8,"w":71,"h":115},
"sourceSize": {"w":160,"h":136}
},
"Run (6).png":
{
"frame": {"x":1287,"y":244,"w":70,"h":120},
"rotated": false,
"trimmed": true,
"spriteSourceSize": {"x":57,"y":6,"w":70,"h":120},
"sourceSize": {"w":160,"h":136}
},
"Run (7).png":
{
"frame": {"x":1404,"y":244,"w":71,"h":115},
"rotated": false,
"trimmed": true,
"spriteSourceSize": {"x":55,"y":5,"w":71,"h":115},
"sourceSize": {"w":160,"h":136}
},
"Run (8).png":
{
"frame": {"x":1521,"y":244,"w":70,"h":115},
"rotated": false,
"trimmed": true,
"spriteSourceSize": {"x":57,"y":6,"w":70,"h":115},
"sourceSize": {"w":160,"h":136}
},
"Slide (1).png":
{
"frame": {"x":1638,"y":244,"w":85,"h":100},
"rotated": false,
"trimmed": true,
"spriteSourceSize": {"x":45,"y":28,"w":85,"h":100},
"sourceSize": {"w":160,"h":136}
},
"Slide (2).png":
{
"frame": {"x":1755,"y":244,"w":86,"h":100},
"rotated": false,
"trimmed": true,
"spriteSourceSize": {"x":44,"y":27,"w":86,"h":100},
"sourceSize": {"w":160,"h":136}
},
"Slide (3).png":
{
"frame": {"x":1872,"y":244,"w":87,"h":98},
"rotated": false,
"trimmed": true,
"spriteSourceSize": {"x":43,"y":27,"w":87,"h":98},
"sourceSize": {"w":160,"h":136}
},
"Slide (4).png":
{
"frame": {"x":1872,"y":244,"w":87,"h":98},
"rotated": false,
"trimmed": true,
"spriteSourceSize": {"x":43,"y":27,"w":87,"h":98},
"sourceSize": {"w":160,"h":136}
},
"Slide (5).png":
{
"frame": {"x":1755,"y":244,"w":86,"h":100},
"rotated": false,
"trimmed": true,
"spriteSourceSize": {"x":44,"y":27,"w":86,"h":100},
"sourceSize": {"w":160,"h":136}
}},
"meta": {
"app": "https://www.codeandweb.com/texturepacker",
"version": "1.0",
"image": "rhb_trimmed.png",
"format": "RGBA8888",
"size": {"w":1989,"h":366},
"scale": "1",
"smartupdate": "$TexturePacker:SmartUpdate:57b52b5f31c0bdebc34af7514c40da17:cbdcd04de8b7f111714940a6eac7b511:521d204853d0d2bba515b142dc3ea799$"
}
}
================================================
FILE: static/styles.css
================================================
#ui {
position: absolute;
}
@font-face {
font-family: 'Ken Future';
src: url('kenney_future_narrow-webfont.woff2');
}
#score {
font-family: 'Ken Future';
font-size: 16pt;
width: 200px;
position: absolute;
left: 400px;
top: 40px;
}
button {
font-family: 'Ken Future';
background: -72px -60px url('Button.svg');
border: none;
width: 82px;
height: 33px;
position: absolute;
transform: scale(1.8) translate(150px, 100px);
}
button:hover {
background: -158px -60px url('Button.svg');
}
button:active {
background: -244px -60px url('Button.svg');
}
================================================
FILE: static/tiles.json
================================================
{"frames": {
"1.png":
{
"frame": {"x":1,"y":132,"w":128,"h":128},
"rotated": false,
"trimmed": false,
"spriteSourceSize": {"x":0,"y":0,"w":128,"h":128},
"sourceSize": {"w":128,"h":128}
},
"2.png":
{
"frame": {"x":262,"y":1,"w":128,"h":128},
"rotated": false,
"trimmed": false,
"spriteSourceSize": {"x":0,"y":0,"w":128,"h":128},
"sourceSize": {"w":128,"h":128}
},
"3.png":
{
"frame": {"x":261,"y":261,"w":128,"h":128},
"rotated": false,
"trimmed": false,
"spriteSourceSize": {"x":0,"y":0,"w":128,"h":128},
"sourceSize": {"w":128,"h":128}
},
"4.png":
{
"frame": {"x":1,"y":1,"w":129,"h":129},
"rotated": false,
"trimmed": false,
"spriteSourceSize": {"x":0,"y":0,"w":129,"h":129},
"sourceSize": {"w":129,"h":129}
},
"5.png":
{
"frame": {"x":392,"y":1,"w":128,"h":128},
"rotated": false,
"trimmed": false,
"spriteSourceSize": {"x":0,"y":0,"w":128,"h":128},
"sourceSize": {"w":128,"h":128}
},
"6.png":
{
"frame": {"x":391,"y":131,"w":128,"h":128},
"rotated": false,
"trimmed": false,
"spriteSourceSize": {"x":0,"y":0,"w":128,"h":128},
"sourceSize": {"w":128,"h":128}
},
"7.png":
{
"frame": {"x":521,"y":131,"w":128,"h":128},
"rotated": false,
"trimmed": false,
"spriteSourceSize": {"x":0,"y":0,"w":128,"h":128},
"sourceSize": {"w":128,"h":128}
},
"8.png":
{
"frame": {"x":391,"y":261,"w":128,"h":128},
"rotated": false,
"trimmed": false,
"spriteSourceSize": {"x":0,"y":0,"w":128,"h":128},
"sourceSize": {"w":128,"h":128}
},
"9.png":
{
"frame": {"x":521,"y":261,"w":128,"h":128},
"rotated": false,
"trimmed": false,
"spriteSourceSize": {"x":0,"y":0,"w":128,"h":128},
"sourceSize": {"w":128,"h":128}
},
"10.png":
{
"frame": {"x":1,"y":262,"w":128,"h":128},
"rotated": false,
"trimmed": false,
"spriteSourceSize": {"x":0,"y":0,"w":128,"h":128},
"sourceSize": {"w":128,"h":128}
},
"11.png":
{
"frame": {"x":131,"y":132,"w":128,"h":128},
"rotated": false,
"trimmed": false,
"spriteSourceSize": {"x":0,"y":0,"w":128,"h":128},
"sourceSize": {"w":128,"h":128}
},
"12.png":
{
"frame": {"x":132,"y":1,"w":128,"h":128},
"rotated": false,
"trimmed": false,
"spriteSourceSize": {"x":0,"y":0,"w":128,"h":128},
"sourceSize": {"w":128,"h":128}
},
"13.png":
{
"frame": {"x":261,"y":391,"w":128,"h":93},
"rotated": false,
"trimmed": false,
"spriteSourceSize": {"x":0,"y":0,"w":128,"h":93},
"sourceSize": {"w":128,"h":93}
},
"14.png":
{
"frame": {"x":391,"y":391,"w":128,"h":93},
"rotated": false,
"trimmed": false,
"spriteSourceSize": {"x":0,"y":0,"w":128,"h":93},
"sourceSize": {"w":128,"h":93}
},
"15.png":
{
"frame": {"x":521,"y":391,"w":128,"h":93},
"rotated": false,
"trimmed": false,
"spriteSourceSize": {"x":0,"y":0,"w":128,"h":93},
"sourceSize": {"w":128,"h":93}
},
"16.png":
{
"frame": {"x":131,"y":262,"w":128,"h":128},
"rotated": false,
"trimmed": false,
"spriteSourceSize": {"x":0,"y":0,"w":128,"h":128},
"sourceSize": {"w":128,"h":128}
},
"17.png":
{
"frame": {"x":522,"y":1,"w":128,"h":99},
"rotated": true,
"trimmed": false,
"spriteSourceSize": {"x":0,"y":0,"w":128,"h":99},
"sourceSize": {"w":128,"h":99}
},
"18.png":
{
"frame": {"x":261,"y":131,"w":128,"h":128},
"rotated": false,
"trimmed": false,
"spriteSourceSize": {"x":0,"y":0,"w":128,"h":128},
"sourceSize": {"w":128,"h":128}
}},
"meta": {
"app": "https://www.codeandweb.com/texturepacker",
"version": "1.0",
"image": "tiles.png",
"format": "RGBA8888",
"size": {"w":650,"h":485},
"scale": "1",
"smartupdate": "$TexturePacker:SmartUpdate:6e3fdfd4ed3d5bfdbef834bd6d5c9225:fb784722f87c0e64fd62408e9c7c372e:accbe1e7e294ded8391337fc1c446319$"
}
}
================================================
FILE: tests/app.rs
================================================
use futures::prelude::*;
use wasm_bindgen::JsValue;
use wasm_bindgen_futures::JsFuture;
use wasm_bindgen_test::{wasm_bindgen_test, wasm_bindgen_test_configure};
wasm_bindgen_test_configure!(run_in_browser);
// This runs a unit test in native Rust, so it can only use Rust APIs.
#[test]
fn rust_test() {
assert_eq!(1, 1);
}
// This runs a unit test in the browser, so it can use browser APIs.
#[wasm_bindgen_test]
fn web_test() {
assert_eq!(1, 1);
}
================================================
FILE: webpack.config.js
================================================
const path = require("path");
const CopyPlugin = require("copy-webpack-plugin");
const WasmPackPlugin = require("@wasm-tool/wasm-pack-plugin");
const dist = path.resolve(__dirname, "dist");
module.exports = {
mode: "production",
entry: {
index: "./js/index.js"
},
output: {
path: dist,
filename: "[name].js"
},
devServer: {
contentBase: dist,
},
plugins: [
new CopyPlugin([
path.resolve(__dirname, "static")
]),
new WasmPackPlugin({
crateDirectory: __dirname,
}),
]
};
gitextract_9erurgo8/ ├── .github/ │ └── workflows/ │ └── build.yml ├── .gitignore ├── .rustfmt ├── .tool-versions ├── Cargo.toml ├── LICENSE ├── README.md ├── js/ │ └── index.js ├── package.json ├── rust-toolchain.toml ├── rustfmt.toml ├── src/ │ ├── browser.rs │ ├── engine.rs │ ├── game.rs │ ├── lib.rs │ ├── segments.rs │ ├── sound.rs │ └── test_browser.rs ├── static/ │ ├── index.html │ ├── rhb.json │ ├── styles.css │ └── tiles.json ├── tests/ │ └── app.rs └── webpack.config.js
SYMBOL INDEX (255 symbols across 7 files)
FILE: src/browser.rs
function window (line 27) | pub fn window() -> Result<Window> {
function document (line 31) | pub fn document() -> Result<Document> {
function canvas (line 37) | pub fn canvas() -> Result<HtmlCanvasElement> {
function context (line 45) | pub fn context() -> Result<CanvasRenderingContext2d> {
function spawn_local (line 59) | pub fn spawn_local<F>(future: F)
function fetch_with_str (line 66) | pub async fn fetch_with_str(resource: &str) -> Result<JsValue> {
function fetch_response (line 72) | pub async fn fetch_response(resource: &str) -> Result<Response> {
function fetch_json (line 79) | pub async fn fetch_json(json_path: &str) -> Result<JsValue> {
function fetch_array_buffer (line 90) | pub async fn fetch_array_buffer(resource: &str) -> Result<ArrayBuffer> {
function new_image (line 103) | pub fn new_image() -> Result<HtmlImageElement> {
type LoopClosure (line 107) | pub type LoopClosure = Closure<dyn FnMut(f64)>;
function create_raf_closure (line 108) | pub fn create_raf_closure(f: impl FnMut(f64) + 'static) -> LoopClosure {
function request_animation_frame (line 112) | pub fn request_animation_frame(callback: &LoopClosure) -> Result<i32> {
function closure_once (line 118) | pub fn closure_once<F, A, R>(fn_once: F) -> Closure<F::FnMut>
function closure_wrap (line 125) | pub fn closure_wrap<T: WasmClosure + ?Sized>(data: Box<T>) -> Closure<T> {
function now (line 129) | pub fn now() -> Result<f64> {
function draw_ui (line 136) | pub fn draw_ui(html: &str) -> Result<()> {
function hide_ui (line 142) | pub fn hide_ui() -> Result<()> {
function find_ui (line 159) | fn find_ui() -> Result<Element> {
function find_html_element_by_id (line 166) | pub fn find_html_element_by_id(id: &str) -> Result<HtmlElement> {
function test_error_loading_json (line 187) | async fn test_error_loading_json() {
FILE: src/engine.rs
type SheetRect (line 17) | pub struct SheetRect {
type Cell (line 26) | pub struct Cell {
type Sheet (line 32) | pub struct Sheet {
type Point (line 37) | pub struct Point {
type Rect (line 43) | pub struct Rect {
method new (line 50) | pub const fn new(position: Point, width: i16, height: i16) -> Self {
method new_from_x_y (line 58) | pub const fn new_from_x_y(x: i16, y: i16, width: i16, height: i16) -> ...
method intersects (line 62) | pub fn intersects(&self, rect: &Rect) -> bool {
method right (line 69) | pub fn right(&self) -> i16 {
method bottom (line 73) | pub fn bottom(&self) -> i16 {
method set_x (line 77) | pub fn set_x(&mut self, x: i16) {
method x (line 81) | pub fn x(&self) -> i16 {
method y (line 85) | pub fn y(&self) -> i16 {
type Renderer (line 90) | pub struct Renderer {
method clear (line 95) | pub fn clear(&self, rect: &Rect) {
method draw_image (line 104) | pub fn draw_image(&self, image: &HtmlImageElement, frame: &Rect, desti...
method draw_entire_image (line 120) | pub fn draw_entire_image(&self, image: &HtmlImageElement, position: &P...
method draw_rect (line 127) | pub fn draw_rect(&self, bounding_box: &Rect) {
method draw_text (line 140) | pub fn draw_text(&self, text: &str, location: &Point) -> Result<()> {
function load_image (line 149) | pub async fn load_image(source: &str) -> Result<HtmlImageElement> {
type Game (line 181) | pub trait Game {
method initialize (line 182) | async fn initialize(&self) -> Result<Box<dyn Game>>;
method update (line 183) | fn update(&mut self, keystate: &KeyState);
method draw (line 184) | fn draw(&self, renderer: &Renderer);
constant FRAME_SIZE (line 188) | const FRAME_SIZE: f32 = 1.0 / 60.0 * 1000.0;
type GameLoop (line 189) | pub struct GameLoop {
method start (line 196) | pub async fn start(game: impl Game) -> Result<()> {
type SharedLoopClosure (line 193) | type SharedLoopClosure = Rc<RefCell<Option<LoopClosure>>>;
function draw_frame_rate (line 244) | unsafe fn draw_frame_rate(renderer: &Renderer, frame_time: f64) {
type KeyState (line 267) | pub struct KeyState {
method new (line 272) | fn new() -> Self {
method is_pressed (line 278) | pub fn is_pressed(&self, code: &str) -> bool {
method set_pressed (line 282) | fn set_pressed(&mut self, code: &str, event: web_sys::KeyboardEvent) {
method set_released (line 286) | fn set_released(&mut self, code: &str) {
type KeyPress (line 291) | enum KeyPress {
function process_input (line 296) | fn process_input(state: &mut KeyState, keyevent_receiver: &mut Unbounded...
function prepare_input (line 309) | fn prepare_input() -> Result<UnboundedReceiver<KeyPress>> {
function add_click_handler (line 339) | pub fn add_click_handler(elem: HtmlElement) -> UnboundedReceiver<()> {
type Image (line 353) | pub struct Image {
method new (line 359) | pub fn new(element: HtmlImageElement, position: Point) -> Self {
method draw (line 368) | pub fn draw(&self, renderer: &Renderer) {
method bounding_box (line 372) | pub fn bounding_box(&self) -> &Rect {
method move_horizontally (line 376) | pub fn move_horizontally(&mut self, distance: i16) {
method set_x (line 380) | pub fn set_x(&mut self, x: i16) {
method right (line 384) | pub fn right(&self) -> i16 {
type SpriteSheet (line 389) | pub struct SpriteSheet {
method new (line 395) | pub fn new(sheet: Sheet, image: HtmlImageElement) -> Self {
method cell (line 399) | pub fn cell(&self, name: &str) -> Option<&Cell> {
method draw (line 403) | pub fn draw(&self, renderer: &Renderer, source: &Rect, destination: &R...
type Audio (line 409) | pub struct Audio {
method new (line 419) | pub fn new() -> Result<Self> {
method load_sound (line 425) | pub async fn load_sound(&self, filename: &str) -> Result<Sound> {
method play_sound (line 435) | pub fn play_sound(&self, sound: &Sound) -> Result<()> {
method play_looping_sound (line 439) | pub fn play_looping_sound(&self, sound: &Sound) -> Result<()> {
type Sound (line 414) | pub struct Sound {
function two_rects_that_intersect_on_the_left (line 449) | fn two_rects_that_intersect_on_the_left() {
FILE: src/game.rs
constant HEIGHT (line 18) | const HEIGHT: i16 = 600;
constant TIMELINE_MINIMUM (line 19) | const TIMELINE_MINIMUM: i16 = 1000;
constant OBSTACLE_BUFFER (line 20) | const OBSTACLE_BUFFER: i16 = 20;
type WalkTheDog (line 22) | pub struct WalkTheDog {
method new (line 27) | pub fn new() -> Self {
type WalkTheDogStateMachine (line 32) | enum WalkTheDogStateMachine {
method new (line 39) | fn new(walk: Walk) -> Self {
method update (line 43) | fn update(self, keystate: &KeyState) -> Self {
method draw (line 51) | fn draw(&self, renderer: &Renderer) {
method from (line 111) | fn from(state: ReadyEndState) -> Self {
method from (line 189) | fn from(state: WalkingEndState) -> Self {
method from (line 228) | fn from(state: GameOverEndState) -> Self {
method from (line 243) | fn from(state: WalkTheDogState<Walking>) -> Self {
method from (line 249) | fn from(state: WalkTheDogState<GameOver>) -> Self {
method from (line 255) | fn from(state: WalkTheDogState<Ready>) -> Self {
type WalkTheDogState (line 60) | struct WalkTheDogState<T> {
function draw (line 66) | fn draw(&self, renderer: &Renderer) {
type Ready (line 71) | struct Ready;
function new (line 74) | fn new(walk: Walk) -> WalkTheDogState<Ready> {
function run_right (line 82) | fn run_right(&mut self) {
function start_running (line 86) | fn start_running(mut self) -> WalkTheDogState<Walking> {
function update (line 95) | fn update(mut self, keystate: &KeyState) -> ReadyEndState {
type ReadyEndState (line 105) | enum ReadyEndState {
type Walking (line 119) | struct Walking;
function end_game (line 122) | fn end_game(self) -> WalkTheDogState<GameOver> {
function update (line 137) | fn update(mut self, keystate: &KeyState) -> WalkingEndState {
type WalkingEndState (line 183) | enum WalkingEndState {
type GameOver (line 197) | struct GameOver {
method new_game_pressed (line 237) | fn new_game_pressed(&mut self) -> bool {
function update (line 202) | fn update(mut self) -> GameOverEndState {
function new_game (line 210) | fn new_game(self) -> WalkTheDogState<Ready> {
type GameOverEndState (line 222) | enum GameOverEndState {
type Obstacle (line 260) | pub trait Obstacle {
method check_intersection (line 261) | fn check_intersection(&self, boy: &mut RedHatBoy);
method draw (line 262) | fn draw(&self, renderer: &Renderer);
method move_horizontally (line 263) | fn move_horizontally(&mut self, x: i16);
method right (line 264) | fn right(&self) -> i16;
method check_intersection (line 312) | fn check_intersection(&self, boy: &mut RedHatBoy) {
method draw (line 326) | fn draw(&self, renderer: &Renderer) {
method move_horizontally (line 349) | fn move_horizontally(&mut self, x: i16) {
method right (line 356) | fn right(&self) -> i16 {
method check_intersection (line 1010) | fn check_intersection(&self, boy: &mut RedHatBoy) {
method draw (line 1016) | fn draw(&self, renderer: &Renderer) {
method move_horizontally (line 1020) | fn move_horizontally(&mut self, x: i16) {
method right (line 1024) | fn right(&self) -> i16 {
type Platform (line 267) | pub struct Platform {
method new (line 275) | pub fn new(
method bounding_boxes (line 306) | fn bounding_boxes(&self) -> &Vec<Rect> {
type RedHatBoy (line 364) | pub struct RedHatBoy {
method new (line 371) | fn new(sprite_sheet: Sheet, image: HtmlImageElement, audio: Audio, sou...
method reset (line 379) | fn reset(boy: Self) -> Self {
method run_right (line 388) | fn run_right(&mut self) {
method slide (line 392) | fn slide(&mut self) {
method jump (line 396) | fn jump(&mut self) {
method knock_out (line 400) | fn knock_out(&mut self) {
method land_on (line 404) | fn land_on(&mut self, position: i16) {
method update (line 408) | fn update(&mut self) {
method knocked_out (line 412) | fn knocked_out(&self) -> bool {
method pos_y (line 416) | fn pos_y(&self) -> i16 {
method velocity_y (line 420) | fn velocity_y(&self) -> i16 {
method walking_speed (line 424) | fn walking_speed(&self) -> i16 {
method frame_name (line 428) | fn frame_name(&self) -> String {
method current_sprite (line 436) | fn current_sprite(&self) -> Option<&Cell> {
method bounding_box (line 440) | fn bounding_box(&self) -> Rect {
method destination_box (line 452) | fn destination_box(&self) -> Rect {
method draw (line 463) | fn draw(&self, renderer: &Renderer) {
type RedHatBoyStateMachine (line 480) | enum RedHatBoyStateMachine {
method transition (line 499) | fn transition(self, event: Event) -> Self {
method frame_name (line 525) | fn frame_name(&self) -> &str {
method context (line 536) | fn context(&self) -> &RedHatBoyContext {
method knocked_out (line 547) | fn knocked_out(&self) -> bool {
method update (line 551) | fn update(self) -> Self {
method from (line 557) | fn from(state: RedHatBoyState<Idle>) -> Self {
method from (line 563) | fn from(state: RedHatBoyState<Running>) -> Self {
method from (line 569) | fn from(state: RedHatBoyState<Sliding>) -> Self {
method from (line 575) | fn from(state: RedHatBoyState<Jumping>) -> Self {
method from (line 581) | fn from(state: RedHatBoyState<Falling>) -> Self {
method from (line 587) | fn from(state: RedHatBoyState<KnockedOut>) -> Self {
method from (line 593) | fn from(state: SlidingEndState) -> Self {
method from (line 602) | fn from(state: JumpingEndState) -> Self {
method from (line 611) | fn from(state: FallingEndState) -> Self {
type Event (line 489) | pub enum Event {
constant FLOOR (line 623) | const FLOOR: i16 = 479;
constant PLAYER_HEIGHT (line 624) | const PLAYER_HEIGHT: i16 = HEIGHT - FLOOR;
constant RUNNING_SPEED (line 625) | const RUNNING_SPEED: i16 = 4;
constant STARTING_POINT (line 626) | const STARTING_POINT: i16 = -20;
constant IDLE_FRAMES (line 627) | const IDLE_FRAMES: u8 = 29;
constant RUNNING_FRAMES (line 628) | const RUNNING_FRAMES: u8 = 23;
constant JUMPING_FRAMES (line 629) | const JUMPING_FRAMES: u8 = 35;
constant SLIDING_FRAMES (line 630) | const SLIDING_FRAMES: u8 = 14;
constant FALLING_FRAMES (line 631) | const FALLING_FRAMES: u8 = 29;
constant IDLE_FRAME_NAME (line 632) | const IDLE_FRAME_NAME: &str = "Idle";
constant RUN_FRAME_NAME (line 633) | const RUN_FRAME_NAME: &str = "Run";
constant SLIDING_FRAME_NAME (line 634) | const SLIDING_FRAME_NAME: &str = "Slide";
constant JUMPING_FRAME_NAME (line 635) | const JUMPING_FRAME_NAME: &str = "Jump";
constant FALLING_FRAME_NAME (line 636) | const FALLING_FRAME_NAME: &str = "Dead";
constant JUMP_SPEED (line 637) | const JUMP_SPEED: i16 = -25;
constant GRAVITY (line 638) | const GRAVITY: i16 = 1;
constant TERMINAL_VELOCITY (line 639) | const TERMINAL_VELOCITY: i16 = 20;
type RedHatBoyState (line 642) | pub struct RedHatBoyState<S> {
function context (line 648) | pub fn context(&self) -> &RedHatBoyContext {
function update_context (line 652) | fn update_context(&mut self, frames: u8) {
type Idle (line 658) | pub struct Idle;
function new (line 661) | pub fn new(audio: Audio, jump_sound: Sound) -> Self {
function frame_name (line 677) | pub fn frame_name(&self) -> &str {
function update (line 681) | pub fn update(mut self) -> RedHatBoyState<Idle> {
function run (line 686) | pub fn run(self) -> RedHatBoyState<Running> {
type Running (line 695) | pub struct Running;
function frame_name (line 698) | pub fn frame_name(&self) -> &str {
function update (line 702) | pub fn update(mut self) -> RedHatBoyState<Running> {
function jump (line 707) | pub fn jump(self) -> RedHatBoyState<Jumping> {
function slide (line 718) | pub fn slide(self) -> RedHatBoyState<Sliding> {
function knock_out (line 725) | pub fn knock_out(self) -> RedHatBoyState<Falling> {
function land_on (line 732) | pub fn land_on(self, position: i16) -> RedHatBoyState<Running> {
type Jumping (line 741) | pub struct Jumping;
type JumpingEndState (line 743) | pub enum JumpingEndState {
function frame_name (line 749) | pub fn frame_name(&self) -> &str {
function knock_out (line 753) | pub fn knock_out(self) -> RedHatBoyState<Falling> {
function update (line 760) | pub fn update(mut self) -> JumpingEndState {
function land_on (line 770) | pub fn land_on(self, position: i16) -> RedHatBoyState<Running> {
type Sliding (line 779) | pub struct Sliding;
type SlidingEndState (line 781) | pub enum SlidingEndState {
function frame_name (line 787) | pub fn frame_name(&self) -> &str {
function stand (line 791) | pub fn stand(self) -> RedHatBoyState<Running> {
function knock_out (line 798) | pub fn knock_out(self) -> RedHatBoyState<Falling> {
function update (line 805) | pub fn update(mut self) -> SlidingEndState {
function land_on (line 815) | pub fn land_on(self, position: i16) -> RedHatBoyState<Sliding> {
type Falling (line 824) | pub struct Falling;
function frame_name (line 827) | pub fn frame_name(&self) -> &str {
function knock_out (line 831) | pub fn knock_out(self) -> RedHatBoyState<KnockedOut> {
function update (line 838) | pub fn update(mut self) -> FallingEndState {
type FallingEndState (line 848) | pub enum FallingEndState {
type KnockedOut (line 854) | pub struct KnockedOut;
function frame_name (line 857) | pub fn frame_name(&self) -> &str {
type RedHatBoyContext (line 863) | pub struct RedHatBoyContext {
method update (line 872) | pub fn update(mut self, frame_count: u8) -> Self {
method reset_frame (line 892) | fn reset_frame(mut self) -> Self {
method set_vertical_velocity (line 897) | fn set_vertical_velocity(mut self, y: i16) -> Self {
method run_right (line 902) | fn run_right(mut self) -> Self {
method stop (line 907) | fn stop(mut self) -> Self {
method set_on (line 913) | fn set_on(mut self, position: i16) -> Self {
method play_jump_sound (line 919) | fn play_jump_sound(self) -> Self {
type Walk (line 928) | pub struct Walk {
method knocked_out (line 939) | fn knocked_out(&self) -> bool {
method reset (line 943) | fn reset(walk: Self) -> Self {
method draw (line 959) | fn draw(&self, renderer: &Renderer) {
method velocity (line 973) | fn velocity(&self) -> i16 {
method generate_next_segment (line 977) | fn generate_next_segment(&mut self) {
type Barrier (line 999) | pub struct Barrier {
method new (line 1004) | pub fn new(image: Image) -> Self {
method initialize (line 1031) | async fn initialize(&self) -> Result<Box<dyn Game>> {
method update (line 1085) | fn update(&mut self, keystate: &KeyState) {
method draw (line 1092) | fn draw(&self, renderer: &Renderer) {
function rightmost (line 1101) | fn rightmost(obstacle_list: &[Box<dyn Obstacle>]) -> i16 {
function test_transition_from_game_over_to_new_game (line 1121) | fn test_transition_from_game_over_to_new_game() {
FILE: src/lib.rs
function main_js (line 14) | pub fn main_js() -> Result<(), JsValue> {
FILE: src/segments.rs
constant LOW_PLATFORM (line 7) | const LOW_PLATFORM: i16 = 420;
constant HIGH_PLATFORM (line 8) | const HIGH_PLATFORM: i16 = 375;
constant FIRST_PLATFORM (line 9) | const FIRST_PLATFORM: i16 = 370;
constant STONE_ON_GROUND (line 11) | const STONE_ON_GROUND: i16 = 546;
constant FLOATING_PLATFORM_SPRITES (line 13) | const FLOATING_PLATFORM_SPRITES: [&str; 3] = ["13.png", "14.png", "15.pn...
constant PLATFORM_WIDTH (line 14) | const PLATFORM_WIDTH: i16 = 384;
constant PLATFORM_HEIGHT (line 15) | const PLATFORM_HEIGHT: i16 = 93;
constant PLATFORM_EDGE_WIDTH (line 16) | const PLATFORM_EDGE_WIDTH: i16 = 60;
constant PLATFORM_EDGE_HEIGHT (line 17) | const PLATFORM_EDGE_HEIGHT: i16 = 54;
constant FLOATING_PLATFORM_BOUNDING_BOXES (line 18) | const FLOATING_PLATFORM_BOUNDING_BOXES: [Rect; 3] = [
function create_floating_platform (line 34) | fn create_floating_platform(sprite_sheet: Rc<SpriteSheet>, position: Poi...
function stone_and_platform (line 43) | pub fn stone_and_platform(
function platform_and_stone (line 68) | pub fn platform_and_stone(
FILE: src/sound.rs
function create_audio_context (line 7) | pub fn create_audio_context() -> Result<AudioContext> {
function create_buffer_source (line 11) | fn create_buffer_source(ctx: &AudioContext) -> Result<AudioBufferSourceN...
function connect_with_audio_node (line 16) | fn connect_with_audio_node(
function create_track_source (line 25) | fn create_track_source(ctx: &AudioContext, buffer: &AudioBuffer) -> Resu...
type LOOPING (line 32) | pub enum LOOPING {
function play_sound (line 37) | pub fn play_sound(ctx: &AudioContext, buffer: &AudioBuffer, looping: LOO...
function decode_audio_data (line 48) | pub async fn decode_audio_data(
FILE: tests/app.rs
function rust_test (line 10) | fn rust_test() {
function web_test (line 16) | fn web_test() {
Condensed preview — 24 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (92K chars).
[
{
"path": ".github/workflows/build.yml",
"chars": 1284,
"preview": "on: [push]\n\nname: build\n\njobs:\n build:\n runs-on: ubuntu-latest\n steps:\n - uses: actions/checkout@v2\n - "
},
{
"path": ".gitignore",
"chars": 90,
"preview": "node_modules\n/dist\n/target\n/pkg\n/wasm-pack.log\n\n# Local Netlify folder\n.netlify\n\n.DS_Store"
},
{
"path": ".rustfmt",
"chars": 17,
"preview": "edition = \"2018\"\n"
},
{
"path": ".tool-versions",
"chars": 15,
"preview": "nodejs 16.13.0\n"
},
{
"path": "Cargo.toml",
"chars": 1701,
"preview": "# You must change these to your own details.\n[package]\nname = \"rust-webpack-template\"\ndescription = \"Walk the Dog - the "
},
{
"path": "LICENSE",
"chars": 1062,
"preview": "MIT License\n\nCopyright (c) 2021 Packt\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof t"
},
{
"path": "README.md",
"chars": 7198,
"preview": "\n\n\n# Game Development with Rust and WebAssembly\nGame Development with Rust and WebAssembly, published by Packt\n\n<a href="
},
{
"path": "js/index.js",
"chars": 48,
"preview": "import(\"../pkg/index.js\").catch(console.error);\n"
},
{
"path": "package.json",
"chars": 579,
"preview": "{\n \"author\": \"You <you@example.com>\",\n \"name\": \"rust-webpack-template\",\n \"version\": \"0.1.0\",\n \"scripts\": {\n \"buil"
},
{
"path": "rust-toolchain.toml",
"chars": 68,
"preview": "[toolchain]\nchannel = \"1.57.0\"\ntargets = [\"wasm32-unknown-unknown\"]\n"
},
{
"path": "rustfmt.toml",
"chars": 17,
"preview": "edition = \"2018\"\n"
},
{
"path": "src/browser.rs",
"chars": 5574,
"preview": "use anyhow::{anyhow, Result};\nuse js_sys::ArrayBuffer;\nuse std::future::Future;\n\nuse wasm_bindgen::{\n closure::WasmCl"
},
{
"path": "src/engine.rs",
"chars": 12825,
"preview": "use crate::browser::{self, LoopClosure};\nuse crate::sound;\nuse anyhow::{anyhow, Result};\nuse async_trait::async_trait;\nu"
},
{
"path": "src/game.rs",
"chars": 33441,
"preview": "use std::rc::Rc;\n\nuse anyhow::{anyhow, Result};\nuse async_trait::async_trait;\nuse futures::channel::mpsc::UnboundedRecei"
},
{
"path": "src/lib.rs",
"chars": 515,
"preview": "#[macro_use]\nmod browser;\nmod engine;\nmod game;\nmod segments;\nmod sound;\n\nuse engine::GameLoop;\nuse game::WalkTheDog;\nus"
},
{
"path": "src/segments.rs",
"chars": 2410,
"preview": "use std::rc::Rc;\nuse web_sys::HtmlImageElement;\n\nuse crate::engine::{Image, Point, Rect, SpriteSheet};\nuse crate::game::"
},
{
"path": "src/sound.rs",
"chars": 1975,
"preview": "use anyhow::{anyhow, Result};\nuse js_sys::ArrayBuffer;\nuse wasm_bindgen::JsCast;\nuse wasm_bindgen_futures::JsFuture;\nuse"
},
{
"path": "src/test_browser.rs",
"chars": 0,
"preview": ""
},
{
"path": "static/index.html",
"chars": 539,
"preview": "<!DOCTYPE html>\n<html>\n<head>\n <meta charset=\"UTF-8\">\n <title>My Rust + Webpack project!</title>\n <link rel=\"styleshe"
},
{
"path": "static/rhb.json",
"chars": 10264,
"preview": "{\"frames\": {\n\n\"Dead (1).png\":\n{\n\t\"frame\": {\"x\":0,\"y\":0,\"w\":71,\"h\":115},\n\t\"rotated\": false,\n\t\"trimmed\": true,\n\t\"spriteSou"
},
{
"path": "static/styles.css",
"chars": 616,
"preview": "#ui {\n position: absolute;\n}\n\n@font-face {\n font-family: 'Ken Future';\n src: url('kenney_future_narrow-webfont.woff"
},
{
"path": "static/tiles.json",
"chars": 3590,
"preview": "{\"frames\": {\n\n\"1.png\":\n{\n\t\"frame\": {\"x\":1,\"y\":132,\"w\":128,\"h\":128},\n\t\"rotated\": false,\n\t\"trimmed\": false,\n\t\"spriteSource"
},
{
"path": "tests/app.rs",
"chars": 460,
"preview": "use futures::prelude::*;\nuse wasm_bindgen::JsValue;\nuse wasm_bindgen_futures::JsFuture;\nuse wasm_bindgen_test::{wasm_bin"
},
{
"path": "webpack.config.js",
"chars": 534,
"preview": "const path = require(\"path\");\nconst CopyPlugin = require(\"copy-webpack-plugin\");\nconst WasmPackPlugin = require(\"@wasm-t"
}
]
About this extraction
This page contains the full source code of the PacktPublishing/Game-Development-with-Rust-and-WebAssembly GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 24 files (82.8 KB), approximately 23.5k tokens, and a symbol index with 255 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.
Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.