Functions
Declare FUNCTIONs with default, optional, variadic and REF parameters, return typed values, overload by argument count, hook into the Engine's lifecycle, and pass handles.
In one line#
A FUNCTION is a named block of code that takes parameters and gives back a
value. It runs from start to end in one go, inside the current frame. This page
covers how to declare one, how values go in and out, which names the Engine
calls by itself, and the handles that let a value point at a variable.
Declaring a function#
FUNCTION <name>[<sigil>]([<parameter>, ...]) [AS <type>[[]...]]
<statements>
ENDFUNCTION [<value>]
FUNCTION Area(width, height)
ENDFUNCTION width * height
FUNCTION ShowTitle()
Console("Level 1")
ENDFUNCTION
ShowTitle() ' Level 1
Console(Str(Area(3, 4))) ' 12
ENDFUNCTIONis one word.END FUNCTIONis refused with "FUNCTION requires ENDFUNCTION".- A call always carries parentheses, even with no argument:
ShowTitle(). - Order does not matter. You can call a function above the line that
declares it, and declare it after the main
DO … LOOP. - Names are case-insensitive.
area(3, 4)callsArea. - Declare functions at the top level of a file, never inside an
IF, a loop or another function. - A user function wins over a catalogue command of the same name. Name a
function
Strand everyStr(...)in the program calls yours.
⚠️ A bare name is not a call. ShowTitle alone on a line compiles, and
fails only when the program runs, because the compiler reads it as a variable.
Write ShowTitle().
The PureBasic spellings are aliases: Procedure is FUNCTION,
ProcedureReturn is EXITFUNCTION, EndProcedure is ENDFUNCTION. The typed
form Procedure.s Name() is not accepted: write Procedure Name() AS string.
Procedure Greet(name AS string) AS string
ProcedureReturn "Hello " + name
EndProcedure
Console(Greet("Ada")) ' Hello Ada
| Dialect | What changes in aukimi |
|---|---|
| AGK, DarkBASIC | Same FUNCTION / ENDFUNCTION value / EXITFUNCTION value. aukimi adds default values, OPTIONAL, variadic parameters and overloads. |
| PureBasic | Procedure works as an alias, but types are written AS <type>, not .s or .i after the keyword. |
| Visual Basic | No Sub: a FUNCTION that returns nothing is a procedure. REF goes after the parameter name (see below). |
Returning a value#
There are three ways to leave a function:
| You write | Where | What it does |
|---|---|---|
ENDFUNCTION <value> | the last line | returns the value when the code reaches the end |
EXITFUNCTION [<value>] | anywhere in the body | leaves at once, with or without a value |
RETURN [<value>] | anywhere in the body | the same as EXITFUNCTION inside a function |
FUNCTION Grade$(score)
IF score >= 90 THEN EXITFUNCTION "A"
IF score >= 50
RETURN "B"
ENDIF
ENDFUNCTION "C"
Console(Grade$(95) + Grade$(60) + Grade$(10)) ' ABC
EXITFUNCTIONworks on a one-lineIF … THEN, as above.- A function that ends without a value gives back nothing. If you print
that result, you see
undefined. - A function with a return type (
AS integer,AS string…) gives back the default value of that type instead:0,"", an empty record.
⚠️ EXIT FUNCTION in two words is not refused, but the program cannot run.
Write EXITFUNCTION.
FUNCTION Sign(n)
IF n < 0 THEN EXIT FUNCTION -1
ENDFUNCTION 1Typed returns#
AS <type> after the parentheses converts every returned value to that type.
FUNCTION Whole(n) AS integer
ENDFUNCTION n
FUNCTION Label(n) AS string
ENDFUNCTION n
Console(Str(Whole(7.9))) ' 7
Console(Label(3) + Label(4)) ' 34
- A record type (
AS Vec) or an array type (AS integer[], up to six[]) returns an independent copy. Changing what you received never changes the original. - An unknown type is a compile error: "Unknown BASIC return type".
TYPE Vec
x AS float
y AS float
ENDTYPE
FUNCTION MakeVec(x AS float, y AS float) AS Vec
LOCAL v AS Vec
v.x = x
v.y = y
ENDFUNCTION v
p AS Vec
p = MakeVec(3, 4)
Console(Str(p.x + p.y)) ' 7A sigil on a function name#
A sigil ($, #, %, &) can end a function name, as in classic BASIC. It is
a label for the reader, not a conversion.
FUNCTION Half#(n)
ENDFUNCTION n / 2
FUNCTION Title$(name$)
ENDFUNCTION "Sir " + name$
Console(Str(Half#(7))) ' 3.5
Console(Title("Robin")) ' Sir Robin
- You may call the function with or without its sigil:
Title("Robin")andTitle$("Robin")are the same call. - The sigil never converts the value.
Whole%(9.8)with noASreturns9.8. To convert, writeAS integer. - The sigil is not part of the name.
FUNCTION Score(a)andFUNCTION Score$(a)declare the same function, so writing both is an error.
Parameters#
[OPTIONAL] <name>[<sigil>] [REF | BYREF] [AS <type>[[]...]] [= <default>]
PARAMARRAY <name> [AS <type>]
<name> [AS <type>]...
The second and third lines are the variadic forms. They must be the last parameter.
Default values and OPTIONAL#
FUNCTION Box(width, height = width)
ENDFUNCTION width * height
FUNCTION Greet$(name$ = "stranger", OPTIONAL mark AS string)
ENDFUNCTION "Hello " + name$ + mark
Console(Str(Box(3))) ' 9
Console(Str(Box(3, 2))) ' 6
Console(Greet$()) ' Hello stranger
Console(Greet$("Ada", "!")) ' Hello Ada!
= <default>makes a parameter optional. The default is computed at each call, so it may use an earlier parameter (height = width).OPTIONALwithout a default gives the default of the parameter's type:0for a number,""forAS string, an empty record or array.- A required parameter cannot follow an optional one: compile error.
⚠️ A sigil does not type an optional parameter. OPTIONAL name$ receives
0, not "", when it is left out. Write OPTIONAL name AS string, or give it a
default: name$ = "".
⚠️ You cannot skip an argument in the middle. F(1, , 5) is not refused,
but the empty slot disappears and 5 lands in the second parameter. Pass every
argument up to the last one you need: F(1, 2, 5).
Variadic parameters: PARAMARRAY and ...#
The last parameter can collect every remaining argument into an array.
FUNCTION Average#(PARAMARRAY values AS float)
LOCAL sum# = 0
IF values.length < 0 THEN EXITFUNCTION 0
FOR i = 0 TO values.length
sum# = sum# + values[i]
NEXT i
ENDFUNCTION sum# / (values.length + 1)
FUNCTION Join$(separator$, parts...)
result$ = ""
FOR i = 0 TO parts.length
IF i > 0 THEN result$ = result$ + separator$
result$ = result$ + parts(i)
NEXT i
ENDFUNCTION result$
Console(Str(Average#(2, 4, 9))) ' 5
Console(Str(Average#())) ' 0
Console(Join$("-", "a", "b", "c")) ' a-b-c
PARAMARRAY valuesandvalues...are the same thing.- With a type, write
PARAMARRAY values AS floatorvalues AS float.... The formvalues... AS floatis refused. - Every value is converted to the type, and records are copied.
- A variadic parameter cannot have a default and cannot be
REF.
⚠️ .length is the last index, not the count (as in AGK). Three values give
values.length = 2, and no value gives -1. Loop with
FOR i = 0 TO values.length, and count with values.length + 1.
Typed parameters#
AS <type> converts the argument when the function starts.
FUNCTION Pad$(text AS string, width AS integer)
WHILE Len(text) < width
text = "0" + text
ENDWHILE
ENDFUNCTION text
Console(Pad$(42, 5.7)) ' 00042
Here 42 became the text "42", and 5.7 became the integer 5.
- Any numeric type,
string,HANDLE, aTYPEyou declared, or an array of them (AS integer[],AS integer[][], up to six dimensions). - An unknown type is a compile error: "Unknown BASIC parameter type".
⚠️ AS Sprite is not a parameter type. The family declaration that turns on
dot notation works on a variable, not on a parameter, and
FUNCTION Move(s AS Sprite) is refused. Inside a function, call the full
command: SpriteX(s), SpritePosition(s, x, y), not s.x.
Copies, and REF#
By default a function works on a copy of what you pass. REF passes the
variable itself, so the function can change it.
TYPE Hero
hp AS integer
ENDTYPE
FUNCTION HealCopy(h AS Hero)
h.hp = 100
ENDFUNCTION
FUNCTION HealRef(h REF AS Hero)
h.hp = 100
ENDFUNCTION
hero AS Hero
hero.hp = 10
HealCopy(hero)
Console(Str(hero.hp)) ' 10
HealRef(hero)
Console(Str(hero.hp)) ' 100
| Parameter | A number or a string | A record or an array |
|---|---|---|
p (no type) | copy | ⚠️ shared: changes reach the caller |
p AS Hero, p AS integer[] | copy, converted | copy |
p REF AS Hero | the caller's variable | the caller's variable |
⚠️ An untyped parameter does not copy a record or an array. Only a typed parameter does. If a function must not touch the original, give the parameter its type.
Where REF goes#
<name> REF AS <type>
<name> BYREF AS <type>
REF and BYREF are the same word. Both go after the name and before
AS, and AS <type> is required.
FUNCTION SwapValues(a REF AS integer, b REF AS integer)
LOCAL t AS integer = a
a = b
b = t
ENDFUNCTION
x AS integer = 1
y AS integer = 2
SwapValues(x, y)
Console(Str(x) + " " + Str(y)) ' 2 1
The Visual Basic order is refused ("Invalid BASIC parameter"), and so is REF
without a type:
FUNCTION Bump(BYREF n AS integer)
n = n + 1
ENDFUNCTION
- The argument must be a place you can assign: a variable, a record field, an
array element, the current element of a list, or a map entry. A literal or an
expression is refused:
SwapValues(1, y)andSwapValues(x + 1, y)give "REF argument is not assignable". - REF forwards. A function can pass its own
REFparameter on to anotherREFparameter, and the change reaches the first caller. - The type still applies. A
REF AS integerthat receives a float variable writes an integer into it. - A
REFparameter cannot beOPTIONALand cannot be variadic.
⚠️ Do not put a sigil on a REF parameter. text$ REF AS string compiles, but
the program does not run as soon as the body reads text$. Write
text REF AS string.
Overloads by argument count#
Several functions can share a name if they take a different number of arguments.
FUNCTION Area(side)
ENDFUNCTION side * side
FUNCTION Area(width, height)
ENDFUNCTION width * height
Console(Str(Area(3))) ' 9
Console(Str(Area(3, 4))) ' 12
- The choice is made at compile time, on the argument count only. Types never select an overload.
- Each function accepts a range of counts: its required parameters at least, all its parameters at most, no limit with a variadic. Two ranges must not overlap.
- A call that no version accepts is a compile error: "No BASIC overload of Area accepts 1 argument(s)".
FUNCTION Area(side)
ENDFUNCTION side * side
FUNCTION Area(width, OPTIONAL height = 1)
ENDFUNCTION width * height
This one is refused with "Ambiguous BASIC overload: Area": a call with one argument would fit both.
Variables inside a function#
Parameters and LOCAL variables belong to the function. They are created at
each call and disappear when it returns. GLOBAL variables are shared by the
whole program.
OPTION EXPLICIT
GLOBAL score AS integer = 0
FUNCTION AddPoints(amount AS integer) AS integer
LOCAL bonus AS integer = amount DIV 10
score = score + amount + bonus
ENDFUNCTION score
AddPoints(50)
Console(Str(AddPoints(20))) ' 77
What happens when a function assigns a name that also exists at the top of the program:
| At the top of the program | Inside the function |
|---|---|
GLOBAL score (anywhere, even after the function) | the shared variable |
score AS integer = 0 (typed, no keyword) | the shared variable |
score = 0 on a line above the function | the shared variable |
score = 0 on a line below the function | ⚠️ a new local variable |
LOCAL score | ⚠️ a new local variable |
Reading works in every case, once the line that creates the variable has run. Only assigning creates the surprise.
⚠️ LOCAL at the top level keeps a variable out of the functions' reach. A
function that writes score = score + 1 then gets its own score, and the
program stops with "Cannot access 'score' before initialization". Declare
shared variables with GLOBAL.
Turn on
OPTION EXPLICITand declare shared variables withGLOBAL. An undeclared name inside a function then becomes a compile error, instead of a silent local copy.
Recursion works: a function can call itself.
FUNCTION Fact(n AS integer) AS integer
IF n <= 1 THEN EXITFUNCTION 1
ENDFUNCTION n * Fact(n - 1)
Console(Str(Fact(5))) ' 120
See Variables and types for
GLOBAL, LOCAL and OPTION EXPLICIT in full.
What a function cannot do#
A function runs to its end without giving the frame back. So nothing that waits is allowed inside it.
| Inside a FUNCTION | Result |
|---|---|
END | compile error: "END is not supported inside FUNCTION; use EXITFUNCTION" |
Wait(...), Sleep(...) | compile error |
AWAITTASK, AWAITALL, AWAITANY | compile error |
Sync(), Swap(), YieldTask() | ⚠️ no compile error, see below |
⚠️ Never call Sync(), Swap() or YieldTask() in a function. In a program
with a DO … LOOP or an END, the compiler accepts it but the program cannot
start. In a program without them, the call does not wait for anything.
FUNCTION Pause()
Sync()
ENDFUNCTION
DO
Pause()
Sync()
LOOP
To wait across frames, use the main loop, a GOSUB subroutine (which may wait),
or a task. See Control flow and
The game loop and tasks.
Lifecycle functions#
A program can run in two ways. Either it owns a frame loop (DO … Sync() … LOOP), or it declares functions with reserved names and the Engine calls
them. After compiling a script, the Engine looks for these names:
| Function | When the Engine calls it | Arguments |
|---|---|---|
start() | once, after the script has loaded | none |
update(dt#) | every frame | seconds since the last frame (at most 0.1) |
fixedUpdate(step#) | at a fixed rate, for physics | the fixed step, 1/60 s |
onCollisionEnter(other), onCollisionStay(other), onCollisionExit(other) | when a physics contact starts, lasts or ends | the other entity |
onTriggerEnter(other), onTriggerExit(other) | when an entity enters or leaves a trigger | the other entity |
onMessage(message$, data, sender) | when another script sends a message, for example with EntitySend("door", "open") | the message, its data, the sender |
onAnimationEvent(name$, data) | when an animation reaches an event | the event name and its data |
main() | instead of start(), if the program has one | none |
onStart, onUpdate, onFixedUpdate are accepted for the first three.
onCollision and onTrigger mean onCollisionEnter and onTriggerEnter.
onAnimEvent means onAnimationEvent.
GLOBAL elapsed# = 0
GLOBAL hits AS integer = 0
FUNCTION start()
Console("ready")
ENDFUNCTION
FUNCTION update(dt#)
elapsed# = elapsed# + dt#
ENDFUNCTION
FUNCTION onCollisionEnter(other)
hits = hits + 1
Console("hit " + other.name)
ENDFUNCTION
FUNCTION onMessage(message$, data, sender)
IF message$ = "reset" THEN hits = 0
ENDFUNCTION
- They keep their name. In BASIC the case does not matter (
Updateworks); the Engine receives the exact spelling it expects. - They cannot be overloaded. A second
updateis a compile error: "BASIC lifecycle function cannot be overloaded". - The lines outside any function run once, when the script loads, before
start(). That is the place forGLOBALdeclarations and setup. - No
Sync()inupdate. The Engine callsupdateonce per frame; the frame ends when the function returns. - An error in
updatedoes not stop it. The error is reported, andupdateis called again on the next frame, so the same error repeats. Anupdatethat takes more than 200 ms for 5 frames in a row is switched off. - You can still call these functions yourself, like any other.
⚠️ A DO … LOOP program is run as main(). The loop, and any top-level
Wait() or Sleep(), make the compiler build a main(). The Engine then calls
main() and never calls start(). Do not declare your own FUNCTION main()
in such a program: the loop replaces it without a warning.
⚠️ lateUpdate and onDestroy keep their reserved names, but the editor does
not call them on an entity's script at the time of writing. Do not rely on
them for game logic or cleanup.
Handles#
A handle is a value that points at a variable. You can store it, pass it and return it, then read or write the variable through it.
@<target>
<name> AS HANDLE
<name> AS POINTER
GetRef(<handle>)
SetRef(<handle>, <value>)
IsRef(<handle>)
ReleaseRef(<handle>)
OPTION EXPLICIT
GLOBAL hp AS integer = 100
GLOBAL link AS HANDLE = @hp
SetRef(link, 75)
Console(Str(hp)) ' 75
Console(Str(GetRef(link))) ' 75
ReleaseRef(link)
IF NOT IsRef(link) THEN Console("released") ' released
Console(Str(hp)) ' 75
| You write | What it does |
|---|---|
@target | makes a handle to target |
x AS HANDLE, x AS POINTER | a variable that holds a handle (POINTER is the same type). It starts empty. |
GetRef(h) | reads the variable h points at |
SetRef(h, value) | writes it, converted to the target's type |
IsRef(h) | true if h still points at something |
ReleaseRef(h) | ends the handle. The variable itself is not touched. |
What @ accepts: a variable, a record field (@hero.hp), an array element
(@scores[1]), the current element of a list, a map entry (@names("ann")).
@42 is a compile error: "BASIC @ requires an assignable variable, field,
array, list, or map element".
OPTION EXPLICIT
GLOBAL gold AS integer = 5
FUNCTION AddGold(target AS HANDLE, amount AS integer)
SetRef(target, GetRef(target) + amount)
ENDFUNCTION
FUNCTION MakeCounter() AS HANDLE
LOCAL counter AS integer = 0
ENDFUNCTION @counter
AddGold(@gold, 10)
Console(Str(gold)) ' 15
GLOBAL c AS HANDLE = MakeCounter()
SetRef(c, GetRef(c) + 1)
SetRef(c, GetRef(c) + 1)
Console(Str(GetRef(c))) ' 2
- A handle keeps its target alive.
MakeCounterreturns a handle to aLOCAL, and the counter survives the end of the function. - The target's type wins. A handle to an
AS uint8variable storesSetRef(p, 300)as44. - Reading a record through a handle gives a copy when you assign it, like any record assignment.
When a handle stops working#
| Event | IsRef(h) | GetRef(h) / SetRef(h, …) |
|---|---|---|
ReleaseRef(h) | false | runtime error: "BASIC reference handle has been released" |
DELETE of the record that holds the field | false | runtime error |
a HANDLE variable never assigned | false | runtime error |
Test IsRef(h) before using a handle you are not sure about. A handle is never
left pointing at freed memory.
Handles, REF and classic pointers#
REF parameter | Handle | |
|---|---|---|
| Lives | during one call | as long as you keep it |
| Can be stored, returned, put in a record | no | yes |
| Syntax | p REF AS T in the parameter list | @target, then GetRef / SetRef |
PureBasic's @variable gives a memory address, and PeekI / PokeI read and
write through it. An aukimi handle is not a number: there is no address
arithmetic, no cast to an integer, and no way to make one point at memory you
did not name. That is what makes it safe on every target.