|
ChevyRay
Guest
|
 |
« on: October 28, 2011, 01:31:30 AM » |
|
 While the game I'm developing is largely interactive, only rarely taking control of the character, it still is loaded with lots of events in which I need to take control of several objects in the scene to create some sort of cohesive scene/event when they occur. I'll give an example scene, not unlike something that may happen in my game project: The player is running along, doing their thing. But when they pass a certain point, an event is triggered that causes a bunch of NPCs to come running in (you do not lose control, though). They all stop in their respective places, and after a short moment, control is then suspended and your character turns to face them in the same instant a bit of dialogue sprouts from one of them. The dialogue waits for player input to continue, and perhaps switches between characters as each speak, emulating a conversation, before control is returned to the player. Basically, I'm not sure what an efficient and non-kitten-destroying method of scripting this type of event would be. I'm not sure what a good way is to write the events themselves, how these events know/reference specific objects in the scene (especially ones that may not exist until the event is triggered, or even not until partway through the event), and what the best approach for doing this is that will allow me to give/take control away from the player, even if it's mid-event, without breaking the system. So I guess my question is directed towards people who have either written or worked with a system that's good for doing this kind of thing, if you have any suggestions (bonus points for actually supplying examples/evidence to support your suggestions), I'd love to hear them, and if possible this can even be a topic for discussing the coding/design of these types of systems in general. EDIT: Oh yes, I forgot to mention! The game is currently being coded in C#/XNA. I've written a full level editor for the game that I can add new stuff onto as I please.
|
|
|
|
« Last Edit: October 28, 2011, 01:36:34 AM by ChevyRay »
|
Logged
|
|
|
|
|
Evan Balster
|
 |
