Skip to content

Instantly share code, notes, and snippets.

@17cupsofcoffee
Last active September 8, 2021 11:13
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save 17cupsofcoffee/bb57dc1ebfe6507a262513117d0b1901 to your computer and use it in GitHub Desktop.
Save 17cupsofcoffee/bb57dc1ebfe6507a262513117d0b1901 to your computer and use it in GitHub Desktop.
Multi-threaded Asset Loading with Tetra
//! 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