Les fonctions

Déclarer des FUNCTION avec paramètres par défaut, optionnels, variadiques et REF, rendre des valeurs typées, surcharger selon le nombre d'arguments, s'accrocher au cycle de vie de l'Engine et passer des poignées.

En un mot#

Une FUNCTION est un bloc de code nommé qui reçoit des paramètres et rend une valeur. Elle s'exécute du début à la fin d'un seul tenant, dans l'image en cours. Cette page explique comment la déclarer, comment les valeurs entrent et sortent, quels noms l'Engine appelle de lui-même, et les poignées qui permettent à une valeur de désigner une variable.

Déclarer une fonction#

FUNCTION <name>[<sigil>]([<parameter>, ...]) [AS <type>[[]...]]
  <statements>
ENDFUNCTION [<value>]
FUNCTION Area(width, height)
ENDFUNCTION width * height

FUNCTION ShowTitle()
  Console("Niveau 1")
ENDFUNCTION

ShowTitle()                ' Niveau 1
Console(Str(Area(3, 4)))   ' 12
  • ENDFUNCTION s'écrit en un mot. END FUNCTION est refusé avec « FUNCTION requires ENDFUNCTION ».
  • Un appel porte toujours ses parenthèses, même sans argument : ShowTitle().
  • L'ordre n'a pas d'importance. On peut appeler une fonction plus haut que la ligne qui la déclare, et la déclarer après le DO … LOOP principal.
  • Les noms sont insensibles à la casse. area(3, 4) appelle Area.
  • Déclarez les fonctions au premier niveau d'un fichier, jamais dans un IF, une boucle ou une autre fonction.
  • Une fonction utilisateur l'emporte sur une commande du catalogue de même nom. Nommez une fonction Str et chaque Str(...) du programme appelle la vôtre.

⚠️ Un nom seul n'est pas un appel. ShowTitle seul sur une ligne compile, et n'échoue qu'à l'exécution, parce que le compilateur le lit comme une variable. Écrivez ShowTitle().

Les graphies PureBasic sont des alias : Procedure vaut FUNCTION, ProcedureReturn vaut EXITFUNCTION, EndProcedure vaut ENDFUNCTION. La forme typée Procedure.s Name() n'est pas acceptée : écrivez Procedure Name() AS string.

Procedure Greet(name AS string) AS string
  ProcedureReturn "Bonjour " + name
EndProcedure

Console(Greet("Ada"))   ' Bonjour Ada
DialecteCe qui change dans aukimi
AGK, DarkBASICMêmes FUNCTION / ENDFUNCTION valeur / EXITFUNCTION valeur. aukimi ajoute les valeurs par défaut, OPTIONAL, les paramètres variadiques et les surcharges.
PureBasicProcedure marche comme alias, mais les types s'écrivent AS <type>, pas .s ou .i après le mot-clé.
Visual BasicPas de Sub : une FUNCTION qui ne rend rien fait office de procédure. REF se place après le nom du paramètre (voir plus bas).

Rendre une valeur#

Il y a trois façons de quitter une fonction :

Vous écrivezOùCe que cela fait
ENDFUNCTION <valeur>la dernière lignerend la valeur quand le code arrive au bout
EXITFUNCTION [<valeur>]n'importe où dans le corpssort tout de suite, avec ou sans valeur
RETURN [<valeur>]n'importe où dans le corpsla même chose qu'EXITFUNCTION dans une fonction
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
  • EXITFUNCTION marche sur un IF … THEN d'une ligne, comme ci-dessus.
  • Une fonction qui se termine sans valeur ne rend rien. Si vous affichez ce résultat, vous voyez undefined.
  • Une fonction qui a un type de retour (AS integer, AS string…) rend à la place la valeur par défaut de ce type : 0, "", un enregistrement vide.

⚠️ EXIT FUNCTION en deux mots n'est pas refusé, mais le programme ne peut pas tourner. Écrivez EXITFUNCTION.

FUNCTION Sign(n)
  IF n < 0 THEN EXIT FUNCTION -1
ENDFUNCTION 1

Les retours typés#