« Reply #1 on: October 28, 2011, 01:33:42 AM » |
|
(I told him this on Skype, just noting it's been discussed) I recommend the cave story route of a crazy-simple homebrew scripting system: http://lotlot.net/misc/doukutsu/tsc.txtEDIT: I also predict someone is going to recommend you incorporate an actual scripting language, and advise you do NOT, unless you have more to use it for. It's a lot of work.
|
|
|
|
|
Logged
|
|
|
|
|
eclectocrat
|
 |
« Reply #2 on: October 28, 2011, 01:52:31 AM » |
|
EDIT: I also predict someone is going to recommend you incorporate an actual scripting language, and advise you do NOT, unless you have more to use it for. It's a lot of work.
In my experience, writing and debugging a script system is much more difficult than integrating a lightweight one. The general structure you might want to look at is that of a queue of actions. ie: define gameloop: get input update timers check action queue <- here's the magic... draw things etc...
So whenever any entity needs to act, you place an action in the queue. Most of the time, the action will just revolve around whatever the player is doing, but sometimes, during scripted sequences, you can push a hella-lot-of NPC actions and have them resolve in sequence. That's the general gist, but there's far more detail to it. You've got to decide how many actions you want to process per loop iteration, when to return to the loop for rendering, if you want to use multiple queue's for multiple actors, etc...
|
|
|
|
|
Logged
|
|
|
|
|
ChevyRay
Guest
|
 |
« Reply #3 on: October 28, 2011, 01:55:51 AM » |
|
|
|
|
|
|
Logged
|
|
|
|
|
Triplefox
|
 |
« Reply #4 on: October 28, 2011, 03:33:39 AM » |
|
"Code as content" - which is what this is - is an area where custom syntax is really useful to lighten the iteration times. A timeline language with a tiny bit of instances/named variables is a good start - the last (working) one I used was a studio-internal thingy that looked sort of like this - it's been some years now: 0.0 turnTo(nearestInstanceOf("player")); 10.0 walkTo(43,43); 20.0 turnTo(nearestInstanceOf("player")); 30.0 walkTo(0,0); 40.0 goto(0.0);
~describes a patrol cycle where the controlled instance looks at the player, walks somewhere, looks at the player again, walks back, then restarts. It was not actually as pretty as my example, being sort of hacked together from a shmup spawner language originally. The syntax was based around nested function calls, thus complex things were often Lisp-like, yet with C-style syntax. But the type system was too inconsistent to feel like Lisp, or like C for that matter. We didn't have a function to subtract two numbers until near the end of the project. Strings and numbers bashed up against each other in arbitrary ways. The overall concept showed promise though and we did ship, so it was not a failure. If you already have some form of scripting, you can bolt the timeline on by pre-processing the source to split up the lines into calls at various moments. General-purpose scripting languages could work for this; when they're powerful, though, they open up a can of worms with the integration by having their own type system, memory allocation, etc. I'd rather point you towards doing a custom solution - it's not really wheel reinvention since the purpose of this language isn't really to do computation, it's to fling things around the screen in cinematically interesting ways, and that can depend a lot on the game engine. For custom things here are your syntactical options, in roughly ascending order of complexity: Assembler-like: each statement does exactly one thing - no stack, no fancy nesting stuff, no magical temporary variables instanced inside a one-liner. Verbose but effective, especially if you're exposing very high-level things to begin with. BASIC is kind of an evolution of "assembler semantics," applying bolted-on special cases to streamline common things like logic, looping, strings and arithmetic.
Forth-like: variables get pushed on a stack when declared, then words(nee function calls) pop them off and process them. Simple and super powerful, but also error-checks poorly since it's easy to end up with a stack of the wrong size.
Lisp-like: nested expressions that get evaluated by travelling up and down a tree, implicitly retaining a stack to do computation. Very powerful like Forth, and checks a bit better since you can more easily compare function inputs and outputs.
Others: C-like languages, Pascal, etc. These syntaxes mostly build on the tree forms of Lisp to add various bits of syntactical sugar. Added value is heavily debated - as Lisp boosters often point out, you can always write a macro to do an equivalent. (but to some extent this argument is repeatable all the way down to macroassemblers; treating code as data is basically our most powerful structuring tool, and it doesn't matter what face you give it)
FWIW my current opinion on best bang-for-the-buck for a timeline language would come from something BASIC-like with some type checking. Something like this: declare I int
@0 I := getPlayerDist if I < 100 : 10 @1 goto 0 @10 say "You are close to me!"
Here I call to some API function for the player's distance from the entity each second using a goto loop. If the player is less than 100 units away, the entity goes to @10 and says he's close, and then the script terminates. The extensions for different situations(the entity starts moving on the trigger, anims play, etc.) suggest themselves from here. You may have noticed that the "I" variable seems superfluous - this is something implied in the semantics I've picked out. Once we have a variable and we know what it is, logic and mutability is easy; but even adding the bit of sugar that would let us call getPlayerDist from "if" burdens the language implementation, since we no longer get to assume that the "if" statement always acts on pre-declared variables. This kind of decision is up to you, but it distracts from the core problem: It's supposed to do cutscenes.Instead I would point to other features: the type declaration makes it easy to allocate all the memory statically per script(always a nice thing) and make a few sanity checks at compile time. When code gets repetitive you could apply subroutines. The expectation is that no one script is going to be some monstrous engineering challenge, so you don't need to lighten the syntax very much, especially if you opt to include more powerful types and structuring tools like arrays and macro expansion. (And you always have the out of writing a hard part in the engine!) You'll also want to be able to tie collision into the system to listen to player-driven events like "hit" or "pushed". You can do a lot with polling like what I did in the example, but it gets pretty hacky. Listening to events, of course, motivates some decision on whether each entity gets parallel or procedural timelines when many events get fired. In the past I waffled on this, but now I would just point to ZZT-OOP, which is procedural per-entity and does the job tremendously well by running a global timeline per entity, letting the events work as labels and getting "locked" or "unlocked" in a stack. It suffices for the vast majority of stuff in these types of games(everything from hp counts in boss fights to one-time story events) - we expect characters to only do one thing at a time, so the need for combinations doesn't arise so often. ZZT-OOP also suggests message passing methods - simply apply names or tags to entities and allow them to direct each other to the named labels, as if they were events. There are always ways to inject parallelism back into the system by having a global controller, special-case code, etc. Last of all, do whatever you can to make it easier to iterate on these, or preferably to get them right in one pass. Having to play these scripted events over and over to fix minor issues can add up really quickly.
|
|
|
|
|
Logged
|
|
|
|
|
st33d
Guest
|
 |
« Reply #5 on: October 28, 2011, 04:06:47 AM » |
|
In a recent project for Nitrome I had to build an event system. What I discovered, was that to do it well I had to build a game engine within a game engine. Hear me out on this one... The LevelEvent hijacks the camera, and any elements that need to be a part of the event. The rest of the game is frozen (paused). Then the camera moves to the event location, simulates those elements in the event, and when everything is done the camera scrolls back to the player and releases control of those elements. The principal use of this is switches. You trigger a switch, then the game shows you what has happened - animating a door open or activating a fireball launcher. But this forms a perfect foundation for scripted events. You trigger a switch, then the game shows you some npc actions, you tidy it up, then get back to the gameplay. When you have full control of an event, conversation is simple. For storing a conversation I use this format: (barry:hello there) (garry:alright mate!)
And I capture those out into an array with the following regex operation: public static const MATCH_BRACKETED_CONTENT:RegExp =/(?<=\()[^\(\)]*(?=\))/; public static const REPLACE_BRACKETED_CONTENT:RegExp =/\([^\(\)]*\)/;
...
do{ var line:Array = conversation.match(MATCH_BRACKETED_CONTENT); if(line && line.length > 0){ speech.push(line[0]); } conversation = conversation.replace(REPLACE_BRACKETED_CONTENT, ""); } while(line && line.length > 0);
|
|
|
|
|
Logged
|
|
|
|
|
DeadPixel
|
 |
« Reply #6 on: October 28, 2011, 04:17:12 AM » |
|
Since I assume you're the only coder on the project using an external 'language' to script in seems redudant since you're already using C# which is pretty easily parseable. I haven't implemented this myself, but a simple system might be look like this: struct ScriptCommand { public string Command; public object Data;
public ScriptCommand(string command, object data) { this.Command = command; this.Data = data; } }
// In your base game object Queue<ScriptCommand> commands = new Queue<ScriptCommand>();
public void AddScriptCommand(ScriptCommand command) { commands.Enqueue(command); }
protected virtual void ProcessScriptCommands(GameTime gameTime) { if(commands.Count > 0) { ScriptCommand command = commands.Peek(); bool commandComplete = false; switch(command.Command) { case "walkTo": // Move the object. Vector2 position = (Vector2)command.Data; this.Position += Vector2.Normalize((position - this.Position)) * this.WalkSpeed; if(this.Position >= position) commandComplete = true; break; case "turnTo": // Turn... break; case "say": // Chat bubbles... commandComplete = true; break; } if(commandComplete) commands.Dequeue(); } }
// In derived objects
public void Update(GameTime gameTime) { this.ProcessScriptCommands(gameTime); }
protected override void ProcessScriptCommands(GameTime gameTime) { // Handle object specific commands then process global ones from base. base.ProcessScriptCommands(gameTime); }
// In scene...
Actor myActor = new Actor(); myActor.AddScriptCommand("walkTo", new Vector2(30, 40)); myActor.AddScriptCommand("wait", 300); myActor.AddScriptCommand("say", "Nice to meet you!");
public void Update(GameTime gameTime) { myActor.Update(gameTime); }
Then add in some timings/delays, flesh out the commands, and you're done.
|
|
|
|
|
Logged
|
|
|
|
|
ChevyRay
Guest
|
 |
« Reply #7 on: October 28, 2011, 04:27:15 AM » |
|
Yeah, I think I know more or less how I want to code the events that actually control the objects, but it's the development process that's weighing on me. For example, scenes will take a lot of tweaking, meaning if I was just coding it right into the game engine, I'd have to run, watch, stop, change values, run, watch, stop, change values, etc. I'm already getting a headache from the tedium, and I'm just talking about it.
|
|
|
|
|
Logged
|
|
|
|
|
DeadPixel
|
 |
« Reply #8 on: October 28, 2011, 04:30:58 AM » |
|
So you're more worried about how to implement scripting controls from your editor, rather than how to implement a cutscene/machinima system?
|
|
|
|
|
Logged
|
|
|
|
|
eclectocrat
|
 |
« Reply #9 on: October 28, 2011, 04:36:29 AM » |
|
I like DeadPixel's example code. If you really don't need conditionals at all, then why not read a plaintext file? actorName.walkTo(30, 40) actorName.wait(300) actorName.say("Nice to meet you!")
That should be very easy to parse and allow you to have a faster dev cycle. Just designate a special button during development that resets the actors and reloads the script file you are testing (for example 'P'). Keep a notepad instance open alongside your game and you can edit, press control-S, alt-TAB back to your game, press P to try the new script.
|
|
|
|
|
Logged
|
|
|
|
|
ChevyRay
Guest
|
 |
« Reply #10 on: October 28, 2011, 04:40:53 AM » |
|
I'm more worried about it, but interested in the whole shabang. Already this is good, just want to hear what methods folks used, how well/bad they worked, so I can get some sort of useful perspective on it before I destroy my game's source.
Getting some good ideas for syntax stuff too, need to figure out how to parse files properly of course, never used regular expressions before (other than just copypasta-ing ones off the internet I needed), so that's good.
It definitely seems like I'll have to add in instance naming to the editor, and use that in the scripts. Will probably add in a script editor as well, so I can write/test/write/test easily without ever having to re-build.
This is probably the approach I am leaning towards, though it's gonna take me awhile to get the scripts saving/loading/reading properly, gotta make sure I do it right the first time, tired of having to re-write stupidly written classes.
|
|
|
|
|
Logged
|
|
|
|
|
DeadPixel
|
 |
« Reply #11 on: October 28, 2011, 04:45:45 AM » |
|
Reflection can be your best friend in these cases!  Being able to edit script values on the fly in-game can be very useful for quick iteration. Then you can just save out to file to load later once everything is kosher. If your entities are rather straight forward (no complex data types as public members, etc) then XNA's XML loading can be an easy fit for you. You can then use the content pipeline to load your entities and their scripted data directly. Otherwise, doing it by hand isn't too hard either.
|
|
|
|
|
Logged
|
|
|
|
|
ChevyRay
Guest
|
 |
« Reply #12 on: October 28, 2011, 04:47:13 AM » |
|
I guess a difficult situation for me to describe using this system is specifically scripts that need to create objects. Such as the example in my original post: character appear from the left, run in, and stop. My script needs to know which objects to create, and what name to give them (so it can give them commands).
One thing I can think of that'd likely be a simple/elegant solution would be a flag for "disabled" entities in the editor, where I can actually create objects but disable them, so when the scene starts, they are just tossed into a cache, where they keep their instance name.
Then, I'll have simple create() and destroy() functions in the scripts for moving these objects to and from the disabled bin in the scene.
Then I could place objects, give them all the starting parameters/positioning/names I want right in the editor using all the tools I've build, and would know how/what I'm referencing in the scripts.
This sounds nice... HMMmmmm
|
|
|
|
|
Logged
|
|
|
|
|
ChevyRay
Guest
|
 |
« Reply #13 on: October 28, 2011, 04:48:57 AM » |
|
Reflection can be your best friend in these cases!  Being able to edit script values on the fly in-game can be very useful for quick iteration. Then you can just save out to file to load later once everything is kosher. If your entities are rather straight forward (no complex data types as public members, etc) then XNA's XML loading can be an easy fit for you. You can then use the content pipeline to load your entities and their scripted data directly. Otherwise, doing it by hand isn't too hard either. My second reply you ninja'd addressed this as well, but yeah, already using XML serialization for saving/loading the entire scenes via the editor, so I've already got a format working for the entities, though I think the "anything that's in a scene/event exists from the start, nothing is actually *created* by scripts" is a safer and less confusing solution for my purposes.
|
|
|
|
|
Logged
|
|
|
|
|
randomnine
|
 |
« Reply #14 on: October 28, 2011, 04:51:00 AM » |
|
For player/NPC control, I recommend abstracting their control out to a Controller interface. This interface has functions like "shouldFaceLeft()", "shouldMoveRight()", "shouldJump()", etc - including ways to trigger special animations. The player's actor normally has a Controller attached which interprets keyboard input as specific signals. NPC actors have Controllers attached which run AI and fire off control signals. The actor code itself simply interprets these input signals - it doesn't care whether it's being controlled by a player, AI, or script.
During a cutscene, you override the normal controller with a dead simple one that lets you switch control signals on and off manually from code/script. You can add slightly more elaborate functions like "move left until x = blah" later.
You restore control to the player/AI by reconnecting the normal controller. I use this setup in all my games now and it makes switching between player/AI control and various kinds of scripted behaviour a doddle.
For actually lining up cutscene events... an action queue/timeline is a good approach, but whatever you do, take into consideration variable length events. This can involve things like moving an actor to a required position from an unknown starting position, letting players fast forward dialogue, or having dialogue stay up until players press a button.
As for having to watch the whole thing and iterate to get the pacing and everything right... there's literally no way round that (though for sanity's sake, for cutscenes over 30 seconds or so, I'd suggest implementing a method of fast-forwarding to key points by simply setting all actor positions/states). If you can't launch your game and start a given cutscene in a few seconds, you either need to get your game launching faster or you'll have to build in a scripting language and support reloading scripts at runtime to get iteration times down.
|
|
|
|
|
Logged
|
|
|
|
|