Hi,
After having dabbled a lot in itemScripts/charScripts lately, I'd like to share a couple of things I've noticed that may save some hair tearing for other people.
There is already
a good introduction elsewhere, but I'd like to mention a few additions/clarifications:
General• After creating a new script, only specify a name for the script. Do
not try to specify a path, the (EE) editor will automatically put it in the right place and create the necessary folders (and trying to specify a path will mostly result in errors about you trying to select a wrong location)
• After creating/modifying a script, you do not have to reload the module. It is sufficient to reload the story (open the story editor and press F8)
• After reloading a level (via File -> Reload level), you also have to explicitly reload the story if you want the OnInit() event handlers of your charScripts/itemScripts to run again (which is almost always the case!)
Scripting language• When declaring a variable, the syntax is
TYPE:NAME without any spaces before or after the ":"! Adding any whitespace around the ":" will result in a syntax error.
• You can give a global or local variable an initial value via "=", e.g.
INT:_Count = 0. The number of spaces around the "="-sign does not matter.
• The "local" variables of an EVENT or BEHAVIOUR are like static variables in C. This means that they keep their value once assigned,
even if they are declared with an initial value. E.g.
INT:_MyVar = 0 means that the first time this EVENT/BEHAVIOUR is entered _MyVar will be equal to 0, but on subsequent invocations it will have whatever value it had when the EVENT/BEHAVIOUR exited the last time.
• An extra variable type that is not mentioned in the aforementioned tutorial is FIXEDSTRING. Variables of this type can be used to specify variable names to e.g.
SetVar()• Some calls that can take multiples types of data, such the aforementioned
SetVar(), may need to explicitly know the type of a constant to determine what kind of data you are passing. You can create typed constants by prepending the type to the constant like with a variable declaration, such as
SetVar(_SomeChar,"SomeItemVar",ITEM:null)• Unlike in the story editor, you
cannot replace
OUT-parameters to calls in conditional checks with constants to avoid having to declare a variable and checking its value later. Unfortunately, such constructs to not result in compile time or run time errors. I.e., this will compile and run, but the THEN-clause will always run assuming
_SomeItem has a global variable called
%GlobalntVar, regardless of its value:
IF "c1"
GetVar(INT:0,_SomeItem,"GlobalIntVar")
THEN
ItemEvent(__Me,"SomeItem.GlobalIntVar == 0! Or any other value, really.")
ENDIF
Debugging• Sometimes when saving your script the editor will check it for errors, and sometimes it doesn't. When it doesn't, explicitly choosing Build->Compile or Build->Build All won't help. The only way to check your script for errors in that case is to start the module in the editor (smiley face on the top left of the main editor window). Any detected errors will appear in the MessagePanel at the bottom of the main editor window. After correcting the errors, make sure to reload the story for your corrections to be applied. New errors may be detected after fixing previous ones. Scripts with errors in them are disabled.
• The line numbers mentioned for error messages are wrong if your script contains any completely blank lines: the blank lines are not counted at all. To work around this, ensure every line of your script contains at least a space or tab.
• The error messages in the MessagePanel may be cut off, even after dragging the (invisible) divider to the right. The full messages are logged to errors.txt in the folder containing the editor binary ("Divinity - Original Sin Enhanced Edition"\"The Divinity Engine Enhanced Edition"), but unfortunately the output to that file is buffered. This means you may have to quit the editor to be able to see all messages in it.
• There is a "Debug" checkbox on the top right of item/charScripts, but I have no idea what it does apart from showing a sidebar with script section names.
• Unlike for story scripts, things that happen in item/charScripts are not logged to osirislog.log. You can work around this by making the items/characters to which you attach scripts global, and subsequently adding things like
ItemEvent(__Me,"entering event X"),
ItemEvent(__Me,"taking then-branch for check X") and even
CharacterItemEvent(__Me,_SomeItem,"this is the item I'm getting"). These events will then be logged to osirislog.log along with their concrete parameter values (so you can use other variables besides __Me if you wish to log which items/characters are involved). In all cases, make sure everything you pass to the events is global if you want their values to be logged.
• An alternative debugging method, also mentioned elsewhere on the forum, is to play various effects. My favourite ones are pillar loop effects with different colours, as they can be used to create a persistent visual of the order in which things happened, and to debug locations (FLOAT3, but also character/item locations). E.g.
ItemPlayLoopEffect(_,__Me,"FX_GP_LightPillarRed_A") (type
PlayLoop in the script editor to see the other variants). I ignore the return value (the first parameter, a handle to the effect) since you only need it to remove the pillar again. If you want to get rid of them at some point, reload the level (followed by reloading your scripts, as mentioned earlier). Other pillar loop effects are
FX_GP_LightPillarWhite_A,
FX_GP_LightPillarBlue_A,
FX_GP_LightPillarGreen_A,
FX_GP_LightPillarPurple_A and
FX_GP_LightPillarOrange_A.
Design/code patterns• If you have code that would set or get array elements from a fixed length array in a loop in most imperative programming languages, you can probably achieve the same effect with separate variables combined with
GetElement(), and
GetVar()/SetVar(). E.g.
INIT
INT:%IntVar1
INT:%IntVar2
INT:%IntVar3
EVENTS
EVENT ResetVars
VARS
FIXEDSTRING:_VarName
INT:_Counter
ON
OnCharacterEvent(__Me,"ResetVars")
THEN
// don't just declare "INT:_Counter = 0" above, as that initialisation would
// only apply the first time this code is executed!
Set(_Counter,0)
WHILE "c1"
IsLessThen(_Counter,3) // I really want to know who is responsible for this typo :)
DO
// Warning: do NOT use FIXEDSTRING:"IntVar1" etc, that seems to break things
// when you get to 4 or 5 elements, and it's not necessary as the compiler will
// automatically convert these string constants to FIXEDSTRING as needed
GetElement(_VarName,_Counter,"IntVar1","IntVar2","IntVar3")
SetVar(__Me,_VarName,0)
ENDWHILE
• You can also use FIXEDSTRING variables with
GetVar() and
SetVar() to create you own "OUT"-parameters (or even
IN/OUT) to events and functions. Or even for introspection, since
GetVar() will return false if a variable with that name does not exist. E.g. (from my "rails" WIP; fragment, may not compile as is):
INIT
ITEM:__Me
// Parameters to/result from MaybeAssignTrackConnection function
FLOAT3:%MATC_CheckPos // position of the endpoint of the current track
ITEM:%MATC_CheckOtherItem // other track of which the endpoints we should try to match to this track's endpoint
FIXEDSTRING:%MATC_ConnectingItemName // if we match an endpoint of the other track, connect it to this track's "ConnectingItemName" field
// Names of the fields for the tracks connecting to the current track
// (one incoming and at most two outgoing tracks)
FIXEDSTRING:%IncomingTrackName = "IncomingTrack"
FIXEDSTRING:%OutgoingTrackName1 = "OutgoingTrack1"
FIXEDSTRING:%OutgoingTrackName2 = "OutgoingTrack2"
// Names of the fields with the positions of the current track's connections
FIXEDSTRING:%IncomingPosName = "BeginPos"
FIXEDSTRING:%OutgoingPosName1 = "EndPos1"
FIXEDSTRING:%OutgoingPosName2 = "EndPos2"
EVENTS
// For each nearby track, iterate over all of our connection points
EVENT OnIterate_NearbyTracks
VARS
ITEM:_Item
ITEM:_TrackConnection
ITEM:_TrackConnectionPosition
INT:_Index
FIXEDSTRING:_PosVarName
ON
OnIterateItem(_Item,"RAILS_OnIterate_NearbyTracks")
ACTIONS
// IterateItemsNear(__Me,..) also iterates over __Me itself
IF "c1"
IsEqual(__Me,_Item)
THEN
RETURN
ENDIF
Set(%MATC_CheckOtherItem,_Item)
Set(_Index,0)
// Loop over all of our connection points
WHILE "c1"
IsLessThen(_Index,3)
DO
GetElement(%MATC_ConnectingItemName,_Index,%IncomingTrackName,%OutgoingTrackName1,%OutgoingTrackName2)
GetElement(_PosVarName,_Index,%IncomingPosName,%OutgoingPosName1,%OutgoingPosName2)
// Connection point not yet connected -> try to connect it to the current iterator item
IF "c1&c2&c3"
GetVar(_TrackConnection,__Me,%MATC_ConnectingItemName)
IsEqual(_TrackConnection,null)
GetVar(%MATC_CheckPos,__Me,_PosVarName)
THEN
CallFunction("MaybeAssignTrackConnection")
// don't exit even if connected, since in theory there may be
// multiple connections between the two items
ENDIF
Add(_Index,1)
ENDWHILE
// Check whether our selected connection point is the same as a connection point
// of another track, and if so assign us to that connection point, and them to ours
//
// Parameters:
// * ITEM:%MATC_CheckOtherItem : the other track
// * FLOAT3:%MATC_CheckPos : the position of our connection point
// * FIXEDSTRING:%MATC_ConnectingItemName : the name of the field representing our connection point
// Result: /
EVENT MaybeAssignTrackConnection
VARS
FLOAT3:_OtherConnectionPos
ITEM:_OtherConnectionTrack
FLOAT:_ConnectionsDistance
INT:_Index
FIXEDSTRING:_TrackVarName
FIXEDSTRING:_PosVarName
ON
OnFunction("MaybeAssignTrackConnection")
ACTIONS
// Loop over all possible connection points of the other track
Set(_Index,0)
WHILE "c1"
IsLessThen(_Index,3)
DO
GetElement(_TrackVarName,_Index,%IncomingTrackName,%OutgoingTrackName1,%OutgoingTrackName2)
GetElement(_PosVarName,_Index,%IncomingPosName,%OutgoingPosName1,%OutgoingPosName2)
// Does this connection point exist (c1), is it not yet assigned (c2) and
// does it connect to our connection point (c3&c4&c5)?
IF "c1&c2&c3&c4&c5"
GetVar(_OtherConnectionTrack,%MATC_CheckOtherItem,_TrackVarName)
IsEqual(_OtherConnectionTrack,null)
GetVar(_OtherConnectionPos,%MATC_CheckOtherItem,_PosVarName)
GetDistance(_ConnectionsDistance,%MATC_CheckPos,_OtherConnectionPos)
IsLessThen(_ConnectionsDistance,%ConnectionFuzz)
THEN
// Connect them to us and us to them
SetVar(%MATC_CheckOtherItem,_TrackVarName,__Me)
SetVar(__Me,%MATC_ConnectingItemName,%MATC_CheckOtherItem)
// Finished for this connection point of ours
RETURN
ENDIF
Add(_Index,1)
ENDWHILE
This code is part of
RAILS_ConnectedTrackBase.itemScript, which is imported/used by all tracks (straight/corner/switch). A track has one incoming track and up to two outgoing tracks. What happens here is that
RAILS_OnIterate_NearbyTracks iteratively sets
MATC_ConnectingItemName to the different connection points that a track may have, checks whether it exists (via
GetVar) and if so, it calls
MaybeAssignTrackConnection and passes the name of that variable via
MATC_ConnectingItemName. That routine can then use
SetVar to change that variable's value if desired.