Last active
September 8, 2021 11:13
-
-
Save 17cupsofcoffee/bb57dc1ebfe6507a262513117d0b1901 to your computer and use it in GitHub Desktop.
Multi-threaded Asset Loading with Tetra
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
//! This is an example of one way you could achieve multi-threaded asset loading with Tetra | |
//! (or any other Rust game framework). | |
//! | |
//! The design is intended to be similar to: | |
//! | |
//! * https://github.com/kikito/love-loader | |
//! * https://github.com/libgdx/libgdx/wiki/Managing-your-assets | |
//! | |
//! This should not be taken as production-ready code (for one thing, it only supports | |
//! textures!) or the 'best' way of implementing this functionality. It's just an example | |
//! to hopefully give you some ideas. | |
//! | |
//! You may also want to look at some of these crates, rather than implementing everything | |
//! yourself: | |
//! | |
//! * https://github.com/amethyst/atelier-assets | |
//! * https://github.com/phaazon/warmy | |
//! * https://github.com/zakarumych/goods | |
//! * https://github.com/a1phyr/assets_manager | |
use std::collections::HashMap; | |
use std::sync::mpsc::{channel, Receiver}; | |
use std::thread; | |
use std::time::Duration; | |
use tetra::graphics::text::{Font, Text}; | |
use tetra::graphics::{self, Color, Texture}; | |
use tetra::math::Vec2; | |
use tetra::{Context, ContextBuilder, State}; | |
/// This is a silly function to simulate long loading times, obviously don't use this | |
/// in your own code :p | |
fn load_slowly(path: &str) -> Vec<u8> { | |
thread::sleep(Duration::from_secs(2)); | |
std::fs::read(path).unwrap() | |
} | |
/// This is the data we'll send back from our loader thread to the main thread. | |
/// | |
/// Notice that we can't build `Texture` on the loader thread, because it relies on | |
/// the `Context`. Other asset types (such as `Sound`) that don't have that | |
/// limitation could be completely built on the loader thread. | |
enum LoaderEvent { | |
Texture(String, Vec<u8>), | |
// This could be extended with more events/asset types, if you wanted. | |
} | |
struct AssetLoader { | |
texture_paths: Vec<String>, | |
} | |
impl AssetLoader { | |
fn new() -> AssetLoader { | |
AssetLoader { | |
texture_paths: Vec::new(), | |
} | |
} | |
fn add_texture(&mut self, path: impl Into<String>) { | |
self.texture_paths.push(path.into()); | |
} | |
fn start(self) -> Assets { | |
// A channel is a nice way of sending one-way data from one thread to another. | |
let (sender, receiver) = channel(); | |
// We'll use the number of queued assets to determine our progress later on. | |
let queued_count = self.texture_paths.len(); | |
// You could potentially use a thread pool here, or something higher-level | |
// like Rayon, but I don't know how necessary that would be unless you had | |
// some really chonky assets. | |
thread::spawn(move || { | |
for path in self.texture_paths { | |
let data = load_slowly(&path); | |
// This sends the loaded data back over to the main thread | |
// for further processing. | |
// | |
// Alternatively, you could use a channel to send | |
// the loader's progress, and `join` the thread to get the | |
// loaded data once loading is complete. This seems simpler, | |
// though! | |
sender.send(LoaderEvent::Texture(path, data)).unwrap(); | |
} | |
}); | |
Assets { | |
queued_count, | |
loaded_count: 0, | |
receiver, | |
// I'll store the loaded textures in a HashMap so I can link them back | |
// to their paths, but you can store them however you'd like. | |
textures: HashMap::new(), | |
} | |
} | |
} | |
struct Assets { | |
queued_count: usize, | |
loaded_count: usize, | |
receiver: Receiver<LoaderEvent>, | |
textures: HashMap<String, Texture>, | |
} | |
impl Assets { | |
fn get_texture(&self, path: &str) -> Option<&Texture> { | |
self.textures.get(path) | |
} | |
fn update(&mut self, ctx: &mut Context) -> tetra::Result { | |
// We use try_recv here to avoid blocking the main thread (that'd make our game | |
// freeze). | |
// | |
// try_recv will return Ok if anything has been sent, or Err if there's no messages | |
// or the sender has gone away (e.g. when the loader thread finishes). So we can | |
// just check for Ok values and ignore the rest. | |
while let Ok(event) = self.receiver.try_recv() { | |
match event { | |
LoaderEvent::Texture(id, data) => { | |
// Now that we're back on the main thread, we can turn that image | |
// data into an actual texture. | |
let texture = Texture::from_file_data(ctx, &data)?; | |
self.textures.insert(id, texture); | |
// Increment the loaded count so that the progress is accurate. | |
self.loaded_count += 1; | |
} | |
} | |
} | |
Ok(()) | |
} | |
fn done_loading(&self) -> bool { | |
self.loaded_count == self.queued_count | |
} | |
fn progress(&self) -> f32 { | |
// This gives us a number from 0.0 to 1.0, which is nice for scaling | |
// progress bars. | |
self.loaded_count as f32 / self.queued_count as f32 | |
} | |
} | |
struct GameState { | |
assets: Assets, | |
text: Text, | |
} | |
impl GameState { | |
fn new(ctx: &mut Context) -> tetra::Result<GameState> { | |
let mut loader = AssetLoader::new(); | |
loader.add_texture("./resources/texture_1.png"); | |
loader.add_texture("./resources/texture_2.png"); | |
loader.add_texture("./resources/texture_3.png"); | |
let assets = loader.start(); | |
Ok(GameState { | |
assets, | |
text: Text::new( | |
"0% loaded", | |
Font::vector(ctx, "./resources/DejaVuSansMono.ttf", 64.0)?, | |
), | |
}) | |
} | |
} | |
impl State for GameState { | |
fn update(&mut self, ctx: &mut Context) -> tetra::Result { | |
if self.assets.done_loading() { | |
// All the queued assets have been loaded, so we can use them now! | |
// | |
// If you don't like having your assets in a hashmap, you could extract | |
// them into their own struct here, or hand the assets off to another | |
// scene, etc. | |
} else { | |
// Assets are still loading, so we should process any that have become ready since | |
// the last time we checked. | |
// | |
// This is where we can handle logic that needs to be run on the main thread, | |
// e.g. creating textures on the GPU. | |
self.assets.update(ctx)?; | |
// We can also check the progress: | |
self.text | |
.set_content(format!("{}% loaded", self.assets.progress() * 100.0)) | |
} | |
Ok(()) | |
} | |
fn draw(&mut self, ctx: &mut Context) -> tetra::Result { | |
if self.assets.done_loading() { | |
graphics::clear(ctx, Color::rgb(0.392, 0.584, 0.929)); | |
// Getting the texture from the HashMap every frame is a bit | |
// inefficient, this is just a simple example though :p | |
let texture = self | |
.assets | |
.get_texture("./resources/wabbit_alpha_1.png") | |
.unwrap(); | |
graphics::draw(ctx, texture, Vec2::zero()); | |
} else { | |
graphics::clear(ctx, Color::BLACK); | |
graphics::draw(ctx, &self.text, Vec2::zero()); | |
} | |
Ok(()) | |
} | |
} | |
fn main() -> tetra::Result { | |
ContextBuilder::new("Hello, world!", 1280, 720) | |
.build()? | |
.run(GameState::new) | |
} | |
// MIT License | |
// | |
// Copyright (c) 2021 Joe Clay | |
// | |
// Permission is hereby granted, free of charge, to any person obtaining a copy | |
// of this software and associated documentation files (the "Software"), to deal | |
// in the Software without restriction, including without limitation the rights | |
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell | |
// copies of the Software, and to permit persons to whom the Software is | |
// furnished to do so, subject to the following conditions: | |
// | |
// The above copyright notice and this permission notice shall be included in all | |
// copies or substantial portions of the Software. | |
// | |
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR | |
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, | |
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE | |
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER | |
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, | |
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE | |
// SOFTWARE. |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment