├── .gitignore ├── CHANGELOG.md ├── Cargo.toml ├── LICENSE ├── README.md ├── about ├── gwg-stack.afdesign ├── gwg-stack.png ├── logo.png ├── logo.svg └── supported_platforms.svg ├── examples ├── 01_super_simple.rs ├── 02_hello_world.rs ├── 03_drawing.rs ├── 04_snake.rs ├── animation.rs ├── astroblasto.rs ├── blend_modes.rs ├── bunnymark.rs ├── canvas_subframe.rs ├── colorspace.rs ├── input_test.rs ├── meshbatch.rs ├── render_to_image.rs ├── resources.tar ├── shader.rs ├── shadows-unfinished.rs ├── sounds.rs ├── spritebatch.rs ├── text.rs └── transforms.rs ├── js └── js_bundle.js ├── resources ├── LiberationMono-Regular.ttf ├── SIL Open Font License.txt ├── Tangerine_Regular.ttf ├── angle.png ├── background.png ├── basic_150.glslv ├── bg_top.png ├── boom.ogg ├── dimmer_150.glslf ├── dragon1.png ├── pew.flac ├── pew.ogg ├── pew.wav ├── player.png ├── player_sheet.png ├── rock.png ├── shot.png ├── sound.ogg ├── tile.png └── wabbit_alpha.png └── src ├── audio.rs ├── conf.rs ├── context.rs ├── error.rs ├── event.rs ├── filesystem.rs ├── goodies.rs ├── goodies ├── camera.rs ├── matrix_transform_2d.rs └── scene.rs ├── graphics.rs ├── graphics ├── canvas.rs ├── context.rs ├── drawparam.rs ├── font.png ├── image.rs ├── mesh.rs ├── shader.rs ├── spritebatch.rs ├── text.rs └── types.rs ├── input.rs ├── input ├── gamepad.rs ├── input_handler.rs ├── keyboard.rs └── mouse.rs ├── lib.rs └── timer.rs /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | **/*.rs.bk 3 | Cargo.lock -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # 0.6.0 2 | 3 | Rewrote the library to deal with the loss of `EventHandlerFree` from miniquad, meaning we don't get to own the miniquad 4 | context anymore. 5 | 6 | Therefore it has now to be handed around a lot. 7 | 8 | On the upside, improvements made from the long alpha and the next steps after that inside of miniquad are now part of gwg as well. 9 | 10 | ## Added 11 | 12 | * added the `shader.rs` example from `ggez`, though there's still an error somewhere, keeping it from working properly... 13 | (in case you find out how to fix it, please let me know!) 14 | 15 | # 0.5.0 16 | 17 | ## Added 18 | 19 | * added `filesystem::load_file_async` to allow for file loading on WASM as well 20 | 21 | ## Changed 22 | 23 | * updated `miniquad` to 0.3.0-alpha.38 (highest version currently compatible with the docker build script and without 24 | an icon bug on windows) 25 | * changed `offset` behavior and added default error type to `EventHandler` to match `ggez` 0.7.0 26 | * made `filesystem::open` check the cache first and then only load files whenever they're not present in the cache 27 | * `filesystem::open` now internally uses `miniquad::fs::load_file`, allowing to load files on Android now as well 28 | * ```cargo 29 | [package.metadata.android] 30 | assets = "/" 31 | ``` 32 | can be used in your `Cargo.toml` file to specify which folder to include in the apk as the assets folder 33 | 34 | # 0.4.2 35 | 36 | * added a dependency on `twox_hash 1.5.0` to coerce semver into using a `rand < 0.8` in order to avoid `getrandom`, so 37 | that gwg compiles on wasm again 38 | 39 | # 0.4.1 40 | 41 | * fixed a memory leak caused by drawable structs not releasing their resources 42 | * fixed `miniquad` version 43 | 44 | # 0.4.0 45 | 46 | ## Added 47 | 48 | * added a re-export of miniquad for those who desire/need more control 49 | * added `set`, `get_sprites` and `get_sprites_mut` to `SpriteBatch` for consistency with `ggez` 0.6.1 50 | * added an "audio" feature, making audio optional 51 | 52 | ## Changed 53 | 54 | * updated audio to use `quad-snd` 0.2.2 - changing the audio API a little 55 | * changed `05_astroblasto.rs` into just `astroblasto.rs` to allow for easier building on Android (as leading numbers seem to be disallowed there) 56 | 57 | ## Fixed 58 | 59 | * fixed remaining clippy lints -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "good-web-game" 3 | version = "0.6.1" 4 | authors = ["not-fl3 ", "PSteinhaus"] 5 | license = "MIT" 6 | edition = "2021" 7 | description = "An alternative implementation of the ggez game engine, based on miniquad" 8 | repository = "https://github.com/ggez/good-web-game" 9 | keywords = ["game", "ggez", "miniquad", "2D", "engine"] 10 | readme = "README.md" 11 | categories = ["game-engines"] 12 | exclude = [ 13 | "about", 14 | "examples/resources.tar", 15 | "resources", 16 | "!LiberationMono-Regular.ttf" 17 | ] 18 | 19 | [dependencies] 20 | bitflags = "1.1.0" 21 | bytemuck = "1.7.2" 22 | bytemuck_derive = "1.0.1" 23 | rustc-hash = "1.0.1" 24 | lazy_static = "1.3.0" 25 | mint = "0.5" 26 | cgmath = { version = "0.17", features = ["mint"] } 27 | twox-hash = "=1.5.0" # necessary to force a version of rand < 0.8 to avoid getrandom 28 | glyph_brush = "0.7" 29 | miniquad = "=0.3.13" 30 | image = { version = "0.22", default-features = false, features = ["png_codec"] } 31 | serde = "1" 32 | serde_derive = "1" 33 | log = "0.4" 34 | tar = { version = "0.4", default-features = false } 35 | lyon = { version = "0.17.5", optional = true } 36 | smart-default = "0.6" 37 | quad-snd = { version = "0.2.2", optional = true } 38 | zip = { version = "0.5", default-features = false } 39 | approx = "0.5" 40 | 41 | [target.'cfg(not(target_arch = "wasm32"))'.dependencies] 42 | gilrs = "0.8" 43 | 44 | [features] 45 | default = [ "mesh", "audio" ] 46 | mesh = [ "lyon" ] 47 | audio = [ "quad-snd" ] 48 | log-impl = ["miniquad/log-impl"] 49 | jpeg = [ "image/jpeg" ] 50 | 51 | [dev-dependencies] 52 | quad-rand = "0.2.1" 53 | oorandom = "11" 54 | glam = { version = "0.21.3", features = ["mint", "bytemuck"]} 55 | keyframe = "1.0.4" 56 | keyframe_derive = "1.0.0" 57 | num-traits = "0.2" 58 | num-derive = "0.3" 59 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 ggez 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | # Good Web Game 4 | 5 | [![Discord chat](https://img.shields.io/discord/710177966440579103.svg?label=discord%20chat)](https://discord.gg/jum3Fjek2A) 6 | [![Docs Status](https://docs.rs/good-web-game/badge.svg)](https://docs.rs/good-web-game) 7 | [![license](https://img.shields.io/badge/license-MIT-blue.svg)](https://github.com/ggez/good-web-game/blob/master/LICENSE) 8 | [![Crates.io](https://img.shields.io/crates/v/good-web-game.svg)](https://crates.io/crates/good-web-game) 9 | 10 | good-web-game is a wasm32-unknown-unknown implementation of a [ggez](https://github.com/ggez/ggez) subset on top of [miniquad](https://github.com/not-fl3/miniquad/). Originally built to run [Zemeroth](https://github.com/ozkriff/zemeroth) on the web. 11 | 12 | It currently supports most of the ggez 0.7.0 API. If you're already working with ggez you might use this library to port your game to the web (or even mobile). 13 | Since it also runs well on desktop it also offers an alternative implementation of ggez, which might always come in handy. 14 | 15 | If you are just looking for a well supported, more serious, minimal high-level engine on top of miniquad you might want to take a look at [macroquad](https://github.com/not-fl3/macroquad/). 16 | 17 | ## Supported Platforms 18 | 19 | The idea behind good-web-game is to offer a way to easily port ggez games to the web and even to mobile platforms. As it's mostly a subset of ggez, porting from good-web-game to ggez is even simpler. 20 | 21 |

22 | 23 |

24 | 25 | Note that we don't give any guarantees for iOS / macOS support, as we currently simply don't have Macs lying around to test it on. In theory, it _should_ work though. 26 | 27 | ## Status 28 | 29 | "good-web-game" implements most of the ggez 0.7.0 API. 30 | 31 | ### Differences 32 | 33 | * boilerplate code differs slightly, [as shown here](https://github.com/PSteinhaus/PSteinhaus.github.io/tree/main/ggez/web-examples#ggez-animation-example) 34 | * audio API differs somewhat due to use of `quad-snd` instead of `rodio` for easy portability 35 | * if you want to run on the web, shaders have to be written in GLSL100, due to support for WebGL1 36 | * API for creation of shaders and their corresponding uniform structs differs slightly, but the workflow remains the same, see [the `shader` example](examples/shader.rs) 37 | 38 | ### Missing / Not available: 39 | 40 | * ggez (and therefore good-web-game) usually loads files in a blocking fashion, which doesn't work on WASM 41 | * loading files asynchronously is possible through [`load_file_async`](https://docs.rs/good-web-game/0.5.0/good_web_game/filesystem/fn.load_file_async.html) everywhere though 42 | * filesystem with writing access (if you need it take a look at [`quad-storage`](https://github.com/optozorax/quad-storage)) 43 | * writing your own event loop (doesn't make much sense on callback-only platforms like HTML5) 44 | * spatial audio (overall audio support is still relatively limited) 45 | * resolution control in fullscreen mode 46 | * setting window position / size (the latter is available on Windows, but buggy) 47 | * screenshot function 48 | * window icon 49 | * gamepad support on WASM (as `gilrs` depends on wasm-bindgen) 50 | 51 | ## Demo 52 | 53 | Running Zemeroth: 54 | 55 | ![screen](https://i.imgur.com/TjvCNwa.jpg) 56 | 57 | You can also check out [astroblasto running on the web](https://psteinhaus.github.io/gwg-example/) ([source](https://github.com/PSteinhaus/PSteinhaus.github.io/tree/main/gwg-example)). 58 | 59 | ## Building for different platforms 60 | 61 | To build and run an example as a native binary: 62 | 63 | ``` 64 | cargo run --example astroblasto 65 | ``` 66 | 67 | ### WebAssembly 68 | 69 | ``` 70 | rustup target add wasm32-unknown-unknown 71 | cargo build --example astroblasto --target wasm32-unknown-unknown 72 | ``` 73 | 74 | And then use the following .html to load .wasm: 75 | 76 |
index.html 77 | 78 | ```html 79 | 80 | 81 | 82 | 83 | TITLE 84 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | ``` 109 |
110 | 111 | To run it you need a server. An easy way to start one: 112 | 113 | ``` 114 | cargo install basic-http-server 115 | basic-http-server . 116 | ``` 117 | 118 | ### Android 119 | 120 | 121 | Recommended way to build for android is using Docker.
122 | miniquad uses a slightly modifed version of `cargo-apk` 123 | 124 | ``` 125 | docker run --rm -v (your project folder):/root/src -w /root/src notfl3/cargo-apk cargo quad-apk build --example astroblasto 126 | ``` 127 | 128 | APK file will be in `target/android-artifacts/(debug|release)/apk` 129 | 130 | With "log-impl" enabled all log calls will be forwarded to the adb console. 131 | No code modifications for Android required. 132 | 133 | Note that all examples starting with numbers (for example `03_drawing.rs`) won't install correctly. You need to remove the leading numbers: `drawing.rs` 134 | 135 | ### iOS 136 | 137 | See miniquad iOS [sample project](https://github.com/Gordon-F/miniquad_ios_example). 138 | 139 | ## On blurry graphics 140 | 141 | You may run into somewhat blurry graphics. This is caused by high-dpi rendering: 142 | 143 | When run on a system with a scaling factor unequal to 1 the graphics may appear blurry, due to the drawbuffer being scaled up, to achieve a window of the size requested by your OS. 144 | This size is usually "the size you specified in `Conf`" * "your OS scaling factor". 145 | 146 | To avoid this set `Conf::high_dpi` to `true`. This leads to the drawbuffer being the size of your actual physical window. It also means though that you can't be sure how big your drawable space will actually be, as this will then depend on where the program is being run. 147 | 148 | We aim towards changing this, so that windows are always created with the physical size specified in `Conf`, but that's not directly supported by miniquad currently. 149 | 150 | ## Architecture 151 | 152 | Here is how `good-web-game` fits into your rust-based game: 153 | 154 | ![software stack](about/gwg-stack.png?raw=true "good-web-game software stack") 155 | -------------------------------------------------------------------------------- /about/gwg-stack.afdesign: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ggez/good-web-game/11ce17b8076a4c3eb7b8d809ce4066342b8e412c/about/gwg-stack.afdesign -------------------------------------------------------------------------------- /about/gwg-stack.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ggez/good-web-game/11ce17b8076a4c3eb7b8d809ce4066342b8e412c/about/gwg-stack.png -------------------------------------------------------------------------------- /about/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ggez/good-web-game/11ce17b8076a4c3eb7b8d809ce4066342b8e412c/about/logo.png -------------------------------------------------------------------------------- /about/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 21 | 23 | 26 | 30 | 34 | 35 | 46 | 47 | 72 | 74 | 75 | 77 | image/svg+xml 78 | 80 | 81 | 82 | 83 | 84 | 89 | 101 | 104 | 107 | 112 | 113 | 118 | 119 | 122 | 125 | 126 | 129 | 132 | 133 | 134 | 135 | -------------------------------------------------------------------------------- /examples/01_super_simple.rs: -------------------------------------------------------------------------------- 1 | //! This has been created as a test for the mouse input functions, but it's simple enough, so why not 2 | 3 | extern crate good_web_game as ggez; 4 | 5 | use ggez::input::mouse::set_cursor_grabbed; 6 | use ggez::{event::EventHandler, miniquad, Context, GameResult}; 7 | 8 | fn main() -> GameResult<()> { 9 | ggez::start(ggez::conf::Conf::default(), |_context, _quad_ctx| { 10 | Box::new(MousePos(0., 0.)) 11 | }) 12 | } 13 | 14 | struct MousePos(f32, f32); 15 | 16 | impl EventHandler for MousePos { 17 | fn update( 18 | &mut self, 19 | ctx: &mut Context, 20 | quad_ctx: &mut miniquad::graphics::GraphicsContext, 21 | ) -> GameResult<()> { 22 | set_cursor_grabbed(ctx, quad_ctx, true); 23 | println!("UPDATE: delta: {:?}", ggez::input::mouse::delta(ctx)); 24 | Ok(()) 25 | } 26 | 27 | fn draw( 28 | &mut self, 29 | ctx: &mut Context, 30 | quad_ctx: &mut miniquad::graphics::GraphicsContext, 31 | ) -> GameResult<()> { 32 | ggez::graphics::present(ctx, quad_ctx) 33 | } 34 | 35 | fn mouse_motion_event( 36 | &mut self, 37 | _ctx: &mut Context, 38 | _quad_ctx: &mut miniquad::graphics::GraphicsContext, 39 | x: f32, 40 | y: f32, 41 | dx: f32, 42 | dy: f32, 43 | ) { 44 | let delta = (x - self.0, y - self.1); 45 | *self = MousePos(x, y); 46 | println!( 47 | "{delta:6?} == ({dx:6?}, {dy:6?}) = {eq}", 48 | delta = delta, 49 | dx = dx, 50 | dy = dy, 51 | eq = delta == (dx, dy) 52 | ); 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /examples/02_hello_world.rs: -------------------------------------------------------------------------------- 1 | //! Basic hello world example. 2 | extern crate good_web_game as ggez; 3 | 4 | use ggez::event; 5 | use ggez::graphics; 6 | use ggez::{Context, GameResult}; 7 | use std::env; 8 | use std::path; 9 | 10 | // First we make a structure to contain the game's state 11 | struct MainState { 12 | frames: usize, 13 | text: graphics::Text, 14 | } 15 | 16 | impl MainState { 17 | fn new(ctx: &mut Context) -> GameResult { 18 | // The ttf file will be in your resources directory. Later, we 19 | // will mount that directory so we can omit it in the path here. 20 | let font = graphics::Font::new(ctx, "/LiberationMono-Regular.ttf")?; 21 | let text = graphics::Text::new(("Hello world!", font, 48.0)); 22 | 23 | let s = MainState { frames: 0, text }; 24 | Ok(s) 25 | } 26 | } 27 | 28 | // Then we implement the `ggez:event::EventHandler` trait on it, which 29 | // requires callbacks for updating and drawing the game state each frame. 30 | // 31 | // The `EventHandler` trait also contains callbacks for event handling 32 | // that you can override if you wish, but the defaults are fine. 33 | impl event::EventHandler for MainState { 34 | fn update( 35 | &mut self, 36 | _ctx: &mut Context, 37 | _quad_ctx: &mut miniquad::graphics::GraphicsContext, 38 | ) -> GameResult { 39 | Ok(()) 40 | } 41 | 42 | fn draw( 43 | &mut self, 44 | ctx: &mut Context, 45 | quad_ctx: &mut miniquad::graphics::GraphicsContext, 46 | ) -> GameResult { 47 | graphics::clear(ctx, quad_ctx, [0.1, 0.2, 0.3, 1.0].into()); 48 | 49 | // Drawables are drawn from their top-left corner. 50 | let offset = self.frames as f32 / 10.0; 51 | let dest_point = cgmath::Point2::new(offset, offset); 52 | graphics::draw(ctx, quad_ctx, &self.text, (dest_point,))?; 53 | graphics::present(ctx, quad_ctx)?; 54 | 55 | self.frames += 1; 56 | if (self.frames % 100) == 0 { 57 | println!("FPS: {}", ggez::timer::fps(ctx)); 58 | println!("drawable size: {:?}", graphics::drawable_size(quad_ctx)); 59 | } 60 | 61 | Ok(()) 62 | } 63 | } 64 | 65 | // Now our main function: 66 | pub fn main() -> GameResult { 67 | // We add the CARGO_MANIFEST_DIR/resources to the resource paths 68 | // so that ggez will look in our cargo project directory for files. 69 | let resource_dir = if let Ok(manifest_dir) = env::var("CARGO_MANIFEST_DIR") { 70 | let mut path = path::PathBuf::from(manifest_dir); 71 | path.push("resources"); 72 | path 73 | } else { 74 | path::PathBuf::from("./resources") 75 | }; 76 | 77 | ggez::start( 78 | ggez::conf::Conf::default() 79 | .cache(Some(include_bytes!("resources.tar"))) 80 | .physical_root_dir(Some(resource_dir)), 81 | |mut context, mut _quad_ctx| Box::new(MainState::new(&mut context).unwrap()), 82 | ) 83 | } 84 | -------------------------------------------------------------------------------- /examples/03_drawing.rs: -------------------------------------------------------------------------------- 1 | //! https://github.com/ggez/ggez/blob/master/examples/03_drawing.rs 2 | //! A collection of semi-random shape and image drawing examples. 3 | 4 | extern crate glam; 5 | extern crate good_web_game as ggez; 6 | 7 | use ggez::event; 8 | use ggez::graphics::{self, Canvas, Color, DrawMode, DrawParam, FilterMode}; 9 | use ggez::input::keyboard::{KeyCode, KeyMods}; 10 | use ggez::timer; 11 | use ggez::{Context, GameResult}; 12 | //use lyon::lyon_tessellation::FillOptions; 13 | use ggez::miniquad; 14 | use std::env; 15 | use std::path; 16 | 17 | type Point2 = glam::Vec2; 18 | 19 | struct MainState { 20 | image1: graphics::Image, 21 | image2_linear: graphics::Image, 22 | image2_nearest: graphics::Image, 23 | meshes: Vec, 24 | zoomlevel: f32, 25 | canvas: Canvas, 26 | use_canvas: bool, 27 | } 28 | 29 | impl MainState { 30 | fn new(ctx: &mut Context, quad_ctx: &mut miniquad::GraphicsContext) -> GameResult { 31 | let image1 = graphics::Image::new(ctx, quad_ctx, "dragon1.png")?; 32 | let image2_linear = graphics::Image::new(ctx, quad_ctx, "shot.png")?; 33 | let mut image2_nearest = graphics::Image::new(ctx, quad_ctx, "shot.png")?; 34 | image2_nearest.set_filter(graphics::FilterMode::Nearest); 35 | let canvas = Canvas::with_window_size(ctx, quad_ctx)?; 36 | 37 | let meshes = vec![ 38 | build_mesh(ctx, quad_ctx)?, 39 | build_textured_triangle(ctx, quad_ctx)?, 40 | ]; 41 | let s = MainState { 42 | image1, 43 | image2_linear, 44 | image2_nearest, 45 | meshes, 46 | zoomlevel: 1.0, 47 | canvas, 48 | use_canvas: false, 49 | }; 50 | 51 | Ok(s) 52 | } 53 | } 54 | 55 | fn build_mesh( 56 | ctx: &mut Context, 57 | quad_ctx: &mut miniquad::GraphicsContext, 58 | ) -> GameResult { 59 | let mb = &mut graphics::MeshBuilder::new(); 60 | 61 | mb.line( 62 | &[ 63 | Point2::new(200.0, 200.0), 64 | Point2::new(400.0, 200.0), 65 | Point2::new(400.0, 400.0), 66 | Point2::new(200.0, 400.0), 67 | Point2::new(200.0, 300.0), 68 | ], 69 | 4.0, 70 | Color::new(1.0, 0.0, 0.0, 1.0), 71 | )?; 72 | 73 | mb.ellipse( 74 | DrawMode::fill(), 75 | Point2::new(600.0, 200.0), 76 | 50.0, 77 | 120.0, 78 | 1.0, 79 | Color::new(1.0, 1.0, 0.0, 1.0), 80 | )?; 81 | 82 | mb.circle( 83 | DrawMode::fill(), 84 | Point2::new(600.0, 380.0), 85 | 40.0, 86 | 1.0, 87 | Color::new(1.0, 0.0, 1.0, 1.0), 88 | )?; 89 | 90 | mb.build(ctx, quad_ctx) 91 | } 92 | 93 | fn build_textured_triangle( 94 | ctx: &mut Context, 95 | quad_ctx: &mut miniquad::GraphicsContext, 96 | ) -> GameResult { 97 | let mb = &mut graphics::MeshBuilder::new(); 98 | let triangle_verts = vec![ 99 | graphics::Vertex { 100 | pos: [100.0, 100.0], 101 | uv: [1.0, 1.0], 102 | color: [1.0, 0.0, 0.0, 1.0], 103 | }, 104 | graphics::Vertex { 105 | pos: [0.0, 100.0], 106 | uv: [0.0, 1.0], 107 | color: [0.0, 1.0, 0.0, 1.0], 108 | }, 109 | graphics::Vertex { 110 | pos: [0.0, 0.0], 111 | uv: [0.0, 0.0], 112 | color: [0.0, 0.0, 1.0, 1.0], 113 | }, 114 | ]; 115 | 116 | let triangle_indices = vec![0, 1, 2]; 117 | 118 | let mut i = graphics::Image::new(ctx, quad_ctx, "rock.png")?; 119 | i.set_filter(FilterMode::Nearest); 120 | mb.raw(&triangle_verts, &triangle_indices, Some(i))?; 121 | mb.build(ctx, quad_ctx) 122 | } 123 | 124 | impl event::EventHandler for MainState { 125 | fn update( 126 | &mut self, 127 | ctx: &mut Context, 128 | _quad_ctx: &mut miniquad::GraphicsContext, 129 | ) -> GameResult { 130 | const DESIRED_FPS: u32 = 60; 131 | 132 | while timer::check_update_time(ctx, DESIRED_FPS) { 133 | self.zoomlevel += 0.01; 134 | } 135 | Ok(()) 136 | } 137 | 138 | fn draw(&mut self, ctx: &mut Context, quad_ctx: &mut miniquad::GraphicsContext) -> GameResult { 139 | if self.use_canvas { 140 | graphics::set_canvas(ctx, Some(&self.canvas)); 141 | } 142 | 143 | graphics::clear(ctx, quad_ctx, [0.1, 0.2, 0.3, 1.0].into()); 144 | // let src = graphics::Rect::new(0.25, 0.25, 0.5, 0.5); 145 | // let src = graphics::Rect::one(); 146 | let dst = cgmath::Point2::new(20.0, 20.0); 147 | graphics::draw(ctx, quad_ctx, &self.image1, (dst,))?; 148 | 149 | let dst = cgmath::Point2::new(200.0, 100.0); 150 | let dst2 = cgmath::Point2::new(400.0, 400.0); 151 | let scale = cgmath::Vector2::new(10.0, 10.0); 152 | //let shear = graphics::Point::new(self.zoomlevel, self.zoomlevel); 153 | graphics::draw( 154 | ctx, 155 | quad_ctx, 156 | &self.image2_linear, 157 | graphics::DrawParam::new() 158 | // src: src, 159 | .dest(dst) 160 | .rotation(self.zoomlevel) 161 | // offset: Point2::new(-16.0, 0.0), 162 | .scale(scale) 163 | .color(graphics::Color::new(1.0, 1.0, 1.0, 1.0)), // shear: shear, 164 | )?; 165 | graphics::draw( 166 | ctx, 167 | quad_ctx, 168 | &self.image2_nearest, 169 | graphics::DrawParam::new() 170 | //.src(src) 171 | .dest(dst2) 172 | .rotation(self.zoomlevel) 173 | .offset(Point2::new(0.5, 0.5)) 174 | .scale(scale), // shear: shear, 175 | )?; 176 | 177 | let rect = graphics::Rect::new(450.0, 450.0, 50.0, 50.0); 178 | let r1 = graphics::Mesh::new_rectangle( 179 | ctx, 180 | quad_ctx, 181 | graphics::DrawMode::fill(), 182 | rect, 183 | graphics::Color::WHITE, 184 | )?; 185 | graphics::draw(ctx, quad_ctx, &r1, DrawParam::default())?; 186 | 187 | let rect = graphics::Rect::new(450.0, 450.0, 50.0, 50.0); 188 | let r2 = graphics::Mesh::new_rectangle( 189 | ctx, 190 | quad_ctx, 191 | graphics::DrawMode::fill(), 192 | rect, 193 | graphics::Color::new(1.0, 0.0, 0.0, 1.0), 194 | )?; 195 | graphics::draw(ctx, quad_ctx, &r2, DrawParam::default())?; 196 | //graphics::rectangle(ctx, graphics::WHITE, graphics::DrawMode::fill(), rect)?; 197 | 198 | // let rect = graphics::Rect::new(450.0, 450.0, 50.0, 50.0); 199 | // graphics::rectangle( 200 | // ctx, 201 | // graphics::Color::new(1.0, 0.0, 0.0, 1.0), 202 | // graphics::DrawMode::stroke(1.0), 203 | // rect, 204 | // )?; 205 | 206 | for m in &self.meshes { 207 | graphics::draw(ctx, quad_ctx, m, DrawParam::new())?; 208 | } 209 | 210 | if self.use_canvas { 211 | graphics::set_canvas(ctx, None); 212 | graphics::draw(ctx, quad_ctx, &self.canvas, DrawParam::new())?; 213 | } 214 | 215 | graphics::present(ctx, quad_ctx)?; 216 | Ok(()) 217 | } 218 | 219 | fn key_down_event( 220 | &mut self, 221 | _ctx: &mut Context, 222 | _quad_ctx: &mut miniquad::GraphicsContext, 223 | _keycode: KeyCode, 224 | _keymods: KeyMods, 225 | _repeat: bool, 226 | ) { 227 | self.use_canvas ^= true; 228 | println!("use_canvas: {}", self.use_canvas); 229 | } 230 | } 231 | 232 | pub fn main() -> GameResult { 233 | let resource_dir = if let Ok(manifest_dir) = env::var("CARGO_MANIFEST_DIR") { 234 | let mut path = path::PathBuf::from(manifest_dir); 235 | path.push("resources"); 236 | path 237 | } else { 238 | path::PathBuf::from("./resources") 239 | }; 240 | 241 | ggez::start( 242 | ggez::conf::Conf::default() 243 | .cache(Some(include_bytes!("resources.tar"))) 244 | .physical_root_dir(Some(resource_dir)), 245 | //.sample_count(16), 246 | |mut context, quad_ctx| Box::new(MainState::new(&mut context, quad_ctx).unwrap()), 247 | ) 248 | } 249 | -------------------------------------------------------------------------------- /examples/blend_modes.rs: -------------------------------------------------------------------------------- 1 | //! An example drawing semi-transparent venn diagrams 2 | //! using different blend modes. 3 | //! 4 | //! It also shows why you'd usually want to draw canvases 5 | //! using the `Premultiplied` blend mode 6 | //! (for more explanations on this see https://github.com/ggez/ggez/issues/694#issuecomment-853724926) 7 | 8 | extern crate good_web_game as ggez; 9 | 10 | use ggez::event::EventHandler; 11 | use ggez::graphics::{self, BlendMode, Color, DrawParam, Drawable}; 12 | use ggez::miniquad; 13 | use ggez::{Context, GameResult}; 14 | use glam::Vec2; 15 | use std::env; 16 | use std::path; 17 | 18 | struct MainState { 19 | circle: graphics::Mesh, 20 | canvas: graphics::Canvas, 21 | font: graphics::Font, 22 | } 23 | 24 | impl MainState { 25 | fn new(ctx: &mut Context, quad_ctx: &mut miniquad::GraphicsContext) -> GameResult { 26 | let circle = graphics::Mesh::new_circle( 27 | ctx, 28 | quad_ctx, 29 | graphics::DrawMode::fill(), 30 | Vec2::new(0.0, 0.0), 31 | 40.0, 32 | 0.5, 33 | Color::WHITE, 34 | )?; 35 | 36 | let mut canvas = graphics::Canvas::with_window_size(ctx, quad_ctx)?; 37 | canvas.set_blend_mode(Some(BlendMode::Alpha)); 38 | 39 | let font = graphics::Font::new(ctx, "/LiberationMono-Regular.ttf")?; 40 | 41 | let s = Self { 42 | circle, 43 | canvas, 44 | font, 45 | }; 46 | Ok(s) 47 | } 48 | 49 | fn draw_venn( 50 | &self, 51 | ctx: &mut Context, 52 | quad_ctx: &mut miniquad::GraphicsContext, 53 | pos: Vec2, 54 | name: &str, 55 | ) -> GameResult<()> { 56 | const TRI_COLORS: [Color; 3] = [ 57 | Color::new(0.8, 0., 0., 0.5), 58 | Color::new(0., 0.8, 0., 0.5), 59 | Color::new(0., 0., 0.8, 0.5), 60 | ]; 61 | 62 | const OFFSET: f32 = 20.; 63 | const REL_POSITIONS: [[f32; 2]; 3] = [ 64 | [-OFFSET, -OFFSET / 2.], 65 | [OFFSET, -OFFSET / 2.], 66 | [0., OFFSET], 67 | ]; 68 | 69 | // draw the diagram 70 | for i in 0..3 { 71 | self.circle.draw( 72 | ctx, 73 | quad_ctx, 74 | DrawParam::default() 75 | .dest(pos + Vec2::from(REL_POSITIONS[i])) 76 | .color(TRI_COLORS[i]), 77 | )?; 78 | } 79 | 80 | // draw text naming the blend mode 81 | let text = graphics::Text::new((name, self.font, 20.0)); 82 | let text_offset = Vec2::new(0., -90.); 83 | graphics::draw( 84 | ctx, 85 | quad_ctx, 86 | &text, 87 | graphics::DrawParam::new() 88 | .dest(pos + text_offset) 89 | .color(Color::WHITE) 90 | .offset(Vec2::new(0.5, 0.5)), 91 | )?; 92 | 93 | Ok(()) 94 | } 95 | 96 | fn draw_venn_diagrams( 97 | &mut self, 98 | ctx: &mut Context, 99 | quad_ctx: &mut miniquad::GraphicsContext, 100 | ) -> GameResult<()> { 101 | let (w, h) = graphics::drawable_size(quad_ctx); 102 | let y = h / 4.; 103 | const MODE_COUNT: usize = 5; 104 | let x_step = w / (MODE_COUNT + 1) as f32; 105 | 106 | // draw with Alpha 107 | self.circle.set_blend_mode(Some(BlendMode::Alpha)); 108 | self.draw_venn(ctx, quad_ctx, [x_step, y].into(), "Alpha")?; 109 | 110 | // draw with Add 111 | self.circle.set_blend_mode(Some(BlendMode::Add)); 112 | self.draw_venn(ctx, quad_ctx, [x_step * 2., y].into(), "Add")?; 113 | 114 | // draw with Sub 115 | self.circle.set_blend_mode(Some(BlendMode::Subtract)); 116 | self.draw_venn(ctx, quad_ctx, [x_step * 3., y].into(), "Subtract")?; 117 | 118 | // draw with Multiply 119 | self.circle.set_blend_mode(Some(BlendMode::Multiply)); 120 | self.draw_venn(ctx, quad_ctx, [x_step * 4., y].into(), "Multiply")?; 121 | 122 | // draw with Replace 123 | self.circle.set_blend_mode(Some(BlendMode::Replace)); 124 | self.draw_venn(ctx, quad_ctx, [x_step * 5., y].into(), "Replace")?; 125 | 126 | Ok(()) 127 | } 128 | } 129 | 130 | impl EventHandler for MainState { 131 | fn update( 132 | &mut self, 133 | _: &mut Context, 134 | _quad_ctx: &mut miniquad::GraphicsContext, 135 | ) -> GameResult<()> { 136 | Ok(()) 137 | } 138 | 139 | fn draw( 140 | &mut self, 141 | ctx: &mut Context, 142 | quad_ctx: &mut miniquad::GraphicsContext, 143 | ) -> GameResult<()> { 144 | // also draw everything onto the canvas 145 | graphics::set_canvas(ctx, Some(&self.canvas)); 146 | graphics::clear(ctx, quad_ctx, Color::new(0., 0., 0., 0.)); 147 | self.draw_venn_diagrams(ctx, quad_ctx)?; 148 | 149 | // draw the canvas onto the screen 150 | graphics::set_canvas(ctx, None); 151 | graphics::clear(ctx, quad_ctx, Color::new(0.3, 0.3, 0.3, 1.0)); 152 | // draw everything directly onto the screen once 153 | self.draw_venn_diagrams(ctx, quad_ctx)?; 154 | 155 | let (_, height) = graphics::drawable_size(quad_ctx); 156 | self.canvas.draw( 157 | ctx, 158 | quad_ctx, 159 | DrawParam::default().dest(mint::Point2 { 160 | x: 0., 161 | y: height / 2., 162 | }), 163 | )?; 164 | 165 | // draw text pointing out which is which 166 | let (_w, h) = graphics::drawable_size(quad_ctx); 167 | let y = h / 2.; 168 | 169 | let text = graphics::Text::new(("drawn directly:", self.font, 20.0)); 170 | graphics::draw( 171 | ctx, 172 | quad_ctx, 173 | &text, 174 | graphics::DrawParam::new() 175 | .dest(Vec2::new(8., 4.)) 176 | .color(Color::WHITE), 177 | )?; 178 | let text = 179 | graphics::Text::new(("drawn onto a (transparent black) canvas:", self.font, 20.0)); 180 | graphics::draw( 181 | ctx, 182 | quad_ctx, 183 | &text, 184 | graphics::DrawParam::new() 185 | .dest(Vec2::new(8., 4. + y)) 186 | .color(Color::WHITE), 187 | )?; 188 | 189 | graphics::present(ctx, quad_ctx)?; 190 | Ok(()) 191 | } 192 | 193 | fn key_down_event( 194 | &mut self, 195 | _ctx: &mut Context, 196 | _quad_ctx: &mut miniquad::GraphicsContext, 197 | _keycode: ggez::event::KeyCode, 198 | _keymod: ggez::event::KeyMods, 199 | repeat: bool, 200 | ) { 201 | if !repeat { 202 | if let Some(BlendMode::Alpha) = self.canvas.blend_mode() { 203 | self.canvas.set_blend_mode(Some(BlendMode::Premultiplied)); 204 | println!("Drawing canvas with premultiplied alpha mode"); 205 | } else { 206 | self.canvas.set_blend_mode(Some(BlendMode::Alpha)); 207 | println!("Drawing canvas with default alpha mode"); 208 | } 209 | } 210 | } 211 | } 212 | 213 | pub fn main() -> GameResult { 214 | let resource_dir = if let Ok(manifest_dir) = env::var("CARGO_MANIFEST_DIR") { 215 | let mut path = path::PathBuf::from(manifest_dir); 216 | path.push("resources"); 217 | path 218 | } else { 219 | path::PathBuf::from("./resources") 220 | }; 221 | 222 | ggez::start( 223 | ggez::conf::Conf::default() 224 | .cache(Some(include_bytes!("resources.tar"))) 225 | .physical_root_dir(Some(resource_dir)), 226 | |mut context, quad_ctx| Box::new(MainState::new(&mut context, quad_ctx).unwrap()), 227 | ) 228 | } 229 | -------------------------------------------------------------------------------- /examples/bunnymark.rs: -------------------------------------------------------------------------------- 1 | /// Based on the bunnymark example from [`tetra`](https://crates.io/crates/tetra) 2 | /// which is based on https://github.com/openfl/openfl-samples/tree/master/demos/BunnyMark 3 | /// Original BunnyMark (and sprite) by Iain Lobb 4 | extern crate good_web_game as ggez; 5 | use quad_rand as qrand; 6 | 7 | use std::env; 8 | use std::path; 9 | 10 | use ggez::graphics::{spritebatch::SpriteBatch, Color, Image}; 11 | use ggez::Context; 12 | use ggez::*; 13 | 14 | use glam::*; 15 | 16 | // NOTE: Using a high number here yields worse performance than adding more bunnies over 17 | // time - I think this is due to all of the RNG being run on the same tick... 18 | const INITIAL_BUNNIES: usize = 1000; 19 | const WIDTH: u16 = 800; 20 | const HEIGHT: u16 = 600; 21 | const GRAVITY: f32 = 0.5; 22 | 23 | struct Bunny { 24 | position: Vec2, 25 | velocity: Vec2, 26 | } 27 | 28 | impl Bunny { 29 | fn new() -> Bunny { 30 | let x_vel = qrand::gen_range(0.0, 5.0); 31 | let y_vel = qrand::gen_range(0.0, 5.0) - 2.5; 32 | 33 | Bunny { 34 | position: Vec2::new(0.0, 0.0), 35 | velocity: Vec2::new(x_vel, y_vel), 36 | } 37 | } 38 | } 39 | 40 | struct GameState { 41 | texture: Image, 42 | bunnies: Vec, 43 | max_x: f32, 44 | max_y: f32, 45 | 46 | click_timer: i32, 47 | bunnybatch: SpriteBatch, 48 | batched_drawing: bool, 49 | } 50 | 51 | impl GameState { 52 | fn new( 53 | ctx: &mut Context, 54 | quad_ctx: &mut miniquad::GraphicsContext, 55 | ) -> ggez::GameResult { 56 | // We just use the same RNG seed every time. 57 | qrand::srand(12345); 58 | let texture = Image::new(ctx, quad_ctx, "/wabbit_alpha.png")?; 59 | let mut bunnies = Vec::with_capacity(INITIAL_BUNNIES); 60 | let max_x = (WIDTH - texture.width()) as f32; 61 | let max_y = (HEIGHT - texture.height()) as f32; 62 | 63 | for _ in 0..INITIAL_BUNNIES { 64 | bunnies.push(Bunny::new()); 65 | } 66 | 67 | let bunnybatch = SpriteBatch::new(texture.clone()); 68 | 69 | Ok(GameState { 70 | texture, 71 | bunnies, 72 | max_x, 73 | max_y, 74 | 75 | click_timer: 0, 76 | bunnybatch, 77 | batched_drawing: true, 78 | }) 79 | } 80 | } 81 | 82 | impl event::EventHandler for GameState { 83 | fn update( 84 | &mut self, 85 | _ctx: &mut Context, 86 | _quad_ctx: &mut miniquad::GraphicsContext, 87 | ) -> GameResult { 88 | if self.click_timer > 0 { 89 | self.click_timer -= 1; 90 | } 91 | 92 | for bunny in &mut self.bunnies { 93 | bunny.position += bunny.velocity; 94 | bunny.velocity += Vec2::new(0.0, GRAVITY); 95 | 96 | if bunny.position.x > self.max_x { 97 | bunny.velocity *= Vec2::new(-1.0, 0.); 98 | bunny.position.x = self.max_x; 99 | } else if bunny.position.x < 0.0 { 100 | bunny.velocity *= Vec2::new(-1.0, 0.0); 101 | bunny.position.x = 0.0; 102 | } 103 | 104 | if bunny.position.y > self.max_y { 105 | bunny.velocity.y *= -0.8; 106 | bunny.position.y = self.max_y; 107 | 108 | // Flip a coin 109 | if qrand::gen_range(0, 2) > 0 { 110 | bunny.velocity -= Vec2::new(0.0, 3.0 + (qrand::gen_range(0.0, 4.0))); 111 | } 112 | } else if bunny.position.y < 0.0 { 113 | bunny.velocity.y = 0.0; 114 | bunny.position.y = 0.0; 115 | } 116 | } 117 | 118 | Ok(()) 119 | } 120 | 121 | fn draw(&mut self, ctx: &mut Context, quad_ctx: &mut miniquad::GraphicsContext) -> GameResult { 122 | graphics::clear(ctx, quad_ctx, Color::from((0.392, 0.584, 0.929))); 123 | 124 | if self.batched_drawing { 125 | self.bunnybatch.clear(); 126 | for bunny in &self.bunnies { 127 | self.bunnybatch.add((bunny.position,)); 128 | } 129 | graphics::draw(ctx, quad_ctx, &self.bunnybatch, (Vec2::new(0.0, 0.0),))?; 130 | } else { 131 | for bunny in &self.bunnies { 132 | graphics::draw(ctx, quad_ctx, &self.texture, (bunny.position,))?; 133 | } 134 | } 135 | 136 | /* 137 | graphics::set_window_title( 138 | ctx, 139 | &format!( 140 | "BunnyMark - {} bunnies - {:.0} FPS - batched drawing: {}", 141 | self.bunnies.len(), 142 | timer::fps(ctx), 143 | self.batched_drawing 144 | ), 145 | ); 146 | */ 147 | graphics::present(ctx, quad_ctx)?; 148 | 149 | Ok(()) 150 | } 151 | 152 | fn mouse_button_down_event( 153 | &mut self, 154 | _ctx: &mut Context, 155 | _quad_ctx: &mut miniquad::GraphicsContext, 156 | button: input::mouse::MouseButton, 157 | _x: f32, 158 | _y: f32, 159 | ) { 160 | if button == input::mouse::MouseButton::Left && self.click_timer == 0 { 161 | for _ in 0..INITIAL_BUNNIES { 162 | self.bunnies.push(Bunny::new()); 163 | } 164 | self.click_timer = 10; 165 | } 166 | } 167 | 168 | fn key_down_event( 169 | &mut self, 170 | _ctx: &mut Context, 171 | _quad_ctx: &mut miniquad::GraphicsContext, 172 | keycode: event::KeyCode, 173 | _keymods: event::KeyMods, 174 | _repeat: bool, 175 | ) { 176 | if keycode == event::KeyCode::Space { 177 | self.batched_drawing = !self.batched_drawing; 178 | } 179 | } 180 | } 181 | 182 | fn main() -> GameResult { 183 | let resource_dir = if let Ok(manifest_dir) = env::var("CARGO_MANIFEST_DIR") { 184 | let mut path = path::PathBuf::from(manifest_dir); 185 | path.push("resources"); 186 | path 187 | } else { 188 | path::PathBuf::from("./resources") 189 | }; 190 | 191 | ggez::start( 192 | ggez::conf::Conf::default() 193 | .cache(Some(include_bytes!("resources.tar"))) 194 | .physical_root_dir(Some(resource_dir)), 195 | |context, quad_ctx| Box::new(GameState::new(context, quad_ctx).unwrap()), 196 | ) 197 | } 198 | -------------------------------------------------------------------------------- /examples/canvas_subframe.rs: -------------------------------------------------------------------------------- 1 | //! https://github.com/ggez/ggez/blob/master/examples/canvas_subframe.rs 2 | //! Sprite batch is not really implemented yet, so its extremely slow. 3 | //! But the framebuffer works fine, nice! 4 | //! 5 | //! An example of how to use a `SpriteBatch`. 6 | //! 7 | 8 | extern crate cgmath; 9 | extern crate good_web_game as ggez; 10 | 11 | use ggez::event; 12 | use ggez::graphics; 13 | use ggez::miniquad; 14 | use ggez::timer; 15 | use ggez::{Context, GameResult}; 16 | 17 | type Point2 = cgmath::Point2; 18 | type Vector2 = cgmath::Vector2; 19 | 20 | struct MainState { 21 | spritebatch: graphics::spritebatch::SpriteBatch, 22 | canvas: graphics::Canvas, 23 | draw_pt: Point2, 24 | draw_vec: Vector2, 25 | } 26 | 27 | impl MainState { 28 | fn new(ctx: &mut Context, quad_ctx: &mut miniquad::GraphicsContext) -> GameResult { 29 | let image = graphics::Image::new(ctx, quad_ctx, "/tile.png").unwrap(); 30 | let spritebatch = graphics::spritebatch::SpriteBatch::new(image); 31 | let canvas = graphics::Canvas::with_window_size(ctx, quad_ctx)?; 32 | let draw_pt = Point2::new(0.0, 0.0); 33 | let draw_vec = Vector2::new(1.0, 1.0); 34 | let s = MainState { 35 | spritebatch, 36 | canvas, 37 | draw_pt, 38 | draw_vec, 39 | }; 40 | Ok(s) 41 | } 42 | } 43 | 44 | impl MainState { 45 | fn draw_spritebatch( 46 | &mut self, 47 | ctx: &mut Context, 48 | quad_ctx: &mut miniquad::GraphicsContext, 49 | ) -> GameResult { 50 | graphics::set_canvas(ctx, Some(&self.canvas)); 51 | graphics::clear(ctx, quad_ctx, graphics::Color::WHITE); 52 | 53 | // Freeze the animation so things are easier to see. 54 | let time = 2000; 55 | //let time = (timer::duration_to_f64(timer::time_since_start(ctx)) * 1000.0) as u32; 56 | let cycle = 10_000; 57 | for x in 0..150 { 58 | for y in 0..150 { 59 | let x = x as f32; 60 | let y = y as f32; 61 | let p = graphics::DrawParam::new() 62 | .dest(Point2::new(x * 10.0, y * 10.0)) 63 | // scale: graphics::Point::new(0.0625, 0.0625), 64 | .scale(Vector2::new( 65 | ((time % cycle * 2) as f32 / cycle as f32 * 6.28) 66 | .cos() 67 | .abs() 68 | * 0.0625, 69 | ((time % cycle * 2) as f32 / cycle as f32 * 6.28) 70 | .cos() 71 | .abs() 72 | * 0.0625, 73 | )) 74 | .rotation(-2.0 * ((time % cycle) as f32 / cycle as f32 * 6.28)); 75 | self.spritebatch.add(p); 76 | } 77 | } 78 | 79 | let param = graphics::DrawParam::new() 80 | .dest(Point2::new( 81 | ((time % cycle) as f32 / cycle as f32 * 6.28).cos() * 50.0 + 150.0, 82 | ((time % cycle) as f32 / cycle as f32 * 6.28).sin() * 50.0 + 250.0, 83 | )) 84 | .scale(Vector2::new( 85 | ((time % cycle) as f32 / cycle as f32 * 6.28).sin().abs() * 2.0 + 1.0, 86 | ((time % cycle) as f32 / cycle as f32 * 6.28).sin().abs() * 2.0 + 1.0, 87 | )) 88 | .rotation((time % cycle) as f32 / cycle as f32 * 6.28) 89 | // WARNING: Using an offset != (0.,0.) on a spritebatch may come with a significant performance cost. 90 | // This is due to the fact that the total dimensions of everything drawn by it have to be calculated. 91 | // See SpriteBatch::draw and SpriteBatch::dimensions for more information. 92 | .offset(Point2::new(0.5, 0.5)); 93 | 94 | graphics::draw(ctx, quad_ctx, &self.spritebatch, param)?; 95 | self.spritebatch.clear(); 96 | graphics::set_canvas(ctx, None); 97 | Ok(()) 98 | } 99 | } 100 | 101 | impl event::EventHandler for MainState { 102 | fn update( 103 | &mut self, 104 | ctx: &mut Context, 105 | quad_ctx: &mut miniquad::GraphicsContext, 106 | ) -> GameResult { 107 | if timer::ticks(ctx) % 100 == 0 { 108 | println!("Delta frame time: {:?} ", timer::delta(ctx)); 109 | println!("Average FPS: {}", timer::fps(ctx)); 110 | } 111 | 112 | // Bounce the rect if necessary 113 | let (w, h) = graphics::drawable_size(quad_ctx); 114 | if self.draw_pt.x + (w as f32 / 2.0) > (w as f32) || self.draw_pt.x < 0.0 { 115 | self.draw_vec.x *= -1.0; 116 | } 117 | if self.draw_pt.y + (h as f32 / 2.0) > (h as f32) || self.draw_pt.y < 0.0 { 118 | self.draw_vec.y *= -1.0; 119 | } 120 | self.draw_pt += self.draw_vec; 121 | Ok(()) 122 | } 123 | 124 | fn draw(&mut self, ctx: &mut Context, quad_ctx: &mut miniquad::GraphicsContext) -> GameResult { 125 | graphics::clear(ctx, quad_ctx, [0.1, 0.2, 0.3, 1.0].into()); 126 | self.draw_spritebatch(ctx, quad_ctx)?; 127 | let dims = self.canvas.image().dimensions(); 128 | let src_x = self.draw_pt.x / dims.w; 129 | let src_y = self.draw_pt.y / dims.h; 130 | graphics::draw( 131 | ctx, 132 | quad_ctx, 133 | &self.canvas, 134 | graphics::DrawParam::new() 135 | .dest(self.draw_pt) 136 | .src(graphics::Rect::new(src_x, src_y, 0.5, 0.5)), 137 | )?; 138 | 139 | graphics::present(ctx, quad_ctx)?; 140 | Ok(()) 141 | } 142 | } 143 | 144 | pub fn main() -> GameResult { 145 | ggez::start( 146 | ggez::conf::Conf::default().cache(Some(include_bytes!("resources.tar"))), 147 | |mut context, quad_ctx| Box::new(MainState::new(&mut context, quad_ctx).unwrap()), 148 | ) 149 | } 150 | -------------------------------------------------------------------------------- /examples/colorspace.rs: -------------------------------------------------------------------------------- 1 | //! An example demonstrating sRGB color spaces. 2 | //! 3 | //! sRGB is one of those things that's a bit subtle, a bit obscure, 4 | //! and easy to get wrong, but worth knowing because it crops up 5 | //! from time to time and makes things mysteriously "look wrong" in 6 | //! some cases. It also is sort of overloaded to do two things at 7 | //! once. The first is what you will find [Wikipedia talking about](https://en.wikipedia.org/wiki/Srgb), 8 | //! which is "how do you prove that 'green' on my monitor is the same 9 | //! as 'green' on your monitor?". That part we can safely ignore, 10 | //! since it's a job for your monitor manufacturer. 11 | //! 12 | //! The other part is [gamma 13 | //! correction](https://en.wikipedia.org/wiki/Gamma_correction) which 14 | //! deals with the fact that the response of the human visual system 15 | //! is non-linear, or in non-science talk, if you make a pixel twice 16 | //! as bright, it doesn't *look* twice as bright. To make something 17 | //! *look* twice as bright, you have to make it about 2^2.2 times 18 | //! brighter. The exact math for how to fiddle the numbers to make 19 | //! things Look Nice is defined as part of the sRGB standard, so often 20 | //! a color scheme that complies with this gamma correction process is 21 | //! just called "sRGB". 22 | //! 23 | //! In a perfect world, we don't ever have to worry about this. 24 | //! Images are generally all stored in the sRGB color space (or 25 | //! something similar to it), monitors all display in the sRGB color 26 | //! space, and our graphics drivers know how to do whatever is 27 | //! necessary to make the two line up. The problem comes because we 28 | //! are programmers, and have to be able to poke things instead of 29 | //! just using pre-loaded assets. So the question is: if you do 30 | //! `Color::new(0.5, 0.0, 0.0, 1.0)` and `Color::new(1.0, 0.0, 0.0, 1.0)`, 31 | //! will the second color LOOK twice as bright as the first one? 32 | //! 33 | //! So we have to know what color space we are talking about when we 34 | //! say `Color`! Are we talking about linear, "real" color where your 35 | //! pixel puts out twice as many photons for the second color as the 36 | //! first? Or are we talking about sRGB color, where the pixel 37 | //! actually LOOKS twice as bright? And if we want to, say, write a 38 | //! shader that does math to these colors, AND to colors that come 39 | //! from images that use the sRGB color space, how do we make sure 40 | //! everything matches? To make it even worse, the sRGB conversion 41 | //! done by graphics drivers is toggle-able, and can be set on a 42 | //! per-render-target or per-texture basis, so it's possible for 43 | //! things to get REAL mixed up in subtle ways. 44 | //! 45 | //! The Right Answer, as far as I know, is this: All colors that a 46 | //! human specifies or touches are sRGB-encoded, so a number that is 47 | //! twice as big then . We do our color math in shaders, and (if we 48 | //! set our render target to be an sRGB texture, which ggez always 49 | //! does) the graphics driver will turn the linear colors we specify 50 | //! into sRGB colors. So if we do `vec4 x = vec4(0.25, 0.0, 0.0, 1.0);`, 51 | //! assigning one pixel `x` and another `x * 2` will make the 52 | //! second one *look* twice as bright as the first. 53 | //! 54 | //! BUT, this process also has to be done on INPUT as well; if we pass 55 | //! the shader a value taken from an image file, that image file is in 56 | //! sRGB color. The graphics driver must THEN convert the sRGB color into 57 | //! a linear color when passing it to the shader, so that if we get the 58 | //! color and call it `x`, assigning one output pixel `x` and another 59 | //! `x * 2` again makes the second one LOOK twice as bright as the first. 60 | //! Then it converts the value back on the way out. 61 | //! 62 | //! ggez should handle all of this for you. `graphics::Color` is 63 | //! explicitly a sRGB-corrected color, all textures including the 64 | //! final render target are sRGB-enabled, and when you provide 65 | //! a linear color to something like `graphics::Mesh` it turns it 66 | //! into sRGB for you to match everything else. The purpose of this 67 | //! example is to show that this actually *works* correctly! 68 | 69 | extern crate good_web_game as ggez; 70 | 71 | use ggez::event; 72 | use ggez::graphics::{self, Color, DrawParam}; 73 | use ggez::miniquad; 74 | use ggez::{Context, GameResult}; 75 | use glam::*; 76 | 77 | /// This is a nice aqua test color that will look a lot brighter 78 | /// than it should if we mess something up. 79 | /// See https://github.com/ggez/ggez/issues/209 for examples. 80 | const AQUA: graphics::Color = graphics::Color::new(0.0078, 0.7647, 0.6039, 1.0); 81 | 82 | struct MainState { 83 | demo_mesh: graphics::Mesh, 84 | square_mesh: graphics::Mesh, 85 | demo_image: graphics::Image, 86 | demo_text: graphics::Text, 87 | demo_spritebatch: graphics::spritebatch::SpriteBatch, 88 | } 89 | 90 | impl MainState { 91 | fn new(ctx: &mut Context, quad_ctx: &mut miniquad::GraphicsContext) -> GameResult { 92 | let demo_mesh = graphics::Mesh::new_circle( 93 | ctx, 94 | quad_ctx, 95 | graphics::DrawMode::fill(), 96 | Vec2::new(0.0, 0.0), 97 | 100.0, 98 | 2.0, 99 | AQUA, 100 | )?; 101 | let square_mesh = graphics::Mesh::new_rectangle( 102 | ctx, 103 | quad_ctx, 104 | graphics::DrawMode::fill(), 105 | graphics::Rect::new(0.0, 0.0, 400.0, 400.0), 106 | Color::WHITE, 107 | )?; 108 | let demo_image = graphics::Image::solid(ctx, quad_ctx, 200, AQUA)?; 109 | let demo_text = graphics::Text::new(graphics::TextFragment { 110 | text: "-".to_string(), 111 | color: Some(AQUA), 112 | font: Some(graphics::Font::default()), 113 | scale: Some(graphics::PxScale::from(300.0)), 114 | }); 115 | let demo_spritebatch = graphics::spritebatch::SpriteBatch::new(demo_image.clone()); 116 | 117 | let s = MainState { 118 | demo_mesh, 119 | square_mesh, 120 | demo_image, 121 | demo_text, 122 | demo_spritebatch, 123 | }; 124 | Ok(s) 125 | } 126 | } 127 | 128 | impl event::EventHandler for MainState { 129 | fn update( 130 | &mut self, 131 | _ctx: &mut Context, 132 | _quad_ctx: &mut miniquad::GraphicsContext, 133 | ) -> GameResult { 134 | Ok(()) 135 | } 136 | 137 | fn draw(&mut self, ctx: &mut Context, quad_ctx: &mut miniquad::GraphicsContext) -> GameResult { 138 | graphics::clear(ctx, quad_ctx, AQUA); 139 | 140 | // Draw a white square so we can see things 141 | graphics::draw( 142 | ctx, 143 | quad_ctx, 144 | &self.square_mesh, 145 | DrawParam::default().dest(Vec2::new(200.0, 100.0)), 146 | )?; 147 | 148 | // Draw things partially over the white square so we can see 149 | // where they are; they SHOULD be the same color as the 150 | // background. 151 | 152 | // mesh 153 | graphics::draw( 154 | ctx, 155 | quad_ctx, 156 | &self.demo_mesh, 157 | DrawParam::default().dest(Vec2::new(150.0, 200.0)), 158 | )?; 159 | 160 | // image 161 | graphics::draw( 162 | ctx, 163 | quad_ctx, 164 | &self.demo_image, 165 | DrawParam::default().dest(Vec2::new(450.0, 200.0)), 166 | )?; 167 | 168 | // text 169 | graphics::draw( 170 | ctx, 171 | quad_ctx, 172 | &self.demo_text, 173 | DrawParam::default().dest(Vec2::new(150.0, 135.0)), 174 | )?; 175 | 176 | // spritebatch 177 | self.demo_spritebatch.add( 178 | DrawParam::default() 179 | .dest(Vec2::new(250.0, 350.0)) 180 | .scale(Vec2::new(0.25, 0.25)), 181 | ); 182 | self.demo_spritebatch.add( 183 | DrawParam::default() 184 | .dest(Vec2::new(250.0, 425.0)) 185 | .scale(Vec2::new(0.1, 0.1)), 186 | ); 187 | graphics::draw( 188 | ctx, 189 | quad_ctx, 190 | &self.demo_spritebatch, 191 | DrawParam::default().dest(Vec2::new(0.0, 0.0)), 192 | )?; 193 | self.demo_spritebatch.clear(); 194 | 195 | graphics::present(ctx, quad_ctx)?; 196 | Ok(()) 197 | } 198 | } 199 | 200 | pub fn main() -> GameResult { 201 | use std::env; 202 | use std::path; 203 | let resource_dir = if let Ok(manifest_dir) = env::var("CARGO_MANIFEST_DIR") { 204 | let mut path = path::PathBuf::from(manifest_dir); 205 | path.push("resources"); 206 | path 207 | } else { 208 | path::PathBuf::from("./resources") 209 | }; 210 | 211 | ggez::start( 212 | ggez::conf::Conf::default() 213 | .cache(Some(include_bytes!("resources.tar"))) 214 | .physical_root_dir(Some(resource_dir)), 215 | |mut context, quad_ctx| Box::new(MainState::new(&mut context, quad_ctx).unwrap()), 216 | ) 217 | } 218 | -------------------------------------------------------------------------------- /examples/input_test.rs: -------------------------------------------------------------------------------- 1 | //! Example that just prints out all the input events. 2 | 3 | extern crate glam; 4 | extern crate good_web_game as ggez; 5 | 6 | use ggez::event::{self, KeyCode, KeyMods, MouseButton}; 7 | use ggez::graphics::{self, Color, DrawMode}; 8 | use ggez::input; 9 | use ggez::miniquad; 10 | 11 | #[cfg(not(any(target_arch = "wasm32", target_os = "ios", target_os = "android",)))] 12 | use ggez::input::gamepad::{ 13 | gilrs::{Axis, Button}, 14 | GamepadId, 15 | }; 16 | use ggez::{Context, GameResult}; 17 | use glam::*; 18 | 19 | struct MainState { 20 | pos_x: f32, 21 | pos_y: f32, 22 | mouse_down: bool, 23 | } 24 | 25 | impl MainState { 26 | fn new() -> MainState { 27 | MainState { 28 | pos_x: 100.0, 29 | pos_y: 100.0, 30 | mouse_down: false, 31 | } 32 | } 33 | } 34 | 35 | impl event::EventHandler for MainState { 36 | fn update( 37 | &mut self, 38 | ctx: &mut Context, 39 | _quad_ctx: &mut miniquad::GraphicsContext, 40 | ) -> GameResult { 41 | if input::keyboard::is_key_pressed(ctx, KeyCode::A) { 42 | println!("The A key is pressed"); 43 | if input::keyboard::is_mod_active(ctx, input::keyboard::KeyMods::SHIFT) { 44 | println!("The shift key is held too."); 45 | } 46 | println!( 47 | "Full list of pressed keys: {:?}", 48 | input::keyboard::pressed_keys(ctx) 49 | ); 50 | } 51 | Ok(()) 52 | } 53 | 54 | fn draw(&mut self, ctx: &mut Context, quad_ctx: &mut miniquad::GraphicsContext) -> GameResult { 55 | graphics::clear(ctx, quad_ctx, [0.1, 0.2, 0.3, 1.0].into()); 56 | 57 | let rectangle = graphics::Mesh::new_rectangle( 58 | ctx, 59 | quad_ctx, 60 | DrawMode::fill(), 61 | graphics::Rect { 62 | x: self.pos_x, 63 | y: self.pos_y, 64 | w: 400.0, 65 | h: 300.0, 66 | }, 67 | Color::WHITE, 68 | )?; 69 | graphics::draw(ctx, quad_ctx, &rectangle, (glam::Vec2::new(0.0, 0.0),))?; 70 | 71 | graphics::present(ctx, quad_ctx)?; 72 | Ok(()) 73 | } 74 | 75 | fn mouse_button_down_event( 76 | &mut self, 77 | _ctx: &mut Context, 78 | _quad_ctx: &mut miniquad::GraphicsContext, 79 | button: MouseButton, 80 | x: f32, 81 | y: f32, 82 | ) { 83 | self.mouse_down = true; 84 | println!("Mouse button pressed: {:?}, x: {}, y: {}", button, x, y); 85 | } 86 | 87 | fn mouse_button_up_event( 88 | &mut self, 89 | _ctx: &mut Context, 90 | _quad_ctx: &mut miniquad::GraphicsContext, 91 | button: MouseButton, 92 | x: f32, 93 | y: f32, 94 | ) { 95 | self.mouse_down = false; 96 | println!("Mouse button released: {:?}, x: {}, y: {}", button, x, y); 97 | } 98 | 99 | fn mouse_motion_event( 100 | &mut self, 101 | _ctx: &mut Context, 102 | _quad_ctx: &mut miniquad::GraphicsContext, 103 | x: f32, 104 | y: f32, 105 | xrel: f32, 106 | yrel: f32, 107 | ) { 108 | if self.mouse_down { 109 | // Mouse coordinates are PHYSICAL coordinates, but here we want logical coordinates. 110 | 111 | // If you simply use the initial coordinate system, then physical and logical 112 | // coordinates are identical. 113 | self.pos_x = x; 114 | self.pos_y = y; 115 | 116 | // If you change your screen coordinate system you need to calculate the 117 | // logical coordinates like this: 118 | /* 119 | let screen_rect = graphics::screen_coordinates(_ctx); 120 | let size = graphics::window(_ctx).inner_size(); 121 | self.pos_x = (x / (size.width as f32)) * screen_rect.w + screen_rect.x; 122 | self.pos_y = (y / (size.height as f32)) * screen_rect.h + screen_rect.y; 123 | */ 124 | } 125 | println!( 126 | "Mouse motion, x: {}, y: {}, relative x: {}, relative y: {}", 127 | x, y, xrel, yrel 128 | ); 129 | } 130 | 131 | fn mouse_wheel_event( 132 | &mut self, 133 | _ctx: &mut Context, 134 | _quad_ctx: &mut miniquad::GraphicsContext, 135 | x: f32, 136 | y: f32, 137 | ) { 138 | println!("Mousewheel event, x: {}, y: {}", x, y); 139 | } 140 | 141 | fn key_down_event( 142 | &mut self, 143 | _ctx: &mut Context, 144 | _quad_ctx: &mut miniquad::GraphicsContext, 145 | keycode: KeyCode, 146 | keymod: KeyMods, 147 | repeat: bool, 148 | ) { 149 | println!( 150 | "Key pressed: {:?}, modifier {:?}, repeat: {}", 151 | keycode, keymod, repeat 152 | ); 153 | } 154 | 155 | fn key_up_event( 156 | &mut self, 157 | _ctx: &mut Context, 158 | _quad_ctx: &mut miniquad::GraphicsContext, 159 | keycode: KeyCode, 160 | keymod: KeyMods, 161 | ) { 162 | println!("Key released: {:?}, modifier {:?}", keycode, keymod); 163 | } 164 | 165 | fn text_input_event( 166 | &mut self, 167 | _ctx: &mut Context, 168 | _quad_ctx: &mut miniquad::GraphicsContext, 169 | ch: char, 170 | ) { 171 | println!("Text input: {}", ch); 172 | } 173 | 174 | #[cfg(not(any(target_arch = "wasm32", target_os = "ios", target_os = "android",)))] 175 | fn gamepad_button_down_event( 176 | &mut self, 177 | _ctx: &mut Context, 178 | _quad_ctx: &mut miniquad::GraphicsContext, 179 | btn: Button, 180 | id: GamepadId, 181 | ) { 182 | println!("Gamepad button pressed: {:?} Gamepad_Id: {:?}", btn, id); 183 | } 184 | 185 | #[cfg(not(any(target_arch = "wasm32", target_os = "ios", target_os = "android",)))] 186 | fn gamepad_button_up_event( 187 | &mut self, 188 | _ctx: &mut Context, 189 | _quad_ctx: &mut miniquad::GraphicsContext, 190 | btn: Button, 191 | id: GamepadId, 192 | ) { 193 | println!("Gamepad button released: {:?} Gamepad_Id: {:?}", btn, id); 194 | } 195 | 196 | #[cfg(not(any(target_arch = "wasm32", target_os = "ios", target_os = "android",)))] 197 | fn gamepad_axis_event( 198 | &mut self, 199 | _ctx: &mut Context, 200 | _quad_ctx: &mut miniquad::GraphicsContext, 201 | axis: Axis, 202 | value: f32, 203 | id: GamepadId, 204 | ) { 205 | println!( 206 | "Axis Event: {:?} Value: {} Gamepad_Id: {:?}", 207 | axis, value, id 208 | ); 209 | } 210 | 211 | /* 212 | fn focus_event(&mut self, _ctx: &mut Context, gained: bool) { 213 | if gained { 214 | println!("Focus gained"); 215 | } else { 216 | println!("Focus lost"); 217 | } 218 | } 219 | */ 220 | } 221 | 222 | pub fn main() -> GameResult { 223 | //let cb = ggez::ContextBuilder::new("input_test", "ggez").window_mode( 224 | // conf::WindowMode::default() 225 | // .fullscreen_type(conf::FullscreenType::Windowed) 226 | // .resizable(true), 227 | //); 228 | 229 | // remove the comment to see how physical mouse coordinates can differ 230 | // from logical game coordinates when the screen coordinate system changes 231 | // graphics::set_screen_coordinates(&mut ctx, Rect::new(20., 50., 2000., 1000.)); 232 | 233 | // alternatively, resizing the window also leads to screen coordinates 234 | // and physical window size being out of sync 235 | 236 | ggez::start( 237 | ggez::conf::Conf::default() 238 | .cache(Some(include_bytes!("resources.tar"))) 239 | .window_resizable(true), 240 | |_context, _quad_ctx| Box::new(MainState::new()), 241 | ) 242 | } 243 | -------------------------------------------------------------------------------- /examples/meshbatch.rs: -------------------------------------------------------------------------------- 1 | //! An example of how to use a `MeshBatch`. 2 | 3 | extern crate good_web_game as ggez; 4 | 5 | use ggez::event; 6 | use ggez::graphics::{self, Color}; 7 | use ggez::miniquad; 8 | use ggez::timer; 9 | use ggez::{Context, GameResult}; 10 | use glam::*; 11 | use oorandom::Rand32; 12 | use std::f32::consts::PI; 13 | 14 | const TWO_PI: f32 = 2.0 * PI; 15 | 16 | struct MainState { 17 | mesh_batch: graphics::MeshBatch, 18 | } 19 | 20 | impl MainState { 21 | fn new(ctx: &mut Context, quad_ctx: &mut miniquad::GraphicsContext) -> GameResult { 22 | let mut rng = Rand32::new(12345); 23 | let mesh = graphics::MeshBuilder::new() 24 | .circle( 25 | graphics::DrawMode::stroke(4.0), 26 | Vec2::new(0.0, 0.0), 27 | 8.0, 28 | 1.0, 29 | (0, 0, 255).into(), 30 | )? 31 | .line( 32 | &[Vec2::new(0.0, 0.0), Vec2::new(8.0, 0.0)], 33 | 2.0, 34 | (255, 255, 0).into(), 35 | )? 36 | .build(ctx, quad_ctx)?; 37 | 38 | let mut mesh_batch = graphics::MeshBatch::new(mesh)?; 39 | 40 | // Generate enough instances to fill the entire screen 41 | let items_x = (graphics::drawable_size(quad_ctx).0 / 16.0) as u32; 42 | let items_y = (graphics::drawable_size(quad_ctx).1 / 16.0) as u32; 43 | for x in 1..items_x { 44 | for y in 1..items_y { 45 | let x = x as f32; 46 | let y = y as f32; 47 | 48 | let p = graphics::DrawParam::new() 49 | .dest(Vec2::new(x * 16.0, y * 16.0)) 50 | .rotation(rng.rand_float() * TWO_PI); 51 | 52 | mesh_batch.add(p); 53 | } 54 | } 55 | 56 | // Randomly shuffle generated instances. 57 | // We will update the first 50 of them later. 58 | mesh_batch.get_instance_params_mut(); 59 | //.shuffle(&mut thread_rng()); 60 | 61 | let s = MainState { mesh_batch }; 62 | Ok(s) 63 | } 64 | } 65 | 66 | impl event::EventHandler for MainState { 67 | #[allow(clippy::needless_range_loop)] 68 | fn update( 69 | &mut self, 70 | ctx: &mut Context, 71 | _quad_ctx: &mut miniquad::GraphicsContext, 72 | ) -> GameResult { 73 | if timer::ticks(ctx) % 100 == 0 { 74 | println!("Delta frame time: {:?} ", timer::delta(ctx)); 75 | println!("Average FPS: {}", timer::fps(ctx)); 76 | } 77 | 78 | // Update first 50 instances in the mesh batch 79 | let delta_time = (timer::duration_to_f64(timer::delta(ctx)) * 1000.0) as f32; 80 | let instances = self.mesh_batch.get_instance_params_mut(); 81 | for i in 0..50 { 82 | if let graphics::Transform::Values { 83 | ref mut rotation, .. 84 | } = instances[i].trans 85 | { 86 | if (i % 2) == 0 { 87 | *rotation += 0.001 * TWO_PI * delta_time; 88 | if *rotation > TWO_PI { 89 | *rotation -= TWO_PI; 90 | } 91 | } else { 92 | *rotation -= 0.001 * TWO_PI * delta_time; 93 | if *rotation < 0.0 { 94 | *rotation += TWO_PI; 95 | } 96 | } 97 | } 98 | } 99 | 100 | Ok(()) 101 | } 102 | 103 | fn draw(&mut self, ctx: &mut Context, quad_ctx: &mut miniquad::GraphicsContext) -> GameResult { 104 | graphics::clear(ctx, quad_ctx, Color::BLACK); 105 | 106 | // Flush the first 50 instances in the batch to make our changes visible 107 | // to the graphics card. 108 | self.mesh_batch 109 | .flush_range(ctx, quad_ctx, graphics::MeshIdx(0), 50)?; 110 | 111 | // Draw the batch 112 | let param = graphics::DrawParam::default(); 113 | // Uncomment this line to see the effect of an offset on a MeshBatch 114 | // This is somewhat expensive though, as it results in the MeshBatch 115 | // having to calculate its drawn dimensions every frame, to be able 116 | // to apply the offset based on that. 117 | //let param = param.offset([0.5, 0.5]); 118 | self.mesh_batch.draw(ctx, quad_ctx, param)?; 119 | 120 | graphics::present(ctx, quad_ctx)?; 121 | Ok(()) 122 | } 123 | } 124 | 125 | pub fn main() -> GameResult { 126 | ggez::start(ggez::conf::Conf::default(), |mut context, quad_ctx| { 127 | Box::new(MainState::new(&mut context, quad_ctx).unwrap()) 128 | }) 129 | } 130 | -------------------------------------------------------------------------------- /examples/render_to_image.rs: -------------------------------------------------------------------------------- 1 | //! An example of how to draw to `Image`'s using the `Canvas` type. 2 | 3 | extern crate good_web_game as ggez; 4 | 5 | use ggez::event; 6 | use ggez::graphics::{self, Color, DrawParam}; 7 | use ggez::miniquad; 8 | use ggez::{Context, GameResult}; 9 | 10 | type Point2 = glam::Vec2; 11 | type Vector2 = glam::Vec2; 12 | 13 | struct MainState { 14 | canvas: graphics::Canvas, 15 | text: graphics::Text, 16 | } 17 | 18 | impl MainState { 19 | fn new(ctx: &mut Context, quad_ctx: &mut miniquad::GraphicsContext) -> GameResult { 20 | let canvas = graphics::Canvas::with_window_size(ctx, quad_ctx)?; 21 | let font = graphics::Font::default(); 22 | let text = graphics::Text::new(("Hello world!", font, 24.0)); 23 | Ok(MainState { canvas, text }) 24 | } 25 | } 26 | 27 | impl event::EventHandler for MainState { 28 | fn update( 29 | &mut self, 30 | _ctx: &mut Context, 31 | _quad_ctx: &mut miniquad::GraphicsContext, 32 | ) -> GameResult { 33 | Ok(()) 34 | } 35 | 36 | fn draw(&mut self, ctx: &mut Context, quad_ctx: &mut miniquad::GraphicsContext) -> GameResult { 37 | // first lets render to our canvas 38 | graphics::set_canvas(ctx, Some(&self.canvas)); 39 | graphics::clear(ctx, quad_ctx, [0.1, 0.2, 0.3, 1.0].into()); 40 | 41 | graphics::draw( 42 | ctx, 43 | quad_ctx, 44 | &self.text, 45 | (Point2::new(400.0, 300.0), Color::WHITE), 46 | )?; 47 | 48 | // now lets render our scene once in the top left and in the bottom 49 | // right 50 | let window_size = graphics::drawable_size(quad_ctx); 51 | let scale = Vector2::new( 52 | 0.5 * window_size.0 as f32 / self.canvas.width() as f32, 53 | 0.5 * window_size.1 as f32 / self.canvas.height() as f32, 54 | ); 55 | // let scale = Vector2::new(1.0, 1.0); 56 | graphics::set_canvas(ctx, None); 57 | graphics::clear(ctx, quad_ctx, Color::new(0.0, 0.0, 0.0, 1.0)); 58 | graphics::draw( 59 | ctx, 60 | quad_ctx, 61 | &self.canvas, 62 | DrawParam::default() 63 | .dest(Point2::new(0.0, 0.0)) 64 | .scale(scale), 65 | )?; 66 | graphics::draw( 67 | ctx, 68 | quad_ctx, 69 | &self.canvas, 70 | DrawParam::default() 71 | .dest(Point2::new(400.0, 300.0)) 72 | .scale(scale), 73 | )?; 74 | graphics::present(ctx, quad_ctx)?; 75 | 76 | Ok(()) 77 | } 78 | 79 | fn resize_event( 80 | &mut self, 81 | ctx: &mut Context, 82 | quad_ctx: &mut miniquad::GraphicsContext, 83 | width: f32, 84 | height: f32, 85 | ) { 86 | println!("drawable size: {:?}", graphics::drawable_size(quad_ctx)); 87 | let new_rect = graphics::Rect::new(0.0, 0.0, width, height); 88 | graphics::set_screen_coordinates(ctx, new_rect).unwrap(); 89 | } 90 | } 91 | 92 | pub fn main() -> GameResult { 93 | ggez::start(ggez::conf::Conf::default(), |mut context, quad_ctx| { 94 | Box::new(MainState::new(&mut context, quad_ctx).unwrap()) 95 | }) 96 | } 97 | -------------------------------------------------------------------------------- /examples/resources.tar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ggez/good-web-game/11ce17b8076a4c3eb7b8d809ce4066342b8e412c/examples/resources.tar -------------------------------------------------------------------------------- /examples/shader.rs: -------------------------------------------------------------------------------- 1 | //! A very simple shader example. 2 | 3 | extern crate good_web_game as ggez; 4 | 5 | use ggez::event; 6 | use ggez::graphics::{ 7 | self, Color, DrawMode, ShaderMeta, UniformBlockLayout, UniformDesc, UniformType, 8 | }; 9 | use ggez::miniquad; 10 | use ggez::timer; 11 | use ggez::{Context, GameResult}; 12 | //use std::env; 13 | //use std::path; 14 | 15 | // Define the input struct for our shader. 16 | fn shader_meta() -> ShaderMeta { 17 | ShaderMeta { 18 | images: vec!["Texture".to_string()], 19 | uniforms: UniformBlockLayout { 20 | uniforms: vec![ 21 | UniformDesc::new("u_Rate", UniformType::Float1), 22 | // `Projection` always comes last, as it is appended by gwg internally 23 | UniformDesc::new("Projection", UniformType::Mat4), 24 | ], 25 | }, 26 | } 27 | } 28 | 29 | use bytemuck_derive::{Pod, Zeroable}; 30 | #[repr(C)] 31 | #[derive(Copy, Clone, Zeroable, Pod)] 32 | // The attributes above ensure that this struct derives bytemuck::Pod, 33 | // which is necessary for it to be passed to `graphics::set_uniforms` 34 | pub struct ExtraUniforms { 35 | pub u_rate: f32, 36 | } 37 | 38 | struct MainState { 39 | shader_id: graphics::ShaderId, 40 | dim: f32, 41 | } 42 | 43 | impl MainState { 44 | fn new(ctx: &mut Context, quad_ctx: &mut miniquad::GraphicsContext) -> GameResult { 45 | let dim = 0.5; 46 | let shader_id = graphics::Shader::new( 47 | ctx, 48 | quad_ctx, 49 | "/basic_150.glslv", 50 | "/dimmer_150.glslf", 51 | shader_meta(), 52 | None, 53 | )?; 54 | Ok(MainState { shader_id, dim }) 55 | } 56 | } 57 | 58 | impl event::EventHandler for MainState { 59 | fn update( 60 | &mut self, 61 | ctx: &mut Context, 62 | _quad_ctx: &mut miniquad::GraphicsContext, 63 | ) -> GameResult { 64 | self.dim = 0.5 + (((timer::ticks(ctx) as f32) / 100.0).cos() / 2.0); 65 | graphics::set_uniforms(ctx, self.shader_id, ExtraUniforms { u_rate: self.dim }); 66 | Ok(()) 67 | } 68 | 69 | fn draw(&mut self, ctx: &mut Context, quad_ctx: &mut miniquad::GraphicsContext) -> GameResult { 70 | graphics::clear(ctx, quad_ctx, [0.1, 0.2, 0.3, 1.0].into()); 71 | 72 | let circle = graphics::Mesh::new_circle( 73 | ctx, 74 | quad_ctx, 75 | DrawMode::fill(), 76 | glam::Vec2::new(100.0, 300.0), 77 | 100.0, 78 | 2.0, 79 | Color::WHITE, 80 | )?; 81 | graphics::draw(ctx, quad_ctx, &circle, (glam::Vec2::new(0.0, 0.0),))?; 82 | 83 | { 84 | let _lock = graphics::use_shader(ctx, self.shader_id); 85 | let circle = graphics::Mesh::new_circle( 86 | ctx, 87 | quad_ctx, 88 | DrawMode::fill(), 89 | glam::Vec2::new(400.0, 300.0), 90 | 100.0, 91 | 2.0, 92 | Color::WHITE, 93 | )?; 94 | graphics::draw(ctx, quad_ctx, &circle, (glam::Vec2::new(0.0, 0.0),))?; 95 | } 96 | 97 | let circle = graphics::Mesh::new_circle( 98 | ctx, 99 | quad_ctx, 100 | DrawMode::fill(), 101 | glam::Vec2::new(700.0, 300.0), 102 | 100.0, 103 | 2.0, 104 | Color::WHITE, 105 | )?; 106 | graphics::draw(ctx, quad_ctx, &circle, (glam::Vec2::new(0.0, 0.0),))?; 107 | 108 | graphics::present(ctx, quad_ctx)?; 109 | Ok(()) 110 | } 111 | } 112 | 113 | pub fn main() -> GameResult { 114 | /* 115 | let resource_dir = if let Ok(manifest_dir) = env::var("CARGO_MANIFEST_DIR") { 116 | let mut path = path::PathBuf::from(manifest_dir); 117 | path.push("resources"); 118 | path 119 | } else { 120 | path::PathBuf::from("./resources") 121 | }; 122 | */ 123 | ggez::start( 124 | ggez::conf::Conf::default().cache(Some(include_bytes!("resources.tar"))), 125 | |mut context, quad_ctx| Box::new(MainState::new(&mut context, quad_ctx).unwrap()), 126 | ) 127 | } 128 | -------------------------------------------------------------------------------- /examples/sounds.rs: -------------------------------------------------------------------------------- 1 | extern crate glam; 2 | extern crate good_web_game as ggez; 3 | 4 | use ggez::audio; 5 | use ggez::event; 6 | use ggez::graphics; 7 | use ggez::input; 8 | use ggez::{Context, GameResult}; 9 | 10 | use ggez::event::quit; 11 | use quad_snd::PlaySoundParams; 12 | 13 | struct MainState { 14 | sound: audio::Source, 15 | volume: f32, 16 | } 17 | 18 | impl MainState { 19 | fn new(ctx: &mut Context) -> GameResult { 20 | let sound = audio::Source::new(ctx, "/sound.ogg")?; 21 | let s = MainState { 22 | sound, 23 | volume: PlaySoundParams::default().volume, 24 | }; 25 | Ok(s) 26 | } 27 | 28 | fn play_once(&mut self, ctx: &mut Context) -> GameResult { 29 | self.sound.set_repeat(false); 30 | self.sound.play(ctx) 31 | } 32 | 33 | fn play_repeating(&mut self, ctx: &mut Context) -> GameResult { 34 | self.sound.set_repeat(true); 35 | self.sound.play(ctx) 36 | } 37 | 38 | fn stop(&self, ctx: &mut Context) -> GameResult { 39 | self.sound.stop(ctx) 40 | } 41 | 42 | fn increase_volume(&mut self, ctx: &mut Context) -> GameResult { 43 | self.volume += 0.1; 44 | self.sound.set_volume(ctx, self.volume) 45 | } 46 | 47 | fn decrease_volume(&mut self, ctx: &mut Context) -> GameResult { 48 | self.volume -= 0.1; 49 | self.sound.set_volume(ctx, self.volume) 50 | } 51 | } 52 | 53 | impl event::EventHandler for MainState { 54 | fn update( 55 | &mut self, 56 | _ctx: &mut Context, 57 | _quad_ctx: &mut miniquad::GraphicsContext, 58 | ) -> GameResult { 59 | Ok(()) 60 | } 61 | 62 | fn draw(&mut self, ctx: &mut Context, quad_ctx: &mut miniquad::GraphicsContext) -> GameResult { 63 | graphics::clear(ctx, quad_ctx, [0.1, 0.2, 0.3, 1.0].into()); 64 | 65 | let text = graphics::Text::new("Press number key 1 to to play a sound,\n2 to play repeated,\n3 to stop,\nUp to increase volume,\nDown to decrease volume,\nor escape to quit."); 66 | graphics::draw( 67 | ctx, 68 | quad_ctx, 69 | &text, 70 | (glam::Vec2::new(100.0, 100.), graphics::Color::WHITE), 71 | )?; 72 | 73 | graphics::present(ctx, quad_ctx)?; 74 | Ok(()) 75 | } 76 | 77 | fn key_down_event( 78 | &mut self, 79 | ctx: &mut Context, 80 | _quad_ctx: &mut miniquad::GraphicsContext, 81 | keycode: event::KeyCode, 82 | _keymod: input::keyboard::KeyMods, 83 | _repeat: bool, 84 | ) { 85 | match keycode { 86 | event::KeyCode::Key1 => self.play_once(ctx).unwrap(), 87 | event::KeyCode::Key2 => self.play_repeating(ctx).unwrap(), 88 | event::KeyCode::Key3 => self.stop(ctx).unwrap(), 89 | event::KeyCode::Up => self.increase_volume(ctx).unwrap(), 90 | event::KeyCode::Down => self.decrease_volume(ctx).unwrap(), 91 | event::KeyCode::Escape => { 92 | quit(ctx); 93 | } 94 | _ => (), 95 | } 96 | } 97 | } 98 | 99 | pub fn main() -> GameResult { 100 | ggez::start( 101 | ggez::conf::Conf::default().cache(Some(include_bytes!("resources.tar"))), 102 | |ctx, _quad_ctx| Box::new(MainState::new(ctx).unwrap()), 103 | ) 104 | } 105 | -------------------------------------------------------------------------------- /examples/spritebatch.rs: -------------------------------------------------------------------------------- 1 | //! An example of how to use a `SpriteBatch`. 2 | //! 3 | 4 | extern crate cgmath; 5 | extern crate good_web_game as ggez; 6 | 7 | use ggez::event; 8 | use ggez::graphics; 9 | use ggez::miniquad; 10 | 11 | use ggez::timer; 12 | use ggez::{Context, GameResult}; 13 | 14 | use cgmath::{Point2, Vector2}; 15 | 16 | struct MainState { 17 | spritebatch: graphics::spritebatch::SpriteBatch, 18 | } 19 | 20 | impl MainState { 21 | fn new(ctx: &mut Context, quad_ctx: &mut miniquad::GraphicsContext) -> GameResult { 22 | let image = graphics::Image::new(ctx, quad_ctx, "tile.png").unwrap(); 23 | let batch = graphics::spritebatch::SpriteBatch::new(image); 24 | let s = MainState { spritebatch: batch }; 25 | Ok(s) 26 | } 27 | } 28 | 29 | impl event::EventHandler for MainState { 30 | fn update( 31 | &mut self, 32 | ctx: &mut Context, 33 | _quad_ctx: &mut miniquad::GraphicsContext, 34 | ) -> GameResult { 35 | if timer::ticks(ctx) % 100 == 0 { 36 | println!("Delta frame time: {:?} ", timer::delta(ctx)); 37 | println!("Average FPS: {}", timer::fps(ctx)); 38 | } 39 | Ok(()) 40 | } 41 | 42 | fn draw(&mut self, ctx: &mut Context, quad_ctx: &mut miniquad::GraphicsContext) -> GameResult { 43 | graphics::clear(ctx, quad_ctx, graphics::Color::BLACK); 44 | 45 | let time = (timer::duration_to_f64(timer::time_since_start(ctx)) * 1000.0) as u32; 46 | let cycle = 10_000; 47 | for x in 0..150 { 48 | for y in 0..150 { 49 | let x = x as f32; 50 | let y = y as f32; 51 | let p = graphics::DrawParam::new() 52 | .dest(Point2::new(x * 10.0, y * 10.0)) 53 | .scale(Vector2::new( 54 | ((time % cycle * 2) as f32 / cycle as f32 * 6.28) 55 | .cos() 56 | .abs() 57 | * 0.0625, 58 | ((time % cycle * 2) as f32 / cycle as f32 * 6.28) 59 | .cos() 60 | .abs() 61 | * 0.0625, 62 | )) 63 | .rotation(-2.0 * ((time % cycle) as f32 / cycle as f32 * 6.28)); 64 | self.spritebatch.add(p); 65 | } 66 | } 67 | 68 | let param = graphics::DrawParam::new() 69 | .dest(Point2::new( 70 | ((time % cycle) as f32 / cycle as f32 * 6.28).cos() * 50.0 + 150.0, 71 | ((time % cycle) as f32 / cycle as f32 * 6.28).sin() * 50.0 - 150.0, 72 | )) 73 | .scale(Vector2::new( 74 | ((time % cycle) as f32 / cycle as f32 * 6.28).sin().abs() * 2.0 + 1.0, 75 | ((time % cycle) as f32 / cycle as f32 * 6.28).sin().abs() * 2.0 + 1.0, 76 | )) 77 | .rotation((time % cycle) as f32 / cycle as f32 * 6.28) 78 | // applying a src parameter to a spritebatch globally has no effect 79 | //.src([0.25,0.25,0.5,0.5].into()) 80 | .offset(Point2::new(750.0, 750.0)); 81 | 82 | graphics::draw(ctx, quad_ctx, &self.spritebatch, param)?; 83 | self.spritebatch.clear(); 84 | 85 | graphics::present(ctx, quad_ctx)?; 86 | Ok(()) 87 | } 88 | } 89 | 90 | pub fn main() -> GameResult { 91 | ggez::start( 92 | ggez::conf::Conf::default().cache(Some(include_bytes!("resources.tar"))), 93 | |mut context, quad_ctx| Box::new(MainState::new(&mut context, quad_ctx).unwrap()), 94 | ) 95 | } 96 | -------------------------------------------------------------------------------- /examples/text.rs: -------------------------------------------------------------------------------- 1 | //! This example demonstrates how to use `Text` to draw TrueType font texts efficiently. 2 | 3 | extern crate good_web_game as ggez; 4 | 5 | use ggez::event; 6 | use ggez::graphics::{self, Align, Color, DrawParam, Font, PxScale, Text, TextFragment}; 7 | use ggez::miniquad; 8 | use ggez::timer; 9 | use ggez::{Context, GameResult}; 10 | use glam::Vec2; 11 | use std::collections::BTreeMap; 12 | use std::env; 13 | 14 | /// Creates a random RGB color. 15 | fn random_color(rng: &mut oorandom::Rand32) -> Color { 16 | Color::new(rng.rand_float(), rng.rand_float(), rng.rand_float(), 1.0) 17 | } 18 | 19 | struct App { 20 | // Doesn't have to be a `BTreeMap`; it's handy if you care about specific elements, 21 | // want to retrieve them by trivial handles, and have to preserve ordering. 22 | texts: BTreeMap<&'static str, Text>, 23 | rng: oorandom::Rand32, 24 | } 25 | 26 | impl App { 27 | #[allow(clippy::needless_update)] 28 | fn new(ctx: &mut Context) -> GameResult { 29 | let mut texts = BTreeMap::new(); 30 | 31 | // We just use a fixed RNG seed for simplicity. 32 | let mut rng = oorandom::Rand32::new(314159); 33 | 34 | // This is the simplest way to create a drawable text; 35 | // the color, font, and scale will be default: white, LiberationMono-Regular, 16px unform. 36 | // Note that you don't even have to load a font: LiberationMono-Regular is baked into `ggez` itself. 37 | let text = Text::new("Hello, World!"); 38 | // Store the text in `App`s map, for drawing in main loop. 39 | texts.insert("0_hello", text); 40 | 41 | // This is what actually happens in `Text::new()`: the `&str` gets 42 | // automatically converted into a `TextFragment`. 43 | let mut text = Text::new(TextFragment { 44 | // `TextFragment` stores a string, and optional parameters which will override those 45 | // of `Text` itself. This allows inlining differently formatted lines, words, 46 | // or even individual letters, into the same block of text. 47 | text: "Small red fragment".to_string(), 48 | color: Some(Color::new(1.0, 0.0, 0.0, 1.0)), 49 | // `Font` is a handle to a loaded TTF, stored inside the `Context`. 50 | // `Font::default()` always exists and maps to LiberationMono-Regular. 51 | font: Some(graphics::Font::default()), 52 | scale: Some(PxScale::from(10.0)), 53 | // This doesn't do anything at this point; can be used to omit fields in declarations. 54 | ..Default::default() 55 | }); 56 | 57 | // More fragments can be appended at any time. 58 | text.add(" default fragment, should be long enough to showcase everything") 59 | // `add()` can be chained, along with most `Text` methods. 60 | .add(TextFragment::new(" magenta fragment").color(Color::new(1.0, 0.0, 1.0, 1.0))) 61 | .add(" another default fragment, to really drive the point home"); 62 | 63 | // This loads a new TrueType font into the context and 64 | // returns a `Font` referring to it. 65 | let fancy_font = Font::new(ctx, "/Tangerine_Regular.ttf")?; 66 | 67 | // `Font` is really only an integer handle, and can be copied around. 68 | text.add( 69 | TextFragment::new(" fancy fragment") 70 | .font(fancy_font) 71 | .scale(PxScale::from(25.0)), 72 | ) 73 | .add(" and a default one, for symmetry"); 74 | // Store a copy of the built text, retain original for further modifications. 75 | texts.insert("1_demo_text_1", text.clone()); 76 | 77 | // Text can be wrapped by setting it's bounds, in screen coordinates; 78 | // vertical bound will cut off the extra off the bottom. 79 | // Alignment within the bounds can be set by `Align` enum. 80 | text.set_bounds(Vec2::new(400.0, f32::INFINITY), Align::Left); 81 | texts.insert("1_demo_text_2", text.clone()); 82 | 83 | text.set_bounds(Vec2::new(500.0, f32::INFINITY), Align::Right); 84 | texts.insert("1_demo_text_3", text.clone()); 85 | 86 | // This can be used to set the font and scale unformatted fragments will use. 87 | // Color is specified when drawing (or queueing), via `DrawParam`. 88 | // Side note: TrueType fonts aren't very consistent between themselves in terms 89 | // of apparent scale - this font with default scale will appear too small. 90 | text.set_font(fancy_font, PxScale::from(16.0)) 91 | .set_bounds(Vec2::new(300.0, f32::INFINITY), Align::Center); 92 | texts.insert("1_demo_text_4", text); 93 | 94 | // These methods can be combined to easily create a variety of simple effects. 95 | let chroma_string = "Not quite a rainbow."; 96 | // `default()` exists pretty much specifically for this usecase. 97 | let mut chroma_text = Text::default(); 98 | for ch in chroma_string.chars() { 99 | chroma_text.add(TextFragment::new(ch).color(random_color(&mut rng))); 100 | } 101 | texts.insert("2_rainbow", chroma_text); 102 | 103 | let wonky_string = "So, so wonky."; 104 | let mut wonky_text = Text::default(); 105 | for ch in wonky_string.chars() { 106 | wonky_text 107 | .add(TextFragment::new(ch).scale(PxScale::from(10.0 + 24.0 * rng.rand_float()))); 108 | } 109 | texts.insert("3_wonky", wonky_text); 110 | 111 | Ok(App { texts, rng }) 112 | } 113 | } 114 | 115 | impl event::EventHandler for App { 116 | fn update( 117 | &mut self, 118 | ctx: &mut Context, 119 | _quad_ctx: &mut miniquad::GraphicsContext, 120 | ) -> GameResult { 121 | const DESIRED_FPS: u32 = 60; 122 | while timer::check_update_time(ctx, DESIRED_FPS) {} 123 | Ok(()) 124 | } 125 | 126 | fn draw(&mut self, ctx: &mut Context, quad_ctx: &mut miniquad::GraphicsContext) -> GameResult { 127 | graphics::clear(ctx, quad_ctx, [0.1, 0.2, 0.3, 1.0].into()); 128 | 129 | // `Text` can be used in "immediate mode", but it's slightly less efficient 130 | // in most cases, and horrifically less efficient in a few select ones 131 | // (using `.width()` or `.height()`, for example). 132 | let fps = timer::fps(ctx); 133 | let fps_display = Text::new(format!("FPS: {}", fps)); 134 | // When drawing through these calls, `DrawParam` will work as they are documented. 135 | graphics::draw( 136 | ctx, 137 | quad_ctx, 138 | &fps_display, 139 | (Vec2::new(200.0, 0.0), Color::WHITE), 140 | )?; 141 | 142 | let mut height = 0.0; 143 | for text in self.texts.values() { 144 | // Calling `.queue()` for all bits of text that can share a `DrawParam`, 145 | // followed with `::draw_queued()` with said params, is the intended way. 146 | graphics::queue_text(ctx, text, Vec2::new(20.0, 20.0 + height), None); 147 | //height += 20.0 + text.height(ctx) as f32; 148 | height += 20.0 + text.height(ctx) as f32; 149 | } 150 | // When drawing via `draw_queued()`, `.offset` in `DrawParam` will be 151 | // in screen coordinates, and `.color` will be ignored. 152 | graphics::draw_queued_text( 153 | ctx, 154 | quad_ctx, 155 | DrawParam::default(), 156 | None, 157 | graphics::FilterMode::Linear, 158 | )?; 159 | 160 | // Individual fragments within the `Text` can be replaced; 161 | // this can be used for inlining animated sentences, words, etc. 162 | if let Some(text) = self.texts.get_mut("1_demo_text_3") { 163 | // `.fragments_mut()` returns a mutable slice of contained fragments. 164 | // Fragments are indexed in order of their addition, starting at 0 (of course). 165 | text.fragments_mut()[3].color = Some(random_color(&mut self.rng)); 166 | } 167 | 168 | // Another animation example. Note, this is very inefficient as-is. 169 | let wobble_string = "WOBBLE"; 170 | let mut wobble = Text::default(); 171 | for ch in wobble_string.chars() { 172 | wobble.add( 173 | TextFragment::new(ch).scale(PxScale::from(10.0 + 6.0 * self.rng.rand_float())), 174 | ); 175 | } 176 | let wobble_width = wobble.width(ctx); 177 | let wobble_height = wobble.height(ctx); 178 | graphics::queue_text( 179 | ctx, 180 | &wobble, 181 | Vec2::new(0.0, 0.0), 182 | Some(Color::new(0.0, 1.0, 1.0, 1.0)), 183 | ); 184 | let t = Text::new(format!( 185 | "width: {}\nheight: {}", 186 | wobble_width, wobble_height 187 | )); 188 | graphics::queue_text(ctx, &t, Vec2::new(0.0, 20.0), None); 189 | graphics::draw_queued_text( 190 | ctx, 191 | quad_ctx, 192 | DrawParam::new() 193 | .dest(Vec2::new(500.0, 300.0)) 194 | .rotation(-0.5), 195 | None, 196 | graphics::FilterMode::Linear, 197 | )?; 198 | 199 | graphics::present(ctx, quad_ctx)?; 200 | //timer::yield_now(); 201 | Ok(()) 202 | } 203 | 204 | fn resize_event( 205 | &mut self, 206 | ctx: &mut Context, 207 | _quad_ctx: &mut miniquad::GraphicsContext, 208 | width: f32, 209 | height: f32, 210 | ) { 211 | graphics::set_screen_coordinates(ctx, graphics::Rect::new(0.0, 0.0, width, height)) 212 | .unwrap(); 213 | } 214 | } 215 | 216 | pub fn main() -> GameResult { 217 | if cfg!(debug_assertions) && env::var("yes_i_really_want_debug_mode").is_err() { 218 | eprintln!( 219 | "Note: Release mode will improve performance greatly.\n \ 220 | e.g. use `cargo run --example text --release`" 221 | ); 222 | } 223 | 224 | ggez::start( 225 | ggez::conf::Conf::default().cache(Some(include_bytes!("resources.tar"))), 226 | |context, _quad_ctx| Box::new(App::new(context).unwrap()), 227 | ) 228 | } 229 | -------------------------------------------------------------------------------- /examples/transforms.rs: -------------------------------------------------------------------------------- 1 | //! Demonstrates various projection and matrix fiddling/testing. 2 | 3 | extern crate good_web_game as ggez; 4 | 5 | use ggez::event::{self, KeyCode, KeyMods}; 6 | use ggez::graphics::{self, Color, DrawMode}; 7 | use ggez::miniquad; 8 | use ggez::{Context, GameResult}; 9 | use glam::*; 10 | use std::env; 11 | use std::path; 12 | 13 | struct MainState { 14 | pos_x: f32, 15 | gridmesh: graphics::Mesh, 16 | angle: graphics::Image, 17 | screen_bounds: Vec, 18 | screen_bounds_idx: usize, 19 | } 20 | 21 | impl MainState { 22 | const GRID_INTERVAL: f32 = 100.0; 23 | const GRID_SIZE: usize = 10; 24 | const GRID_POINT_RADIUS: f32 = 5.0; 25 | 26 | fn new(ctx: &mut Context, quad_ctx: &mut miniquad::GraphicsContext) -> GameResult { 27 | let angle = graphics::Image::new(ctx, quad_ctx, "/angle.png")?; 28 | let gridmesh_builder = &mut graphics::MeshBuilder::new(); 29 | for x in 0..Self::GRID_SIZE { 30 | for y in 0..Self::GRID_SIZE { 31 | let fx = x as f32; 32 | let fy = y as f32; 33 | let fsize = Self::GRID_SIZE as f32; 34 | let point = Vec2::new(fx * Self::GRID_INTERVAL, fy * Self::GRID_INTERVAL); 35 | let color = graphics::Color::new(fx / fsize, 0.0, fy / fsize, 1.0); 36 | gridmesh_builder.circle( 37 | DrawMode::fill(), 38 | point, 39 | Self::GRID_POINT_RADIUS, 40 | 2.0, 41 | color, 42 | )?; 43 | } 44 | } 45 | let gridmesh = gridmesh_builder.build(ctx, quad_ctx)?; 46 | // An array of rects to cycle the screen coordinates through. 47 | let screen_bounds = vec![ 48 | graphics::Rect::new(0.0, 0.0, 800.0, 600.0), 49 | graphics::Rect::new(0.0, 600.0, 800.0, -600.0), 50 | ]; 51 | let screen_bounds_idx = 0; 52 | let s = MainState { 53 | pos_x: 0.0, 54 | gridmesh, 55 | angle, 56 | screen_bounds, 57 | screen_bounds_idx, 58 | }; 59 | Ok(s) 60 | } 61 | 62 | fn draw_coord_labels( 63 | &self, 64 | ctx: &mut Context, 65 | quad_ctx: &mut miniquad::GraphicsContext, 66 | ) -> GameResult { 67 | for x in 0..Self::GRID_SIZE { 68 | for y in 0..Self::GRID_SIZE { 69 | let point = Vec2::new( 70 | x as f32 * Self::GRID_INTERVAL, 71 | y as f32 * Self::GRID_INTERVAL, 72 | ); 73 | let s = format!("({}, {})", point.x, point.y); 74 | let t = graphics::Text::new(s); 75 | graphics::draw(ctx, quad_ctx, &t, (point,))? 76 | } 77 | } 78 | Ok(()) 79 | } 80 | } 81 | 82 | impl event::EventHandler for MainState { 83 | fn update( 84 | &mut self, 85 | _ctx: &mut Context, 86 | _quad_ctx: &mut miniquad::GraphicsContext, 87 | ) -> GameResult { 88 | self.pos_x = self.pos_x % 800.0 + 1.0; 89 | Ok(()) 90 | } 91 | 92 | fn draw(&mut self, ctx: &mut Context, quad_ctx: &mut miniquad::GraphicsContext) -> GameResult { 93 | graphics::clear(ctx, quad_ctx, [0.1, 0.2, 0.3, 1.0].into()); 94 | 95 | let origin = Vec2::ZERO; 96 | graphics::draw(ctx, quad_ctx, &self.gridmesh, (origin, Color::WHITE))?; 97 | 98 | let param = graphics::DrawParam::new() 99 | .dest(Vec2::new(400.0, 400.0)) 100 | .rotation(self.pos_x / 100.0) 101 | .offset(Vec2::new(0.5, 0.5)) 102 | .scale(Vec2::new(1.0, 1.0)); 103 | 104 | self.draw_coord_labels(ctx, quad_ctx)?; 105 | 106 | graphics::draw(ctx, quad_ctx, &self.angle, param)?; 107 | 108 | graphics::present(ctx, quad_ctx)?; 109 | Ok(()) 110 | } 111 | 112 | fn key_down_event( 113 | &mut self, 114 | ctx: &mut Context, 115 | _quad_ctx: &mut miniquad::GraphicsContext, 116 | keycode: KeyCode, 117 | _keymod: KeyMods, 118 | _repeat: bool, 119 | ) { 120 | if let event::KeyCode::Space = keycode { 121 | self.screen_bounds_idx = (self.screen_bounds_idx + 1) % self.screen_bounds.len(); 122 | graphics::set_screen_coordinates(ctx, self.screen_bounds[self.screen_bounds_idx]) 123 | .unwrap(); 124 | } 125 | } 126 | } 127 | 128 | pub fn main() -> GameResult { 129 | let resource_dir = if let Ok(manifest_dir) = env::var("CARGO_MANIFEST_DIR") { 130 | let mut path = path::PathBuf::from(manifest_dir); 131 | path.push("resources"); 132 | path 133 | } else { 134 | path::PathBuf::from("./resources") 135 | }; 136 | 137 | ggez::start( 138 | ggez::conf::Conf::default() 139 | .cache(Some(include_bytes!("resources.tar"))) 140 | .physical_root_dir(Some(resource_dir)), 141 | |context, quad_ctx| Box::new(MainState::new(context, quad_ctx).unwrap()), 142 | ) 143 | } 144 | -------------------------------------------------------------------------------- /resources/LiberationMono-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ggez/good-web-game/11ce17b8076a4c3eb7b8d809ce4066342b8e412c/resources/LiberationMono-Regular.ttf -------------------------------------------------------------------------------- /resources/SIL Open Font License.txt: -------------------------------------------------------------------------------- 1 | This Font Software is licensed under the SIL Open Font License, Version 1.1. 2 | This license is copied below, and is also available with a FAQ at: http://scripts.sil.org/OFL 3 | 4 | ----------------------------------------------------------- 5 | SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 6 | ----------------------------------------------------------- 7 | 8 | PREAMBLE 9 | The goals of the Open Font License (OFL) are to stimulate worldwide development of collaborative font projects, to support the font creation efforts of academic and linguistic communities, and to provide a free and open framework in which fonts may be shared and improved in partnership with others. 10 | 11 | The OFL allows the licensed fonts to be used, studied, modified and redistributed freely as long as they are not sold by themselves. The fonts, including any derivative works, can be bundled, embedded, redistributed and/or sold with any software provided that any reserved names are not used by derivative works. The fonts and derivatives, however, cannot be released under any other type of license. The requirement for fonts to remain under this license does not apply to any document created using the fonts or their derivatives. 12 | 13 | DEFINITIONS 14 | "Font Software" refers to the set of files released by the Copyright Holder(s) under this license and clearly marked as such. This may include source files, build scripts and documentation. 15 | 16 | "Reserved Font Name" refers to any names specified as such after the copyright statement(s). 17 | 18 | "Original Version" refers to the collection of Font Software components as distributed by the Copyright Holder(s). 19 | 20 | "Modified Version" refers to any derivative made by adding to, deleting, or substituting -- in part or in whole -- any of the components of the Original Version, by changing formats or by porting the Font Software to a new environment. 21 | 22 | "Author" refers to any designer, engineer, programmer, technical writer or other person who contributed to the Font Software. 23 | 24 | PERMISSION & CONDITIONS 25 | Permission is hereby granted, free of charge, to any person obtaining a copy of the Font Software, to use, study, copy, merge, embed, modify, redistribute, and sell modified and unmodified copies of the Font Software, subject to the following conditions: 26 | 27 | 1) Neither the Font Software nor any of its individual components, in Original or Modified Versions, may be sold by itself. 28 | 29 | 2) Original or Modified Versions of the Font Software may be bundled, redistributed and/or sold with any software, provided that each copy contains the above copyright notice and this license. These can be included either as stand-alone text files, human-readable headers or in the appropriate machine-readable metadata fields within text or binary files as long as those fields can be easily viewed by the user. 30 | 31 | 3) No Modified Version of the Font Software may use the Reserved Font Name(s) unless explicit written permission is granted by the corresponding Copyright Holder. This restriction only applies to the primary font name as presented to the users. 32 | 33 | 4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font Software shall not be used to promote, endorse or advertise any Modified Version, except to acknowledge the contribution(s) of the Copyright Holder(s) and the Author(s) or with their explicit written permission. 34 | 35 | 5) The Font Software, modified or unmodified, in part or in whole, must be distributed entirely under this license, and must not be distributed under any other license. The requirement for fonts to remain under this license does not apply to any document created using the Font Software. 36 | 37 | TERMINATION 38 | This license becomes null and void if any of the above conditions are not met. 39 | 40 | DISCLAIMER 41 | THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM OTHER DEALINGS IN THE FONT SOFTWARE. -------------------------------------------------------------------------------- /resources/Tangerine_Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ggez/good-web-game/11ce17b8076a4c3eb7b8d809ce4066342b8e412c/resources/Tangerine_Regular.ttf -------------------------------------------------------------------------------- /resources/angle.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ggez/good-web-game/11ce17b8076a4c3eb7b8d809ce4066342b8e412c/resources/angle.png -------------------------------------------------------------------------------- /resources/background.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ggez/good-web-game/11ce17b8076a4c3eb7b8d809ce4066342b8e412c/resources/background.png -------------------------------------------------------------------------------- /resources/basic_150.glslv: -------------------------------------------------------------------------------- 1 | #version 100 2 | 3 | attribute vec2 position; 4 | attribute vec2 texcoord; 5 | attribute vec4 color0; 6 | 7 | attribute vec4 Source; 8 | attribute vec4 Color; 9 | attribute mat4 Model; 10 | 11 | varying lowp vec4 color; 12 | varying lowp vec2 uv; 13 | 14 | uniform mat4 Projection; 15 | 16 | uniform float depth; 17 | 18 | void main() { 19 | gl_Position = Projection * Model * vec4(position, 0, 1); 20 | gl_Position.z = depth; 21 | color = Color * color0; 22 | uv = texcoord * Source.zw + Source.xy; 23 | } -------------------------------------------------------------------------------- /resources/bg_top.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ggez/good-web-game/11ce17b8076a4c3eb7b8d809ce4066342b8e412c/resources/bg_top.png -------------------------------------------------------------------------------- /resources/boom.ogg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ggez/good-web-game/11ce17b8076a4c3eb7b8d809ce4066342b8e412c/resources/boom.ogg -------------------------------------------------------------------------------- /resources/dimmer_150.glslf: -------------------------------------------------------------------------------- 1 | #version 100 2 | 3 | varying lowp vec4 color; 4 | varying lowp vec2 uv; 5 | 6 | uniform sampler2D Texture; 7 | uniform lowp float u_Rate; 8 | 9 | void main() { 10 | gl_FragColor = texture2D(Texture, uv) * color * u_Rate; 11 | } -------------------------------------------------------------------------------- /resources/dragon1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ggez/good-web-game/11ce17b8076a4c3eb7b8d809ce4066342b8e412c/resources/dragon1.png -------------------------------------------------------------------------------- /resources/pew.flac: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ggez/good-web-game/11ce17b8076a4c3eb7b8d809ce4066342b8e412c/resources/pew.flac -------------------------------------------------------------------------------- /resources/pew.ogg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ggez/good-web-game/11ce17b8076a4c3eb7b8d809ce4066342b8e412c/resources/pew.ogg -------------------------------------------------------------------------------- /resources/pew.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ggez/good-web-game/11ce17b8076a4c3eb7b8d809ce4066342b8e412c/resources/pew.wav -------------------------------------------------------------------------------- /resources/player.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ggez/good-web-game/11ce17b8076a4c3eb7b8d809ce4066342b8e412c/resources/player.png -------------------------------------------------------------------------------- /resources/player_sheet.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ggez/good-web-game/11ce17b8076a4c3eb7b8d809ce4066342b8e412c/resources/player_sheet.png -------------------------------------------------------------------------------- /resources/rock.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ggez/good-web-game/11ce17b8076a4c3eb7b8d809ce4066342b8e412c/resources/rock.png -------------------------------------------------------------------------------- /resources/shot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ggez/good-web-game/11ce17b8076a4c3eb7b8d809ce4066342b8e412c/resources/shot.png -------------------------------------------------------------------------------- /resources/sound.ogg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ggez/good-web-game/11ce17b8076a4c3eb7b8d809ce4066342b8e412c/resources/sound.ogg -------------------------------------------------------------------------------- /resources/tile.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ggez/good-web-game/11ce17b8076a4c3eb7b8d809ce4066342b8e412c/resources/tile.png -------------------------------------------------------------------------------- /resources/wabbit_alpha.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ggez/good-web-game/11ce17b8076a4c3eb7b8d809ce4066342b8e412c/resources/wabbit_alpha.png -------------------------------------------------------------------------------- /src/audio.rs: -------------------------------------------------------------------------------- 1 | //! Loading and playing sounds. 2 | //! 3 | //! Note that audio functionality in good-web-game is very different from ggez, as it uses quad-snd 4 | //! instead of rodio, for maximum portability. 5 | 6 | use crate::{filesystem, Context, GameResult}; 7 | use std::collections::HashMap; 8 | 9 | #[cfg(all(feature = "audio", not(target_os = "ios")))] 10 | use quad_snd::{AudioContext as QuadSndContext, Sound as QuadSndSound}; 11 | 12 | #[cfg(all(feature = "audio", not(target_os = "ios")))] 13 | pub use quad_snd::PlaySoundParams; 14 | 15 | #[cfg(any(not(feature = "audio"), target_os = "ios"))] 16 | mod dummy_audio { 17 | use crate::audio::PlaySoundParams; 18 | 19 | pub struct AudioContext {} 20 | 21 | impl AudioContext { 22 | pub fn new() -> AudioContext { 23 | AudioContext {} 24 | } 25 | 26 | pub fn pause(&mut self) {} 27 | 28 | pub fn resume(&mut self) {} 29 | } 30 | 31 | pub struct Sound {} 32 | 33 | impl Sound { 34 | pub fn load(_ctx: &mut AudioContext, _data: &[u8]) -> Sound { 35 | Sound {} 36 | } 37 | 38 | pub fn is_loaded(&self) -> bool { 39 | true 40 | } 41 | 42 | pub fn play(&mut self, _ctx: &mut AudioContext, _params: PlaySoundParams) {} 43 | 44 | pub fn stop(&mut self, _ctx: &mut AudioContext) {} 45 | 46 | pub fn set_volume(&mut self, _ctx: &mut AudioContext, _volume: f32) {} 47 | } 48 | } 49 | 50 | #[cfg(any(not(feature = "audio"), target_os = "ios"))] 51 | use dummy_audio::{AudioContext as QuadSndContext, Sound as QuadSndSound}; 52 | 53 | #[cfg(any(not(feature = "audio"), target_os = "ios"))] 54 | pub struct PlaySoundParams { 55 | pub looped: bool, 56 | pub volume: f32, 57 | } 58 | 59 | pub struct AudioContext { 60 | native_ctx: QuadSndContext, 61 | sounds: HashMap, 62 | id: usize, 63 | } 64 | 65 | impl AudioContext { 66 | pub fn new() -> AudioContext { 67 | AudioContext { 68 | native_ctx: QuadSndContext::new(), 69 | sounds: HashMap::new(), 70 | id: 0, 71 | } 72 | } 73 | 74 | #[cfg(target_os = "android")] 75 | pub fn pause(&mut self) { 76 | self.native_ctx.pause() 77 | } 78 | 79 | #[cfg(target_os = "android")] 80 | pub fn resume(&mut self) { 81 | self.native_ctx.resume() 82 | } 83 | } 84 | 85 | impl Default for AudioContext { 86 | fn default() -> Self { 87 | AudioContext::new() 88 | } 89 | } 90 | 91 | #[derive(Debug, Clone, Copy, PartialEq, Eq)] 92 | pub struct Sound(usize); 93 | 94 | pub struct Source { 95 | sound: Sound, 96 | params: PlaySoundParams, 97 | } 98 | 99 | impl Source { 100 | /// Load audio file. 101 | /// 102 | /// Attempts to automatically detect the format of the source of data. 103 | pub fn new(ctx: &mut Context, path: &str) -> GameResult { 104 | use std::io::Read; 105 | 106 | let mut file = filesystem::open(ctx, path)?; 107 | 108 | let mut bytes = vec![]; 109 | file.bytes.read_to_end(&mut bytes)?; 110 | 111 | Self::from_bytes(ctx, bytes.as_slice()) 112 | } 113 | 114 | /// Load audio from file. 115 | /// 116 | /// Attempts to automatically detect the format of the source of data. 117 | pub fn from_bytes(ctx: &mut Context, bytes: &[u8]) -> GameResult { 118 | let sound = QuadSndSound::load(&mut ctx.audio_context.native_ctx, bytes); 119 | 120 | // only on wasm the sound is not ready right away 121 | #[cfg(target_arch = "wasm32")] 122 | while sound.is_loaded() { 123 | std::thread::yield_now(); 124 | } 125 | 126 | let id = ctx.audio_context.id; 127 | ctx.audio_context.sounds.insert(id, sound); 128 | ctx.audio_context.id += 1; 129 | Ok(Source { 130 | sound: Sound(id), 131 | params: PlaySoundParams::default(), 132 | }) 133 | } 134 | 135 | pub fn play(&self, ctx: &mut Context) -> GameResult<()> { 136 | let ctx = &mut ctx.audio_context; 137 | let sound = &mut ctx.sounds.get_mut(&self.sound.0).unwrap(); 138 | 139 | let params = PlaySoundParams { 140 | looped: self.params.looped, 141 | volume: self.params.volume, 142 | }; 143 | sound.play(&mut ctx.native_ctx, params); 144 | Ok(()) 145 | } 146 | 147 | pub fn stop(&self, ctx: &mut Context) -> GameResult { 148 | let ctx = &mut ctx.audio_context; 149 | let sound = &mut ctx.sounds.get_mut(&self.sound.0).unwrap(); 150 | 151 | sound.stop(&mut ctx.native_ctx); 152 | Ok(()) 153 | } 154 | 155 | pub fn set_volume(&mut self, ctx: &mut Context, volume: f32) -> GameResult<()> { 156 | let ctx = &mut ctx.audio_context; 157 | self.params.volume = volume; 158 | let sound = &mut ctx.sounds.get_mut(&self.sound.0).unwrap(); 159 | 160 | sound.set_volume(&mut ctx.native_ctx, volume); 161 | Ok(()) 162 | } 163 | 164 | pub fn volume(&self) -> f32 { 165 | self.params.volume 166 | } 167 | 168 | pub fn set_repeat(&mut self, repeat: bool) { 169 | self.params.looped = repeat; 170 | } 171 | 172 | pub fn repeat(&self) -> bool { 173 | self.params.looped 174 | } 175 | } 176 | -------------------------------------------------------------------------------- /src/conf.rs: -------------------------------------------------------------------------------- 1 | use miniquad::conf::{LinuxBackend, LinuxX11Gl, Platform}; 2 | use std::path::PathBuf; 3 | 4 | /// Holds configuration values setting different options when starting the game, some of which 5 | /// can't be changed later. 6 | #[derive(Debug)] 7 | pub struct Conf { 8 | /// `Filesystem::open` will try to read from this dir if there's no such file in the cache. 9 | /// 10 | /// Note that this won't work on platforms where `std::fs` is unavailable, like WASM. 11 | pub physical_root_dir: Option, 12 | pub(crate) cache: Option<&'static [u8]>, 13 | pub(crate) quad_conf: miniquad::conf::Conf, 14 | } 15 | 16 | impl Default for Conf { 17 | fn default() -> Conf { 18 | Conf { 19 | physical_root_dir: None, 20 | cache: None, 21 | quad_conf: miniquad::conf::Conf { 22 | window_title: "An easy, good game".to_string(), 23 | window_width: 800, 24 | window_height: 600, 25 | high_dpi: false, 26 | fullscreen: false, 27 | sample_count: 1, 28 | window_resizable: false, 29 | icon: None, 30 | platform: Platform { 31 | linux_x11_gl: LinuxX11Gl::GLXWithEGLFallback, 32 | linux_backend: LinuxBackend::X11WithWaylandFallback, 33 | swap_interval: None, 34 | framebuffer_alpha: false, 35 | }, 36 | }, 37 | } 38 | } 39 | } 40 | 41 | impl Conf { 42 | /// Set the root of your filesystem. 43 | /// 44 | /// Default: `None` 45 | pub fn physical_root_dir(mut self, val: Option) -> Self { 46 | self.physical_root_dir = val; 47 | self 48 | } 49 | /// Set the cache, holding embedded files for later use. 50 | /// 51 | /// Default: `miniquad::conf::Cache::No` 52 | pub fn cache(mut self, val: Option<&'static [u8]>) -> Self { 53 | self.cache = val; 54 | self 55 | } 56 | /// Set the window title 57 | /// 58 | /// Default: "An easy, good game" 59 | pub fn window_title(mut self, val: String) -> Self { 60 | self.quad_conf.window_title = val; 61 | self 62 | } 63 | /// Set the window width in logical pixels. 64 | /// 65 | /// Note: See [`high_dpi`](#method.high_dpi) for physical width. 66 | /// 67 | /// Default: `800` 68 | pub fn window_width(mut self, val: i32) -> Self { 69 | self.quad_conf.window_width = val; 70 | self 71 | } 72 | /// Set the window height in logical pixels. 73 | /// 74 | /// Note: See [`high_dpi`](#method.high_dpi) for physical height. 75 | /// 76 | /// Default: `600` 77 | pub fn window_height(mut self, val: i32) -> Self { 78 | self.quad_conf.window_height = val; 79 | self 80 | } 81 | /// Sets whether the rendering canvas is full-resolution on HighDPI displays. 82 | /// * If set to `false` the rendering canvas will be created with the logical window size and 83 | /// then scaled when rendering, to account for the difference between logical and physical size. 84 | /// * If set to `true` the rendering canvas will be created with the physical window size, which 85 | /// can differ from the logical window size due to high-dpi scaling, leading to your drawable space 86 | /// possibly having a different size than specified. 87 | /// 88 | /// Default: `false` 89 | pub fn high_dpi(mut self, val: bool) -> Self { 90 | self.quad_conf.high_dpi = val; 91 | self 92 | } 93 | /// Set whether to run in fullscreen mode. 94 | /// 95 | /// Default: `false` 96 | pub fn fullscreen(mut self, val: bool) -> Self { 97 | self.quad_conf.fullscreen = val; 98 | self 99 | } 100 | /// Set how many samples should be used in MSAA. 101 | /// 102 | /// Default: `1` 103 | pub fn sample_count(mut self, val: i32) -> Self { 104 | self.quad_conf.sample_count = val; 105 | self 106 | } 107 | /// Set whether the window should be resizable by the user. 108 | /// 109 | /// Default: `false` 110 | pub fn window_resizable(mut self, val: bool) -> Self { 111 | self.quad_conf.window_resizable = val; 112 | self 113 | } 114 | } 115 | 116 | impl From for miniquad::conf::Conf { 117 | fn from(conf: Conf) -> Self { 118 | conf.quad_conf 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /src/context.rs: -------------------------------------------------------------------------------- 1 | #[cfg(not(any(target_arch = "wasm32", target_os = "ios", target_os = "android",)))] 2 | use crate::input::gamepad; 3 | use crate::{ 4 | audio, 5 | filesystem::Filesystem, 6 | graphics, 7 | input::{input_handler::InputHandler, KeyboardContext, MouseContext}, 8 | timer::TimeContext, 9 | }; 10 | 11 | /// A `Context` is an object that holds on to global resources. 12 | /// It basically tracks hardware state such as the screen, audio 13 | /// system, timers, and so on. Generally this type can **not** 14 | /// be shared/sent between threads and only one `Context` can exist at a time. Trying 15 | /// to create a second one will fail. 16 | /// 17 | /// Most functions that interact with the hardware, for instance 18 | /// drawing things, playing sounds, or loading resources (which then 19 | /// need to be transformed into a format the hardware likes) will need 20 | /// to access the `Context`. It is an error to create some type that 21 | /// relies upon a `Context`, such as `Image`, and then drop the `Context` 22 | /// and try to draw the old `Image` with the new `Context`. Most types 23 | /// include checks to make this panic in debug mode, but it's not perfect. 24 | /// 25 | /// All fields in this struct are basically undocumented features, 26 | /// only here to make it easier to debug, or to let advanced users 27 | /// hook into the guts of ggez and make it do things it normally 28 | /// can't. Most users shouldn't need to touch these things directly, 29 | /// since implementation details may change without warning. The 30 | /// public and stable API is `ggez`'s module-level functions and 31 | /// types. 32 | pub struct Context { 33 | /// Filesystem state 34 | pub filesystem: Filesystem, 35 | /// Audio context 36 | pub audio_context: audio::AudioContext, 37 | /// Graphics state 38 | pub gfx_context: graphics::GraphicsContext, 39 | /// Mouse context 40 | pub mouse_context: MouseContext, 41 | /// Keyboard context 42 | pub keyboard_context: KeyboardContext, 43 | #[cfg(not(any(target_arch = "wasm32", target_os = "ios", target_os = "android",)))] 44 | /// Gamepad context 45 | pub gamepad_context: Box, 46 | /// Timer state 47 | pub timer_context: TimeContext, 48 | /// Controls whether or not the event loop should be running. 49 | /// Set this with `ggez::event::quit()`. 50 | pub continuing: bool, 51 | } 52 | 53 | impl Context { 54 | pub(crate) fn new(quad_ctx: &mut miniquad::Context, filesystem: Filesystem) -> Context { 55 | let input_handler = InputHandler::new(); 56 | #[cfg(not(any(target_arch = "wasm32", target_os = "ios", target_os = "android",)))] 57 | { 58 | let gamepad_context: Box = 59 | if let Ok(g_context) = gamepad::GilrsGamepadContext::new() { 60 | Box::new(g_context) 61 | } else { 62 | Box::new(gamepad::NullGamepadContext::default()) 63 | }; 64 | 65 | Context { 66 | filesystem, 67 | gfx_context: graphics::GraphicsContext::new(quad_ctx), 68 | audio_context: audio::AudioContext::new(), 69 | mouse_context: MouseContext::new(input_handler), 70 | keyboard_context: KeyboardContext::new(), 71 | gamepad_context, 72 | timer_context: TimeContext::new(), 73 | continuing: true, 74 | } 75 | } 76 | #[cfg(any(target_arch = "wasm32", target_os = "ios", target_os = "android",))] 77 | { 78 | Context { 79 | filesystem, 80 | gfx_context: graphics::GraphicsContext::new(quad_ctx), 81 | audio_context: audio::AudioContext::new(), 82 | mouse_context: MouseContext::new(input_handler), 83 | keyboard_context: KeyboardContext::new(), 84 | timer_context: TimeContext::new(), 85 | continuing: true, 86 | } 87 | } 88 | } 89 | 90 | pub(crate) fn framebuffer(&mut self) -> Option { 91 | self.gfx_context 92 | .canvas 93 | .as_ref() 94 | .map(|canvas| canvas.offscreen_pass) 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /src/error.rs: -------------------------------------------------------------------------------- 1 | //! Error types and conversion functions. 2 | 3 | use std::error::Error; 4 | use std::fmt; 5 | 6 | /// An enum containing all kinds of game framework errors. 7 | #[derive(Debug)] 8 | pub enum GameError { 9 | /// Something went wrong trying to read from a file 10 | IOError(std::io::Error), 11 | /// Something went wrong compiling shaders 12 | ShaderProgramError(String), 13 | /// Something went wrong with the `gilrs` gamepad-input library. 14 | GamepadError(String), 15 | /// Something went wrong with the `lyon` shape-tesselation library 16 | LyonError(String), 17 | /// SoundMixer in the context should be created explicitly from some of the interaction callbacks 18 | /// Thats the only way to get audio to works on web :( 19 | MixerNotCreated, 20 | SoundError, 21 | UnknownError(String), 22 | /// Unable to find a resource; the `Vec` is the paths it searched for and associated errors 23 | ResourceNotFound(String, Vec<(std::path::PathBuf, GameError)>), 24 | /// An error in the filesystem layout 25 | FilesystemError(String), 26 | /// An error trying to load a resource, such as getting an invalid image file. 27 | ResourceLoadError(String), 28 | /// Something went wrong in the renderer 29 | RenderError(String), 30 | /// A custom error type for use by users of ggez. 31 | /// This lets you handle custom errors that may happen during your game (such as, trying to load a malformed file for a level) 32 | /// using the same mechanism you handle ggez's other errors. 33 | /// 34 | /// Please include an informative message with the error. 35 | CustomError(String), 36 | } 37 | 38 | impl fmt::Display for GameError { 39 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 40 | write!(f, "GameError {:?}", self) 41 | } 42 | } 43 | 44 | impl Error for GameError { 45 | fn cause(&self) -> Option<&dyn Error> { 46 | match *self { 47 | GameError::IOError(ref e) => Some(e), 48 | _ => None, 49 | } 50 | } 51 | } 52 | 53 | /// A convenient result type consisting of a return type and a `GameError` 54 | pub type GameResult = Result; 55 | 56 | impl From for GameError { 57 | fn from(e: std::io::Error) -> GameError { 58 | GameError::IOError(e) 59 | } 60 | } 61 | /* 62 | impl From for GameError { 63 | fn from(e: miniquad_text_rusttype::Error) -> GameError { 64 | GameError::TTFError(e) 65 | } 66 | } 67 | */ 68 | #[cfg(feature = "mesh")] 69 | impl From for GameError { 70 | fn from(s: lyon::lyon_tessellation::TessellationError) -> GameError { 71 | let errstr = format!( 72 | "Error while tesselating shape (did you give it an infinity or NaN?): {:?}", 73 | s 74 | ); 75 | GameError::LyonError(errstr) 76 | } 77 | } 78 | 79 | #[cfg(feature = "mesh")] 80 | impl From for GameError { 81 | fn from(s: lyon::lyon_tessellation::geometry_builder::GeometryBuilderError) -> GameError { 82 | let errstr = format!( 83 | "Error while building geometry (did you give it too many vertices?): {:?}", 84 | s 85 | ); 86 | GameError::LyonError(errstr) 87 | } 88 | } 89 | 90 | impl From for GameError { 91 | fn from(e: zip::result::ZipError) -> GameError { 92 | let errstr = format!("Zip error: {}", e); 93 | GameError::ResourceLoadError(errstr) 94 | } 95 | } 96 | 97 | #[cfg(not(any(target_arch = "wasm32", target_os = "ios", target_os = "android",)))] 98 | impl From for GameError { 99 | fn from(s: gilrs::Error) -> GameError { 100 | let errstr = format!("Gamepad error: {}", s); 101 | GameError::GamepadError(errstr) 102 | } 103 | } 104 | 105 | impl From for GameError { 106 | fn from(e: miniquad::ShaderError) -> GameError { 107 | let errstr = format!("Shader creation error: {}", e); 108 | GameError::ShaderProgramError(errstr) 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /src/event.rs: -------------------------------------------------------------------------------- 1 | use crate::context::Context; 2 | 3 | #[cfg(not(any(target_arch = "wasm32", target_os = "ios", target_os = "android",)))] 4 | use crate::input::gamepad::GamepadId; 5 | pub use crate::input::keyboard::KeyMods; 6 | pub use crate::input::MouseButton; 7 | use crate::GameError; 8 | #[cfg(not(any(target_arch = "wasm32", target_os = "ios", target_os = "android",)))] 9 | use gilrs::{Axis, Button}; 10 | pub use miniquad::{graphics::GraphicsContext, KeyCode, TouchPhase}; 11 | 12 | /// Used in [`EventHandler`](trait.EventHandler.html) 13 | /// to specify where an error originated 14 | #[derive(Debug, Copy, Clone, Eq, PartialEq)] 15 | pub enum ErrorOrigin { 16 | /// error originated in `update()` 17 | Update, 18 | /// error originated in `draw()` 19 | Draw, 20 | } 21 | 22 | /// A trait defining event callbacks. This is your primary interface with 23 | /// `ggez`'s event loop. Implement this trait for a type and 24 | /// override at least the [`update()`](#tymethod.update) and 25 | /// [`draw()`](#tymethod.draw) methods, then pass it to 26 | /// [`event::run()`](fn.run.html) to run the game's mainloop. 27 | /// 28 | /// The default event handlers do nothing, apart from 29 | /// [`key_down_event()`](#tymethod.key_down_event), which will by 30 | /// default exit the game if the escape key is pressed. Just 31 | /// override the methods you want to use. 32 | pub trait EventHandler 33 | where 34 | E: std::error::Error, 35 | { 36 | /// Called upon each logic update to the game. 37 | /// This should be where the game's logic takes place. 38 | fn update(&mut self, _ctx: &mut Context, _quad_ctx: &mut GraphicsContext) -> Result<(), E>; 39 | /// Called to do the drawing of your game. 40 | /// You probably want to start this with 41 | /// [`graphics::clear()`](../graphics/fn.clear.html) and end it 42 | /// with [`graphics::present()`](../graphics/fn.present.html). 43 | fn draw(&mut self, _ctx: &mut Context, _quad_ctx: &mut GraphicsContext) -> Result<(), E>; 44 | /// Called when the user resizes the window, or when it is resized 45 | /// via [`graphics::set_drawable_size()`](../graphics/fn.set_drawable_size.html). 46 | fn resize_event( 47 | &mut self, 48 | _ctx: &mut Context, 49 | _quad_ctx: &mut GraphicsContext, 50 | _width: f32, 51 | _height: f32, 52 | ) { 53 | } 54 | /// The mouse was moved; it provides both absolute x and y coordinates in the window, 55 | /// and relative x and y coordinates compared to its last position. 56 | fn mouse_motion_event( 57 | &mut self, 58 | _ctx: &mut Context, 59 | _quad_ctx: &mut GraphicsContext, 60 | _x: f32, 61 | _y: f32, 62 | _dx: f32, 63 | _dy: f32, 64 | ) { 65 | } 66 | fn touch_event( 67 | &mut self, 68 | ctx: &mut Context, 69 | _quad_ctx: &mut GraphicsContext, 70 | phase: TouchPhase, 71 | _id: u64, 72 | x: f32, 73 | y: f32, 74 | ) { 75 | ctx.mouse_context.input_handler.handle_mouse_move(x, y); 76 | // TODO: this is ugly code duplication; I'll fix it later when getting rid of the `InputHandler` 77 | let old_pos = crate::mouse::last_position(ctx); 78 | let dx = x - old_pos.x; 79 | let dy = y - old_pos.y; 80 | // update the frame delta value 81 | let old_delta = crate::mouse::delta(ctx); 82 | ctx.mouse_context 83 | .set_delta((old_delta.x + dx, old_delta.y + dy).into()); 84 | ctx.mouse_context.set_last_position((x, y).into()); 85 | match phase { 86 | TouchPhase::Started => { 87 | ctx.mouse_context 88 | .input_handler 89 | .handle_mouse_down(miniquad::MouseButton::Left); 90 | self.mouse_button_down_event(ctx, _quad_ctx, MouseButton::Left, x, y); 91 | } 92 | TouchPhase::Moved => { 93 | self.mouse_motion_event(ctx, _quad_ctx, x, y, dx, dy); 94 | } 95 | TouchPhase::Ended | TouchPhase::Cancelled => { 96 | ctx.mouse_context 97 | .input_handler 98 | .handle_mouse_up(miniquad::MouseButton::Left); 99 | self.mouse_button_up_event(ctx, _quad_ctx, MouseButton::Left, x, y); 100 | } 101 | } 102 | } 103 | /// The mousewheel was scrolled, vertically (y, positive away from and negative toward the user) 104 | /// or horizontally (x, positive to the right and negative to the left). 105 | fn mouse_wheel_event( 106 | &mut self, 107 | _ctx: &mut Context, 108 | _quad_ctx: &mut GraphicsContext, 109 | _x: f32, 110 | _y: f32, 111 | ) { 112 | } 113 | /// A mouse button was pressed. 114 | fn mouse_button_down_event( 115 | &mut self, 116 | _ctx: &mut Context, 117 | _quad_ctx: &mut GraphicsContext, 118 | _button: MouseButton, 119 | _x: f32, 120 | _y: f32, 121 | ) { 122 | } 123 | /// A mouse button was released. 124 | fn mouse_button_up_event( 125 | &mut self, 126 | _ctx: &mut Context, 127 | _quad_ctx: &mut GraphicsContext, 128 | _button: MouseButton, 129 | _x: f32, 130 | _y: f32, 131 | ) { 132 | } 133 | 134 | /// A keyboard button was pressed. 135 | /// 136 | /// The default implementation of this will call `ggez::event::quit()` 137 | /// when the escape key is pressed. If you override this with 138 | /// your own event handler you have to re-implment that 139 | /// functionality yourself. 140 | fn key_down_event( 141 | &mut self, 142 | ctx: &mut Context, 143 | _quad_ctx: &mut GraphicsContext, 144 | keycode: KeyCode, 145 | _keymods: KeyMods, 146 | _repeat: bool, 147 | ) { 148 | if keycode == KeyCode::Escape { 149 | quit(ctx); 150 | } 151 | } 152 | 153 | /// A keyboard button was released. 154 | fn key_up_event( 155 | &mut self, 156 | _ctx: &mut Context, 157 | _quad_ctx: &mut GraphicsContext, 158 | _keycode: KeyCode, 159 | _keymods: KeyMods, 160 | ) { 161 | } 162 | 163 | /// A unicode character was received, usually from keyboard input. 164 | /// This is the intended way of facilitating text input. 165 | fn text_input_event( 166 | &mut self, 167 | _ctx: &mut Context, 168 | _quad_ctx: &mut GraphicsContext, 169 | _character: char, 170 | ) { 171 | } 172 | 173 | #[cfg(not(any(target_arch = "wasm32", target_os = "ios", target_os = "android",)))] 174 | /// A gamepad button was pressed; `id` identifies which gamepad. 175 | /// Use [`input::gamepad()`](../input/fn.gamepad.html) to get more info about 176 | /// the gamepad. 177 | fn gamepad_button_down_event( 178 | &mut self, 179 | _ctx: &mut Context, 180 | _quad_ctx: &mut GraphicsContext, 181 | _btn: Button, 182 | _id: GamepadId, 183 | ) { 184 | } 185 | 186 | #[cfg(not(any(target_arch = "wasm32", target_os = "ios", target_os = "android",)))] 187 | /// A gamepad button was released; `id` identifies which gamepad. 188 | /// Use [`input::gamepad()`](../input/fn.gamepad.html) to get more info about 189 | /// the gamepad. 190 | fn gamepad_button_up_event( 191 | &mut self, 192 | _ctx: &mut Context, 193 | _quad_ctx: &mut GraphicsContext, 194 | _btn: Button, 195 | _id: GamepadId, 196 | ) { 197 | } 198 | 199 | #[cfg(not(any(target_arch = "wasm32", target_os = "ios", target_os = "android",)))] 200 | /// A gamepad axis moved; `id` identifies which gamepad. 201 | /// Use [`input::gamepad()`](../input/fn.gamepad.html) to get more info about 202 | /// the gamepad. 203 | fn gamepad_axis_event( 204 | &mut self, 205 | _ctx: &mut Context, 206 | _quad_ctx: &mut GraphicsContext, 207 | _axis: Axis, 208 | _value: f32, 209 | _id: GamepadId, 210 | ) { 211 | } 212 | 213 | /// Something went wrong, causing a `GameError`. 214 | /// If this returns true, the error was fatal, so the event loop ends, aborting the game. 215 | fn on_error( 216 | &mut self, 217 | _ctx: &mut Context, 218 | _quad_ctx: &mut GraphicsContext, 219 | _origin: ErrorOrigin, 220 | _e: E, 221 | ) -> bool { 222 | true 223 | } 224 | } 225 | 226 | /// Terminates the [`ggez::event::run()`](fn.run.html) loop by setting 227 | /// [`Context.continuing`](struct.Context.html#structfield.continuing) 228 | /// to `false`. 229 | /// 230 | /// NOTE: Doesn't end the application on Wasm, as that's not really possible, 231 | /// but stops the `update` function from running. 232 | pub fn quit(ctx: &mut Context) { 233 | ctx.continuing = false; 234 | } 235 | -------------------------------------------------------------------------------- /src/filesystem.rs: -------------------------------------------------------------------------------- 1 | // large parts directly stolen from macroquad: https://github.com/not-fl3/macroquad/blob/854aa50302a00ce590d505e28c9ecc42ae24be58/src/file.rs 2 | 3 | use std::sync::{Arc, Mutex}; 4 | use std::{collections::HashMap, io, path}; 5 | 6 | use crate::GameError::ResourceLoadError; 7 | use crate::{conf::Conf, Context, GameError, GameResult}; 8 | use std::panic::panic_any; 9 | 10 | #[derive(Debug, Clone)] 11 | pub struct File { 12 | pub bytes: io::Cursor>, 13 | } 14 | 15 | impl io::Read for File { 16 | fn read(&mut self, buf: &mut [u8]) -> io::Result { 17 | self.bytes.read(buf) 18 | } 19 | } 20 | 21 | /// A structure that contains the filesystem state and cache. 22 | #[derive(Debug)] 23 | pub struct Filesystem { 24 | root: Option, 25 | files: HashMap, 26 | } 27 | 28 | impl Filesystem { 29 | #[allow(clippy::redundant_closure)] 30 | pub(crate) fn new(conf: &Conf) -> Filesystem { 31 | let mut files = HashMap::new(); 32 | 33 | if let Some(tar_file) = conf.cache { 34 | let mut archive = tar::Archive::new(tar_file); 35 | 36 | for file in archive.entries().unwrap_or_else(|e| panic_any(e)) { 37 | use std::io::Read; 38 | 39 | let mut file = file.unwrap_or_else(|e| panic_any(e)); 40 | let filename = 41 | std::path::PathBuf::from(file.path().unwrap_or_else(|e| panic_any(e))); 42 | let mut buf = vec![]; 43 | 44 | file.read_to_end(&mut buf).unwrap_or_else(|e| panic_any(e)); 45 | if !buf.is_empty() { 46 | files.insert( 47 | filename, 48 | File { 49 | bytes: io::Cursor::new(buf), 50 | }, 51 | ); 52 | } 53 | } 54 | } 55 | 56 | let root = conf.physical_root_dir.clone(); 57 | Filesystem { root, files } 58 | } 59 | 60 | /// Opens the given `path` and returns the resulting `File` 61 | /// in read-only mode. 62 | pub fn open>(&mut self, path: P) -> GameResult { 63 | let mut path = path::PathBuf::from(path.as_ref()); 64 | 65 | // workaround for ggez-style pathes: in ggez paths starts with "/", while in the cache 66 | // dictionary they are presented without "/" 67 | if let Ok(stripped) = path.strip_prefix("/") { 68 | path = path::PathBuf::from(stripped); 69 | } 70 | 71 | // first check the cache 72 | if self.files.contains_key(&path) { 73 | Ok(self.files[&path].clone()) 74 | } else { 75 | // the file is not inside the cache, so it has to be loaded (locally, or via http url) 76 | let file = self.load_file(&path)?; 77 | Ok(file) 78 | } 79 | } 80 | 81 | #[cfg(not(target_os = "wasm32"))] 82 | /// Load file from the path and block until its loaded 83 | /// Will use filesystem on PC and Android and fail on WASM 84 | fn load_file>(&self, path: P) -> GameResult { 85 | fn load_file_inner(path: &str) -> GameResult> { 86 | let contents = Arc::new(Mutex::new(None)); 87 | 88 | { 89 | let contents = contents.clone(); 90 | let err_path = path.to_string(); 91 | 92 | miniquad::fs::load_file(path, move |bytes| { 93 | *contents.lock().unwrap() = Some(bytes.map_err(|kind| { 94 | GameError::ResourceLoadError(format!( 95 | "Couldn't load file {}: {}", 96 | err_path, kind 97 | )) 98 | })); 99 | }); 100 | } 101 | 102 | // wait until the file has been loaded 103 | // as miniquad::fs::load_file internally uses non-asynchronous loading for everything 104 | // except wasm, waiting should only ever occur on wasm (TODO: since this holds the main 105 | // thread hostage no progress is ever made and this just blocks forever... perhaps this 106 | // could be worked around by using "asyncify", but that would be both hard and also 107 | // require an additional post processing step on the generated wasm file) 108 | loop { 109 | let mut contents_guard = contents.lock().unwrap(); 110 | if let Some(contents) = contents_guard.take() { 111 | return contents; 112 | } 113 | drop(contents_guard); 114 | std::thread::yield_now(); 115 | } 116 | } 117 | 118 | #[cfg(target_os = "ios")] 119 | let _ = std::env::set_current_dir(std::env::current_exe().unwrap().parent().unwrap()); 120 | 121 | let path = path 122 | .as_ref() 123 | .as_os_str() 124 | .to_os_string() 125 | .into_string() 126 | .map_err(|os_string| { 127 | ResourceLoadError(format!("utf-8-invalid path: {:?}", os_string)) 128 | })?; 129 | 130 | #[cfg(not(target_os = "android"))] 131 | let path = if let Some(ref root) = self.root { 132 | format!( 133 | "{}/{}", 134 | root.as_os_str() 135 | .to_os_string() 136 | .into_string() 137 | .map_err(|os_string| ResourceLoadError(format!( 138 | "utf-8-invalid root: {:?}", 139 | os_string 140 | )))?, 141 | path 142 | ) 143 | } else { 144 | path 145 | }; 146 | 147 | let buf = load_file_inner(&path)?; 148 | let bytes = io::Cursor::new(buf); 149 | Ok(File { bytes }) 150 | } 151 | 152 | #[cfg(target_os = "wasm32")] 153 | /// Load file from the path and block until its loaded 154 | /// Will use filesystem on PC and Android and fail on WASM 155 | fn load_file>(&self, path: P) -> GameResult { 156 | Err(GameError::ResourceLoadError(format!( 157 | "Couldn't load file {}", 158 | path.as_display() 159 | ))) 160 | } 161 | } 162 | 163 | /// Opens the given path and returns the resulting `File` 164 | /// in read-only mode. 165 | /// 166 | /// Checks the [embedded tar file](../conf/struct.Conf.html#method.high_dpi), if there is one, first and if the file cannot be found there 167 | /// continues to either load the file using the OS-filesystem, or just fail on WASM, as blocking loads 168 | /// are impossible there. 169 | pub fn open>(ctx: &mut Context, path: P) -> GameResult { 170 | ctx.filesystem.open(path) 171 | } 172 | 173 | /// Loads a file from the path returning an `Option` that will be `Some` once it has been loaded (or loading it failed). 174 | /// Will use filesystem on PC and Android and a http request on WASM. 175 | /// 176 | /// Note: Don't wait for the `Option` to become `Some` inside of a loop, as that would create an infinite loop 177 | /// on WASM, where progress on the GET request can only be made _between_ frames of your application. 178 | pub fn load_file_async>(path: P) -> Arc>>> { 179 | // TODO: Create an example showcasing the use of this. 180 | let contents = Arc::new(Mutex::new(None)); 181 | let path = path 182 | .as_ref() 183 | .as_os_str() 184 | .to_os_string() 185 | .into_string() 186 | .map_err(|os_string| ResourceLoadError(format!("utf-8-invalid path: {:?}", os_string))); 187 | 188 | if let Ok(path) = path { 189 | let contents = contents.clone(); 190 | 191 | miniquad::fs::load_file(&*(path.clone()), move |response| { 192 | let result = match response { 193 | Ok(bytes) => Ok(File { 194 | bytes: io::Cursor::new(bytes), 195 | }), 196 | Err(e) => Err(GameError::ResourceLoadError(format!( 197 | "Couldn't load file {}: {}", 198 | path, e 199 | ))), 200 | }; 201 | *contents.lock().unwrap() = Some(result); 202 | }); 203 | } 204 | 205 | contents 206 | } 207 | -------------------------------------------------------------------------------- /src/goodies.rs: -------------------------------------------------------------------------------- 1 | mod camera; 2 | 3 | pub mod matrix_transform_2d; 4 | pub mod scene; 5 | 6 | pub use self::camera::Camera; 7 | -------------------------------------------------------------------------------- /src/goodies/camera.rs: -------------------------------------------------------------------------------- 1 | use cgmath::{Matrix3, Matrix4, Point2, Rad, SquareMatrix, Transform, Vector2}; 2 | 3 | use crate::goodies::matrix_transform_2d::*; 4 | 5 | pub struct Camera { 6 | pub position: Point2, 7 | pub visible_field_width: f32, 8 | pub rotation: f32, 9 | 10 | pub screen_width: f32, 11 | pub screen_height: f32, 12 | 13 | canvas_matrix: Matrix3, 14 | gl_matrix: Matrix4, 15 | } 16 | 17 | impl Default for Camera { 18 | fn default() -> Camera { 19 | Camera { 20 | position: Point2::new(0., 0.), 21 | visible_field_width: 500., 22 | rotation: 0., 23 | screen_width: 100., 24 | screen_height: 100., 25 | canvas_matrix: Matrix3::identity(), 26 | gl_matrix: Matrix4::identity(), 27 | } 28 | } 29 | } 30 | 31 | impl Camera { 32 | pub fn screen_to_world_point(&self, point: Point2) -> Point2 { 33 | self.canvas_matrix.invert().unwrap().transform_point(point) 34 | } 35 | 36 | pub fn screen_to_world_vector(&self, vector: Vector2) -> Vector2 { 37 | let matrix = self.canvas_matrix.invert().unwrap(); 38 | 39 | Transform::>::transform_vector(&matrix, vector) 40 | } 41 | 42 | pub fn set_visible_field(&mut self, field: f32) { 43 | self.visible_field_width = field; 44 | self.update_matrix(); 45 | } 46 | 47 | pub fn update_screen_size(&mut self, w: f32, h: f32) { 48 | self.screen_width = w; 49 | self.screen_height = h; 50 | 51 | self.update_matrix(); 52 | } 53 | 54 | pub fn world_to_screen_point(&self, point: Point2) -> Point2 { 55 | self.canvas_matrix.transform_point(point) 56 | } 57 | 58 | pub fn world_to_screen_vector(&self, vector: Vector2) -> Vector2 { 59 | Transform::>::transform_vector(&self.canvas_matrix, vector) 60 | } 61 | 62 | pub fn canvas_matrix(&self) -> Matrix3 { 63 | self.canvas_matrix 64 | } 65 | 66 | pub fn gl_matrix(&self) -> Matrix4 { 67 | self.gl_matrix 68 | } 69 | 70 | pub fn set_position(&mut self, position: Point2) { 71 | self.position = position; 72 | self.update_matrix(); 73 | } 74 | 75 | fn update_matrix(&mut self) { 76 | let screen_center = Vector2::new(self.screen_width / 2., self.screen_height / 2.); 77 | let camera_center = self.position; 78 | 79 | let translate0 = Matrix3::from_translation(screen_center); 80 | let scale = Matrix3::from_nonuniform_scale( 81 | self.screen_width / self.visible_field_width, 82 | self.screen_width / self.visible_field_width, 83 | ); 84 | let translate1 = 85 | Matrix3::from_translation(Vector2::new(-camera_center.x, -camera_center.y)); 86 | 87 | let rotation = Matrix3::from_angle_z(Rad(self.rotation)); 88 | 89 | self.canvas_matrix = translate0 * rotation * scale * translate1; 90 | 91 | self.gl_matrix = cgmath::ortho( 92 | camera_center.x - self.visible_field_width / 2., 93 | camera_center.x + self.visible_field_width / 2., 94 | camera_center.y 95 | + self.screen_height / self.screen_width * self.visible_field_width / 2., 96 | camera_center.y 97 | - self.screen_height / self.screen_width * self.visible_field_width / 2., 98 | -1.0, 99 | 1.0, 100 | ); 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /src/goodies/matrix_transform_2d.rs: -------------------------------------------------------------------------------- 1 | use cgmath::{BaseFloat, Matrix3, Vector2}; 2 | 3 | pub trait Transform2d { 4 | fn from_translation(v: Vector2) -> Self; 5 | fn from_scale(value: S) -> Self; 6 | fn from_nonuniform_scale(x: S, y: S) -> Self; 7 | } 8 | 9 | impl Transform2d for Matrix3 { 10 | /// Create a homogeneous transformation matrix from a translation vector. 11 | #[inline] 12 | #[rustfmt::skip] 13 | fn from_translation(v: Vector2) -> Matrix3 { 14 | Matrix3::new( 15 | S::one(), S::zero(), S::zero(), 16 | S::zero(), S::one(), S::zero(), 17 | v.x, v.y, S::one(), 18 | ) 19 | } 20 | 21 | /// Create a homogeneous transformation matrix from a scale value. 22 | #[inline] 23 | fn from_scale(value: S) -> Matrix3 { 24 | Matrix3::from_nonuniform_scale(value, value) 25 | } 26 | 27 | /// Create a homogeneous transformation matrix from a set of scale values. 28 | #[inline] 29 | #[rustfmt::skip] 30 | fn from_nonuniform_scale(x: S, y: S) -> Matrix3 { 31 | Matrix3::new( 32 | x, S::zero(), S::zero(), 33 | S::zero(), y, S::zero(), 34 | S::zero(), S::zero(), S::one(), 35 | ) 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/graphics/canvas.rs: -------------------------------------------------------------------------------- 1 | use crate::{ 2 | graphics::{BlendMode, DrawParam, Drawable, FilterMode, Image, Rect}, 3 | Context, GameResult, 4 | }; 5 | 6 | use crate::graphics::drawparam::Transform; 7 | use miniquad::{RenderPass, Texture, TextureFormat, TextureParams}; 8 | 9 | #[derive(Clone, Debug)] 10 | pub struct Canvas { 11 | image: Image, 12 | pub(crate) offscreen_pass: RenderPass, 13 | } 14 | 15 | impl Canvas { 16 | /// Create a new `Canvas` with the specified size. 17 | pub fn new( 18 | ctx: &mut Context, 19 | quad_ctx: &mut miniquad::graphics::GraphicsContext, 20 | width: u16, 21 | height: u16, 22 | ) -> GameResult { 23 | let texture = Texture::new_render_texture( 24 | quad_ctx, 25 | TextureParams { 26 | width: width as u32, 27 | height: height as u32, 28 | format: TextureFormat::RGBA8, 29 | ..Default::default() 30 | }, 31 | ); 32 | 33 | let image = Image::from_texture(quad_ctx, texture, ctx.gfx_context.default_filter)?; 34 | 35 | let offscreen_pass = RenderPass::new(quad_ctx, texture, None); 36 | 37 | Ok(Canvas { 38 | image, 39 | offscreen_pass, 40 | }) 41 | } 42 | 43 | /// Create a new `Canvas` with the current window dimensions. 44 | pub fn with_window_size( 45 | ctx: &mut Context, 46 | quad_ctx: &mut miniquad::graphics::GraphicsContext, 47 | ) -> GameResult { 48 | use crate::graphics; 49 | let (w, h) = graphics::drawable_size(quad_ctx); 50 | // Default to no multisampling 51 | Canvas::new(ctx, quad_ctx, w as u16, h as u16) 52 | } 53 | 54 | /// Gets the backend `Image` that is being rendered to. 55 | pub fn image(&self) -> &Image { 56 | &self.image 57 | } 58 | 59 | /// Return the width of the canvas. 60 | pub fn width(&self) -> u16 { 61 | self.image.width 62 | } 63 | 64 | /// Return the height of the canvas. 65 | pub fn height(&self) -> u16 { 66 | self.image.height 67 | } 68 | 69 | /// Returns the dimensions of the canvas. 70 | pub fn dimensions(&self) -> Rect { 71 | Rect::new(0.0, 0.0, f32::from(self.width()), f32::from(self.height())) 72 | } 73 | 74 | /// Get the filter mode for the image. 75 | pub fn filter(&self) -> FilterMode { 76 | self.image.filter() 77 | } 78 | 79 | /// Set the filter mode for the canvas. 80 | pub fn set_filter(&mut self, mode: FilterMode) { 81 | self.image.set_filter(mode) 82 | } 83 | 84 | /// Destroys the `Canvas` and returns the `Image` it contains. 85 | pub fn into_inner(self) -> Image { 86 | // TODO: This texture is created with different settings 87 | // than the default; does that matter? 88 | self.image 89 | } 90 | } 91 | 92 | impl Drawable for Canvas { 93 | fn draw( 94 | &self, 95 | ctx: &mut Context, 96 | quad_ctx: &mut miniquad::graphics::GraphicsContext, 97 | param: DrawParam, 98 | ) -> GameResult { 99 | // We have to mess with the scale to make everything 100 | // be its-unit-size-in-pixels. 101 | let scale_x = param.src.w * f32::from(self.width()); 102 | let scale_y = param.src.h * f32::from(self.height()); 103 | 104 | let scaled_param = match param.trans { 105 | Transform::Values { scale, .. } => { 106 | let s_param = param.scale(mint::Vector2 { 107 | x: scale.x * scale_x, 108 | y: scale.y * scale_y, 109 | }); 110 | s_param.transform(s_param.trans.to_bare_matrix()) 111 | } 112 | Transform::Matrix(m) => param.transform( 113 | cgmath::Matrix4::from(m) 114 | * cgmath::Matrix4::from_nonuniform_scale(scale_x, scale_y, 1.0), 115 | ), 116 | }; 117 | 118 | // Gotta flip the image on the Y axis here 119 | // to account for OpenGL's origin being at the bottom-left. 120 | let new_param = flip_draw_param_vertical(scaled_param); 121 | 122 | self.image.draw_image_raw(ctx, quad_ctx, new_param) 123 | } 124 | 125 | fn set_blend_mode(&mut self, blend_mode: Option) { 126 | self.image.set_blend_mode(blend_mode); 127 | } 128 | 129 | fn blend_mode(&self) -> Option { 130 | self.image.blend_mode() 131 | } 132 | 133 | fn dimensions(&self, _: &mut Context) -> Option { 134 | Some(self.image.dimensions()) 135 | } 136 | } 137 | 138 | #[rustfmt::skip] 139 | /// equal to 140 | /// 141 | /// Matrix4::from_translation(cgmath::vec3(0.0, 1.0, 0.0)) * Matrix4::from_nonuniform_scale(1.0, -1.0, 1.0), 142 | const FLIP_MATRIX: cgmath::Matrix4 = cgmath::Matrix4::new( 143 | 1.0, 0.0, 0.0, 0.0, 144 | 0.0, -1.0, 0.0, 0.0, 145 | 0.0, 0.0, 1.0, 0.0, 146 | 0.0, 1.0, 0.0, 1.0, 147 | ); 148 | 149 | fn flip_draw_param_vertical(param: DrawParam) -> DrawParam { 150 | let param = if let Transform::Matrix(mat) = param.trans { 151 | param.transform(cgmath::Matrix4::from(mat) * FLIP_MATRIX) 152 | } else { 153 | panic!("Can not be called with a non-matrix DrawParam"); 154 | }; 155 | let new_src = Rect { 156 | x: param.src.x, 157 | y: (1.0 - param.src.h) - param.src.y, 158 | w: param.src.w, 159 | h: param.src.h, 160 | }; 161 | param.src(new_src) 162 | } 163 | 164 | /// Set the `Canvas` to render to. Specifying `Option::None` will cause all 165 | /// rendering to be done directly to the screen. 166 | pub fn set_canvas(ctx: &mut Context, target: Option<&Canvas>) { 167 | ctx.gfx_context.canvas = target.cloned(); 168 | } 169 | -------------------------------------------------------------------------------- /src/graphics/context.rs: -------------------------------------------------------------------------------- 1 | use crate::graphics::{types::Rect, Canvas, FilterMode, Shader, ShaderId}; 2 | use std::rc::Rc; 3 | 4 | use crate::graphics::{spritebatch, BlendMode, DrawParam, Font, Image}; 5 | use cgmath::Matrix4; 6 | use glyph_brush::{GlyphBrush, GlyphBrushBuilder}; 7 | use miniquad::{BufferLayout, PipelineParams, Texture, VertexAttribute, VertexFormat, VertexStep}; 8 | use std::cell::RefCell; 9 | 10 | pub struct GraphicsContext { 11 | pub(crate) screen_rect: Rect, 12 | pub(crate) projection: Matrix4, 13 | pub(crate) white_texture: miniquad::Texture, 14 | pub(crate) canvas: Option, 15 | pub(crate) current_shader: Rc>, 16 | pub(crate) shaders: Vec, 17 | pub(crate) blend_mode: BlendMode, 18 | pub(crate) default_filter: FilterMode, 19 | 20 | pub(crate) glyph_brush: Rc>>, 21 | pub(crate) glyph_cache: Image, 22 | pub(crate) glyph_state: Rc>, 23 | } 24 | 25 | impl GraphicsContext { 26 | pub fn new(quad_ctx: &mut miniquad::graphics::GraphicsContext) -> GraphicsContext { 27 | let projection = cgmath::One::one(); 28 | let screen_rect = Rect::new(-1., -1., 2., 2.); 29 | 30 | let white_texture = Texture::from_rgba8(quad_ctx, 1, 1, &[255, 255, 255, 255]); 31 | let (color_blend, alpha_blend) = BlendMode::Alpha.into(); 32 | 33 | let default_shader = miniquad::Shader::new( 34 | quad_ctx, 35 | default_shader::VERTEX, 36 | default_shader::FRAGMENT, 37 | default_shader::meta(), 38 | ) 39 | .expect("couldn't create default shader"); 40 | 41 | let gwg_default_shader = 42 | crate::graphics::Shader::from_mini_shader(quad_ctx, default_shader, None); 43 | 44 | let default_pipeline = miniquad::Pipeline::with_params( 45 | quad_ctx, 46 | &[ 47 | BufferLayout::default(), 48 | BufferLayout { 49 | step_func: VertexStep::PerInstance, 50 | ..Default::default() 51 | }, 52 | ], 53 | &[ 54 | VertexAttribute::with_buffer("position", VertexFormat::Float2, 0), 55 | VertexAttribute::with_buffer("texcoord", VertexFormat::Float2, 0), 56 | VertexAttribute::with_buffer("color0", VertexFormat::Float4, 0), 57 | VertexAttribute::with_buffer("Source", VertexFormat::Float4, 1), 58 | VertexAttribute::with_buffer("Color", VertexFormat::Float4, 1), 59 | VertexAttribute::with_buffer("Model", VertexFormat::Mat4, 1), 60 | ], 61 | default_shader, 62 | PipelineParams { 63 | color_blend: Some(color_blend), 64 | alpha_blend: Some(alpha_blend), 65 | ..Default::default() 66 | }, 67 | ); 68 | 69 | // pipeline has to be applied whenever the shader (and therefore pipeline) changes 70 | quad_ctx.apply_pipeline(&default_pipeline); 71 | 72 | // Glyph cache stuff. 73 | let font_vec = glyph_brush::ab_glyph::FontArc::try_from_slice(Font::default_font_bytes()) 74 | .expect("Invalid default font bytes, should never happen"); 75 | let glyph_brush = GlyphBrushBuilder::using_font(font_vec).build(); 76 | let (glyph_cache_width, glyph_cache_height) = glyph_brush.texture_dimensions(); 77 | let initial_contents = vec![ 78 | 255; 79 | 4 * usize::try_from(glyph_cache_width).unwrap() 80 | * usize::try_from(glyph_cache_height).unwrap() 81 | ]; 82 | let glyph_cache = Texture::from_rgba8( 83 | quad_ctx, 84 | glyph_cache_width.try_into().unwrap(), 85 | glyph_cache_height.try_into().unwrap(), 86 | &initial_contents, 87 | ); 88 | 89 | let glyph_cache = Image::from_texture(quad_ctx, glyph_cache, FilterMode::Linear).unwrap(); 90 | 91 | let glyph_state = Rc::new(RefCell::new(spritebatch::SpriteBatch::new( 92 | glyph_cache.clone(), 93 | ))); 94 | 95 | GraphicsContext { 96 | projection, 97 | screen_rect, 98 | white_texture, 99 | canvas: None, 100 | current_shader: Rc::new(RefCell::new(0)), 101 | shaders: vec![gwg_default_shader], 102 | blend_mode: BlendMode::Alpha, 103 | default_filter: FilterMode::Linear, 104 | glyph_brush: Rc::new(RefCell::new(glyph_brush)), 105 | glyph_cache, 106 | glyph_state, 107 | } 108 | } 109 | } 110 | 111 | impl GraphicsContext { 112 | /// Sets the raw projection matrix to the given Matrix. 113 | /// 114 | /// Call `update_globals()` to apply after calling this. 115 | pub(crate) fn set_projection(&mut self, mat: Matrix4) { 116 | self.projection = mat; 117 | } 118 | 119 | /// Gets a copy of the raw projection matrix. 120 | pub(crate) fn projection(&self) -> Matrix4 { 121 | self.projection 122 | } 123 | 124 | pub fn set_screen_coordinates(&mut self, rect: crate::graphics::types::Rect) { 125 | self.screen_rect = rect; 126 | self.projection = 127 | cgmath::ortho(rect.x, rect.x + rect.w, rect.y + rect.h, rect.y, -1.0, 1.0); 128 | } 129 | 130 | pub(crate) fn set_blend_mode(&mut self, mode: BlendMode) { 131 | self.blend_mode = mode; 132 | } 133 | 134 | /// Gets the current global 135 | pub(crate) fn blend_mode(&self) -> &BlendMode { 136 | &self.blend_mode 137 | } 138 | } 139 | 140 | pub(crate) mod default_shader { 141 | use crate::graphics::ShaderId; 142 | use miniquad::{ShaderMeta, UniformBlockLayout, UniformDesc, UniformType}; 143 | 144 | /// As the default shader is created first it holds the first id, which is 0. 145 | pub const SHADER_ID: ShaderId = 0; 146 | 147 | pub const VERTEX: &str = r#"#version 100 148 | attribute vec2 position; 149 | attribute vec2 texcoord; 150 | attribute vec4 color0; 151 | 152 | attribute vec4 Source; 153 | attribute vec4 Color; 154 | attribute mat4 Model; 155 | 156 | varying lowp vec4 color; 157 | varying lowp vec2 uv; 158 | 159 | uniform mat4 Projection; 160 | 161 | uniform float depth; 162 | 163 | void main() { 164 | gl_Position = Projection * Model * vec4(position, 0, 1); 165 | gl_Position.z = depth; 166 | color = Color * color0; 167 | uv = texcoord * Source.zw + Source.xy; 168 | }"#; 169 | 170 | pub const FRAGMENT: &str = r#"#version 100 171 | varying lowp vec4 color; 172 | varying lowp vec2 uv; 173 | 174 | uniform sampler2D Texture; 175 | 176 | void main() { 177 | gl_FragColor = texture2D(Texture, uv) * color; 178 | }"#; 179 | 180 | pub fn meta() -> ShaderMeta { 181 | ShaderMeta { 182 | images: vec!["Texture".to_string()], 183 | uniforms: UniformBlockLayout { 184 | uniforms: vec![UniformDesc::new("Projection", UniformType::Mat4)], 185 | }, 186 | } 187 | } 188 | } 189 | -------------------------------------------------------------------------------- /src/graphics/drawparam.rs: -------------------------------------------------------------------------------- 1 | use crate::graphics::{Color, Rect}; 2 | use cgmath::Matrix; 3 | 4 | /// A struct that represents where to put a `Drawable`. 5 | /// 6 | /// This can either be a set of individual components, or 7 | /// a single `Matrix4` transform. 8 | #[derive(Debug, Copy, Clone, PartialEq)] 9 | pub enum Transform { 10 | /// Transform made of individual values 11 | Values { 12 | /// The position to draw the graphic expressed as a `Point2`. 13 | dest: mint::Point2, 14 | /// The orientation of the graphic in radians. 15 | rotation: f32, 16 | /// The x/y scale factors expressed as a `Vector2`. 17 | scale: mint::Vector2, 18 | /// An offset from the center for transform operations like scale/rotation, 19 | /// with `0,0` meaning the origin and `1,1` meaning the opposite corner from the origin. 20 | /// By default these operations are done from the top-left corner, so to rotate something 21 | /// from the center specify `Point2::new(0.5, 0.5)` here. 22 | offset: mint::Point2, 23 | }, 24 | /// Transform made of an arbitrary matrix. 25 | /// 26 | /// It should represent the final model matrix of the given drawable. This is useful for 27 | /// situations where, for example, you build your own hierarchy system, where you calculate 28 | /// matrices of each hierarchy item and store a calculated world-space model matrix of an item. 29 | /// This lets you implement transform stacks, skeletal animations, etc. 30 | Matrix(mint::ColumnMatrix4), 31 | } 32 | 33 | impl Default for Transform { 34 | fn default() -> Self { 35 | Transform::Values { 36 | dest: mint::Point2 { x: 0.0, y: 0.0 }, 37 | rotation: 0.0, 38 | scale: mint::Vector2 { x: 1.0, y: 1.0 }, 39 | offset: mint::Point2 { x: 0.0, y: 0.0 }, 40 | } 41 | } 42 | } 43 | 44 | impl Transform { 45 | /// Crunches the transform down to a single matrix, if it's not one already. 46 | pub fn to_matrix(self) -> Self { 47 | Transform::Matrix(self.to_bare_matrix()) 48 | } 49 | 50 | /// Same as `to_matrix()` but just returns a bare `mint` matrix. 51 | pub fn to_bare_matrix(self) -> mint::ColumnMatrix4 { 52 | match self { 53 | Transform::Matrix(m) => m, 54 | Transform::Values { 55 | dest, 56 | rotation, 57 | scale, 58 | offset, 59 | } => { 60 | // Calculate a matrix equivalent to doing this: 61 | // type Vec3 = na::Vector3; 62 | // let o = offset; 63 | // let translate = na::Matrix4::new_translation(&Vec3::new(dest.x, dest.y, 0.0)); 64 | // let offset = na::Matrix4::new_translation(&Vec3::new(offset.x, offset.y, 0.0)); 65 | // let offset_inverse = 66 | // na::Matrix4::new_translation(&Vec3::new(-o.x, -o.y, 0.0)); 67 | // let axis_angle = Vec3::z() * *rotation; 68 | // let rotation = na::Matrix4::new_rotation(axis_angle); 69 | // let scale = na::Matrix4::new_nonuniform_scaling(&Vec3::new(scale.x, scale.y, 1.0)); 70 | // translate * rotation * scale * offset_inverse 71 | // 72 | // Doing the bits manually is faster though, or at least was last I checked. 73 | let (sinr, cosr) = rotation.sin_cos(); 74 | let m00 = cosr * scale.x; 75 | let m01 = -sinr * scale.y; 76 | let m10 = sinr * scale.x; 77 | let m11 = cosr * scale.y; 78 | let m03 = offset.x * (-m00) - offset.y * m01 + dest.x; 79 | let m13 = offset.y * (-m11) - offset.x * m10 + dest.y; 80 | // Welp, this transpose fixes some bug that makes nothing draw, 81 | // that was introduced in commit 2c6b3cc03f34fb240f4246f5a68c75bd85b60eae. 82 | // The best part is, I don't know if this code is wrong, or whether there's 83 | // some reversed matrix multiply or such somewhere else that this cancel 84 | // out. Probably the former though. 85 | cgmath::Matrix4::new( 86 | m00, m01, 0.0, m03, // oh rustfmt you so fine 87 | m10, m11, 0.0, m13, // you so fine you blow my mind 88 | 0.0, 0.0, 1.0, 0.0, // but leave my matrix formatting alone 89 | 0.0, 0.0, 0.0, 1.0, // plz 90 | ) 91 | .transpose() 92 | .into() 93 | } 94 | } 95 | } 96 | } 97 | 98 | #[derive(Debug, Copy, Clone, PartialEq)] 99 | pub struct DrawParam { 100 | /// A portion of the drawable to clip, as a fraction of the whole image. 101 | /// Defaults to the whole image `(0,0 to 1,1)` if omitted. 102 | pub src: Rect, 103 | /// A color to draw the target with. 104 | /// Default: white. 105 | pub color: Color, 106 | /// Where to put the `Drawable`. 107 | pub trans: Transform, 108 | } 109 | 110 | impl Default for DrawParam { 111 | fn default() -> Self { 112 | DrawParam { 113 | src: Rect::one(), 114 | color: Color::WHITE, 115 | trans: Transform::default(), 116 | } 117 | } 118 | } 119 | 120 | impl DrawParam { 121 | /// Create a new DrawParam with default values. 122 | pub fn new() -> Self { 123 | Self::default() 124 | } 125 | 126 | /// Set the source rect 127 | pub fn src(mut self, src: Rect) -> Self { 128 | self.src = src; 129 | self 130 | } 131 | 132 | /// Set the dest point 133 | pub fn dest

