User defined structs


#1

Is it possible to create them entirely in the SkookumIDE, or do I have to define them in C++ (USTRUCT) and use them in that way?

Is using lists an alternative?
A problem with this route is that I’d like to have a list:
!myStruct : { this should be a Real, this should be an actor}
!myStructList : {this should be a list of myStructs}
Is there a way to declare a list with multiple empty types?
For single types:
!myList : Real{}
For multiple?
!myList : {Real,String}{} // error

Also what about #define, enum, or global constants in sk?


#2

Sure, you can make your own custom classes entirely in SkookumScript.

The None class used for nil is entirely written in SkookumScript code - though it is not too exciting.

To make your own struct class you just need to make a class derived from Object, give it the data members you want, a constructor !(), destructor !!() and any other routines as needed and there you go.

You can also wrap around C++ structures and store them either as a whole object or as a pointer to an object on the heap in the user data area of a SkInstance object. There are lots of examples to check out - take a look in Source/SkookumScriptRuntime/Private/Bindings/VectorMath to see how all the vector classes are created.

You can also go the UE4 USTRUCT route though that is only necessary if you also want to expose them to the UE4 C++ reflection system.


Great curated posts to learn about SkookumScript
#3

Yeah, list items have just one shared type. They can either all be the same single type such as Real or a union type <Real|Actor>.

Custom classes are a better way to go.


#4

Defines

SkookumScript doesn’t have a macro language so it does not have a #define mechanism. However there are plans to have conditionally compiled code sections for debugging or different build types, etc. If you have any needs or wants in this area, please let us know.

Enums

The current Enum class type is just a temporary mechanism until enumerations become a proper part of the SkookumScript syntax. It is a hack though it works fairly well. You create a subclass of the Enum class, add all the enumerations as class data members, then initialize the enumerations in the class constructor !() with the values you want. You can see many examples generated from UE4 enumerations as subclasses of Enum.

Here is some additional info on how they work.

UE4 Blueprint enums are recently supported. They generate the appropriate SkookumScript Enum subclasses.

You can see how the future syntax will look with enum-definition and enumerator on the Syntax online documentation page under “Planned and Future Syntax”.

Symbols

Also SkookumScript Symbol objects are a good stand-in for many of the traditional uses for enumerations. They are sort of a hybrid between strings and Integers/enums. They are readable like strings though internally they use a hash value for comparisons so they are as fast as Integers. The main difference from enums is that there is only one Symbol type and they are not constrained to any set of valid values. They basically look like strings that use single quotes.

(Symbol state)
  [
  case state
    'up'    [do_up_stuff]
    'down'  [do_down_stuff]
    'left'  [do_left_stuff]
    'right' [do_right_stuff]
  ]

Global constants

SkookumScript currently doesn’t have true constants though there are a few good approximations.

It is easy to store class data members with a value set in a class constructor !(). Some examples include @@random and @@world which are defined in the Object class so that they are available anywhere. We recommend storing such values in classes that are related to their use. However these class data members can all be modified - they aren’t actually constants.

You can also create constructor methods for the classes of the constants you want. This ensures that a new copy of a value is always what you want and you can freely modify its value without risk. This is done with many common Vector3 directions such as Vector3!axis_x, Vector3!backward, etc.

We may add true constants to the SkookumScript syntax in the future or introduce some sort of parser annotation as a compiler hint to prevent modifications on data members with such tags.


Future Enumeration Syntax
Great curated posts to learn about SkookumScript
#5

I’m having a hard time finding documentation on the use of symbols. Is there a page I’m missing?


#6

There isn’t much documentation for Symbols though they are fairly simple.

Look on the syntax page for the symbol grammar.

Symbols are case sensitive and they can be anywhere from zero characters '' (the null or empty symbol) to 255 characters long. Like strings they can also use escape characters such as \t or \n. They use a CRC32 hash value for their unique id so they can be compared and ordered though their order will not be alphabetical.

