Adventures in Code Generation -- Graphics::GVG
2016-11-29
Vector graphic games, like Battlezone or Asteroids, are old favorites of mine, and I’ve been wanting to make a game with that same style. Partially, that’s because I’m not that artistic, and it’s easy to make the style look cool. Just make everything come together at hard angles and let it go.
I considered SVG for the job, and leaned towards it for a while just for the sake of not falling into Not Invented Here. The problem is that SVG is incredibly complicated, especially for rendering. In a game where I’d likely be writing my own rendering of the SVG standard, I just didn’t want to do it. What’s more, for any kind of complicated effect, I’d probably have to use CSS, which is a second really complicated standard to implement.
So I went off to do it myself. When I inevitably end up having to reimplement some feature of SVG, I’ll just live with that.
Anyway, this led me to make Graphics::GVG. It uses a simple scripting language (parsed by Marpa::R2) to define how to draw your vector art:
%color = #FF33FFFF;
line( %color, 0.0, 0.0, 1.0, 1.1 );
glow {
circle( %color, 0, 0, 0.9 );
rect( %color, 0, 1, 0.7, 0.4 );
}
The drawing commands inside the glow { ... }
block will be rendered with a glow effect. Exactly what that means is up to the renderer.
There’s an included OpenGL renderer, which is where the real fun starts. The script above would be parsed into an Abstract Syntax Tree (AST), and then renderers compile that into the system of their choice. In the OpenGL case, it compiles the AST into a Perl package, which has a draw()
method that does a series of OpenGL functions.
For example, a simple rectangle GVG script:
rect( #993399ff, 0, 0, 1, 1 );
Gets turned into the Perl code below by the OpenGL renderer:
package Graphics::GVG::OpenGLRenderer::0xA87F753EB5E511E691EF9B4C92660F68;
use strict;
use warnings;
use OpenGL qw(:all);
sub new
{
my ($class) = @_;
my $self = {};
bless $self => $class;
return $self;
}
sub draw {
glLineWidth( 1 );
glColor4ub( 153, 51, 153, 255 );
glBegin( GL_LINES );
glVertex2f( 0, 0 );
glVertex2f( 0 + 1, 0 );
glVertex2f( 0 + 1, 0 );
glVertex2f( 0 + 1, 0 + 1 );
glVertex2f( 0 + 1, 0 + 1 );
glVertex2f( 0, 0 + 1 );
glVertex2f( 0, 0 + 1 );
glVertex2f( 0, 0 );
glEnd();
return; }1;
Which isn’t going to win any formatting awards, but it’s not meant to be edited by humans, anyway.
The above gets returned as a string, so you can compile it right into the running program using eval(STRING)
(see Dynamic Code Loading for why you shouldn’t be afraid of this sort of thing). Alternatively, you could save it as a .pm file and load it up that way.
Either way, you get yourself an object from that package with new()
, and then call draw()
on it for each frame.
The generated code could be improved, for certain. For performance, it’ll probably move to vertex buffers. There should also be a way to make predictable package names rather than the UUID. If the overhead of calling all those OpenGL functions ends up being an issue, it could be compiled to a C function that can be called from XS.
In the future, there will be other renderers, which I hope can combine to output one set of package code. Meaning you would call $obj->draw_opengl
for the OpenGL renderer, or $obj->init_chipmunk
to setup the geometry for the Chipmunk2D physics library.