Icemage's Dungeon Siege™ Skrit Skribblings
An Unofficial Skrit Tutorial
How to Read Skrit Components, version 2.00.02
PURPOSE
A lot of people are mystified by Skrit and how it functions in Dungeon Siege.
This document is intended to dispel some of that confusion, and will hopefully
help some aspiring modders out there.
DISCLAIMERS
Any errors in this document are strictly the author's. Use of the
knowledge contained herein is at your own risk. The author is not
responsible for any data corruption, lost time, or other consequences of any
application of this document. Dungeon Siege™ is a registered trademark
of Gas Powered Games, Inc. and Microsoft, Inc.
Copyright ©2002 by Icemage. All rights reserved.
This document may not be reproduced or distributed in whole or in part without
the express written consent of the author.
THE BASICS
So what is a Skrit component?
Well, there are a few common characteristics shared by Skrit components,
so let's start with those:
(A) Most Skrit components reside in one of several places: **
Logic.dsres/world/contentdb/components
:
These Skrits define Skrit code that customizes behavior of various objects in
the
game, including spells, items, and non-player characters (NPCs, such as
shopkeepers and townsfolk)
Logic.dsres/auto :
These Skrits define Skritbots, automated code which handle various in-game
duties.
Note : Skritbots are currently non-functional in DS version 1.00 and 1.09B.
Logic.dsres/art/animations
:
These Skrits handle the work of dealing with specialized animation jobs.
Logic.dsres/global :
These Skrits are accessible by all game elements, and cover a variety of tasks,
such
as controlling moods and generating special effects for spells and items.
Logic.dsres/world/ai/jobs
:
These Skrits define how game objects with Artificial Intelligence routines (such
as
characters and monsters) do things like walk, attack, fidget, etc .
(B) Skrit components are text files written in Skrit, and have a filename
extension of ".skrit". **
(C) Skrit components are "called" by various things in the game, most
often a GO (Skrit shorthand for Game Object) that has to do something unusual to
the game environment.
** The
exception to this rule is "embedded" Skrit, which is Skrit code which
resides in a ".GAS" file, more commonly known as a template file.
If you are just starting to work with Skrit, don't worry about this type of
Skrit code. Embedded Skrit is an advanced topic that lies beyond the scope
of this document, and is very rarely used in any case.
Ok, so now that you have an idea of how to recognize a Skrit component,
what sort of things make up a Skrit component?
Let's take a look at a simple Skrit, spell_lightning.skrit, that
can be found inside the structure of the Logic.dsres file:
Logic.dsres >art >auto >config >ui >world >ai >contentdb >components >chests >commands >dev >doors >elevators >emitters >fx >generators >generic >levers >map_specific >spells >spell_lightning.skrit >test >traps >weapons >templates >global >maps |
Code from spell_lightning.skrit:
///////////////////////////////////////////////////////////////////////// // // File : spell_lightning.skrit // Author(s): Eric Tams // // Copyright © 2001 Gas Powered Games, Inc. All rights reserved. //----------------------------------------------------------------------- // $Revision:: $ $Date:$ //----------------------------------------------------------------------- // // Wrapping lightning damage into the attack component for a spell. // ///////////////////////////////////////////////////////////////////////// property string effect_script$="" doc="Effect script to call for effect"; property float dur$=1.0 doc="How long the effect plays."; owner = GoSkritComponent; #include "k_inc_spl_utils" GoID target$; GoID caster$; startup state Idle$ { event OnGoHandleMessage$( eWorldEvent e$, WorldMessage msg$ ) { if( e$ == WE_REQ_CAST ) { target$ = MakeGoID( msg$.GetData1() ); caster$ = msg$.GetSendFrom(); if( !target$.IsValidMp || !caster$.IsValidMp ) { SetState Finished$; return; } PackLightning$( dur$ ); Siegefx.SRunScript(effect_script$,target$,caster$,params$,owner.GoID, e$ ); this.CreateTimer( 1, dur$ ); } } transition -> Finished$: OnTimer( 1 ); } state Finished$ { event OnEnterState$ { PostWorldMessage( WE_REQ_DELETE, owner.GoID, owner.GoID, 10 ); } } |
This Skrit does something very simple: It takes a spell effect and adds
lightning damage to it.
It does so in several distinct steps:
Step 1 | The Skrit does some preparation before the spell is cast. |
Step 2 | The Skrit waits for a successful attempt to cast the spell. |
Step 3 | The Skrit adds the lightning damage to the effect to be generated by the spell. |
Step 4 | The Skrit calls for the spell's visual effects to begin. |
Step 5 | The Skrit waits for the spell's effect to end. |
Step 6 | The Skrit cleans up and deletes the spell from the game. |
Let's take a item-by-item look at
the code itself now to see how this is accomplished.
At the top of this Skrit, the first thing you'll see is
a bunch of lines that begin with a double slash //. All text following
double slashes represent a comment. Comments are used to
explain and document the code, but do not have any effect on the way the code
actually runs (they are ignored by the program). There are
actually several different ways to comment out code, but for simplicity
we'll just use this one for now.
In this case, the comments remind us about the name of the skrit, the
author, some copyright information, some version information, and a brief
description of what the code actually does.
Step 1 | The Skrit does some preparation before the spell is cast. |
Now, the first two non-commented lines we see in this Skrit are:
property
string effect_script$="" doc="Effect script to call for
effect"; property float dur$=1.0 doc="How long the effect plays."; |
...
...
Huh? What does all this stuff mean?
property
string effect_script$="" doc="Effect script to call for
effect"; property float dur$=1.0 doc="How long the effect plays."; |
The property keyword indicates that the item is a characteristic value that can be set by whatever is calling the Skrit. Don't worry if you don't understand this explanation for now, more will be revealed below.
property
string effect_script$="" doc="Effect
script to call for effect"; property float dur$=1.0 doc="How long the effect plays."; |
The string and float
keywords indicate the data type of each property. Data types
refer to how the game code represents values that need to be tracked or
remembered.
Some common data types used in Skrit are:
Int
"Int" stands for "integer" values, which are non-fractional
positive or negative numbers.
Float
"Float" stands for "floating point decimal" values,
which are rational numbers (numbers that can have fractional parts). Note
that the decimal point is optional for non-fractional values assigned to a Float.
String
In common programming lingo, a string refers to a text value that is
stored and manipulated by a program. In Skrit, string values should
be enclosed in quotation marks(").
Bool
"Bool" stands for a "Boolean" value. Named after
George Boole, a 19th century mathematician, Boolean values can only have one of
two values: True or False.
In this case, the first property is a string (a text value).
The second line tells the game that the second property is a float
(a decimal value with fractions).
property
string effect_script$="" doc="Effect
script to call for effect"; property float dur$=1.0 doc="How long the effect plays."; |
These two items tell the game what the name of the
variables are.
REMINDER: User-defined values must ALWAYS end in a
"$".
The values after the = signs indicate the default value. If the object
that calls the Skrit component doesn't specify a value for a property, the default
value is the value assigned to the property.
So, in this case, if no value is otherwise given for effect_script$, it defaults
to an empty string ("");
if no value is given for dur$, it defaults to a value of 1.0.
property
string effect_script$="" doc="Effect script to
call for effect"; property float dur$=1.0 doc="How long the effect plays."; |
The doc="<some text>"
portion of the command gives a bit of description to each value (to help remind
you what that value represents later if you want to come back and edit the
code).
Note the semicolons (;) at the end of each
line. The semicolons are EXTREMELY important. Dungeon Siege
will not process your Skrit code correctly without a semicolon at the end of
each Skrit command, so pay close attention to them! If you get an error,
the very first thing you should look for is a missing semicolon!
owner = GoSkritComponent; |
owner is a special value recognized by Skrit. It generally refers to the GO (Game Object) that called the Skrit code. In this case, GoSkritComponent is a function that gets the GO of the spell which called this Skrit. Note that, because owner is automatically recognized by Skrit, it is not a user-defined value and so you don't need to add the "$" symbol on the end (this is an exception to the REMINDER above)
#include "k_inc_spl_utils" |
The #include instruction should be familiar to those who have learned C++ or Java programming, but for the uninitiated, this instruction simply tells the game to go look for the code from the file "k_inc_spl_utils.skrit" and add it to this one. This is important because the code needs a function that "k_inc_spl_utils.skrit" provides (PackLightning$). Note that the Skrit file called by a #include instruction can be located anywhere inside the Logic.dsres/logic/world/components folder, or one of its sub-folders.
NOTE: This instruction does NOT use a semicolon at the end, as it is not a Skrit command. It is actually something called a "compiler directive," which is a complex techno-babble term for a very simple concept. A compiler directive is an instruction that is processed before any of the code actually gets translated into something the computer can use. What this means is that the game actually deals with compiler directives before generating the code that runs the game - in this case, it needs to "include" the code from another Skrit file to get the required code for the PackLightning$ function before it starts translating into "computer-speak."
GoID
target$; GoID caster$; |
These two lines tell the game that you want to create
two variables (a variable is a value that can be changed by the
code) to save two GoID values, called target$ and caster$. So
what's a GoID? A GoID is a Game Object Identifier.
One way to think of a GoID is like the address for your house. No
other house has the same address. And so it is with GoIDs :
GoIDs are unique number values which every object in the game
possesses.
No two objects in Dungeon Siege can possess the same GoID, so you can use
them to keep track of things in the game without risk of getting the wrong
object.
Ok, now what?
Ok, before we get further into this, I'll give a brief overview of the concepts of state-based virtual machines, states, transitions, triggers and events. Stay with me, folks - this isn't as bad as it sounds.
What is a state-based virtual machine?
In plain English:
A state-based virtual machine is a machine which operates differently under different conditions (called states).
That's a bit vague, so I'll try to give you some real-world examples:
Example 1: A light switch
Ok, this is one of the simplest machines you use every day. You flip the switch, it turns the light off. You flip it again, it turns the light on. So how does this make it a state-based virtual machine?
State A: Light is OFF
State B: Light is ON
In State A and State B, the same action (flipping the switch), produces a different effect depending on the State that exists at the moment. If the light is OFF (State A), it changes or transitions to State B. If the light is ON (State B), the switch transitions to State A.
Thus:
A state is a
distinct condition which determines how the
machine behaves.
A transition is a change
from one state to another state.
Example 2: A traffic light
I'll assume everyone knows how traffic lights work.
Green means Go.
Yellow means
Caution.
Red means Stop.
How can a traffic light be a state-based virtual machine?
Remember that our simple definition above is that we need a machine that works differently under specific conditions.
State A: Green light
State B: Yellow light
State C: Red light
For most typical traffic lights, State A (Green) transitions to State B (Yellow) after a certain amount of time. Then State B (Yellow) transitions to State C (Red) after another period of time. Lastly, State C (Red) transitions to State A (Green) after yet another pause, and the process starts all over again.
Are you starting to see how this works? Great!
What is a trigger?
A trigger is a condition which causes the machine to exhibit a specific behavior.
In Example 1 above, the trigger could be
considered to be flipping the light switch (causing a transition).
In Example 2, the trigger is the timer in the traffic signal, causing the
transitions between states.
So what is an event?
Events are a special set of triggers that the machine keeps track of automatically. In Example 1, one way we could define the way a light switch works would be:
Startup State A: Light is ON
Trigger: Flip switch
Command: Transition to State B
Event: OnEnterState
Command: Turn light ON
State B: Light is OFF
Trigger: Flip switch
Command: Transition to State A
Event: OnEnterState
Command: Turn light OFF
This example assumes we start with the light ON
(Startup State A). A Startup State is the beginning
condition that we start the machine in. In this case, when we start up, we
"enter State A," which is caught by the OnEnterState event
(literally "do this/these commands when we enter this state").
That event turns the light ON (if it wasn't on already).
Now, the machine (the light switch) waits until something happens to force it to
do something. In this case, when we flip the switch. we cause the trigger
to activate the command to transition to State B.
Once we've transitioned to State B, the OnEnterState event
of State B runs, and turns the light OFF, and we go into a wait
again. The next time the switch is flipped, it will trigger the transition
to State A.
Let's look at this same example again, only from a Skrit-like point of view:
Startup StateA$ // Light is ON
{
Trigger FlippedSwitch$
{
SetState StateB$; //Transition to StateB$
}
Event OnEnterState$
{
Light_Is_On$
= True;
}
}
StateB$ //Light is OFF
{
Transition -> StateA$ : FlippedSwitch$; //Transition to
StateA$
Event OnEnterState$
{
Light_Is_On$ = False;
}
}
Note how I've added the brackets { } to enclose each set of items following a State,
Event, or Trigger definition. Remember also that // denotes where
a comment begins - it's bits of commentary text used to explain what's happening
to us humans, and is not read by the machine.
Now, look at the line:
Transition -> State A$ : FlippedSwitch$; //Transition to
StateA$
You're probably scratching your head saying "Huh? What's that colon
doing there? And why aren't there any brackets?"
This is the way triggered transitions are written in Skrit:
Transition -> State : Condition;
//Transition to State when Condition is met
When the Condition becomes True, the game transitions to the
specified state.
This is functionally equivalent to:
Trigger Condition
{
SetState State; //Transition to State
}
...which is the version you see in State A$.
Now you know about state-based virtual machines, states, transitions,
triggers and events!
See, that wasn't so hard, was it?
Moving on...
Now let's analyze the next block of code from spell_lightning.skrit:
startup
state Idle$ { event OnGoHandleMessage$( eWorldEvent e$, WorldMessage msg$ ) { if( e$ == WE_REQ_CAST ) { target$ = MakeGoID( msg$.GetData1() ); caster$ = msg$.GetSendFrom(); if( !target$.IsValidMp || !caster$.IsValidMp ) { SetState Finished$; return; } PackLightning$( dur$ ); Siegefx.SRunScript(effect_script$,target$,caster$,params$,owner.GoID, e$ ); this.CreateTimer( 1, dur$ ); } } transition -> Finished$: OnTimer( 1 ); } |
Armed with your knowledge of states, events
and transitions, you now can tell several things immediately about this
code:
> The Skrit starts off in the Idle$ state due to the
"startup" keyword.
> The Skrit looks for something called a eWorldEvent.
> The Skrit transitions to the Finished$ state when timer #1
finishes its countdown.
Now I'll fill in the gaps for you:
Step 2 | The Skrit waits for a successful attempt to cast the spell. |
The Idle$ state is the starting condition for this Skrit. While in the Idle$ state, the code waits until something forces a transition to another state. There are two conditions that will cause a transition from the Idle$ state. One is an event, the other is a triggered transition.
event OnGoHandleMessage$( eWorldEvent e$, WorldMessage msg$ ) |
This event occurs whenever anything in the game generates or "posts" a message to the engine (this sort of message is called an eWorldEvent). The game keeps track most things by having everything post messages about what it is doing, so this event will check every message that gets sent to the GO(Game Object) that called the Skrit (this is why the event is called OnGoHandleMessage$).
So how does it know which one it's looking for? That's where the next line comes in:
if( e$ == WE_REQ_CAST ) { ... } |
Experienced coders will recognize the form of a if statement,
but for those who don't know what that is, I'll cover it now. If you
are already familiar with if statements, just click here.
An if statement is a simple yes-or-no test.
If the condition specified inside the parentheses () is True, then
the game will execute the command(s) enclosed by brackets { }.
If the condition is False, then the game will skip all of the
commands enclosed by the brackets { }.
If statements in Skrit take one of several forms:
Form 1: Simple If statement
If
( condition ) { Do_This_Command; Do_This_Other_Command; ... } |
This is the standard form of an If statement as described above.
Form 2: If...Else statement
If
( condition ) { Do_This_Command_If_Condition_Is_True; Do_This_Command_If_Condition_Is_True; ... } Else { Do_This_Command_If_Condition_Is_False; Do_This_Command_If_Condition_Is_False; ... } |
This form adds a new element to the If statement:
an Else clause. An Else clause tells the code what to do
when the Condition is False, rather than True.
Let's look at an example:
If
( treat$ = "cookie") { Eat ( treat$ ); Smile; } Else { GetNewTreat; } |
So if we have a "cookie" we'll eat it, and
smile, otherwise, we go get a new treat. I guess we're picky about our
desserts!
Form 3: If...Else If statement
If
( condition1 ) { Do_This_Command_If_Condition1_Is_True; Do_This_Command_If_Condition1_Is_True; ... } Else If ( condition2 ) { Do_This_Command_If_Condition2_Is_True_But_Condition1_Is_False; Do_This_Command_If_Condition2_Is_True_But_Condition1_Is_False; ... } |
This third form is simply an If statement
attached to the Else clause of another If statement. You can
connect any number of If statements together this way, which will cause
the game to run the commands for the first condition it finds to be True.
In this case,
our code is looking for an eWorldEvent (stored in the variable e$) called
WE_REQ_CAST, which is triggered whenever a GO is used to cast a spell.
If this condition is met, then the code inside the brackets { } is run:
target$ = MakeGoID( msg$.GetData1() ); caster$ = msg$.GetSendFrom(); |
The first line here gets the GoID (Game
Object Identifier, see the definition above
if you missed it) of the target of the spell.
The second line gets the GoID of the caster of the spell.
if( !target$.IsValidMp || !caster$.IsValidMp ) { SetState Finished$; return; } |
This if statement tests two things. The exclamation point ( ! ) stands for "NOT." The double pipe ( || ) stands for "OR."
If we re-read this, we get, in plain English:
"If the target is NOT valid in multiplayer
OR the caster is not valid in multiplayer, then set the state to Finished$(clean
up details before exiting) and return (exit immediately from this Skrit)."
Step 3 | The Skrit adds the lightning damage to the effect to be generated by the spell. |
PackLightning$( dur$ ); |
... calls a function from "k_inc_spl_utils.skrit". You'll recall from above that the line:
#include "k_inc_spl_utils" |
...adds the code from that skrit file to this one when
the game goes to generate the machine code. In this case, the
PackLightning$ function is the one that actually adds the lightning damage into
the spell mechanics.
Step 4 | The Skrit calls for the spell's visual effects to begin. |
Siegefx.SRunScript(effect_script$,target$,caster$,params$,owner.GoID, e$ ); |
This confusing little command creates a visual effect with the SiegeFX engine.
effect_script$ specifies the name of
the script that will create the visual effect.
target$ is the GoID of the target of the spell.
caster$ refers to the caster's GoID.
params$ is borrowed from "k_inc_spl_utils.skrit",
which allows you to add extra options for some effects.
owner.GoID is the GoID of the spell that called this
Skrit.
e$ is the eWorldEvent that triggered the effect (in this
case, WE_REQ_CAST).
this.CreateTimer( 1, dur$ ); |
This command tells the game to set up a timer, with an identifying number 1, which counts down for a number of seconds equal to dur$ (specified in the property section at the top of the Skrit code ).
So the OnGoHandleMessage$ event looks for an attempt to cast the spell which called the Skrit, tells the game engine to add lightning damage to the effect specified by the spell, then runs the effect itself. If either the caster or the target is illegal for the spell, the code aborts immediately.
Step 5 | The Skrit waits for the spell's effect to end. |
transition -> Finished$: OnTimer( 1 ); |
This is a simple triggered transition. Once timer #1 (called by the this.CreateTimer( 1, dur$ ); command) hits 0.0 seconds remaining, the game will transition to state Finished$.
Step 6 | The Skrit cleans up and deletes the spell from the game. |
state
Finished$ { event OnEnterState$ { PostWorldMessage( WE_REQ_DELETE, owner.GoID, owner.GoID, 10 ); } } |
This part should be easy for you to figure out now. Once the Skrit transitions to state Finished$, it posts a message to the game engine asking to delete the specified Game Object (in this case, the copy of the GO of the spell which called this Skrit).
CONCLUSION
As you can see, there's a fair amount of complexity in reading and understanding the Skrit code, but it's certainly not impossible, nor particularly difficult once you get some practice with it. This tutorial should give you the basic tools you need to get a handle on what's happening in the code.
If you have questions, please feel free to post a question the forums at www.DSRealms.com. I'm always happy to help others understand the game.
Happy Skritting!
Icemage
VERSION HISTORY
1.00: 6/10/02
First release.
2.00: 6/12/02
Numerous changes.
- Fixed color coordination. Made color use more
consistent, and brighter.
- Fixed numerous typos, grammatical errors, and punctuational
errors.
- Added table structures for readability
- Made the document friendlier to 800x600 screen sizes
- Expanded and clarified explanations for file locations.
- Expanded explanation of base data types
- Expanded explanation of If...Else structures
2.00.01: 6/12/02
Handed the document to Xaa,
who writes HTML pages and HTML e-books for a living, and he shrieked in terror
at the vivid colors before completely re-formatting the document.
- He also added links to cross-referenced information within
the document to make it easier to navigate.
2.00.02: 10/18/02
Fixed a few formatting and typographical errors.