(mut self, dest_: P) -> Self 134 | where 135 | P: Into>, 136 | { 137 | if let Transform::Values { ref mut dest, .. } = self.trans { 138 | let p: mint::Point2 = dest_.into(); 139 | *dest = p; 140 | self 141 | } else { 142 | panic!("Cannot set values for a DrawParam matrix") 143 | } 144 | } 145 | 146 | /// Set the drawable color. This will be blended with whatever 147 | /// color the drawn object already is. 148 | pub fn color(mut self, color: Color) -> Self { 149 | self.color = color; 150 | self 151 | } 152 | 153 | /// Set the rotation of the drawable. 154 | pub fn rotation(mut self, rot: f32) -> Self { 155 | if let Transform::Values { 156 | ref mut rotation, .. 157 | } = self.trans 158 | { 159 | *rotation = rot; 160 | self 161 | } else { 162 | panic!("Cannot set values for a DrawParam matrix") 163 | } 164 | } 165 | 166 | /// Set the scaling factors of the drawable. 167 | pub fn scale(mut self, scale_: V) -> Self 168 | where 169 | V: Into>, 170 | { 171 | if let Transform::Values { ref mut scale, .. } = self.trans { 172 | let p: mint::Vector2 = scale_.into(); 173 | *scale = p; 174 | self 175 | } else { 176 | panic!("Cannot set values for a DrawParam matrix") 177 | } 178 | } 179 | 180 | /// Set the transformation offset of the drawable. 181 | pub fn offset

