Arrays (Papyrus)

From the CreationKit Wiki
Revision as of 22:54, 29 June 2015 by imported>Bug64 (Made it easier to extend the example to higher dimensions)
Jump to navigation Jump to search

Description

Arrays are special kinds of variables that can hold more then one value of the same type. You select which value you want via a numerical index that ranges from zero to the length of the array, minus one.


For beginners who would like to gain a further understanding of arrays

Although the small description can be used as a limited definition of what an array is, reality is that the concept of an array is a bit deeper, but still very simple to grasp. To illustrate, let's say that you want to have a set of 10 weapons. You could do: Weapon weapon1, Weapon weapon2 ... Weapon weapon10. Tedious, is it not? Imagine having to iterate and check every one of those single variables whether or not they meet the requirements of your inquiry. Ugly, yes? Now that you've got a bitter taste in your mouth, let's explore how this is resolved. Notice that they are all variables of type "Weapon", and that they only differ by their content (a value of some sort) and the number which specifies numerically which weapon is which. Now, let's combine that into something useful. We want an array, a set of variables of a specific data type, and we want a way to index them.


A rather simple formal representation: <identifier> <index_or_count>


That's the gist of it, right? A collection, a structure of data of the same type. An array data structure. Observe an evolution of the upper syntax, C-style: Weapon weapon 10 -> Weapon weapon[10];. Now that the index/count is separated from the identifier, you can access every weapon in the array with your index: weapon[i]. That doesn't seem really that different, what's the benefit of this approach? Ah, now you can use various flow control structures implemented within the language (for, while, do-while loop etc.) to iterate over all the possible indices to compare, change etc. which is impossible with the original approach, since you'd need to construct identifiers at runtime by concatenating the descripting name with the changing index.

Well, that and the mess you'd make. Not to mention other problems, like the lack of virtual memory address adjacency (I'll explain). You see, every variable, function, data structure and pretty much anything you can point at in a programming language has to be addressed in some way. You can't say to the computer: "Yo, get me the value of this variable!". The computer doesn't see variables. Variables of various data types are just human ideas and interpretations of the ones and zeroes running through the veins of your machine. These ones and zeroes (bits -> binary digits) were decided to be resolved into units of eight bits, called bytes. A byte is the smallest addressable construct within a computer's memory and it is this what every variable resolves to (yes, even non-pointer types) in order to read or write a value, a memory address of the variable's first byte. Then, by instructing the computer how large your data type is (eg. 4 bytes) or how precise in terms of bits (eg. 32-bit) it can bring it up and, in short, do awesome things.

Arrays are, memory-wise, just a bunch of bytes stacked together, continuous in memory (virtual memory, that is, physical memory can get messy). From now on, I'll just refer to it as memory. Just remember that the physical layout of memory is not necessarily always continuous. If the system knows the data type of the variable, variable's name or identifier and the desired index, it can calculate the exact memory address of the first byte of the wanted element. How? Well, we mentioned that the byte is the smallest addressable unit. It has an address. Just like you might be living at The Crib of Awesome 44B, New York, a byte of memory gets a number appended to it which the computer uses to track it. And memory is sequential, it's a long street of houses called bytes, to think about it simply.

A data type can be as simple as a byte ( range (2^8)-1 -> 256 - 1 or range [0,255] (unsigned)). Note, a byte is often called a char (short for character), because our symbolic system can be fitted within the boundaries of 256 entries (a simple one at least, there are many ways of representing text on the screen internally). An integer might be precise to a level of 32 bits or even 64 bits. That's 4 bytes and 8 bytes, respectively. So, let's say we are using "classic" 32-bit integers, we want the 6th element of a 10-element array, and we know that the array identifier is anArray.

Remember, anArray points to the memory address of the first byte of the entire array (which is continuous, adjacent elements). And yes, the first byte of the entire array is also the first byte of the first element. We know that each 32-bit integer takes exactly 4 bytes of space. There are 10 elements, therefore, 10 times 4 is 40 bytes. And, by simple logic, 40 memory addresses which are sequential, each one an increment of the former. So if each element takes up exactly 4 bytes of space, which is the first byte of the desired 6th element? Well, 6 times 4 is 24. 24th byte is actually the first byte of the 7th element. Why? We employ zero based counting which makes indexing a lot easier, without compensating, where the first element is indexed as 0. We want the 20th, 21th, 22th and 23th byte, and today's computer architecture can simply grab it based on the offset (4 bytes), base memory address (resolved from the variable which holds it) and the desired element index.