You can convert from a String to a Symbol str>>Symbol or str.Symbol. In some game engines the strings that symbols are based on are only available for debug builds and they are discarded for shipping builds. However, in UE4 the symbol string tables are needed to work with the UE4 API and Blueprints. So in UE4 you can also convert Symbol objects to String objects.

It is generally preferable to use Symbol objects rather than String objects whenever possible since symbols are more efficient both in speed and memory. Symbols share memory so two symbols representing the same value will actually share internal info to save memory.

UE4 uses a similar class called Name (or FName in C++). You will see many UE4 methods that have Name as a parameter. Name objects are not case sensitive and have a different internal representation than Symbol objects. One of the simplest ways to define a Name in SkookumScript code is to make a string literal and then call the Name() conversion method on it - "example".Name.

To find out more about Symbol objects, read through the comments in the methods of the Symbol class.

(We are working on additional online SkookumScript documentation right now.)


#7

I’m trying to define a class in Sk to use as my “struct” and I can’t seem to put values into it

Example:
!var : SortData!
var.@cost := 2.0
println(var.@cost) // prints: nil

I made my struct “SortData” a class derived from "Object"
I added to instance data variables:
@cost
@actor

The members “@actor” and “@cost” have this:
Real !@cost
Actor !@actor

I made a constructor “!” and I tried leaving it empty, I tried making it:
() SortData

printing var I get: “null” (null)

What am I missing?

EDIT: I made the constructor:
()[@cost : 0.0! @actor : Actor!null]
and that does print the correct “2.0”, however, if I print var, it prints “null” (null), and if I add the variable to a list I also get null, null:
!myList : SortData{}
myList.append(var!)
println(myList) // {“null” (null)}


#8

Hmm could it be that you derived SortData from Entity instead of Object? It should be derived form Object so that you find it right before String in the class tree.

Apart from that you are doing basically the right thing. Minor changes - make the constructor like this:

() 
  [
  @cost:  0.0
  @actor: Actor!null
  ]

Then

!var: SortData!
var.@cost := 2.0
println(var.@cost)
!myList: SortData{}
myList.append(var)
println(myList)
println(myList{0}.@cost)
println(myList{0}.@actor)

produces this output for me:

2.0
{SortData}
2.0
"null" <Actor> (null)

#9

To get something more custom or appropriate for printing out your SortData object, you can also make a String() conversion method that builds up your own string representation. By default, the String() conversion method inherited from Object just prints out the class name of the object.

You can also override as_code() which returns a string for use during debugging when building callstack, etc. If you don’t override as_code() it calls the String() conversion method by default.


#10

So why would the assignment fail if I didn’t define the constructor or didn’t define it properly? Should I define things like !copy and assign as well?


#11

Currently, whenever you describe a class it will inherit all the data and routines from its superclass - including any constructors. If you want your constructors or other methods to do anything more than the superclass (including dealing with new data members) you must define them by hand. So you must make a constructor, optionally call the superclass constructor as needed, then do the initial binding of an object for each data member.

The idea was to give full power over how, when and if things are created - though this can be annoying for common code that isn’t just auto-generated. So yes, unless an inherited methods do exactly what you want you will need to create your own, copy constructor !copy() and assignment operator assign() (which is used as :=).

We’ve discussed this on the team and with some existing SkookumScript users and we intend to add more auto generation and warnings if all the required steps are not performed. So just like C++, we will have constructors auto call their superclass constructor and either create default objects for data members or give a warning if a data member isn’t initialized by the end of a constructor. We expect to have this in a near term update.

If you have additional thoughts on this, just let us know.


#12

For example, in C++:
class MyClass {int a;};
MyClass myVar;
myVar.a = 5;
printf("%d", myVar.a); // prints '5’
works fine, but in Sk, if I don’t add a constructor, the equivalent code doesn’t work, it seems strange, that’s all. If initialization of data members is required is not a huge deal but it’s another gotcha that can cause difficult to find issues. Perhaps a warning or error if initialization is missing?


#13