(mut self, offset_: P) -> Self 182 | where 183 | P: Into>, 184 | { 185 | if let Transform::Values { ref mut offset, .. } = self.trans { 186 | let p: mint::Point2 = offset_.into(); 187 | *offset = p; 188 | self 189 | } else { 190 | panic!("Cannot set values for a DrawParam matrix") 191 | } 192 | } 193 | 194 | /// Set the transformation matrix of the drawable. 195 | pub fn transform(mut self, transform: M) -> Self 196 | where 197 | M: Into>, 198 | { 199 | self.trans = Transform::Matrix(transform.into()); 200 | self 201 | } 202 | } 203 | 204 | /// Create a `DrawParam` from a location. 205 | /// Note that this takes a single-element tuple. 206 | /// It's a little weird but keeps the trait implementations 207 | /// from clashing. 208 | impl

From<(P,)> for DrawParam 209 | where 210 | P: Into>, 211 | { 212 | fn from(location: (P,)) -> Self { 213 | DrawParam::new().dest(location.0) 214 | } 215 | } 216 | 217 | /// Create a `DrawParam` from a location and color 218 | impl

From<(P, Color)> for DrawParam 219 | where 220 | P: Into>, 221 | { 222 | fn from((location, color): (P, Color)) -> Self { 223 | DrawParam::new().dest(location).color(color) 224 | } 225 | } 226 | 227 | /// Create a `DrawParam` from a location, rotation and color 228 | impl

