Back in the swing o' things
Well, it seems I've finally found my productivity gland again (after a three hour nap this afternoon, of course).
Stuff is flowing nicely and I should be able to get the plans for this cutscene system totally done here soon. I ran into a fun challenge today with controlling fine-grained timings for different events in a scene. My previous model was barfing terribly for a particular common case:
- Event A happens absolutely 5 seconds into playback (hardcode 5 second offset)
- A 2 second audio clip needs to be played just before the start of event A
- BUT the clip may be 3 seconds in another language
- The clip duration has to be computed automatically!
- Solution: start the audio clip at offset: (event a offset) - (clip duration)
The problem was, in the first incarnation of my timing model, the solution couldn't be expressed - at all. So I redid the timing system and came up with a far more potent solution.
Now I have a very nice framework that lets us build sophisticated and powerful timing interdependencies, with simple and easy-to-implement XML and code.
The secret? Recursion.
I have two recursive data elements: timing attributes (attributes of XML nodes), and manual timing nodes. A timing attribute is a simple XML attribute field that is allowed to take on multiple values depending on what it needs to represent:
- An integer evaluated as an absolute quantity of milliseconds
- A string that refers to a manual timing node's ID tag; the manual node's value is copied (lazily) and used as the value
- A string of the form start:foo that yields the starting time offset of foo (usually a shot displayed in the cutscene, or some other similar event)
- A string of the form end:foo that's similar to start:foo
- A string of the form duration:foo that yields the duration of foo
Any place where a node needs to talk about timing, it uses a timing attribute.
This leaves us to define what a manual timing node is. That's simple:
- A string giving the node's ID tag
- A base value, which is a timing attribute
- An optional offset attribute which adds or subtracts from the base value. Both addition and subtraction are permitted in the same timing node (trust me, this will be useful in a few special cases - but I don't have time to explain right now). The offset attributes are themselves timing attributes.
So if I want to cue a sound effect at some specific point in time, I just do thusly:
<bazaudio offset="specialtimingnode" />
I can now express any timing relationship I want with the correct combination of manual timing nodes and automatically computed timing attributes. Cool stuff.
The other interesting problem that I've come up against is a little more abstract. Basically, the system I've designed is a kitchen-sink cutscene generation and rendering engine. It handles camera movement, flying all the little ships around and blowing them up, showing people talk, overlaying text and graphics onto the screen, etc. etc.
This makes the system ideal for displaying mission briefing sequences, where some talking head gives you some instructions, shows you some cool file footage of stuff exploding, etc.
Both systems are really designed, ultimately, for easy implementation. Everything is done with such recursive structures. The idea is that I can define a few simple pieces of code that handle those recursive elements, instead of writing a huge amount of special-case code to handle all the little variations and gimmicks that the content development team will need to be able to use.
Unfortunately, recursive data construction seems to be something that I understand just fine (and most of the other programmers as well) but just isn't something you use in the art/content realm too often. So the artists are at this point kind of worried that A) my system won't do what they need and B) even if it does they'll never figure it out.
Personally, I think that as soon as I get a couple of concrete examples done for everyone to look at, it'll get a lot more clear. To be fair, I've been thinking about these problems for a couple of months virtually non-stop, whereas everyone else hasn't. So it's only natural that the solution is obviously fine in my mind, but looks a bit iffy to everyone else.
For instance, most mission briefings will look pretty much alike. So from the artists' point of view, the most logical solution is to have a special-case XML and code system that handles those particular variations that they need. To them, it's better to add a special case when they need one. To me, that means code bloat, nasty bugs, and some terribly poor code longevity (as soon as the next project starts and we need different layouts, the code is useless). So I've been doing this other thing instead.
For a while, I was just planning on saying "here's an example, let me know if you need some pointers in getting any particular effects/results you need, I promise it all works and this is the best deal for everyone involved." I think that would have been a perfectly reasonable way to handle it.
Then I had an idea.
I'm constantly raving about building layers of abstraction in code. For some stupid reason, it took me until now to realize that I should also be building layers of abstraction in data.
The solution here is to build a veneer layer that lets the content creation team express notions abstractly and in simple data. Some kind of transformation layer then expands that into the lower-level data, which is pumped into the rest of the code. Since the code understands all this recursive, emergent gibberish, and the artists don't, all I need is a translation bridge between them.
It's really pretty similar to doing something like using XSLT to generate renderable markup from XML data, for instance. Except I'm moving from one abstraction of XML to another, and the actual translation mechanism will probably be a bit of manual code wizardry.
I still need to decide when this will be done. Doing it at design time has the advantage of needing less code and data to be shipped into production, but also requires the content team to "export" from abstract to concrete data formats every time they want to see their work in action. That sucks. So at the moment I'm leaning towards having the transformation be done "on the fly" when the game first loads the data.
This way, we can do things like abstract off the common briefing layouts/formats into one layer. The content developers generate content that targets that layer. Then, the translation code magically turns that into the raw cutscene data, which is much more stable and powerful. Everyone wins.
My brain is now dangerously close to self-combustion, so it's time to go do something Else for a while. With luck I'll have a bunch of this stuff documented (and maybe even implemented) very soon now.
It feels good to get back into things.