This is much easier than chasing a fragmented "set" of variables like weapon1, weapon2, weapon3 etc. which could be on "different corners" of the "memory street". And finding an address of the next variable is much easier by adding the offset to the base memory address (first byte of array) than by resolving a variable's memory address for the value and chasing other "elements" around manually. Why? Well, if you'd try offsetting for example weapon1's memory address by 4 (in hopes of accessing weapon2's memory address (first byte)) you could accidentally "lock onto" a memory address that is actually "meant" to be the first and only byte of a char type variable and the system would cast it as an integer. And with it, the 3 bytes further down the memory lane which could "belong" to a completely different variable. Trying to read this variable would yield gibberish and changing it would irreversibly corrupt the memory space of your application with undefined consequences, likely a crash. Now, you should see why it was pointed out that the computer has no idea about puny human data type concepts. It'll do what you tell it to, regardless of whether it makes sense or not.

What you've seen here are just some very basic, coarse ideas behind the concept of arrays, including some of the logic behind them and a very simple introduction to memory at large. This is a very broad topic and you're encouraged to expand upon the knowledge you've just gained and perhaps later return and recognize some of the simplifications employed here.

Declaring Arrays

 float[] myFloatArray
 ObjectReference[] myObjectArray = new ObjectReference[10]

myFloatArray specifies an empty array of floats. It has a length of 0, and is equal to None. myObjectArray also starts that way, except it is an array of ObjectReference objects. It is then assigned a new array of ObjectReference objects, with 10 elements in it (all of which start with the default value of None). Please note you can only call New within functions (including events) - if you want a variable to start with an empty array, then you can make the array inside the OnInit event.

To create a new array and have it in the "empty" script so all events and functions have access to it you have to (Thanks to DreamKing):

Form[] MyArray

Event OnInit()
 MyArray = New Form[20]
EndEvent

Note that you cannot have an array of arrays, or a multi-dimensional array. You may be able to "fake" your needs with multiple While Loops (example).

You can, of course, make an property that is an array. The editor will show a special UI for it that allows you to add, remove, and re-arrange the array items inside it.

Example:

 Weapon[] Property MyWeapons Auto

NB: You cannot use a variable or expression to specify the length of an array - it must be declared with an Integer Literal.

Broken Example:

  int NoOfItems = 10
  ObjectReference[] MyItems = new ObjectReference[NoOfItems] ; this line does not work

You can however use the SKSE extentions to the Utility Script to create certain types of arrays whose size is based on an integer variable.

Function Parameters

To accept an array as a parameter to a function, use the same syntax as for declaring an array.

Example:

 Function MyArrayFunction(int[] myArray, bool someOtherParameter)
 EndFunction

Returning from Function

To return an array from a function, again, use the same syntax.

Example:

 int[] Function MyReturnArrayFunction()
   int[] someArray = new int[10]
   return someArray
 endFunction

Warnings

  • Trying to access the elements of an undefined array will result in unexpected behavior.
int[] myArray

Function someProcess()
  int i
  while i < 128
    if myArray[i]
      ;if myArray was never defined as a new array (or assigned to point at another array), the above if statement will be true each check.
      ;on the other hand, if you define myArray, the if statement above would evaluate as false each check.
      debug.Trace("myArray[" + i + "] misleadingly seems to have a value in it.")
    endif
    i += 1
  endwhile
EndFunction


Creating Arrays

To create an array, use the "new" keyword, followed by the array's element type, and the size of the array in brackets. The array size must be an integer between 1 and 128 - and cannot be a variable. In other words, the size of the array must be set at compile time. Every element in the array will be set to the element's default value, be that 0, false, "", or None.

Example:

 new bool[5]
 new Weapon[25]

Usually you'll be assigning these directly to new array variables, but if a function wants an array, you can make the new array in the function call, like so:

Example:

 MyArrayFunction(new int[20])

Getting/Setting Elements

To get a single value from an array, or to set it, use brackets with the index of the element you want between them after the variable name. The index can be an integer variable, raw integer, or the result of an expression. The range of valid values is from 0 (for the first element) to the length of the array, minus 1.

Example:

 myArray[20] = newValue
 someRandomValue = myArray[currentIndex]
 myArray[i * 2] = newValue

Note that any other assignment operator will not work with an array:

 ; Will not compile, as the compiler doesn't know how to handle it.
 myArray[3] += 5

If the array elements are other scripts, you can access properties and functions in the same way.

Example:

 DoorArray[currentDoor].Lock()
 objectXPos = ObjectArray[currentObject].X

