Filling in the Blanks: Procedural Glue
This is an essay in the Herodotus series, and a followup to Designing a Story. Please read at least Introducing Herodotus before proceeding.
A Sticky Situation
At the end of the last essay, we found ourselves with a problem: We had all these simulation constructs, but no clear way to, well, construct them. This essay will shed some light on how stories in Rboehm's style could be built.
Pick and Choose
The fundamental variable unit of a Scene, and therefore of a Quest, is a Goal. If we're looking to build a Asker asks Provider for Item goal, we should probably pick the Item based on the Asker. Let's assume that the only Asker for now is the Farmer, though later we can look at ways to pick those too.
Recall that Characters have desires. In that case, we might want to pick an Item from among those desires. This gives us a nice little object-oriented and time-sensitive way to pick which Item to hunt for. And if all of a Character's needs are satisfied? Then we pick a different Character.
//First, we need some new metadata for goals: AskForItemGoal := Goal clone do( item ::= nil asker ::= nil askerTypes ::= list("consumer") provider ::= nil providerTypes ::= list("provider") ) GiveItemGoal := Goal clone do( item ::= nil receiver ::= nil receiverTypes ::= list("consumer") giver ::= nil giverTypes ::= list("provider") ) //Then later, in our goal creator... createAskForItemGoal := method(asker, provider, //In Thucydides, this could be: //asker desires(now) currentDesires := history fluentValue("desires", asker, now) if((currentDesires == nil) or(currentDesires size == 0), //This one is no good, pick a different one //Other conditions for picking different characters could include //incompatible alignment, national affiliation, location, and so on. newAsker := chooseDifferentCharacter(AskForItemGoal askerTypes, list(asker)) return createAskForItemGoal(newAsker, provider) ) desiredItems := currentDesires anyOne //In Thucydides, this could be: //SimObject clone setCategories(desiredItems) allActiveInAt(h, t) actualItem := history fluentValue(desiredItems, nil, now) anyOne AskForItemGoal clone setAsker(asker) setProvider(provider) setItem(actualItem) )
And bam! We get an Item for the Quest that's relevant to the Asker. Now, in the supporting infrastructure, we can spawn in such an Item, perhaps in somebody's inventory or on a spawnLocation in some cave. Perhaps Items have some metadata saying what kinds of places they can spawn in.
About Types
This would be a good place to say something about types in Herodotus and in general. Herodotus uses a very simple mechanism for matching types, where the types requested must all be present in the type matched against. For example, [animal, cat] matches [animal], [cat], and [animal, cat], but doesn't match [animal, dog] or [construction-vehicle, cat]. In the future, Herodotus might use a matching system with rankings, but for now, this is how it works. So, if we define a Donut's type to be [item, food, sweet, round, donut], and if we have a Goal which is looking for a [food, sweet], then the Donut will surely match; but if it's looking for [food, meat], there will be no match. That is, unless there's a Donut clone of type [item, food, meat, round, donut]. In the end, the utility of this type system depends on how consistently it's applied.
Calculated Character Creation
Let's move to a slightly more complex topic: the creation of characters expressly for the purpose of a Scene. This occurs when no existing character is available to fill a role, and it could include, for instance, spawning in the particular Kobold carrying the candles required to fulfill the quest; or it could be the visiting diplomat from another nation. The benefit of doing this with Herodotus is twofold:
- Character archetypes can be chosen from a broad pool, based on the quest, and the archetype chosen can influence the experience.
- Spawned characters, if desired, can be implanted along with their own personal history, provided that history doesn't overlap or damage existing timelines.
To clarify the second point a bit, we'll use the example of the visiting diplomat. Instead of just spawning a "diplomat" at the requisite location, we could also place his time of birth, his nation of origin, his service record, and so on; making him effectively a permanent character that had been there all along. This way, he can be used for other quests later on, and a mystery-solving Scene which required finding out the diplomat's history could be created, too. Mid-timestream insertion is currently dangerous in Herodotus. Future versions will make it safe to use as a general technique, but for now, please just be sure that the fluents placed in the middle of a time stream don't violate the preconditions of later fluents or the invariants of prior fluents.
The synthesis of characters is a complex business; and since I'm preparing for GDC next week, this essay will have to stop here. Hopefully, the approach above shows that the fundamental technique is to query the history, then decide what kind of thing to spawn or use. Future articles will, having explored this path a bit, return to the starting-point and evaluate Herodotus as a service; a Herodotus Simulation Language and Herodotus Query Language will be proposed; and the quest generator will be revisited in a more formal framework. Then, we will explore the application of Herodotus to a game called Stranded, designed by Rob Rix and myself.
If any GDC attendees would like to get in touch with me, please send an e-mail.
Labels: example, herodotus, theory, thucydides
Joe Osborn 2008-02-12
0 Comments:
Post a Comment
<< Home