Look at this piece of code that contrively swaps the two values in the list:
!ml : {4, 0}
!v1 : ml.at(0)
!v2 : ml.at(1)
ml.at_set(0, v2)
ml.at_set(1, v1)
v1 := v2
println(ml) // prints {0, 0}
if I change the following lines it works correctly:
ml.at_set(0, v2!)
ml.at_set(1, v1!)
I surmised it was because parameters are passed by reference, so adding the ‘!’ sends copies instead.
In the OP example, something is not working right because
myList.append(var!) doesn’t work, but myList.append(var) does.
If I’m sending references it could lead to problems like in the example above.
Do I need to define !copy for this to work right?
I have it defined like this:
(SortData entity) SortData [@cost := entity.@cost @actor := entity.@actor this]


#14

You can do this in SkookumScript like this (assuming SortData has an empty constructor here though):

!var: SortData!
var.@cost: 2.0
var.@actor: Actor!null

The important distinction here is between binding (via :) and assigning (via :=) to variables. When you create an empty object instance without a constructor, its member variables are not bound to an object yet. var.@cost: 2.0 for example binds the member variable @cost to an object with value 2.0. After this has happened and the object exists, you can now assign different values to it, e.g. var.@cost := 42.0. See also the following paragraph for clarification, which is part of this post.

SkookumScript variables

In SkookumScript, every variable is a reference to an object that gets stored and/or passed around. That means data in SkookumScript is not stored in a class instance or a local variable by value, but by reference to an object that contains its value. The following short example illustrates this:

!a : 42
!b : a
a := 7

after which the value of b is 7. This is because we are creating an object of class Integer with the value 42 and binding it to the local variable a, then binding that same object to the local variable b (i.e. now both a and b refer to the same Integer object). Thus changing its value via a also affects the value referenced by b.


#15

These should both work, assuming your class has a copy constructor defined.

The copy constructor has to bind the member variables, since their objects do not yet exist, e.g. like this:

(SortData other) SortData [@cost: other.@cost @actor: other.@actor this]

#16

Since you say “contrively” you probably just meant this as a demonstration, though I thought that I should also point out that there is a swap() method that swaps two items in a List object.


#17

Yes, I did know about swap. Demonstrating how passing by reference can lead to unexpected behavior, and wondering why when I used the copy constructor, it didn’t seem work.
Passing by reference can lead to problems that are difficult to track down later down the line. I’d prefer it if the default was the other way around: passing by value and tag references like in other languages.


#18

EDIT: Thanks!.I was assigning not binding in my copy.
What’s a proper ‘assign’ function? The problem is all the examples I see use Unreal data types which are treated differently (&raw).
(SortData other) SortData [@cost := other.@cost @actor := other.@actor this]

If I pass by reference, not using copy, I could get into trouble if I try to shuffle the list somehow, like in the example I listed, where (contrively for demonstration) I swapped 2 values and ended up corrupting the list.


#19

Another thing you could do if it is easier or more correct based on the situation is that you can call a constructor inside another constructor so you can call the default constructor to set things up and then do the custom stuff after that.

(SortData other) SortData
  [
  // Do default construction and make data members
  SortData@!()

  @cost := other.@cost   // assign simple type
  @actor : other.@actor  // reference complex type

  this
  ]

The reason that SkookumScript defaults to pass by reference is that generally most of the objects that you deal with in a script are bigger heap objects: characters, world objects, etc.

We opted not to have two different kinds of types such as with C# for simplicity with both understanding and implementation and also efficiency.

You can sort of pass by value if you make copy of an argument as you are calling a method.

// pass by reference
method(obj1 obj2)

// pass by value
method(obj1! obj2!)

To make an operator method such as assignment := the method name is assign. If you look at a lot of the primitive classes such as Boolean, Integer, Real, String, etc. you can see all the operator normal names and their operator equivalents in square brackets such as assign [:=].


#20

The problem is that the assign function in all of those are not implemented in Sk. They simply show:
(Boolean value) Boolean

For the example in this post for SortData, I have:
(SortData other) SortData [@cost := other.@cost @actor := other.@actor this]
Is that right?