Note that, since arrays are passed and assigned by reference, that any modifications to an array's elements will be reflected in any other variables looking at that array.

Getting Length

You can easily get the length of any array by calling the length property on it. If you assign None to an array, the length will be 0.

Example:

 int ArrayLength = myArray.Length

Assigning/Passing Arrays

Assigning one array to another array, or passing an array to a function, is done just like any other variable. However, note that arrays are passed/assigned by reference, just like objects. In other words, if you assign one array to another, both are looking at the same array - and modifications made to one, will be reflected in the other.

Example:

 int[] Array1 = new int[5]
 int[] Array2 = Array1     ; Array1 and Array2 now look at the same array!
 Array1[0] = 10
 Debug.Trace(Array2[0])    ; Traces out "10", even though you modified the value on Array1

Once an array is no longer referenced, it is garbage collected (destroyed.)

Example:

 int[] Array1 = new int[5]
 int[] Array2 = new int[10]
 Array2 = Array1     ; Array1 and Array2 now look at the same 5 element array, and Array2's original 10 element array is destroyed.

Casting Arrays

Arrays can only be cast to strings, or bools. If you cast an array to a string, it will put each element in the array inside brackets, seperated by commas. If the array is especially long, it may trim the string a little early and put an ellipsis on the end. If you cast an array to a bool, it will be true if the length is non-zero, and false if the length is zero. You cannot cast an array of one type to an array of a different type, even if the elements would successfully cast.

Example:

 Debug.Trace(MyArray)    ; Traces out "[element1, element2, element3]" or "[element1, element2, ...]" if the array is too large
 if (MyArray)
   Debug.Trace("Array has at least one element!")
 else
   Debug.Trace("Array has no elements!")
 endIf

Searching Arrays

(Requires 1.6) Arrays can be searched using two different methods, called "find" and "rfind". Find searches the array from the element you give it (default is element 0) and goes forward through the array to the final element, or the first match it finds, whichever is first. RFind does the same thing, only backwards, starting at the element you give it (default is -1, which means the last element) and going towards element 0. If the item is not found, it will return a negative number for the index.

Find syntax:

int Function Find(;/element type/; akElement, int aiStartIndex = 0) native

RFind syntax:

int Function RFind(;/element type/; akElement, int aiStartIndex = -1) native

Examples:

; Set up an array for the example
string[] myArray = new string[5]
myArray[0] = "Hello"
myArray[1] = "World"
myArray[2] = "Hello"
myArray[3] = "World"
myArray[4] = "Again"

; Prints out "Whee! does not exist in the array"
if myArray.Find("Whee!") < 0
  Debug.Trace("Whee! does not exist in the array!")
else
  Debug.Trace("Whee! exists in the array!")
endIf

; Prints out "The first Hello is at position 0"
Debug.Trace("The first Hello is at position " + myArray.Find("hello"))

; Prints out "The last Hello is at position 2"
Debug.Trace("The last Hello is at position " + myArray.RFind("hello"))

; Prints out "The first Hello in or after position 2 is at position 2"
Debug.Trace("The first Hello in or after position 2 is at position " + myArray.Find("hello", 2))


; Filled by CK or some other mechanism
MagicEffect[] Property EffectList Auto

Event OnMagicEffectApply(ObjectReference akCaster, MagicEffect akEffect)
  if EffectList.Find(akEffect) < 0
    Debug.Trace("Hit by a spell we care about")
  else
    Debug.Trace("Ignoring spell hit")
  endIf
EndEvent

Event OnHit(ObjectReference akAggressor, Form akSource, Projectile akProjectile, bool abPowerAttack, bool abSneakAttack, \
  bool abBashAttack, bool abHitBlocked)
  ; Will not compile - you can't search an array with a value of an incompatible type
  if EffectList.Find(akProjectile) < 0
    Debug.Trace("Hit by a projectile we care about")
  endIf
EndEvent

Warnings

  • The array Find() function cannot be used within an array's element index brackets
;this works fine:
i = myArray.Find(none)  ;(find the first blank element)
myArray[i] = newValue   ;(and fill it with our newValue)

;this will compile, but it will not work correctly in the script, and the value will not be filled as expected:
myArray[myArray.Find(none)] = newValue


Common Tasks

Doing Something to Every Element

Frequently you may want to do something to every element in an array. This is easily done by setting up a while loop with a counter, and then doing something to each element, like so:

Example:

Int iElement = MyObjectArray.Length
While iElement
	iElement -= 1
	MyObjectArray[iElement].Disable()
EndWhile