From<(P, f32, Color)> for DrawParam 229 | where 230 | P: Into>, 231 | { 232 | fn from((location, rotation, color): (P, f32, Color)) -> Self { 233 | DrawParam::new() 234 | .dest(location) 235 | .rotation(rotation) 236 | .color(color) 237 | } 238 | } 239 | 240 | /// Create a `DrawParam` from a location, rotation, offset and color 241 | impl

From<(P, f32, P, Color)> for DrawParam 242 | where 243 | P: Into>, 244 | { 245 | fn from((location, rotation, offset, color): (P, f32, P, Color)) -> Self { 246 | DrawParam::new() 247 | .dest(location) 248 | .rotation(rotation) 249 | .offset(offset) 250 | .color(color) 251 | } 252 | } 253 | 254 | /// Create a `DrawParam` from a location, rotation, offset, scale and color 255 | impl From<(P, f32, P, V, Color)> for DrawParam 256 | where 257 | P: Into>, 258 | V: Into>, 259 | { 260 | fn from((location, rotation, offset, scale, color): (P, f32, P, V, Color)) -> Self { 261 | DrawParam::new() 262 | .dest(location) 263 | .rotation(rotation) 264 | .offset(offset) 265 | .scale(scale) 266 | .color(color) 267 | } 268 | } 269 | -------------------------------------------------------------------------------- /src/graphics/font.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ggez/good-web-game/11ce17b8076a4c3eb7b8d809ce4066342b8e412c/src/graphics/font.png -------------------------------------------------------------------------------- /src/graphics/image.rs: -------------------------------------------------------------------------------- 1 | use cgmath::Matrix4; 2 | use std::path; 3 | use std::sync::atomic::{AtomicBool, Ordering}; 4 | 5 | use crate::{ 6 | error::GameResult, 7 | filesystem, 8 | graphics::{BlendMode, DrawParam, Drawable, InstanceAttributes, Rect}, 9 | Context, GameError, 10 | }; 11 | 12 | use miniquad::{Bindings, Buffer, BufferType, PassAction, Texture}; 13 | 14 | use crate::graphics::{apply_uniforms, Color}; 15 | pub use miniquad::graphics::FilterMode; 16 | use std::sync::Arc; 17 | 18 | #[derive(Clone, Debug)] 19 | pub struct Image { 20 | pub(crate) texture: Texture, 21 | pub(crate) width: u16, 22 | pub(crate) height: u16, 23 | filter: FilterMode, 24 | pub(crate) bindings: Bindings, 25 | blend_mode: Option, 26 | dirty_filter: DirtyFlag, 27 | pub(crate) bindings_clones_hack: Arc<()>, 28 | pub(crate) texture_clones_hack: Arc<()>, 29 | } 30 | 31 | impl Image { 32 | pub fn new>( 33 | ctx: &mut Context, 34 | quad_ctx: &mut miniquad::graphics::GraphicsContext, 35 | path: P, 36 | ) -> GameResult { 37 | use std::io::Read; 38 | 39 | let mut file = filesystem::open(ctx, path)?; 40 | 41 | let mut bytes = vec![]; 42 | file.bytes.read_to_end(&mut bytes)?; 43 | 44 | Self::from_png_bytes(ctx, quad_ctx, &bytes) 45 | } 46 | 47 | pub fn from_png_bytes( 48 | ctx: &mut Context, 49 | quad_ctx: &mut miniquad::graphics::GraphicsContext, 50 | bytes: &[u8], 51 | ) -> GameResult { 52 | match image::load_from_memory(bytes) { 53 | Ok(img) => { 54 | let rgba = img.to_rgba(); 55 | 56 | let width = rgba.width() as u16; 57 | let height = rgba.height() as u16; 58 | let bytes = rgba.into_raw(); 59 | 60 | Image::from_rgba8(ctx, quad_ctx, width, height, &bytes) 61 | } 62 | Err(e) => Err(GameError::ResourceLoadError(e.to_string())), 63 | } 64 | } 65 | 66 | pub fn from_rgba8( 67 | ctx: &mut Context, 68 | quad_ctx: &mut miniquad::graphics::GraphicsContext, 69 | width: u16, 70 | height: u16, 71 | bytes: &[u8], 72 | ) -> GameResult { 73 | let texture = Texture::from_rgba8(quad_ctx, width, height, bytes); 74 | Self::from_texture(quad_ctx, texture, ctx.gfx_context.default_filter) 75 | } 76 | 77 | pub fn from_texture( 78 | ctx: &mut miniquad::Context, 79 | texture: Texture, 80 | filter: FilterMode, 81 | ) -> GameResult { 82 | // if we wanted to we could optimize this a bit by creating this buffer only once and then just cloning the handle 83 | #[rustfmt::skip] 84 | let vertices: [f32; 32] = [0.0, 0.0, // first pos 85 | 0.0, 0.0, // first texcoord 86 | 1.0, 1.0, 1.0, 1.0, // first color 87 | 1.0, 0.0, // second pos 88 | 1.0, 0.0, // second texcoord 89 | 1.0, 1.0, 1.0, 1.0, // second color 90 | 1.0, 1.0, // third pos 91 | 1.0, 1.0, // third texcoord 92 | 1.0, 1.0, 1.0, 1.0, // third color 93 | 0.0, 1.0, // fourth pos 94 | 0.0, 1.0, // fourth texcoord 95 | 1.0, 1.0, 1.0, 1.0]; // fourth color 96 | 97 | let vertex_buffer = Buffer::immutable(ctx, BufferType::VertexBuffer, &vertices); 98 | let attribute_buffer = Buffer::stream( 99 | ctx, 100 | BufferType::VertexBuffer, 101 | std::mem::size_of::(), // start out with space for one instance 102 | ); 103 | 104 | let indices: [u16; 6] = [0, 1, 2, 0, 2, 3]; 105 | let index_buffer = Buffer::immutable(ctx, BufferType::IndexBuffer, &indices); 106 | 107 | let bindings = Bindings { 108 | vertex_buffers: vec![vertex_buffer, attribute_buffer], 109 | index_buffer, 110 | images: vec![texture], 111 | }; 112 | 113 | Ok(Image { 114 | width: texture.width as u16, 115 | height: texture.height as u16, 116 | texture, 117 | bindings, 118 | blend_mode: None, 119 | dirty_filter: DirtyFlag::new(false), 120 | filter, 121 | bindings_clones_hack: Arc::new(()), 122 | texture_clones_hack: Arc::new(()), 123 | }) 124 | } 125 | 126 | /// A little helper function that creates a new `Image` that is just 127 | /// a solid square of the given size and color. Mainly useful for 128 | /// debugging. 129 | pub fn solid( 130 | context: &mut Context, 131 | quad_ctx: &mut miniquad::graphics::GraphicsContext, 132 | size: u16, 133 | color: Color, 134 | ) -> GameResult { 135 | let (r, g, b, a) = color.into(); 136 | let pixel_array: [u8; 4] = [r, g, b, a]; 137 | let size_squared = usize::from(size) * usize::from(size); 138 | let mut buffer = Vec::with_capacity(size_squared); 139 | for _i in 0..size_squared { 140 | buffer.extend(&pixel_array[..]); 141 | } 142 | Image::from_rgba8(context, quad_ctx, size, size, &buffer) 143 | } 144 | 145 | pub fn width(&self) -> u16 { 146 | self.width 147 | } 148 | 149 | pub fn height(&self) -> u16 { 150 | self.height 151 | } 152 | 153 | /// Returns the dimensions of the image. 154 | pub fn dimensions(&self) -> Rect { 155 | Rect::new(0.0, 0.0, self.width() as f32, self.height() as f32) 156 | } 157 | 158 | pub fn set_filter(&mut self, filter: FilterMode) { 159 | self.dirty_filter.store(true); 160 | self.filter = filter; 161 | } 162 | 163 | pub fn filter(&self) -> FilterMode { 164 | self.filter 165 | } 166 | 167 | /// Draws without adapting the scaling. 168 | pub(crate) fn draw_image_raw( 169 | &self, 170 | ctx: &mut Context, 171 | quad_ctx: &mut miniquad::graphics::GraphicsContext, 172 | param: DrawParam, 173 | ) -> GameResult { 174 | let instance = InstanceAttributes::from(¶m); 175 | self.bindings.vertex_buffers[1].update(quad_ctx, &[instance]); 176 | 177 | if self.dirty_filter.load() { 178 | self.dirty_filter.store(false); 179 | self.texture.set_filter(quad_ctx, self.filter); 180 | } 181 | 182 | let pass = ctx.framebuffer(); 183 | quad_ctx.begin_pass(pass, PassAction::Nothing); 184 | quad_ctx.apply_bindings(&self.bindings); 185 | 186 | let shader_id = *ctx.gfx_context.current_shader.borrow(); 187 | let current_shader = &mut ctx.gfx_context.shaders[shader_id]; 188 | quad_ctx.apply_pipeline(¤t_shader.pipeline); 189 | 190 | apply_uniforms(ctx, quad_ctx, shader_id, None); 191 | 192 | let mut custom_blend = false; 193 | if let Some(blend_mode) = self.blend_mode() { 194 | custom_blend = true; 195 | crate::graphics::set_current_blend_mode(quad_ctx, blend_mode) 196 | } 197 | 198 | quad_ctx.draw(0, 6, 1); 199 | 200 | // restore default blend mode 201 | if custom_blend { 202 | crate::graphics::restore_blend_mode(ctx, quad_ctx); 203 | } 204 | 205 | quad_ctx.end_render_pass(); 206 | 207 | Ok(()) 208 | } 209 | } 210 | 211 | impl Drawable for Image { 212 | fn draw( 213 | &self, 214 | ctx: &mut Context, 215 | quad_ctx: &mut miniquad::graphics::GraphicsContext, 216 | param: DrawParam, 217 | ) -> GameResult { 218 | let src_width = param.src.w; 219 | let src_height = param.src.h; 220 | // We have to mess with the scale to make everything 221 | // be its-unit-size-in-pixels. 222 | let scale_x = src_width * f32::from(self.width); 223 | let scale_y = src_height * f32::from(self.height); 224 | 225 | let new_param = match param.trans { 226 | crate::graphics::Transform::Values { scale, .. } => param.scale(mint::Vector2 { 227 | x: scale.x * scale_x, 228 | y: scale.y * scale_y, 229 | }), 230 | crate::graphics::Transform::Matrix(m) => param.transform( 231 | Matrix4::from(m) * Matrix4::from_nonuniform_scale(scale_x, scale_y, 1.0), 232 | ), 233 | }; 234 | 235 | self.draw_image_raw(ctx, quad_ctx, new_param) 236 | } 237 | 238 | fn set_blend_mode(&mut self, mode: Option) { 239 | self.blend_mode = mode; 240 | } 241 | 242 | /// Gets the blend mode to be used when drawing this drawable. 243 | fn blend_mode(&self) -> Option { 244 | self.blend_mode 245 | } 246 | 247 | fn dimensions(&self, _ctx: &mut Context) -> Option { 248 | Some(self.dimensions()) 249 | } 250 | } 251 | 252 | impl Drop for Image { 253 | fn drop(&mut self) { 254 | if Arc::strong_count(&self.bindings_clones_hack) == 1 { 255 | crate::graphics::add_dropped_bindings( 256 | self.bindings.clone(), 257 | Arc::strong_count(&self.texture_clones_hack) == 1, 258 | ); 259 | } 260 | } 261 | } 262 | 263 | #[derive(Debug)] 264 | struct DirtyFlag(AtomicBool); 265 | 266 | impl DirtyFlag { 267 | pub fn new(value: bool) -> Self { 268 | Self(AtomicBool::new(value)) 269 | } 270 | 271 | pub fn load(&self) -> bool { 272 | self.0.load(Ordering::Acquire) 273 | } 274 | 275 | pub fn store(&self, value: bool) { 276 | self.0.store(value, Ordering::Release) 277 | } 278 | } 279 | 280 | impl Clone for DirtyFlag { 281 | fn clone(&self) -> Self { 282 | DirtyFlag(AtomicBool::new(self.0.load(Ordering::Acquire))) 283 | } 284 | } 285 | -------------------------------------------------------------------------------- /src/graphics/spritebatch.rs: -------------------------------------------------------------------------------- 1 | use crate::{ 2 | error::GameResult, 3 | graphics::{ 4 | self, apply_uniforms, transform_rect, BlendMode, DrawParam, FilterMode, InstanceAttributes, 5 | Rect, 6 | }, 7 | Context, 8 | }; 9 | 10 | use std::cell::RefCell; 11 | 12 | use miniquad::{Buffer, BufferType, PassAction}; 13 | 14 | #[derive(Debug)] 15 | pub struct SpriteBatch { 16 | image: RefCell, 17 | sprites: Vec, 18 | gpu_sprites: RefCell>, 19 | } 20 | 21 | /// An index of a particular sprite in a `SpriteBatch`. 22 | #[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] 23 | pub struct SpriteIdx(usize); 24 | 25 | impl SpriteBatch { 26 | /// Creates a new `SpriteBatch`, drawing with the given image. 27 | /// 28 | /// Takes ownership of the `Image`, but cloning an `Image` is 29 | /// cheap since they have an internal `Arc` containing the actual 30 | /// image data. 31 | pub fn new(image: graphics::Image) -> Self { 32 | Self { 33 | image: RefCell::new(image), 34 | sprites: vec![], 35 | gpu_sprites: RefCell::new(vec![]), 36 | } 37 | } 38 | 39 | /// Adds a new sprite to the sprite batch. 40 | /// 41 | /// Returns a handle with which type to modify the sprite using 42 | /// [`set()`](#method.set) 43 | pub fn add

