Preface
The information contained here is best learned by typing it in (not copying!) while you read. Create a new verse script in UEFN and follow along!
Use this information in additon to the Official Verse documentation along with the Verse Language Reference .
UEFN automatically creates a VS Code workspace when you open it which means you will need to have VS Code installed on your computer to edit the script.
Verse Basics
While learning a new programming language, or programming in general, can feel like a daunting task, it's much easier to digest when broken up into chunks. We'll review the very basics of Verse, starting with comments.
Comments
Comments in code are like little notes that you leave for your future self or team members. They are defined with the pound sign (#
) before a line of text.
# This is a comment and won't be recognized as code by UEFN.
# Comments can be put
# on multiple lines
SomeCodeHere() # They can even be put on the same line after code!
# Using in a comment before a line of code means that UEFN will not run that code
# as if it wasn't there at all.
# SomeCodeHere()
I can hear you thinking to yourself, "But I wrote this code, of course I know what it does and how it works!". That may be true a day or even a week from now, but what about 6 months or a year from now? Putting in a helpful comment can save you, and your teammates, hours or even days of headaches! A great example of good commenting pratice is in our Custom NPC Dialog Example.
Numbers
Numbers are widely used in programming. You an use them to perform math operations, track the amount of coins a player has, how full the gas tank in your vehicle is, etc. In Verse, there are 2 types of numbers: rational
and fractional
. Rational numbers have the data type of int
and fractional numbers have the data type of float
, which we'll see more when we talk about variables.
1 # A rational number is a number without a fractional component i.e. no decimal places after the number
-1 # rational numbers can also be negative
3.14 # Fractional numbers are numbers with a decimal point
-3.14 # Fractional numbers can also be negative
Strings
Another important concept in programming is the string
data type. A string
can be a word or a sentence which can be used to display text to the player, for example.
"Verse is my favorite programming language!"
We can see that strings are words separated by spaces surrounded by double quotes ("
).
Concatenation
Concatenation means to combine or append multiple items into a single item. There are two ways in Verse to use string concatenation:
- Strings can use the
+
operator to combine multiple strings into one. - Strings can insert variables into a word or sentence using opening and closing curly braces (
"{CoinCount}"
).
Variables are coming up next and operators after that!
"This is a very " + "long sentence that is " + "broken up with multiple strings"
CoinCount : int = 10
MyName : string = "I have {CoinCount} coins!" # Produces the string "I have 10 coins!"
Variables
Variables are an essential part of programming. A variable allows the programmer to store data of certain types to use, or change, later in the program. Variables in Verse are immutable by default. The naming convention for variables differ from language to language, but in Verse, they start with an upppercase letter using the camel case naming convention.
NumberOfItems : int = 15
Whoa! What does that mean?! Let's break this down:
NumberOfItems
is the variables name. We give it a name so that we can refer to it later in our program.- The syntax
: int
tells UEFN that we want to make this variable an int type. - The last part
= 15
means we give this variable the value of15
.
Let's see some more examples of variables:
NPCName : string = "Viktor"
IsOnFire : logic = false
PI : float = 3.14
Don't worry if you don't recognize some of these data types. We'll go over them in future lessons!
The UEFN documentation has even more examples of the different types of variables you can use!
Updating Variables
If Verse variables are immutable by default, how do we change them? There is a special keyword in Verse called var
. Using var
before the name of a variable makes it mutable, allowing us to change it with the help of the set
keyword.
var IsOnFire : logic = true
...some other code here to put out the fire...
set IsOnFire = false
If you forget to use either the var
or set
keywords, both VS Code will show you errors with a reason why it's failing and UEFN will give you errors when you try to build your Verse code in the editor.
Numbers can use a special operator syntax when updating their value.
We'll get into operators next.
var NumberOfCoins : int = 10
# Update the value
set NumberOfCoins = NumberOfCoins + 5
# or
set NumberOfCoins += 5
We can see that we can use +=
to update the value of the numbers of coins we have without stating the variable name twice i.e. NumberOfCoins = NumberOfCoins + 5
. This special syntax can also be used with other math operators: -=
, *=
, /=
.
Operators
Operators are special constructs in programming that allow us to perform operations like Math. Math is an important skill to have in video game programming. Don't worry! The math we'll do here is really easy to follow. Let's cover the most used operators in Verse.
1 + 1 # The `+` operator allows us to perform addition.
10 - 1 # The `-` operator allows us to perform subtraction.
5 / 2.5 # The `/` operator allows us to perform division.
5 * 2 # The `*` operator allows us to perform multiplication.
Optionals
An optional value is a variable that either does or doesn't have value. Optional values are great when you might have some data or you might not.
var MaybeNPCName : ?string = false
The syntax for optional values is ?data_type
, specifically a question mark (?
) followed by the name of the data type i.e. ?string
. We see that MaybeNPCName
is set to a value of false
to begin with. This means we don't have a string
value when we define this variable.
Updating Optionals
As our program progresses, we now have the need to assign MaybeNPCName
to a string
value.
set MaybeNPCName = option{ "Francis" }
To set the new value of an optional data type, we need to wrap the type of value, string
in this case, with an option
object.
Although not required, it's common to see the word
Maybe
to start an optional variable name. This makes it instantly recognizable that it is an optional value.
Retrieving Values
To retieve the value of an optional data type, we need to use a logical statement
. A logical statement
is a construct that can succeed or fail. Those will be covered next, but here is a preview.
if (Name := MaybeNPCName?):
# ...do something with Name...
Logic and objects will be covered in further detail later.
Logic
Logic is important in any program. It is used to determine if something can or can't happen. We'll go over what logic is and how to use it.
The way we use logic in Verse is by using an if
statement. Like mentioned, if
is used to determine if
something can or can't happen.
IsOnFire : logic = true
if (IsOnFire?):
ScreamInPanic()
When checking a
logical
type in anif
condition with a question mark (?
), this is called aquery
.
The structure of an if
statement is broken down as if (condition-to-check):
. It's required that we use a colon (:
) after the closing parenthesis ()
). The next line needs to start with 4 lines of empty space, equal to pushing the TAB key on your keyboard once.
Verse is a
whitespace dependent
language, meaning if you don't have enough whitespace, or whitespace in the wrong location in your code, it will give you errors.
We also notice that when we set IsOnFire
to true
, we did not use a question mark (?
). With the logic
data type, we "ask" the variable if it is true
or false
. When used in an if
statement, we "ask" the variable by adding a question mark (?
) to the end of the variable name with no spaces.
The line that says ScreamInPanic()
is a method. We'll get into methods a bit later.
If
statements can also be used in different ways. When we ended the line with the colon (:
), it created what is known as a block
. We'll also cover the details of blocks later.
Branching Logic
As stated before, if
is used to determine if something can or can't happen. In programming, this is known as a branch
.
IsOnFire : logic = true
if (IsOnFire?):
ScreamInPanic()
else:
EatSomeLunch()
If we are on fire, we scream in panic! If we are not on fire, we eat some lunch.
Expanding the previous example, we can see that there is new information here. Optionally, after the main if
block, we an add the else
keyword. You will notice that the else
keyword is indented the same amount of spaces that the if
keyword is (0 tabs / spaces, in this case). An else
always has to line up with the if
keyword.
Logic branches can be used many, many times within the same if
statement. We can add additional if
statements when we attach another if
to the else
:
IsOnFire : logic = false
IsHungry : logic = true
if (IsOnFire?):
ScreamInPanic()
else if (IsHungry?):
EatSomeLunch()
else:
PlayFortnite()
If we are on fire, we scream in panic! If we are not on fire and we are hungry, we eat some lunch. Otherwise, we play some Fortnite! 😀
Logical Modifiers
Logical modifiers allow us to combine conditions into a single if
statement. These are able to be mixed and matched to produce some interesting effects.
IsOnFire : logic = false
IsHungry : logic = true
if (IsOnFire? and IsHungry?):
ScreamInPanic()
EatSomeLunch()
else:
PlayFortnite()
If we are on fire and we are hungry, first scream in panic then eat some lunch.
and
is another keyword which means that both conditions have to be true. If they aren't both true at the same time, then we don't perfom the code in that branch. We also have another keyword: or
.
IsOnFire : logic = false
IsHungry : logic = true
if (IsOnFire? or IsHungry?):
ScreamInPanic()
EatSomeLunch()
else:
PlayFortnite()
If we are on fire or we are hungry, first scream in panic then eat some lunch.
or
is used to to check if either condition is true. If at least one of them is true, then perform the code in that branch. Otherwise, move on.
The final keyword we can use to modify an if
statement is not
. not
means, "check if the conditon is the opposite of it's value" (😵), meaning not true
is false
and not false
is true
.
IsOnFire : logic = false
IsHungry : logic = true
if (IsHungry? and not IsOnFire?):
ScreamInPanic()
EatSomeLunch()
else:
PlayFortnite()
If we are hungry and we are not on fire, first scream in panic then eat some lunch.
Equality
When talking about equality in programming, what we're really asking is, "Is value exactly equal to another value or not?". There are two operators that are used for checking equality.
# Check if something is equal with the equality operator `=`
NumberOfCoins : int = 10
if (NumberOfCoins = 10):
# ...perform some more code...
# Check if something is not equal with the inequality operator `<>`
if (NumberOfCoins <> 1):
# ...perform some more code...
We can notice that the equality
operator is the same as assigning a value to a variable, =
, and we have a new operator <>
which checks for inequality
, meaning if two values are not equal to one another.
Logic Context
Vere supplies us with an idea called a context
. A context
is a specific place in our code where special, extraordinary things can happen. It sounds strange at first, and to be honest, it kind of is, but context
s are super powerful. The if
statement provides us with a fallible context
. This means the conditions we check for in an if
statement can fail!
IsOnFire : logic = false
if (IsOnFire?):
# ...do something if on fire...
Since IsOnFire
is given the value of false
, we can see that this if
statement has "failed" to satisfy the condition, thus we will not do anything since we are not on fire.
If Variables
We can also assign a variable inside the condition of an if
statement. This is useful when you have a method that can either succeed or fail, and if it succeeds, then you can retrieve that variable.
if (Coins := GetPlayerCoins[]):
# ...do something with Coins variable...
Wait a minute! We saw how to create a variable and that's not how we did it before! This is a special case where you do not have to define the data type of the variable explicitly. UEFN knows what type of data the GetPlayerCoins
method gives us, which is super handy! We can also notice that we call the method with square brackets ([]
) instead of parenthesis (()
) here. If a method has a chance to fail, we have to use square brackets!
I know I promised we would talk about methods soon... and we will! Stay tuned!
Single Line Expressions
When you have an if
statement that contains a single else
branch, but not else if
, you can use a special syntax to help with readability. This is also known as a ternary
expression.
IsOnFire : logic = true
if (IsOnFire?) then ScreamInPanic() else EatSomeLunch()
# Instead of
if (IsOnFire?):
ScreamInPanic()
else:
EatSomeLunch()
The syntax breakdown here is if (condition) then DoSomethingWhenTrue() else DoSomethingWhenFalse()
.
Single Line Dot Expressions
Single Line Dot expressions make it easy to call fallible methods in sequence, on a single line. The only catch is you can't use an else
block with this format.
IsOnFire : logic = true
if (IsOnFire?) . ScreamInPanic() ; EatSomeLunch()
# Instead of
if (IsOnFire?):
ScreamInPanic()
EatSomeLunch()
The syntax is if (condition) . DoThisIfTrue() ; AndThisIfTrue() ; AnotherThingIfTrue() ...
. It is possible to chain as many statements together that you'd like to perform using a semicolon (;
) as long as the condition is true.
Block Format
The if
statement, along with many other block statements, can be used a block format. We will also use a new keyword here: then
.
if:
IsOnFire?
then:
ScreamInPanic()
EatSomeLunch()
The syntax for the block is a little different as we don't have to wrap parenthesis ()
around the conditions.
if:
# Tab here!
conditions-to-check
then:
# Do something if conditions are true
else:
# Do something if conditions are false
Blocks are code that start with a colon
:
where the body is indented by a TAB (or 4 spaces).Other constructs that can use blocks instead of curly braces
{}
arearray
,map
,class
,struct
,for
andcase
.
Arrays
When we create a variable like MyGrade : string = "A+"
, we can only assign a singluar value to the variable. Arrays help us hold many different values for a single data type.
Numbers : []int = array{} # This creates an empty array
The syntax of an array is a bit different from a regular variable. When we define the data type of the array, we put open and closing square brackets []
before the data type, int
. This tells UEFN that we want to use an array data type. The varaible's value, the information after the equal sign (=
), is a new array object
. Objects are also super important in programming and will be covered later.
An object is defined by it's name followed by open and closing curly braces i.e. array{}
The above example will assign an empty array
to the Numbers
variable. This means it doesn't currently store any data. To create an array that actually stores data, we put some information between the {
and }
.
Numbers : []int = array{ 1, 2, 3, 4, 5 }
Notice how each new number in this array is followed by a comma (,
). This separates the values from each other. For each new piece of information stored in an array, it needs to be separated from the other by a comma.
Indexing
Arrays have this concept called an index
. An index
is simply the location of an item in an array. Indexing an array can fail because indexing an array is by giving it an arbitrary number as an index.
Numbers = []int = array{ 1, 2, 3, 4, 5 }
if (SecondNumber := Numbers[1]):
# ...do something the second number...
There has to be something wrong with the above code, right? Why would the SecondNumber
be at index 1
? Isn't that the first number? In most programming languages, Verse included, arrays are called 0 indexed
, which simply means that the first item in the array starts at index 0
, not 1
. What happens if we want to get the sixth number in the array?
Numbers : []int = array{ 1, 2, 3, 4, 5 }
if (SixthNumber := Numbers[5]):
# ...do something with SixthNumber variable...
This example will halt (stop) the execution of your program immediately. When you try to get the sixth number with the 5th index, it will cause an array out of bounds error to happen, meaning the program tried to access more elements in the array than were defined. When indexing an array in this manner, you must be very careful not to try to access more elements than there are stored inside.
Updating Arrays
Say you want to change the second item inside an array. How would you go about doing that? Items in arrays can be updated or changed. This is another operation that can fail so we need to use an if
statement.
Numbers : []int = array{ 1, 2, 3, 4, 5 }
if (set Numbers[1] = 10) {}
Even though we aren't doing anything in the block of the if
statement, this operation can fail if the progam tries to index the array outside the defined bounds.
As a learning experience, try to index an array outside of its bounds or update an array with an index that is greater than its bounds. What happens?
Iteration
Iteration may be a scary sounding word; I know it was for me when I was a new programmer. It is a term in programming that means "go over each item, one at a time". We can use iterators to process or manipulate each piece of data, one item at a time. This is the "safe" way to index into an array because you cannot go outside of its bounds. To do this, we will introduce the for
keyword:
Numbers : []int = array{ 5, 4, 3, 2, 1 }
for (Number : Numbers):
Print("I have {Number} of lives left!")
Print("I'm dead!")
Print is a method provided by UEFN that allows us to print messages to the log of our game. This example uses string interpolation, which is a topic we will get to when we talk about the details of strings.
The syntax when using for is for (Item : Collection):
. A collection is a group of data bundled together. Arrays are a type of collection and we will cover another collection type shortly.
Code using the for
keyword is often called a for loop
. A for loop
defines another variable inside its parenthesis (()
) to allow us to get at each number, one at a time. The Number
variable can only be used inside the for loop
's block. If you try to use it before or after the loop's block, you will get an error. This is known as scoping.
What the above code will does is define a new variable called Number
which will contain the values 1
, 2
, 3
, 4
, and 5
each time the loop executes. A loop executes over a collection from front to back for
each item inside the collection. When there are no more items, it stop executing and moves onto the next line of code after the block.
Do you know what the output to this for
loop would be? An important part of programming is to be able to visualize, as best you can, the outcome of the code before it's even run!
Did you have a think about it? I sure hope so; it's good for ya 😉.
Answer
I have 5 lives left!
I have 4 lives left!
I have 3 lives left!
I have 2 lives left!
I have 1 lives left!
I'm dead!
Maps
A map
is another Verse collection that holds key-value pairs of data, that is to say that we can assign a piece of data to a specific name (similar to a variable but not the same). Like arrays, they can hold multiple values.
Resources : [string]int = map{}
The syntax for the map
data type is [key_type]value_type
. We have created a map
with string
keys and int
values. Like arrays, we can initialize the map when we create it.
Resources : [string]int = map{ "Wood" => 5, "Stone" => 1, "Steel" => 99, "Coins" => 0 }
When creating the key-value pairs of a map, the syntax is key => value
. This assigns a given value
to a given key
. Maps are really useful when you want to give a collection of similar data names without creating multiple variables for each value.
Remember when I said iterators, like for
, work on collections? We can also use map in for loops
:
Resources : [string]int = map{}
for (Resource -> Amount : Resources):
Print("I have {Amount} {Resource}!")
The syntax of a for loop
when used with a map
is for (Key -> Value : Collection):
. This creates two new variables to use in the for loop
's block. These names can be anything, but it's most useful when they are related to the collection being iterated over.
I'm not going to give you the output to this one. Let me know in the
Official Discord
(@glinesbdev
) server what your solution is.
Updating Maps
Updating a map is similar to updating an array except instead of indexing by a number, we update the value by its key name.
var Resources : [string]int = map{ "Wood" => 5, "Stone" => 1, "Steel" => 99, "Coins" => 0 }
if (set Resources["Coins"] = 10), CoinsAmount := Resources["Coins"]:
Print("I have {CoinsAmount} of Coins!")
No only can we update a map
's value by its key, but we can immediately retreive that new value in a new varaible and then get the contents of that varaible, assuming that those two operations didn't fail.
Removing Items
Unlike arrays, it's possible to remove items from a map
, it's an operation that can fail. This will give you the chance to see a full method definition before we break methods down in detail.
RemoveItemFromMap(Resources : [string]int, KeyToRemove : string) : [string]int =
# Create a new map to replace the old one (Resources)
var Result : [string]int = map{}
# We can combine a `for` and an `if`, in a way. It will iterate over the items in the
# collection if they meet the condition:
#
# If the `KeyToRemove` isn't equal to each key (`Resource`) in the map
# add it to the new map
for (Resource -> Amount : Resources, Key <> KeyToRemove):
set Result[Resource] = Amount
# Returning a result means that the value of this method will give back the new map
return Result
Methods
Methods are a way in programming to encapsulate different behavior or perform different steps in a specific order. Let's take the method example from above and break it down:
RemoveItemFromMap
-> The name of the method we can use to call (activate) the method at another part of our program.(Resources : [string]int, KeyToRemove : string)
-> The signature of the method which dictates which types of variables this method takes, if any, but also how many and what they are called when used in themethod body
.: [string]int
-> This is called thereturn type
of the method. Like variables, methods also can give back specific types of data to work with later.=
-> The equal sign (=
) starts what's called themethod body
where the instructions of the method are defined.
Into the Void
Unlike variables, methods don't have to return any type of data. This is a special type of method in programming called a void
method. void
methods just perform instructions without the expectation that the code calling the method will get anything back in return. These types of methods are useful when you need to perform multiple, potentially repeatable steps in your program.
SetupPlayerInventory() : void =
GiveResources()
GiveWeapons()
GiveBullets()
The great things about methods is that methods can call other methods in their definition!
In this example, we can see that when we call SetupPlayer()
somewhere in our code, we will do 3 things in order:
GiveResources()
GiveWeapons()
GiveBullets()
Methods are best named as what actions they perform i.e.
GiveWeapons()
. This makes it clear what the method is doing so you don't have to revisit the method definition to figure out what it's supposed to be doing in the first place.
Returning Values
When methods are expected to return a specific type of data, conveniently, we use the return
keyword.
GetNumberOfDaysInWeek() : int =
return 7
Although this is a contrived example, it illustrates how we can get values out of methods. Verse also provides a way to return values without using the return
keyword.
GetNumberOfDaysInWeek() : int =
7
# OR
GetNumberOfDaysInWeek() : int = 7
Methods can be defined in single line of code, if it's short enough
The last line of code is always the return
value in a method.
Early Returns
Say we have have an array that contains some numbers. If it has a specific number in the array, we can perform some action and then return
from the method early. This can be used with methods that return a data type or a void
method.
Numbers : []int = array{ 1, 2, 3 }
CalloutTheNumberTwo() : void =
for (Number : Number):
if (Number = 2):
Print("We have the number 2!")
return
Print("We don't have the number 2...")
As we iterate over the Numbers
array, we check to see if we have the number 2
. If we do, we Print
a message to the screen and return
from the function, no longer performing any actions within the method.
We don't have to use
return
on it's own.
As we saw in the Removing Items section for map
, we can combine the for
statement with any number of conditions. If all of those conditions are met, it performs the action.
Numbers : []int = array{ 1, 2, 3 }
SkyIsBlue : logic = true
CalloutTheNumberTwo() : void =
for (Number : Number, Number = 2, SkyIsBlue?):
Print("We have the number 2 and the sky is blue!")
return
Now we cannot do anything after the return
in the for loop
body as we did in the previous example i.e. Print("We don't have the number 2...")
. To be able to interact with the Number
variable in the body of the for loop
, all of the conditions have to be met. Therefore, it will not do anything until the Number
variable is equal to 2
and when SkyIsBlue
is true
. Otherwise, it does nothing.
Side Effects
Methods cause things to happen, which is great! They can also be potentially dangerous... Meaning they can change the data in your program which may be something you didn't expect. This is especially true when using code that you didn't write.
var NumberOfCoins : int = 100
# ...instructions of the program...
ChangeClothes() : void =
# ...defines how clothes are changed...
set NumberOfCoins -= 10 # An unintended side effect of changing our clothes is first buying a new set!
Objects
Objects are also known as container types
. This means they can contain different types of data like variables
and methods
. There are two different types of objects in Verse.
We've already been using objects! The
array{}
object is a good example.
Struct
A struct
, short for structure
, is a data type that is used to hold different types of data. Similar to a map
but can hold different data types as well! These are special types of "variables" called members
. "Variables" is wrapped in double quotes here because they cannot be referenced on their own, rather in the context of the object.
# Define our custom struct
game_state := struct:
NumberOfPlayers : int # It is not required to give a member an initial value
GameStartDelay : float = 3.0 # but we can give it a value, if so desired.
The syntax of creating a struct
is name_of_struct := struct:
. Although not required, objects typically follow a naming convention known as snake case. We also notice that defining the data members
of the struct is very similar to defining any other variable. Although, members
of a struct
cannot be mutable via the var
keyword.
game_state := struct:
NumberOfPlayers : int
var GameStartDelay : float = 3.0
If we attempt this, UEFN will give us the error: Structs may not contain mutable members.
Structs can also have methods defined on them.
game_state := struct:
NumberOfPlayers : int
var GameStartDelay : float = 3.0
(GameState : game_state).GetStartDelay() : float =
GameState.GameStartDelay
We define the method outside the struct's definition as they cannot define methods within their definitions. The syntax to define a method on a struct is (StructNameToUseInDefinition : name_of_struct).NameOfMethod(CanTakeArgumentsOrNot : type) : return_type =
In the above example, we call the struct by the name of GameState
which is used to return the GameStartDelay
value of the struct. We have to define a new name for the struct followed by the struct's type to be able to interact with any of the struct's data members.
Class
The main, and most powerful, container type in Verse is a class
. Classes in programming are special because, like a struct
, it can have data member variables as well as methods.
player_data := class:
MaybePlayerReference : ?player = false # Can set initial values for data members.
NumberOfCoins : int # Not required to set an intial value.
var IsDead : logic = false # Able to use mutable (changeable) values inside of a class.
# We define methods directly in the class' definition!
IsPlayerDead() : logic =
if (IsDead?) then true else false
Like a struct, the syntax to make a new class is name_of_class := class:
. Two other major differences with classes vs structs:
- Variables can be mutable within the class' definition.
- Methods can be defined directly within the class.
To use a class (or struct), we need to make an instance of it. An instance is the concrete value of a class whereas the definitions we've seen so far are like the blueprints for an instance.
PlayerData : player_data = player_data{}
When we create a new variable using our class (or struct) definition, we use the same syntax as we would when defining any other variable. The only difference is that the variable type is the object's name and the value is the object's name followed by curly braces {}
.
This is a very important concept called initialization
or construction
, which means the creation, and optionally set up, of a new object.
Again, just like the
array{}
object we've been using.
We can directly assign the data members of an object within the variable definition, if we want.
PlayerData : player_data = player_data{ NumberOfCoins := 10 }
Note that we didn't assign values for MaybePlayerReference
or IsDead
. This is because in the definition (or blueprint) of the class, it is already set to a default value. You can either keep the default values or give them a new value for a given instance.
DeadPlayerData : player_data = player_data{ NumberOfCoins := 0, IsDead := true }
If you construct a new object and don't set a value for a member without a default value, UEFN will give you this error: Object archetype must initialize data member
NameOfDataMember
.
Construction
Construction of a class is typically a separate method outside of a given class that will create a new instance of that class. This is useful if you want to keep your data members internal
to that class. Constructor methods use an attribute called <constructor>
which is specifically to denote that we're going to be creating an instance of a class.
Attributes and visibility are talked about in Visibility below.
MakePlayerHelper<public><constructor>() : player_helper =
PlayerCount := 10
player_helper<public> := class:
PlayerCount<internal> : int
GetPlayerCount<public>() : int
Classes are the only object types that can use <constructor>
methods. Structs are not allowed to be constructed in this way.
Inheritance
Remember when I said that classes were special when compared to structs? Inheritance
is the secret sauce that classes and have structs do not. If you've ever created a new device using Verse, you've already used inheritance.
# We're going to talk about modules very soon!
using { /Fortnite.com/Devices } # Importing an external module
my_device := class(creative_device):
# ...definition of class...
Inheritance is a way to tell the program, "Okay, I'm not exatly this other class but I want to act like one". This means that you get all of the functionality, methods, data members, etc., of another class to use in your own! This makes making or extending (adding new functionality to) complex objects much easier if most of what they do resides in another class' definition.
The syntax breakdown of inheriting from another class is my_class := class(class_to_inherit):
.
If you inherit from another class, you may think to yourself, "But what can the other class do that I can use?". Hopefully you'll understand why you need to inherit from a specific class, but if not, you can easily find out what each class does with some explanations for each method / data member, if any. There are two primary ways to find this information:
- Use the
UEFN API Reference
website.
- This website has pretty good explanations of each class. (Sometimes lacking a little bit 🙏)
- Every UEFN project comes with the code definitions for everything you can use inside UEFN. These files all end in
.digest.verse
.Fortnite.digest.verse
-> This defines all of the Fortnite specific items like UI buttons, Devices, etc.Verse.digest.verse
-> This defines specifics of the Verse language.UnrealEngine.digest.verse
-> This defines things like building UI in code for a players, mathematical functions, etc.
It is very much worth your time to read through these files. Not only will you learn more about what's available to use, but you will see many more of the advanced language syntax available to all Verse programs.
A great example of a custom creative_device
class is our Custom NPC Dialog Example.
Modules
A module is neither a variable
, data type
, class
, struct
or anything else we talked about so far. Modules are very special in programming. It's a way to group similar functionality into a named group. Sounds similar to a method but modules cannot be invoked (called, activated). Instead, you put the definitions of structs, classes and methods into these modules, but not their definitions.
Helpers := module:
player_helper := class:
ui_helper := class:
# ...other methods, classes and structs here...
To use a module, we need to use a new keyword called using
. This brings everything the module has to offer into the current scope of your file.
Take a look at the .digest.verse
files that I mentioned earlier and you can see lots and lots of examples of modules and how they are structured. You'll also see that you can use additional using
statements in the body of a module as well!
Submodules
Submodules are folders (or directories) that you add to your Verse files. The name of that folder is actually a module! All of the files defined in that folder, which contain classes, methods, etc., can be brought into another file in another submodule.
You can create a submodule by right clicking on the
Content
folder in the Verse explorer.
# Helpers/player_helper.verse
player_helper := class:
# ...definition of the helper class...
# Helpers/players.verse
using { Helpers.PlayerHelper }
players := class:
# Everything in the `player_helper.verse` file is available here!
Structuring your code this way encourages developers to create code that is reusable, which can be re-used as often as needed!
Visibility
Now is a good a time as ever to talk about visibility. Visibility is directly tied to a concept called attributes
. Attributes go along with another concept called specifiers
. I highly recommend you read over Epic's
Specifiers and Attributes
documentation page to understand more about them. We will go over the most commonly used attributes.
Attributes are put onto variables, methods, classes and structs. By default, all visibility attributes are internal
.
DoSomething<private>() : void
AmountOfCoins<internal> : int
player_helper<public> := class:
The syntax breakdown is item<attribute>
. Let's go over each one and what they mean.
Private
Private means that only the code in which this item is encapsulated in can interact with this item. If you import another module and it has private
, classes, structs, data members or methods, you will not be able to access them.
# Helpers/player_helper.verse
player_helper<private> := class:
GetPlayerCount() : int
# Helpers/ui_helper.verse
using { Helpers.PlayerHelper } # The module will still be found
ui_helper := class:
SomeVariable : int = player_helper{}.GetPlayerCount() # This will cause an error because the `player_helper` class is set to private!
Internal
Internal is the default visibility attribute. It can only be used inside the file that it was created in. It is a lot like the private
visibility attribute but it can be used throughout the file and not just the encapsulating object.
MakePlayerHelper<constructor>() : player_helper =
PlayerCount := 10 # We can set this because we are in the same file as the player_helper definition.
player_helper<public> := class:
PlayerCount<internal> : int
Public
Public is the easiest attribute to understand. If something is marked as public, we can use it anywhere!
# Helpers/player_helper.verse
player_helper<public> := class:
GetPlayerCount<public>()
# Helpers/ui_helper.verse
using { Helpers.PlayerHelper }
ui_helper := class:
PlayerHelper : player_helper = player_helper{}
GetTotalPlayers() : int = PlayerHelper.GetPlayerCount() # We can use this since it's a public method in a public class!
If the
GetPlayerCount
method was not public, we could access theplayer_helper
class but not theGetPlayerCount
method.
Conclusion
Although we are at the end of this document, that does not mean the Verse learning stops here! Check out the links to the
Official Verse documentation
as well as the
Verse Language Reference
. These are going to be your best friends on your journey to better learn Verse. Come and check out the
Fortnite Creative Official
Discord server (#uefn-verse
channel).
If you're looking for more to learn, check out our Examples!