Note that the while loop will go as long as the element is greater than 0. This is because the valid elements are zero to length minus one. If you were to make a while loop using ">=" you would error on the last element, because you would be trying run a non-existent element, -1, through the mill.

When counting up, the while loop would look like this:

Int iElement = MyObjectArray.Length
Int iIndex = 0
While iIndex < iElement
   MyObjectArray[iIndex].Disable()
   iIndex += 1
EndWhile


Counting Certain Elements

(Requires 1.6) Sometimes you might want to count how many of X is in an array. You can do this with Find and a while loop, making sure that each find starts at one after the location where the previous find located an element. Make sure it is one past the previous location, otherwise you'll get stuck looking at the same one every time.

Example:

; Counts the number of hellos there are in the array
int currentPosition = myArray.Find("hello")
int count = 0
while currentPosition >= 0 ; Loop until we don't find any more (position is less then zero)
  count += 1
  currentPosition = myArray.Find("hello", currentPosition + 1) ; +1 so we don't find the same one again
endWhile

Debug.Trace("There are " + count + " hellos in the array")

Multi-Dimensional Arrays

There is no explicit support for multi-dimensional arrays (i.e., you can't declare String[][] Property matrix auto) but there are ways to work around this. Two potential solutions are presented below.

Arrays of Arrays

Let's assume we want a two-dimensional array of Strings. Even though we can't do that directly, we can create an array of arrays. More specifically, we can define an object that contains an array of Strings and then create an array of those objects.

For this example, let's assume we're trying to create 3X4 matrix of Strings. First, we attach this simple script to some object like an Activator:

Scriptname BugArrayHolder extends Form  

Alias[] Property aliasArray auto 

Bool[] Property boolArray auto 

Float[] Property floatArray auto 

; the name "formArray" was already taken
Form[] Property _formArray auto 

Int[] Property intArray auto

String[] Property stringArray auto

Each time we create an instance of this Activator, we'll also get an a set of variables capable of holding any type of array. Now we just need to create an array of these Activators.

Scriptname bugMultiDimTest extends ObjectReference  
 
bool Property isCreated Auto
 
; We'll use this to create our arrays
Activator Property arrayHolder auto 

Form[] Property rows auto hidden
 
event onInit()
	if (!isCreated)
		; create the matrix and populate it
		; first we create the array to hold the rows of the matrix
		rows = new Form[3]

		int row = rows.length
		; assign an array of columns to each row
		while row > 0
			row -= 1

			; note that since we must create each column array separately, there's no reason you can't vary the size from one row to the next
			String[] cols = new String[4] ; create our array of strings
			; column arrays are not a type of Form so we must create a Form to wrap around it
			BugArrayHolder holder = ((self.placeAtMe(arrayHolder) as Form) as BugArrayholder) 
			holder.stringArray = cols ; put the column array into our wrapper
			rows[row] = holder ; store our wrapper into the row element

			; Setting each string to "(<row>, <col>)" to prove it was initialized OK
			int col = cols.length
			while col > 0
				col -= 1
				cols[col] = "(" + (row + 1) + ", " + (col + 1) + ")"
			endWhile
		endWhile
		isCreated = true
		Debug.Trace(self+": creation done")
	endif
 
	dumpMatrix()
endEvent

function dumpMatrix()
	int row = 0
	while row < rows.length
		String[] cols = (rows[row] as BugArrayholder).stringArray
		int col = 0
		while col < cols.length
			Debug.Trace(self + ":  " + cols[col])
			col += 1
		endWhile
		row += 1
	endWhile
endFunction

Here's the output written to the Papyrus log which was obtained by letting the script run, saving the game, exiting, loading the save game and then calling dumpMatrix (to verify the array was persisted in the saved game):

[bugMultiDimTest < (FF0010DE)>]:  (1, 1)
[bugMultiDimTest < (FF0010DE)>]:  (1, 2)
[bugMultiDimTest < (FF0010DE)>]:  (1, 3)
[bugMultiDimTest < (FF0010DE)>]:  (1, 4)
[bugMultiDimTest < (FF0010DE)>]:  (2, 1)
[bugMultiDimTest < (FF0010DE)>]:  (2, 2)
[bugMultiDimTest < (FF0010DE)>]:  (2, 3)
[bugMultiDimTest < (FF0010DE)>]:  (2, 4)
[bugMultiDimTest < (FF0010DE)>]:  (3, 1)
[bugMultiDimTest < (FF0010DE)>]:  (3, 2)
[bugMultiDimTest < (FF0010DE)>]:  (3, 3)
[bugMultiDimTest < (FF0010DE)>]:  (3, 4)

This approach can easily be extended to higher dimensions. If we had wanted to extend our example to a 2X3X4 cube, we could have created an array of 2 planes where each plane would be an array of 3 rows and each row would be an array of 4 columns.

Faking with a Single Array

Assuming you have 4 Int Property arrays with elements 0-9 being 0-9, the example below will print 0000 to 9999 incrementally to the screen.

Int Array1Element = 0
While (Array1Element < Array1.Length)
  Int Array2Element = 0
  While (Array2Element < Array2.Length)
    Int Array3Element = 0
    While (Array3Element < Array3.Length)
      Int Array4Element = 0
      While (Array4Element < Array4.Length)
        debug.notification(Array1[Array1Element] + Array2[Array2Element] + Array3[Array3Element] + Array4[Array4Element])
        Array4Element += 1
      endWhile
      Array3Element += 1
     endWhile
     Array2Element += 1
  endWhile
  Array1Element += 1
endWhile

For small multi-dimensional arrays the use of multiple 1D arrays as above is not necessary. For example, as the array size limitation in Papyrus is 128, for a 3D array of size 5x5x5 or less this can be done using a single array. For a 2D array the limit is 11x11. This method works because in memory a 1 dimensional and N dimensional arrays are identical, the only thing that differs is the method of addressing. As such the addressing can be performed manually. The following code demonstrates using a 1D array to simulate a 5x5x5 3D array.

; locals used as references only, not used to allocate array
int arraySizeX = 5		; elements along X axis
int arraySizeY = 5		; elements along Y axis
int arraySizeZ = 5		; elements along Z axis

int[] arrayData

EVENT OnInit()

	arrayData = new int[125]	; must manually multiply array size parameters since integer literal required

EndEVENT

; Get 1D array element index from 3D coordinates. sorts elements in XYZ order.
int Function GetArrayIndexForCoordinates(int x, int y, int z)

	return (z * arraySizeX * arraySizeY + y * arraySizeX + x)

EndFunction

; set value in the integer array using 3D coordinates
Function SetArrayElement(int x, int y, int z, int value)

	int index = GetArrayIndexForCoordinates(x, y, z)
	if(index < arrayData.Length)
		arrayData[index] = value
	Endif

EndFunction

; iterates through array in XYZ sorting order. fastest iterative method.
Function IterativeFunctionLinear()

	int i = 0
	while(i < arrayData.Length)
		SomeOtherFunction(arrayData[i])		; pass to some other function to do whatever
		i += 1
	endWhile

EndFunction

; looped iteration to control sorting order, in this case ZYX. slower iterative method
Function IterativeFunctionLooped()

	int iX = 0
	while(iX < arraySizeX)
		int iY = 0
		while(iY < arraySizeY)
			int iZ = 0 
			while(iZ < arraySizeZ)
				int index = GetArrayIndexForCoordinates(iX, iY, iZ)
				if(index < arrayData.Length)
					SomeOtherFunction(arrayData[index])	; pass to some other function to do whatever
				Endif
				iZ += 1
			EndWhile
			iY += 1
		EndWhile
		iX += 1
	EndWhile

EndFunction

Function SomeOtherFunction(int value)

	; perform some function using value. For example may store forms in the array instead of integers and peform some action upon them here.

EndFunction

Creating a FormID Array

Int variables and properties can be set to hex FormIDs. This can be particularly enabling when using GetFormID, GetForm, and GetFormFromFile. The below will work, but you will find you cannot set an Int[] element as a FormID from within the Creation Kit.

Int iFormID = 0x00000BEE
Game.GetPlayer().PlaceActorAtMe(Game.GetFormFromFile(iFormID, "Killer Bees.ESM") As ActorBase)

The "0x" notation is specific to the Papyrus compiler which internally converts the hex to decimal for you, thus the above works as intended. If, however, you want to store an array of FormIDs with the elements predefined in the Creation Kit, you'll need to first convert them from hex to decimal. Windows' Calculator in "Programmer" mode allows easy conversion. BEE, for instance, is 3054.

Int[] Property iFormIDArray Auto ; Filled with bee actor FormIDs converted from hex to decimal
Game.GetPlayer().PlaceActorAtMe(Game.GetFormFromFile(iFormIDArray[0], "Killer Bees.ESM") As ActorBase)

FormLists

FormLists can be used to create an array of properties/objects for a script:

FormList Script Example: Disabling/enabling a large quantity of objects easily

See Also



Language: English  • français • 日本語