(&mut self, param: P) -> SpriteIdx 44 | where 45 | P: Into, 46 | { 47 | let param = param.into(); 48 | self.sprites.push(param); 49 | SpriteIdx(self.sprites.len() - 1) 50 | } 51 | 52 | /// Alters a sprite in the batch to use the given draw params 53 | pub fn set

(&mut self, handle: SpriteIdx, param: P) -> GameResult 54 | where 55 | P: Into, 56 | { 57 | if handle.0 < self.sprites.len() { 58 | self.sprites[handle.0] = param.into(); 59 | Ok(()) 60 | } else { 61 | Err(crate::error::GameError::RenderError(String::from( 62 | "Provided index is out of bounds.", 63 | ))) 64 | } 65 | } 66 | 67 | /// Returns a reference to the sprites. 68 | pub fn get_sprites(&self) -> &[DrawParam] { 69 | &self.sprites 70 | } 71 | 72 | /// Returns a mutable reference to the sprites. 73 | /// 74 | /// Unlike with `MeshBatch`, manually calling `flush` after altering sprites 75 | /// in this slice is currently unnecessary, as `SpriteBatch` flushes automatically 76 | /// on every draw call. This might change in the future though. 77 | pub fn get_sprites_mut(&mut self) -> &mut [DrawParam] { 78 | &mut self.sprites 79 | } 80 | 81 | /// Removes all data from the sprite batch. 82 | pub fn clear(&mut self) { 83 | self.sprites.clear(); 84 | } 85 | 86 | /// Unwraps and returns the contained `Image` 87 | pub fn into_inner(self) -> graphics::Image { 88 | self.image.into_inner() 89 | } 90 | 91 | /// Replaces the contained `Image`, returning the old one. 92 | pub fn set_image(&mut self, image: graphics::Image) -> graphics::Image { 93 | use std::mem; 94 | 95 | self.gpu_sprites = RefCell::new(vec![]); 96 | let mut self_image = self.image.borrow_mut(); 97 | mem::replace(&mut *self_image, image) 98 | } 99 | 100 | /// Set the filter mode for the SpriteBatch. 101 | pub fn set_filter(&mut self, mode: FilterMode) { 102 | self.image.borrow_mut().set_filter(mode); 103 | } 104 | } 105 | 106 | impl graphics::Drawable for SpriteBatch { 107 | fn draw( 108 | &self, 109 | ctx: &mut Context, 110 | quad_ctx: &mut miniquad::graphics::GraphicsContext, 111 | param: DrawParam, 112 | ) -> GameResult { 113 | { 114 | let mut image = self.image.borrow_mut(); 115 | let mut gpu_sprites = self.gpu_sprites.borrow_mut(); 116 | 117 | if self.sprites.len() > gpu_sprites.len() { 118 | gpu_sprites.resize(self.sprites.len(), InstanceAttributes::default()); 119 | 120 | let buffer = Buffer::stream( 121 | quad_ctx, 122 | BufferType::VertexBuffer, 123 | std::mem::size_of::() * self.sprites.len(), 124 | ); 125 | 126 | image.bindings.vertex_buffers[1].delete(); 127 | image.bindings.vertex_buffers[1] = buffer; 128 | } 129 | 130 | for (n, param) in self.sprites.iter().enumerate() { 131 | let mut new_param = *param; 132 | let src_width = param.src.w; 133 | let src_height = param.src.h; 134 | // We have to mess with the scale to make everything 135 | // be its-unit-size-in-pixels. 136 | let scale_x = src_width * f32::from(image.width); 137 | let scale_y = src_height * f32::from(image.height); 138 | 139 | use crate::graphics::Transform; 140 | new_param = match new_param.trans { 141 | Transform::Values { scale, .. } => new_param.scale(mint::Vector2 { 142 | x: scale.x * scale_x, 143 | y: scale.y * scale_y, 144 | }), 145 | Transform::Matrix(m) => new_param.transform( 146 | cgmath::Matrix4::from(m) 147 | * cgmath::Matrix4::from_nonuniform_scale(scale_x, scale_y, 1.0), 148 | ), 149 | }; 150 | 151 | let instance = InstanceAttributes::from(&new_param); 152 | gpu_sprites[n] = instance; 153 | } 154 | 155 | image.bindings.vertex_buffers[1].update(quad_ctx, &gpu_sprites[0..self.sprites.len()]); 156 | 157 | let pass = ctx.framebuffer(); 158 | quad_ctx.begin_pass(pass, PassAction::Nothing); 159 | quad_ctx.apply_bindings(&image.bindings); 160 | } 161 | let shader_id = *ctx.gfx_context.current_shader.borrow(); 162 | let current_shader = &mut ctx.gfx_context.shaders[shader_id]; 163 | quad_ctx.apply_pipeline(¤t_shader.pipeline); 164 | 165 | apply_uniforms( 166 | ctx, 167 | quad_ctx, 168 | shader_id, 169 | Some(cgmath::Matrix4::from(param.trans.to_bare_matrix())), 170 | ); 171 | 172 | let mut custom_blend = false; 173 | if let Some(blend_mode) = self.blend_mode() { 174 | custom_blend = true; 175 | crate::graphics::set_current_blend_mode(quad_ctx, blend_mode) 176 | } 177 | 178 | quad_ctx.draw(0, 6, self.sprites.len() as i32); 179 | 180 | // restore default blend mode 181 | if custom_blend { 182 | crate::graphics::restore_blend_mode(ctx, quad_ctx); 183 | } 184 | 185 | quad_ctx.end_render_pass(); 186 | 187 | Ok(()) 188 | } 189 | 190 | fn dimensions(&self, _ctx: &mut Context) -> Option { 191 | if self.sprites.is_empty() { 192 | return None; 193 | } 194 | let dimensions = self.image.borrow().dimensions(); 195 | self.sprites 196 | .iter() 197 | .map(|¶m| transform_rect(dimensions, param)) 198 | .fold(None, |acc: Option, rect| { 199 | Some(if let Some(acc) = acc { 200 | acc.combine_with(rect) 201 | } else { 202 | rect 203 | }) 204 | }) 205 | } 206 | 207 | fn set_blend_mode(&mut self, mode: Option) { 208 | self.image.get_mut().set_blend_mode(mode); 209 | } 210 | 211 | fn blend_mode(&self) -> Option { 212 | self.image.borrow().blend_mode() 213 | } 214 | } 215 | -------------------------------------------------------------------------------- /src/input.rs: -------------------------------------------------------------------------------- 1 | pub(crate) mod input_handler; 2 | 3 | #[cfg(not(any(target_arch = "wasm32", target_os = "ios", target_os = "android",)))] 4 | pub mod gamepad; 5 | 6 | pub mod keyboard; 7 | pub mod mouse; 8 | 9 | pub use self::{input_handler::MouseButton, keyboard::KeyboardContext, mouse::MouseContext}; 10 | -------------------------------------------------------------------------------- /src/input/gamepad.rs: -------------------------------------------------------------------------------- 1 | //! Gamepad utility functions. 2 | //! 3 | //! This is going to be a bit of a work-in-progress as gamepad input 4 | //! gets fleshed out. The `gilrs` crate needs help to add better 5 | //! cross-platform support. Why not give it a hand? 6 | use gilrs::ConnectedGamepadsIterator; 7 | use std::fmt; 8 | 9 | pub use gilrs::{self, Event, Gamepad, Gilrs}; 10 | 11 | /// A unique identifier for a particular GamePad 12 | #[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)] 13 | pub struct GamepadId(pub(crate) gilrs::GamepadId); 14 | 15 | use crate::context::Context; 16 | use crate::error::GameResult; 17 | 18 | /// Trait object defining a gamepad/joystick context. 19 | pub trait GamepadContext { 20 | /// Returns a gamepad event. 21 | fn next_event(&mut self) -> Option; 22 | 23 | /// returns the `Gamepad` associated with an id. 24 | fn gamepad(&self, id: GamepadId) -> Gamepad; 25 | 26 | /// returns an iterator over the connected `Gamepad`s. 27 | fn gamepads(&self) -> GamepadsIterator; 28 | } 29 | 30 | /// A structure that contains gamepad state using `gilrs`. 31 | pub struct GilrsGamepadContext { 32 | pub(crate) gilrs: Gilrs, 33 | } 34 | 35 | impl fmt::Debug for GilrsGamepadContext { 36 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 37 | write!(f, "", self) 38 | } 39 | } 40 | 41 | impl GilrsGamepadContext { 42 | pub(crate) fn new() -> GameResult { 43 | let gilrs = Gilrs::new()?; 44 | Ok(GilrsGamepadContext { gilrs }) 45 | } 46 | } 47 | 48 | impl From for GilrsGamepadContext { 49 | /// Converts from a `Gilrs` custom instance to a `GilrsGamepadContext` 50 | fn from(gilrs: Gilrs) -> Self { 51 | Self { gilrs } 52 | } 53 | } 54 | 55 | impl GamepadContext for GilrsGamepadContext { 56 | fn next_event(&mut self) -> Option { 57 | self.gilrs.next_event() 58 | } 59 | 60 | fn gamepad(&self, id: GamepadId) -> Gamepad { 61 | self.gilrs.gamepad(id.0) 62 | } 63 | 64 | fn gamepads(&self) -> GamepadsIterator { 65 | GamepadsIterator { 66 | wrapped: self.gilrs.gamepads(), 67 | } 68 | } 69 | } 70 | 71 | /// An iterator of the connected gamepads 72 | pub struct GamepadsIterator<'a> { 73 | wrapped: ConnectedGamepadsIterator<'a>, 74 | } 75 | 76 | impl<'a> fmt::Debug for GamepadsIterator<'a> { 77 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 78 | write!(f, "", self) 79 | } 80 | } 81 | 82 | impl<'a> Iterator for GamepadsIterator<'a> { 83 | type Item = (GamepadId, Gamepad<'a>); 84 | 85 | fn next(&mut self) -> Option<(GamepadId, Gamepad<'a>)> { 86 | self.wrapped.next().map(|(id, gp)| (GamepadId(id), gp)) 87 | } 88 | } 89 | 90 | /// A structure that implements [`GamepadContext`](trait.GamepadContext.html) 91 | /// but does nothing; a stub for when you don't need it or are 92 | /// on a platform that `gilrs` doesn't support. 93 | #[derive(Debug, Clone, Copy, Default)] 94 | pub(crate) struct NullGamepadContext {} 95 | 96 | impl GamepadContext for NullGamepadContext { 97 | fn next_event(&mut self) -> Option { 98 | None 99 | } 100 | 101 | fn gamepad(&self, _id: GamepadId) -> Gamepad { 102 | panic!("Gamepad module disabled") 103 | } 104 | 105 | fn gamepads(&self) -> GamepadsIterator { 106 | panic!("Gamepad module disabled") 107 | } 108 | } 109 | 110 | /// Returns the `Gamepad` associated with an `id`. 111 | pub fn gamepad(ctx: &Context, id: GamepadId) -> Gamepad { 112 | ctx.gamepad_context.gamepad(id) 113 | } 114 | 115 | /// Return an iterator of all the `Gamepads` that are connected. 116 | pub fn gamepads(ctx: &Context) -> GamepadsIterator { 117 | ctx.gamepad_context.gamepads() 118 | } 119 | 120 | // Properties gamepads might want: 121 | // Number of buttons 122 | // Number of axes 123 | // Name/ID 124 | // Is it connected? (For consoles?) 125 | // Whether or not they support vibration 126 | 127 | /* 128 | /// Lists all gamepads. With metainfo, maybe? 129 | pub fn list_gamepads() { 130 | unimplemented!() 131 | } 132 | 133 | /// Returns the state of the given axis on a gamepad. 134 | pub fn axis() { 135 | unimplemented!() 136 | } 137 | 138 | /// Returns the state of the given button on a gamepad. 139 | pub fn button_pressed() { 140 | unimplemented!() 141 | } 142 | */ 143 | 144 | #[cfg(test)] 145 | mod tests { 146 | use super::*; 147 | 148 | #[test] 149 | fn gilrs_init() { 150 | assert!(GilrsGamepadContext::new().is_ok()); 151 | } 152 | } 153 | -------------------------------------------------------------------------------- /src/input/input_handler.rs: -------------------------------------------------------------------------------- 1 | #![allow(dead_code)] 2 | 3 | use cgmath::Point2; 4 | use std::collections::HashSet; 5 | 6 | use miniquad::MouseButton as QuadMouseButton; 7 | 8 | #[derive(Hash, Debug, Eq, PartialEq, Clone, Copy)] 9 | pub enum MouseButton { 10 | Left, 11 | Middle, 12 | Right, 13 | Button4, 14 | Button5, 15 | } 16 | 17 | impl From for MouseButton { 18 | fn from(button: QuadMouseButton) -> MouseButton { 19 | match button { 20 | QuadMouseButton::Left => MouseButton::Left, 21 | QuadMouseButton::Right => MouseButton::Right, 22 | QuadMouseButton::Middle => MouseButton::Middle, 23 | QuadMouseButton::Unknown => MouseButton::Button4, 24 | } 25 | } 26 | } 27 | 28 | pub struct InputHandler { 29 | pub keys: HashSet, 30 | pub frame_keys: HashSet, 31 | pub mouse_position: Point2, 32 | pub mouse_keys: HashSet, 33 | pub wheel: f32, 34 | } 35 | 36 | impl Default for InputHandler { 37 | fn default() -> Self { 38 | InputHandler::new() 39 | } 40 | } 41 | 42 | impl InputHandler { 43 | pub fn new() -> InputHandler { 44 | InputHandler { 45 | keys: HashSet::new(), 46 | frame_keys: HashSet::new(), 47 | mouse_position: Point2::new(0., 0.), 48 | mouse_keys: HashSet::new(), 49 | wheel: 0., 50 | } 51 | } 52 | 53 | pub fn handle_mouse_move(&mut self, mouse_x: f32, mouse_y: f32) { 54 | self.mouse_position = Point2::new(mouse_x, mouse_y); 55 | } 56 | 57 | pub fn handle_mouse_down(&mut self, button: miniquad::MouseButton) { 58 | self.mouse_keys.insert(MouseButton::from(button)); 59 | } 60 | 61 | pub fn handle_mouse_up(&mut self, button: miniquad::MouseButton) { 62 | self.mouse_keys.remove(&MouseButton::from(button)); 63 | } 64 | 65 | pub fn handle_key_down(&mut self, key: String) { 66 | self.keys.insert(key.clone()); 67 | self.frame_keys.insert(key); 68 | } 69 | 70 | pub fn handle_end_frame(&mut self) { 71 | self.frame_keys.clear(); 72 | self.wheel = 0.; 73 | } 74 | 75 | pub fn handle_key_up(&mut self, key: String) { 76 | self.keys.remove(&key); 77 | } 78 | 79 | pub fn handle_mouse_wheel(&mut self, delta_y: f64) { 80 | self.wheel = delta_y as f32; 81 | } 82 | 83 | pub fn is_key_pressed(&self, key: &str) -> bool { 84 | self.keys.contains(key) 85 | } 86 | 87 | pub fn is_key_down(&self, key: &str) -> bool { 88 | self.frame_keys.contains(key) 89 | } 90 | 91 | pub fn is_mouse_key_down(&self, key: &MouseButton) -> bool { 92 | self.mouse_keys.contains(key) 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /src/input/keyboard.rs: -------------------------------------------------------------------------------- 1 | use bitflags::bitflags; 2 | 3 | use crate::Context; 4 | 5 | /// A key code. 6 | pub use miniquad::KeyCode; 7 | use std::collections::HashSet; 8 | 9 | /// Tracks held down keyboard keys, active keyboard modifiers, 10 | /// and figures out if the system is sending repeat keystrokes. 11 | #[derive(Clone, Debug)] 12 | pub struct KeyboardContext { 13 | active_modifiers: KeyMods, 14 | /// A simple mapping of which key code has been pressed. 15 | /// We COULD use a `Vec` but turning Rust enums to and from 16 | /// integers is unsafe and a set really is what we want anyway. 17 | pressed_keys_set: HashSet, 18 | 19 | // These two are necessary for tracking key-repeat. 20 | last_pressed: Option, 21 | current_pressed: Option, 22 | } 23 | 24 | impl KeyboardContext { 25 | pub(crate) fn new() -> Self { 26 | Self { 27 | active_modifiers: KeyMods::empty(), 28 | // We just use 256 as a number Big Enough For Keyboard Keys to try to avoid resizing. 29 | pressed_keys_set: HashSet::with_capacity(256), 30 | last_pressed: None, 31 | current_pressed: None, 32 | } 33 | } 34 | 35 | pub(crate) fn set_key(&mut self, key: KeyCode, pressed: bool) { 36 | if pressed { 37 | let _ = self.pressed_keys_set.insert(key); 38 | self.last_pressed = self.current_pressed; 39 | self.current_pressed = Some(key); 40 | } else { 41 | let _ = self.pressed_keys_set.remove(&key); 42 | self.current_pressed = None; 43 | } 44 | 45 | self.set_key_modifier(key, pressed); 46 | } 47 | 48 | /// Take a modifier key code and alter our state. 49 | /// 50 | /// Double check that this edge handling is necessary; 51 | /// winit sounds like it should do this for us, 52 | /// see https://docs.rs/winit/0.18.0/winit/struct.KeyboardInput.html#structfield.modifiers 53 | /// 54 | /// ...more specifically, we should refactor all this to consistant-ify events a bit and 55 | /// make winit do more of the work. 56 | /// But to quote Scott Pilgrim, "This is... this is... Booooooring." 57 | fn set_key_modifier(&mut self, key: KeyCode, pressed: bool) { 58 | if pressed { 59 | match key { 60 | KeyCode::LeftShift | KeyCode::RightShift => self.active_modifiers |= KeyMods::SHIFT, 61 | KeyCode::LeftControl | KeyCode::RightControl => { 62 | self.active_modifiers |= KeyMods::CTRL 63 | } 64 | KeyCode::LeftAlt | KeyCode::RightAlt => self.active_modifiers |= KeyMods::ALT, 65 | KeyCode::LeftSuper | KeyCode::RightSuper => self.active_modifiers |= KeyMods::LOGO, 66 | _ => (), 67 | } 68 | } else { 69 | match key { 70 | KeyCode::LeftShift | KeyCode::RightShift => self.active_modifiers -= KeyMods::SHIFT, 71 | KeyCode::LeftControl | KeyCode::RightControl => { 72 | self.active_modifiers -= KeyMods::CTRL 73 | } 74 | KeyCode::LeftAlt | KeyCode::RightAlt => self.active_modifiers -= KeyMods::ALT, 75 | KeyCode::LeftSuper | KeyCode::RightSuper => self.active_modifiers -= KeyMods::LOGO, 76 | _ => (), 77 | } 78 | } 79 | } 80 | 81 | //pub(crate) fn set_modifiers(&mut self, keymods: KeyMods) { 82 | // self.active_modifiers = keymods; 83 | //} 84 | 85 | pub(crate) fn is_key_pressed(&self, key: KeyCode) -> bool { 86 | self.pressed_keys_set.contains(&key) 87 | } 88 | 89 | pub(crate) fn is_key_repeated(&self) -> bool { 90 | if self.last_pressed.is_some() { 91 | self.last_pressed == self.current_pressed 92 | } else { 93 | false 94 | } 95 | } 96 | 97 | pub(crate) fn pressed_keys(&self) -> &HashSet { 98 | &self.pressed_keys_set 99 | } 100 | 101 | pub(crate) fn active_mods(&self) -> KeyMods { 102 | self.active_modifiers 103 | } 104 | } 105 | 106 | impl Default for KeyboardContext { 107 | fn default() -> Self { 108 | Self::new() 109 | } 110 | } 111 | 112 | /// Checks if a key is currently pressed down. 113 | pub fn is_key_pressed(ctx: &Context, key: KeyCode) -> bool { 114 | ctx.keyboard_context.is_key_pressed(key) 115 | } 116 | 117 | /// Checks if the last keystroke sent by the system is repeated, like when a key is held down for a period of time. 118 | pub fn is_key_repeated(ctx: &Context) -> bool { 119 | ctx.keyboard_context.is_key_repeated() 120 | } 121 | 122 | /// Returns a reference to the set of currently pressed keys. 123 | pub fn pressed_keys(ctx: &Context) -> &HashSet { 124 | ctx.keyboard_context.pressed_keys() 125 | } 126 | 127 | /// Returns currently active keyboard modifiers. 128 | pub fn active_mods(ctx: &Context) -> KeyMods { 129 | ctx.keyboard_context.active_mods() 130 | } 131 | 132 | /// Checks if keyboard modifier (or several) is active. 133 | pub fn is_mod_active(ctx: &Context, keymods: KeyMods) -> bool { 134 | ctx.keyboard_context.active_mods().contains(keymods) 135 | } 136 | 137 | bitflags! { 138 | /// Bitflags describing the state of keyboard modifiers, such as `Control` or `Shift`. 139 | #[derive(Default)] 140 | pub struct KeyMods: u8 { 141 | /// No modifiers; equivalent to `KeyMods::default()` and 142 | /// [`KeyMods::empty()`](struct.KeyMods.html#method.empty). 143 | const NONE = 0b0000_0000; 144 | /// Left or right Shift key. 145 | const SHIFT = 0b0000_0001; 146 | /// Left or right Control key. 147 | const CTRL = 0b0000_0010; 148 | /// Left or right Alt key. 149 | const ALT = 0b0000_0100; 150 | /// Left or right Win/Cmd/equivalent key. 151 | const LOGO = 0b0000_1000; 152 | } 153 | } 154 | 155 | impl From for KeyMods { 156 | fn from(quad_mods: miniquad::KeyMods) -> Self { 157 | let mut keymods = KeyMods::NONE; 158 | 159 | if quad_mods.shift { 160 | keymods |= KeyMods::SHIFT; 161 | } 162 | if quad_mods.ctrl { 163 | keymods |= KeyMods::CTRL; 164 | } 165 | if quad_mods.alt { 166 | keymods |= KeyMods::ALT; 167 | } 168 | if quad_mods.logo { 169 | keymods |= KeyMods::LOGO; 170 | } 171 | 172 | keymods 173 | } 174 | } 175 | -------------------------------------------------------------------------------- /src/input/mouse.rs: -------------------------------------------------------------------------------- 1 | use super::input_handler::InputHandler; 2 | use crate::Context; 3 | 4 | use crate::graphics::Point2; 5 | pub use crate::input::input_handler::MouseButton; 6 | 7 | pub struct MouseContext { 8 | pub(crate) input_handler: InputHandler, 9 | last_position: Point2, 10 | delta: Point2, 11 | cursor_grabbed: bool, 12 | cursor_hidden: bool, 13 | cursor_type: miniquad::CursorIcon, 14 | } 15 | 16 | impl MouseContext { 17 | pub fn new(input_handler: InputHandler) -> Self { 18 | MouseContext { 19 | input_handler, 20 | last_position: Point2::new(0., 0.), 21 | delta: Point2::new(0., 0.), 22 | cursor_grabbed: false, 23 | cursor_hidden: false, 24 | cursor_type: miniquad::CursorIcon::Default, 25 | } 26 | } 27 | 28 | pub(crate) fn set_last_position(&mut self, p: Point2) { 29 | self.last_position = p; 30 | } 31 | 32 | /// Resets the value returned by [`mouse::delta`](fn.delta.html) to zero. 33 | /// You shouldn't need to call this, except when you're running your own event loop. 34 | /// In this case call it right at the end, after `draw` and `update` have finished. 35 | pub fn reset_delta(&mut self) { 36 | self.delta = Point2::new(0., 0.); 37 | } 38 | 39 | pub(crate) fn set_delta(&mut self, p: Point2) { 40 | self.delta = p; 41 | } 42 | 43 | pub fn mouse_position(&self) -> cgmath::Point2 { 44 | self.input_handler.mouse_position 45 | } 46 | 47 | pub fn button_pressed(&self, button: MouseButton) -> bool { 48 | self.input_handler.is_mouse_key_down(&button) 49 | } 50 | 51 | pub fn wheel(&self) -> f32 { 52 | self.input_handler.wheel 53 | } 54 | } 55 | 56 | /// The current mouse position in pixels. 57 | pub fn position(ctx: &Context) -> mint::Point2 { 58 | ctx.mouse_context.mouse_position().into() 59 | } 60 | 61 | /// Whether a certain mouse button is currently pressed. 62 | pub fn button_pressed(ctx: &Context, button: MouseButton) -> bool { 63 | ctx.mouse_context.button_pressed(button) 64 | } 65 | 66 | pub fn wheel(ctx: &Context) -> f32 { 67 | ctx.mouse_context.wheel() 68 | } 69 | 70 | /// Get the distance the cursor was moved during the current frame, in pixels. 71 | pub fn delta(ctx: &Context) -> mint::Point2 { 72 | ctx.mouse_context.delta.into() 73 | } 74 | 75 | /// The position the mouse had during the latest `mouse_motion_event` 76 | pub fn last_position(ctx: &Context) -> mint::Point2 { 77 | ctx.mouse_context.last_position.into() 78 | } 79 | 80 | /// Get whether or not the mouse is grabbed (confined to the window) 81 | pub fn cursor_grabbed(ctx: &Context) -> bool { 82 | ctx.mouse_context.cursor_grabbed 83 | } 84 | 85 | /// Set whether or not the mouse is grabbed (confined to the window) 86 | pub fn set_cursor_grabbed( 87 | ctx: &mut Context, 88 | quad_ctx: &mut miniquad::GraphicsContext, 89 | grabbed: bool, 90 | ) { 91 | ctx.mouse_context.cursor_grabbed = grabbed; 92 | quad_ctx.set_cursor_grab(grabbed); 93 | } 94 | 95 | /// Returns the current mouse cursor type of the window. 96 | pub fn cursor_type(ctx: &Context) -> miniquad::CursorIcon { 97 | ctx.mouse_context.cursor_type 98 | } 99 | 100 | /// Modifies the mouse cursor type of the window. 101 | pub fn set_cursor_type( 102 | ctx: &mut Context, 103 | quad_ctx: &mut miniquad::graphics::GraphicsContext, 104 | cursor_type: miniquad::CursorIcon, 105 | ) { 106 | ctx.mouse_context.cursor_type = cursor_type; 107 | quad_ctx.set_mouse_cursor(cursor_type); 108 | } 109 | 110 | /// Set whether or not the mouse is hidden (invisible) 111 | pub fn cursor_hidden(ctx: &Context) -> bool { 112 | ctx.mouse_context.cursor_hidden 113 | } 114 | 115 | /// Set whether or not the mouse is hidden (invisible). 116 | pub fn set_cursor_hidden( 117 | ctx: &mut Context, 118 | quad_ctx: &mut miniquad::graphics::GraphicsContext, 119 | hidden: bool, 120 | ) { 121 | ctx.mouse_context.cursor_hidden = hidden; 122 | quad_ctx.show_mouse(!hidden); 123 | } 124 | -------------------------------------------------------------------------------- /src/timer.rs: -------------------------------------------------------------------------------- 1 | //! Timing and measurement functions. 2 | //! 3 | //! good-web-game does not try to do any framerate limitation by default (because it can't). 4 | //! 5 | //! For a detailed tutorial in how to handle frame timings in games, 6 | //! see 7 | 8 | use crate::context::Context; 9 | 10 | use std::cmp; 11 | use std::f64; 12 | use std::time; 13 | use std::time::Duration; 14 | 15 | type Instant = f64; 16 | 17 | /// Returns the system time. 18 | pub fn time() -> f64 { 19 | miniquad::date::now() 20 | } 21 | 22 | /// A simple buffer that fills 23 | /// up to a limit and then holds the last 24 | /// N items that have been inserted into it, 25 | /// overwriting old ones in a round-robin fashion. 26 | /// 27 | /// It's not quite a ring buffer 'cause you can't 28 | /// remove items from it, it just holds the last N 29 | /// things. 30 | #[derive(Debug, Clone)] 31 | struct LogBuffer 32 | where 33 | T: Clone, 34 | { 35 | head: usize, 36 | size: usize, 37 | /// The number of actual samples inserted, used for 38 | /// smarter averaging. 39 | samples: usize, 40 | contents: Vec, 41 | } 42 | 43 | impl LogBuffer 44 | where 45 | T: Clone + Copy, 46 | { 47 | fn new(size: usize, init_val: T) -> LogBuffer { 48 | LogBuffer { 49 | head: 0, 50 | size, 51 | contents: vec![init_val; size], 52 | // Never divide by 0 53 | samples: 1, 54 | } 55 | } 56 | 57 | /// Pushes a new item into the `LogBuffer`, overwriting 58 | /// the oldest item in it. 59 | fn push(&mut self, item: T) { 60 | self.head = (self.head + 1) % self.contents.len(); 61 | self.contents[self.head] = item; 62 | self.size = cmp::min(self.size + 1, self.contents.len()); 63 | self.samples += 1; 64 | } 65 | 66 | /// Returns a slice pointing at the contents of the buffer. 67 | /// They are in *no particular order*, and if not all the 68 | /// slots are filled, the empty slots will be present but 69 | /// contain the initial value given to [`new()`](#method.new). 70 | /// 71 | /// We're only using this to log FPS for a short time, 72 | /// so we don't care for the second or so when it's inaccurate. 73 | fn contents(&self) -> &[T] { 74 | if self.samples > self.size { 75 | &self.contents 76 | } else { 77 | &self.contents[..self.samples] 78 | } 79 | } 80 | 81 | /// Returns the most recent value in the buffer. 82 | fn latest(&self) -> T { 83 | self.contents[self.head] 84 | } 85 | } 86 | 87 | /// A structure that contains our time-tracking state. 88 | #[derive(Debug)] 89 | pub struct TimeContext { 90 | init_instant: Instant, 91 | last_instant: Instant, 92 | frame_durations: LogBuffer, 93 | residual_update_dt: Duration, 94 | frame_count: usize, 95 | } 96 | 97 | // How many frames we log update times for. 98 | const TIME_LOG_FRAMES: usize = 200; 99 | 100 | impl TimeContext { 101 | /// Creates a new `TimeContext` and initializes the start to this instant. 102 | pub fn new() -> TimeContext { 103 | let initial_dt = time::Duration::from_millis(16); 104 | TimeContext { 105 | init_instant: time(), 106 | last_instant: time(), 107 | frame_durations: LogBuffer::new(TIME_LOG_FRAMES, initial_dt), 108 | residual_update_dt: time::Duration::from_secs(0), 109 | frame_count: 0, 110 | } 111 | } 112 | 113 | /// Update the state of the `TimeContext` to record that 114 | /// another frame has taken place. Necessary for the FPS 115 | /// tracking and [`check_update_time()`](fn.check_update_time.html) 116 | /// functions to work. 117 | /// 118 | /// It's usually not necessary to call this function yourself, 119 | /// [`event::run()`](../event/fn.run.html) will do it for you. 120 | pub fn tick(&mut self) { 121 | let now = time(); 122 | let time_since_last = now - self.last_instant; 123 | self.frame_durations.push(f64_to_duration(time_since_last)); 124 | self.last_instant = now; 125 | self.frame_count += 1; 126 | 127 | self.residual_update_dt += f64_to_duration(time_since_last); 128 | } 129 | } 130 | 131 | impl Default for TimeContext { 132 | fn default() -> Self { 133 | Self::new() 134 | } 135 | } 136 | 137 | /// Get the time between the start of the last frame and the current one; 138 | /// in other words, the length of the last frame. 139 | pub fn delta(ctx: &Context) -> Duration { 140 | let tc = &ctx.timer_context; 141 | tc.frame_durations.latest() 142 | } 143 | 144 | /// Gets the average time of a frame, averaged 145 | /// over the last 200 frames. 146 | pub fn average_delta(ctx: &Context) -> Duration { 147 | let tc = &ctx.timer_context; 148 | let sum: Duration = tc.frame_durations.contents().iter().sum(); 149 | // If our buffer is actually full, divide by its size. 150 | // Otherwise divide by the number of samples we've added 151 | if tc.frame_durations.samples > tc.frame_durations.size { 152 | sum / (tc.frame_durations.size as u32) 153 | } else { 154 | sum / (tc.frame_durations.samples as u32) 155 | } 156 | } 157 | 158 | /// A convenience function to convert a Rust `Duration` type 159 | /// to a (less precise but more useful) `f64`. 160 | /// 161 | /// Does not make sure that the `Duration` is within the bounds 162 | /// of the `f64`. 163 | pub fn duration_to_f64(d: Duration) -> f64 { 164 | d.as_secs() as f64 + d.subsec_nanos() as f64 * 1e-9 165 | } 166 | 167 | /// A convenience function to create a Rust `Duration` type 168 | /// from a (less precise but more useful) `f64`. 169 | /// 170 | /// Only handles positive numbers correctly. 171 | pub fn f64_to_duration(t: f64) -> Duration { 172 | let seconds = t.trunc(); 173 | let nanos = t.fract() * 1.0e9; 174 | 175 | Duration::new(seconds as u64, nanos as u32) 176 | } 177 | 178 | /// Returns a `Duration` representing how long each 179 | /// frame should be to match the given fps. 180 | /// 181 | /// Approximately. 182 | fn fps_as_duration(fps: u32) -> Duration { 183 | let target_dt_seconds = 1.0 / f64::from(fps); 184 | f64_to_duration(target_dt_seconds) 185 | } 186 | 187 | /// Gets the FPS of the game, averaged over the last 188 | /// 200 frames. 189 | pub fn fps(ctx: &Context) -> f64 { 190 | let duration_per_frame = average_delta(ctx); 191 | let seconds_per_frame = duration_to_f64(duration_per_frame); 192 | 1.0 / seconds_per_frame 193 | } 194 | 195 | /// Returns the time since the game was initialized, 196 | /// as reported by the system clock. 197 | pub fn time_since_start(ctx: &Context) -> Duration { 198 | let tc = &ctx.timer_context; 199 | f64_to_duration(time() - tc.init_instant) 200 | } 201 | 202 | /// same as time_since_start, but without f64_to_duration inside 203 | pub fn time_since_start_f64(ctx: &Context) -> f64 { 204 | let tc = &ctx.timer_context; 205 | time() - tc.init_instant 206 | } 207 | 208 | /// This function will return true if the time since the 209 | /// last [`update()`](../event/trait.EventHandler.html#tymethod.update) 210 | /// call has been equal to or greater to 211 | /// the update FPS indicated by the `target_fps`. 212 | /// It keeps track of fractional frames, so if you want 213 | /// 60 fps (16.67 ms/frame) and the game stutters so that 214 | /// there is 40 ms between `update()` calls, this will return 215 | /// `true` twice, and take the remaining 6.67 ms into account 216 | /// in the next frame. 217 | /// 218 | /// The intention is to for it to be called in a while loop 219 | /// in your `update()` callback: 220 | /// 221 | /// ```rust 222 | /// # use ggez::*; 223 | /// # fn update_game_physics() -> GameResult { Ok(()) } 224 | /// # struct State; 225 | /// # impl ya2d::event::EventHandler for State { 226 | /// fn update(&mut self, ctx: &mut Context) -> GameResult { 227 | /// while(timer::check_update_time(ctx, 60)) { 228 | /// update_game_physics()?; 229 | /// } 230 | /// Ok(()) 231 | /// } 232 | /// # fn draw(&mut self, _ctx: &mut Context) -> GameResult { Ok(()) } 233 | /// # } 234 | /// ``` 235 | pub fn check_update_time(ctx: &mut Context, target_fps: u32) -> bool { 236 | let timedata = &mut ctx.timer_context; 237 | 238 | let target_dt = fps_as_duration(target_fps); 239 | if timedata.residual_update_dt > target_dt { 240 | timedata.residual_update_dt -= target_dt; 241 | true 242 | } else { 243 | false 244 | } 245 | } 246 | 247 | /// Returns the fractional amount of a frame not consumed 248 | /// by [`check_update_time()`](fn.check_update_time.html). 249 | /// For example, if the desired 250 | /// update frame time is 40 ms (25 fps), and 45 ms have 251 | /// passed since the last frame, [`check_update_time()`](fn.check_update_time.html) 252 | /// will return `true` and `remaining_update_time()` will 253 | /// return 5 ms -- the amount of time "overflowing" from one 254 | /// frame to the next. 255 | /// 256 | /// The intention is for it to be called in your 257 | /// [`draw()`](../event/trait.EventHandler.html#tymethod.draw) callback 258 | /// to interpolate physics states for smooth rendering. 259 | /// (see ) 260 | pub fn remaining_update_time(ctx: &mut Context) -> Duration { 261 | ctx.timer_context.residual_update_dt 262 | } 263 | 264 | /// Gets the number of times the game has gone through its event loop. 265 | /// 266 | /// Specifically, the number of times that [`TimeContext::tick()`](struct.TimeContext.html#method.tick) 267 | /// has been called by it. 268 | pub fn ticks(ctx: &Context) -> usize { 269 | ctx.timer_context.frame_count 270 | } 271 | --------------------------------------------------------------------------------