I've been learning Rust and Piston at the same time.
For people unaware, Piston is a modular game engine written in Rust.
How modular?
- You can choose between Vulkan, OpenGl, Metal and DirectX for rendering
- You can choose between Glutin, GLFW or SDL2 to create windows and handle input
It does all that while proposing a single interface for all, that completely abstract which backend and window system you are using.
At compile time, thanks to the power of monomorphization, you end up with one single implementation being chosen and compiled.
Coming from the java world, I thought it would be handy to create a list of Renderable
, so a trait that provides a render
function, so I can do something like this:
for renderable in renderables.iter() {
renderable.render();
}
For example, I could have a renderer to draw my map, a renderer to draw the hud, a renderer to draw the inventory or any tooltip window...
Well, we are talking about piston, so when you need to render something, you need to pass in a context, graphics, device...
So I created something like that:
pub trait Renderable {
fn position(&self) -> (i32, i32);
fn size(&self) -> (i32, i32);
fn render<'a, C, G>(&self, render_context: &mut RenderContext<'a, C, G>)
where
C: CharacterCache,
G: Graphics<Texture = <C as CharacterCache>::Texture>;
}
pub struct RenderContext<'a, C, G>
where
C: CharacterCache,
G: Graphics<Texture = <C as CharacterCache>::Texture>,
{
pub grid_size: u32,
pub character_cache: &'a mut C,
pub context: Context,
pub graphics: &'a mut G,
}
(The RenderContext is a way to pass all the needed bit to my renderable in the render method, instead of making the method super long. That might be a mistake too. I should probably ditch it.)
Each rendering backend propose it's own implementation of the Graphics
trait, and that gets monomorphized at compile time to use the one backend you selected, the window you selected.
Then, I wrote a few implementation of the trait for a few of the things I wanted to render, and sure enough, it worked well. I could call on my hud a hud.render(context);
, map.render(context);
and so on.
And then I thought "let's create a RenderList
".
pub struct RenderList {
pub renderables: Vec<Box<dyn Renderable>>,
}
And of course, it doesn't compile. I get
31 | pub renderables: Vec<Box<dyn Renderable>>,
| ^^^^^^^^^^^^^^^^^^^^^^^^ the trait `renderer::Renderable` cannot be made into an object
|
= help: consider moving `render` to another trait
and okay, fair enough, it's not possible like that.
So I got a new idea: Let's make my RenderList
itself monomorphizable!
So I adjusted my Renderable
trait and my RenderList
like this:
pub trait Renderable<C, G>
where
C: CharacterCache,
G: Graphics<Texture = <C as CharacterCache>::Texture>,
{
fn position(&self) -> (i32, i32);
fn size(&self) -> (i32, i32);
fn render(&self, render_context: &mut RenderContext<C, G>);
}
pub struct RenderList<C, G>
where
C: CharacterCache,
G: Graphics<Texture = <C as CharacterCache>::Texture>,
{
pub renderables: Vec<Box<dyn Renderable<C, G>>>,
}
Then, when implementing the trait, I write this:
struct SomethingToDisplay {}
impl<C, G> Renderable<C, G> for SomethingToDisplay
where
C: CharacterCache,
G: Graphics<Texture = <C as CharacterCache>::Texture>,
{
fn position(&self) -> (i32, i32) {
todo!()
}
fn size(&self) -> (i32, i32) {
todo!()
}
fn render(&self, render_context: &mut RenderContext<C, G>) {
todo!()
}
}
And it all works, compile, I can add any Renderable
in my vector.
But. A big, BUT.
Isn't it a bit insane? Am I not falling into anti-patterns, because I'm so used to objects that I want to fit objects in my world views?
Would someone have suggestion for a saner solution, to have a render list with piston and rust?
I'm looking forward to your suggestions