AS <type> après les parenthèses convertit chaque valeur rendue dans ce 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
  • Un type d'enregistrement (AS Vec) ou un type tableau (AS integer[], jusqu'à six []) rend une copie indépendante. Modifier ce que vous avez reçu ne modifie jamais l'original.
  • Un type inconnu est une erreur de compilation : « 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))   ' 7

Un sigil sur le nom d'une fonction#

Un sigil ($, #, %, &) peut terminer le nom d'une fonction, comme en BASIC classique. C'est une indication pour le lecteur, pas une conversion.

FUNCTION Half#(n)
ENDFUNCTION n / 2

FUNCTION Title$(name$)
ENDFUNCTION "Sire " + name$

Console(Str(Half#(7)))     ' 3.5
Console(Title("Robin"))    ' Sire Robin
  • On peut appeler la fonction avec ou sans son sigil : Title("Robin") et Title$("Robin") sont le même appel.
  • Le sigil ne convertit jamais la valeur. Whole%(9.8) sans AS rend 9.8. Pour convertir, écrivez AS integer.
  • Le sigil ne fait pas partie du nom. FUNCTION Score(a) et FUNCTION Score$(a) déclarent la même fonction : écrire les deux est une erreur.

Les paramètres#

[OPTIONAL] <name>[<sigil>] [REF | BYREF] [AS <type>[[]...]] [= <default>]
PARAMARRAY <name> [AS <type>]
<name> [AS <type>]...

Les deuxième et troisième lignes sont les formes variadiques. Elles doivent être le dernier paramètre.

Valeurs par défaut et OPTIONAL#

FUNCTION Box(width, height = width)
ENDFUNCTION width * height

FUNCTION Greet$(name$ = "inconnu", OPTIONAL mark AS string)
ENDFUNCTION "Bonjour " + name$ + mark

Console(Str(Box(3)))           ' 9
Console(Str(Box(3, 2)))        ' 6
Console(Greet$())              ' Bonjour inconnu
Console(Greet$("Ada", " !"))   ' Bonjour Ada !
  • = <défaut> rend un paramètre optionnel. La valeur par défaut est calculée à chaque appel : elle peut donc utiliser un paramètre précédent (height = width).
  • OPTIONAL sans valeur par défaut donne la valeur par défaut du type du paramètre : 0 pour un nombre, "" pour AS string, un enregistrement ou un tableau vide.
  • Un paramètre obligatoire ne peut pas suivre un paramètre optionnel : erreur de compilation.

⚠️ Un sigil ne type pas un paramètre optionnel. OPTIONAL name$ reçoit 0, et non "", quand on l'omet. Écrivez OPTIONAL name AS string, ou donnez-lui une valeur par défaut : name$ = "".

⚠️ On ne peut pas sauter un argument au milieu. F(1, , 5) n'est pas refusé, mais la case vide disparaît et 5 atterrit dans le deuxième paramètre. Passez tous les arguments jusqu'au dernier dont vous avez besoin : F(1, 2, 5).

Les paramètres variadiques : PARAMARRAY et ...#

Le dernier paramètre peut recueillir tous les arguments restants dans un tableau.

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 values et values... sont la même chose.
  • Avec un type, écrivez PARAMARRAY values AS float ou values AS float.... La forme values... AS float est refusée.
  • Chaque valeur est convertie dans le type, et les enregistrements sont copiés.
  • Un paramètre variadique ne peut avoir ni valeur par défaut ni REF.

⚠️ .length est le dernier indice, pas le nombre d'éléments (comme dans AGK). Trois valeurs donnent values.length = 2, et aucune valeur donne -1. Bouclez avec FOR i = 0 TO values.length, et comptez avec values.length + 1.

Les paramètres typés#

AS <type> convertit l'argument quand la fonction démarre.

FUNCTION Pad$(text AS string, width AS integer)
  WHILE Len(text) < width
    text = "0" + text
  ENDWHILE
ENDFUNCTION text

Console(Pad$(42, 5.7))   ' 00042

Ici, 42 est devenu le texte "42", et 5.7 l'entier 5.

  • N'importe quel type numérique, string, HANDLE, un TYPE que vous avez déclaré, ou un tableau de ceux-ci (AS integer[], AS integer[][], jusqu'à six dimensions).
  • Un type inconnu est une erreur de compilation : « Unknown BASIC parameter type ».

⚠️ AS Sprite n'est pas un type de paramètre. La déclaration de famille qui active la notation pointée vaut pour une variable, pas pour un paramètre, et FUNCTION Move(s AS Sprite) est refusé. Dans une fonction, appelez la commande complète : SpriteX(s), SpritePosition(s, x, y), et non s.x.

Les copies, et REF#

Par défaut, une fonction travaille sur une copie de ce que vous lui passez. REF passe la variable elle-même : la fonction peut alors la modifier.

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
ParamètreUn nombre ou une chaîneUn enregistrement ou un tableau
p (sans type)copie⚠️ partagé : les modifications atteignent l'appelant
p AS Hero, p AS integer[]copie, convertiecopie
p REF AS Herola variable de l'appelantla variable de l'appelant

⚠️ Un paramètre sans type ne copie ni un enregistrement ni un tableau. Seul un paramètre typé le fait. Si une fonction ne doit pas toucher à l'original, donnez son type au paramètre.

Où se place REF#

<name> REF AS <type>
<name> BYREF AS <type>

REF et BYREF sont le même mot. Les deux se placent après le nom et avant AS, et AS <type> est obligatoire.

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

L'ordre de Visual Basic est refusé (« Invalid BASIC parameter »), tout comme REF sans type :

FUNCTION Bump(BYREF n AS integer)
  n = n + 1
ENDFUNCTION
  • L'argument doit être un emplacement affectable : une variable, un champ d'enregistrement, un élément de tableau, l'élément courant d'une liste ou une entrée de dictionnaire. Un littéral ou une expression est refusé : SwapValues(1, y) et SwapValues(x + 1, y) donnent « REF argument is not assignable ».
  • REF se transmet. Une fonction peut passer son propre paramètre REF à un autre paramètre REF, et la modification remonte jusqu'au premier appelant.
  • Le type s'applique quand même. Un REF AS integer qui reçoit une variable flottante y écrit un entier.
  • Un paramètre REF ne peut être ni OPTIONAL ni variadique.

⚠️ Ne mettez pas de sigil sur un paramètre REF. text$ REF AS string compile, mais le programme ne tourne pas dès que le corps lit text$. Écrivez text REF AS string.

Les surcharges selon le nombre d'arguments#

Plusieurs fonctions peuvent partager un nom si elles prennent un nombre d'arguments différent.

FUNCTION Area(side)
ENDFUNCTION side * side

FUNCTION Area(width, height)
ENDFUNCTION width * height

Console(Str(Area(3)))      ' 9
Console(Str(Area(3, 4)))   ' 12
  • Le choix se fait à la compilation, sur le seul nombre d'arguments. Les types ne sélectionnent jamais une surcharge.
  • Chaque fonction accepte une plage de nombres : au moins ses paramètres obligatoires, au plus tous ses paramètres, sans limite avec un variadique. Deux plages ne doivent pas se chevaucher.
  • Un appel qu'aucune version n'accepte est une erreur de compilation : « No BASIC overload of Area accepts 1 argument(s) ».
FUNCTION Area(side)
ENDFUNCTION side * side

FUNCTION Area(width, OPTIONAL height = 1)
ENDFUNCTION width * height

Celui-ci est refusé avec « Ambiguous BASIC overload: Area » : un appel à un argument conviendrait aux deux.

Les variables dans une fonction#

Les paramètres et les variables LOCAL appartiennent à la fonction. Ils sont créés à chaque appel et disparaissent au retour. Les variables GLOBAL sont partagées par tout le programme.

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

Ce qui se passe quand une fonction affecte un nom qui existe aussi en haut du programme :

En haut du programmeDans la fonction
GLOBAL score (n'importe où, même après la fonction)la variable partagée
score AS integer = 0 (typée, sans mot-clé)la variable partagée
score = 0 sur une ligne au-dessus de la fonctionla variable partagée
score = 0 sur une ligne en dessous de la fonction⚠️ une nouvelle variable locale
LOCAL score⚠️ une nouvelle variable locale

La lecture marche dans tous les cas, une fois exécutée la ligne qui crée la variable. Seule l'affectation réserve une surprise.

⚠️ LOCAL au premier niveau met une variable hors de portée des fonctions. Une fonction qui écrit score = score + 1 reçoit alors son propre score, et le programme s'arrête sur « Cannot access 'score' before initialization ». Déclarez les variables partagées avec GLOBAL.

Activez OPTION EXPLICIT et déclarez les variables partagées avec GLOBAL. Un nom non déclaré dans une fonction devient alors une erreur de compilation, au lieu d'une copie locale silencieuse.

La récursivité marche : une fonction peut s'appeler elle-même.

FUNCTION Fact(n AS integer) AS integer
  IF n <= 1 THEN EXITFUNCTION 1
ENDFUNCTION n * Fact(n - 1)

Console(Str(Fact(5)))   ' 120

Voir Variables et types pour GLOBAL, LOCAL et OPTION EXPLICIT en détail.

Ce qu'une fonction ne peut pas faire#

Une fonction va jusqu'au bout sans rendre l'image. Rien de ce qui attend n'y est donc permis.

Dans une FUNCTIONRésultat
ENDerreur de compilation : « END is not supported inside FUNCTION; use EXITFUNCTION »
Wait(...), Sleep(...)erreur de compilation
AWAITTASK, AWAITALL, AWAITANYerreur de compilation
Sync(), Swap(), YieldTask()⚠️ pas d'erreur de compilation, voir ci-dessous

⚠️ N'appelez jamais Sync(), Swap() ou YieldTask() dans une fonction. Dans un programme qui a un DO … LOOP ou un END, le compilateur l'accepte mais le programme ne peut pas démarrer. Dans un programme qui n'en a pas, l'appel n'attend rien.

FUNCTION Pause()
  Sync()
ENDFUNCTION

DO
  Pause()
  Sync()
LOOP

Pour attendre d'une image à l'autre, utilisez la boucle principale, un sous-programme GOSUB (qui, lui, peut attendre) ou une tâche. Voir Le flux de contrôle et La boucle de jeu et les tâches.

Les fonctions du cycle de vie#

Un programme peut tourner de deux façons. Soit il a sa propre boucle d'images (DO … Sync() … LOOP), soit il déclare des fonctions aux noms réservés et l'Engine les appelle. Après avoir compilé un script, l'Engine cherche ces noms :

FonctionQuand l'Engine l'appelleArguments
start()une fois, une fois le script chargéaucun
update(dt#)à chaque imageles secondes écoulées depuis l'image précédente (au plus 0,1)
fixedUpdate(step#)à cadence fixe, pour la physiquele pas fixe, 1/60 s
onCollisionEnter(other), onCollisionStay(other), onCollisionExit(other)quand un contact physique commence, dure ou cessel'autre entité
onTriggerEnter(other), onTriggerExit(other)quand une entité entre dans un déclencheur ou en sortl'autre entité
onMessage(message$, data, sender)quand un autre script envoie un message, par exemple avec EntitySend("door", "open")le message, ses données, l'expéditeur
onAnimationEvent(name$, data)quand une animation atteint un événementle nom de l'événement et ses données
main()à la place de start(), si le programme en a uneaucun

onStart, onUpdate et onFixedUpdate sont acceptés pour les trois premiers. onCollision et onTrigger valent onCollisionEnter et onTriggerEnter. onAnimEvent vaut onAnimationEvent.

GLOBAL elapsed# = 0
GLOBAL hits AS integer = 0

FUNCTION start()
  Console("prêt")
ENDFUNCTION

FUNCTION update(dt#)
  elapsed# = elapsed# + dt#
ENDFUNCTION

FUNCTION onCollisionEnter(other)
  hits = hits + 1
  Console("touché par " + other.name)
ENDFUNCTION

FUNCTION onMessage(message$, data, sender)
  IF message$ = "reset" THEN hits = 0
ENDFUNCTION
  • Elles gardent leur nom. En BASIC la casse n'importe pas (Update marche) ; l'Engine reçoit exactement la graphie qu'il attend.
  • Elles ne peuvent pas être surchargées. Un second update est une erreur de compilation : « BASIC lifecycle function cannot be overloaded ».
  • Les lignes hors de toute fonction s'exécutent une fois, au chargement du script, avant start(). C'est la place des déclarations GLOBAL et des préparatifs.
  • Pas de Sync() dans update. L'Engine appelle update une fois par image ; l'image se termine quand la fonction rend la main.
  • Une erreur dans update ne l'arrête pas. L'erreur est signalée, et update est rappelée à l'image suivante : la même erreur se répète. Un update qui prend plus de 200 ms pendant 5 images de suite est désactivé.
  • Vous pouvez toujours appeler ces fonctions vous-même, comme les autres.

⚠️ Un programme à DO … LOOP tourne comme main(). La boucle, ainsi qu'un Wait() ou un Sleep() au premier niveau, font construire au compilateur une main(). L'Engine appelle alors main() et n'appelle jamais start(). Ne déclarez pas votre propre FUNCTION main() dans un tel programme : la boucle la remplace sans prévenir.

⚠️ lateUpdate et onDestroy gardent leurs noms réservés, mais l'éditeur ne les appelle pas sur le script d'une entité à ce jour. Ne comptez pas sur eux pour la logique du jeu ni pour le nettoyage.

Les poignées#

Une poignée est une valeur qui désigne une variable. On peut la ranger, la passer et la rendre, puis lire ou écrire la variable à travers elle.

@<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("libérée")   ' libérée
Console(Str(hp))             ' 75
Vous écrivezCe que cela fait
@ciblefabrique une poignée vers cible
x AS HANDLE, x AS POINTERune variable qui contient une poignée (POINTER est le même type). Elle démarre vide.
GetRef(h)lit la variable que h désigne
SetRef(h, valeur)l'écrit, convertie dans le type de la cible
IsRef(h)vrai si h désigne encore quelque chose
ReleaseRef(h)met fin à la poignée. La variable elle-même n'est pas touchée.

Ce que @ accepte : une variable, un champ d'enregistrement (@hero.hp), un élément de tableau (@scores[1]), l'élément courant d'une liste, une entrée de dictionnaire (@names("ann")). @42 est une erreur de compilation : « 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
  • Une poignée garde sa cible en vie. MakeCounter rend une poignée vers une LOCAL, et le compteur survit à la fin de la fonction.
  • Le type de la cible l'emporte. Une poignée vers une variable AS uint8 range SetRef(p, 300) sous la forme 44.
  • Lire un enregistrement par une poignée donne une copie dès qu'on l'affecte, comme toute affectation d'enregistrement.

Quand une poignée cesse de marcher#

ÉvénementIsRef(h)GetRef(h) / SetRef(h, …)
ReleaseRef(h)fauxerreur d'exécution : « BASIC reference handle has been released »
DELETE de l'enregistrement qui porte le champfauxerreur d'exécution
une variable HANDLE jamais affectéefauxerreur d'exécution

Testez IsRef(h) avant d'utiliser une poignée dont vous n'êtes pas sûr. Une poignée n'est jamais laissée pointée sur de la mémoire libérée.

Poignées, REF et pointeurs classiques#

Paramètre REFPoignée
Durée de viele temps d'un appeltant que vous la gardez
Peut être rangée, rendue, mise dans un enregistrementnonoui
Syntaxep REF AS T dans la liste des paramètres@cible, puis GetRef / SetRef

Le @variable de PureBasic donne une adresse mémoire, et PeekI / PokeI lisent et écrivent à travers elle. Une poignée aukimi n'est pas un nombre : pas d'arithmétique d'adresse, pas de conversion en entier, et aucun moyen de la faire pointer sur une mémoire que vous n'avez pas nommée. C'est ce qui la rend sûre sur toutes les cibles.