Tutorial traduit par BTex, avec la permission de l'auteur
Tim SweeneyMise à jour française : 09-nov.-2000
Ceci est un document technique décrivant le langage UnrealScript. Ce n'est pas un didacticiel, pas plus qu'il ne propose des exemples détaillés de l'utilisation du code d'UnrealScrip. Pour les exemples d'UnrealScript avant de publier des modifications d'Unreal, le lecteur devra s'imprégner du code source des scripts d'Unreal, qui produit dix mille lignes de code en UnrealScript, code qui résout de nombreux problèmes comme ceux de l'IA, les mouvements, les inventaires et les "triggers" (dispositifs de déclenchement à distance) Une bonne manière de débuter est d'imprimer les scripts de "Actor"(acteur), "Object", "Pawn"(créatures), "Inventory" (inventaire) et "Weapons" (armes)
Ce document sous entend que le lecteur a la connaissance du travail en C/C++, que la programmation orientée objet (POO) lui est familier, qu'il a joué à Unreal et qu'il a utilisé l'environnement d'édition de UnrealEd.
Pour les programmeurs qui débutent en POO, il est hautement recommandé d'aller sur Amazon.com ou dans une librairie et d'y acheter une introduction à la programmation Java. Java est très semblable à UnrealScript, et c'est un excellent langage à apprendre du fait de son approche simple et claire.
UnrealScript a été créé pour fournir aux équipes de développement et aux développeurs de seconds niveaux, un langage comportant de puissantes "builtins" (fonctions encapsulées) qui "collent" naturellement aux besoins et nuances de la programmation du jeu.
Les buts principaux d'UnrealScript sont :
Lors des premiers développements d'UnrealScript, plusieurs paradigmes de différentes programmations majeures furent explorés et analysés avant d'arriver au présent résultat. Tout d'abord, j'ai recherché dans les machines virtuelles, Java de Sun et Microsoft pour Windows, les bases du langage de script pour Unreal. Je me suis aperçu que Java n'offrait pas de gros avantages par rapport à C/C++ dans le contexte d'Unreal, ajoutait des restrictions frustrantes du fait d'un manque de particularités du langage (tel le recouvrement des opérateurs), et devenait d'une lenteur insondable du fait de "l'overhead" lié à la commutation de tâches de la machine virtuelle ainsi que de l'inefficacité de la collecte de résidus Java dans le cas d'un grand graphe d'objets. Ensuite, j'ai essayé une rapide implémentation d'UnrealScript dans une variante de Visual Basic, qui marchait bien, mais était moins proche des programmeurs accoutumés au C/C++. La décision finale fut de créer UnrealScript sur une variante C++/Java basée sur le désir de faire coller les concepts spécifiques à la définition du langage lui-même, à la nécessité de vitesse, et de faire quelque chose de familier. Il s'est avéré que c'était une sage décision, car cela à simplifié grandement de nombreux aspects du codage d'Unreal.
Cet exemple illustre une classe simple représentative d'UnrealScript, et met en lumière la syntaxe et les particularités d'UnrealScript. Notez que ce code peut différer en fonction de l'endroit où il est appelé dans le source courant d'Unreal, aussi cette documentation n'est pas une représentation exacte du code.
//=============================================================================
// TriggerLight.
// Une lumière qui peut être allumée où éteinte par un "trigger"
//=============================================================================
class TriggerLight expands Light;
//-----------------------------------------------------------------------------
// Variables.
var() float ChangeTime; // Temps mis par la lumière pour passer de on à off.
var() bool bInitiallyOn; // Indique qu'elle est initialement à l'état on.
var() bool bDelayFullOn; // Délai pour être complètement allumée.
var ELightType InitialType; // Type de lumière initial.
var float InitialBrightness; // Éclairement Initial .
var float Alpha, Direction;
var actor Trigger;
//-----------------------------------------------------------------------------
// Fonction du moteur du jeu.
// Appelée au début de la partie.
function BeginPlay()
{
// Mémorise le type de lumière initiale et initialise le nouveau.
Disable( 'Tick' );
InitialType = LightType;
InitialBrightness = LightBrightness;
if( bInitiallyOn )
{
Alpha = 1.0;
Direction = 1.0;
}
else
{
LightType = LT_None;
Alpha = 0.0;
Direction = -1.0;
}
}
// Appelée quand le temps est dépassé.
function Tick( float DeltaTime )
{
LightType = InitialType;
Alpha += Direction * DeltaTime / ChangeTime;
if( Alpha > 1.0 )
{
Alpha = 1.0;
Disable( 'Tick' );
if( Trigger != None )
Trigger.ResetTrigger();
}
else if( Alpha < 0.0 )
{
Alpha = 0.0;
Disable( 'Tick' );
LightType = LT_None;
if( Trigger != None )
Trigger.ResetTrigger();
}
if( !bDelayFullOn )
LightBrightness = Alpha * InitialBrightness;
else if( (Direction>0 && Alpha!=1) || Alpha==0 )
LightBrightness = 0;
else
LightBrightness = InitialBrightness;
}
//-----------------------------------------------------------------------------
// États publiques.
// Le Trigger fait passer à l'état "allumé".
state() TriggerTurnsOn
{
function Trigger( actor Other, pawn EventInstigator )
{
Trigger = None;
Direction = 1.0;
Enable( 'Tick' );
}
}
// Le Trigger fait passer à l'état "éteint".
state() TriggerTurnsOff
{
function Trigger( actor Other, pawn EventInstigator )
{
Trigger = None;
Direction = -1.0;
Enable( 'Tick' );
}
}
// Le Trigger fait basculer la lumière.
state() TriggerToggle
{
function Trigger( actor Other, pawn EventInstigator )
{
log("Toggle");
Trigger = Other;
Direction *= -1;
Enable( 'Tick' );
}
}
// Le Trigger contrôle la lumière.
state() TriggerControl
{
function Trigger( actor Other, pawn EventInstigator )
{
Trigger = Other;
if( bInitiallyOn ) Direction = -1.0;
else Direction = 1.0;
Enable( 'Tick' );
}
function UnTrigger( actor Other, pawn EventInstigator )
{
Trigger = Other;
if( bInitiallyOn ) Direction = 1.0;
else Direction = -1.0;
Enable( 'Tick' );
}
}
Les éléments clés à regarder dans ce script sont :
La machine virtuelle Unreal est constituée de plusieurs composants: le serveur, le client, le moteur d'interprétation et le moteur de support de code.
Le serveur d'Unreal contrôle tout le déroulement du jeu et les interactions entre joueurs et acteurs. Dans un jeu à un joueur, le client Unreal et le serveur Unreal sont lancés sur la même machine, dans un jeu en réseau, il y a un serveur dédié tournant sur une machine; tous les joueurs se connectent à cette machine et en sont les clients.
Tout jeu se situe à l'intérieur d'un "niveau", un environnement complet contenant géométrie (espace) et acteurs. Du fait que le serveur d'Unreal doit être capable de faire tourner plus d'un niveau simultanément, les niveaux se déroulent indépendamment, ils sont étanches entre eux : les acteurs ne peuvent passer d'un niveau à l'autre, ni communiquer avec les acteurs d'un autre niveau.
Chaque acteur dans une map, peut être soit sous le contrôle d'un joueur (il peut y avoir de nombreux joueurs dans un jeu en réseau) ou sous le contrôle d'un script. Quand un acteur est sous contrôle d'un script, son script définit complètement la manière dont l'acteur interfère avec les autres acteurs.
Avec tous ces acteurs actifs, l'exécution des scripts, l'occurrence des évènements dans le "monde", vous vous demandez probablement si quelqu'un peut comprendre le chemin d'exécution dans UnrealScript. La réponse est la suivante :
Pour traiter le temps, Unreal divise chaque seconde du jeu en "Tick". Un tick est la plus petite unité de temps au cours de laquelle tous les acteurs d'un niveau sont actualisés. Un tick, typiquement, prend entre 1/100 à 1/10ème de seconde. Le tick est simplement limité par la puissance du CPU, plus la machine est rapide, moins le tick durera.
Quelques commandes dans UnrealScript prennent zéro tick pour s'exécuter (c'est à dire, qu'elles s'exécutent sans temps passé dans le jeu), et d'autres prennent de nombreux ticks. Les fonctions qui requièrent du temps de jeu sont appelées "fonctions latentes". Quelques exemples de fonctions latentes comprennent : "Sleep" (dormir), "FinishAnim" (finir l'animation), et "MoveTo" (aller vers) Les fonctions latentes dans UnrealScript peuvent être seulement appelées à partir du code dans un 'état', pas dans un code compris dans une fonction.
Pendant qu'un acteur exécute une fonction latente, l'état de cet acteur ne peut évoluer jusqu'a ce que la fonction latente soit terminée. Ce pendant, d'autres acteurs, où la machine virtuelle, peut appeler des fonctions contenues dans l'acteur. Le résultat de tout cela est que toute les fonctions UnrealScript peuvent être appelées à tout moment, même lorsque des fonctions latentes sont en instance.
En termes de programmation traditionnelle, UnrealScript se déroule comme si chaque acteur, dans un niveau, possédait son propre fil conducteur. En interne, Unreal n'utilise pas de fonctionnalités de Windows, car cela serait inefficace (Windows 95 et Windows NT, ne manipulent pas des centaines de fonctionnalités simultanément d'une manière efficace) A la place, UnrealScript, simule des fonctionnalités (threads) Ceci est transparent pour le code d'UnrealScript, mais devient très apparent quand vous écrivez du code C++ qui interfère avec UnrealScript.
Tous les UnrealScripts s'exécutent en parallelle. S'il y a 100 monstres dans un niveau, tous les scripts de ces 100 monstres s'exécuteront simultanément et indépendamment.
Avant de commencer à travailler avec UnrealScript, il est important de comprendre la relation de haut niveau qui existe entre les objets de Unreal. L'architecture d'Unreal est un point de départ majeur de bien d'autres jeux : Unreal est purement orienté objet (plutôt comme COM/ActiveX), en cela il possède un modèle d'objet bien défini avec le support des concepts orientés objets de hauts niveaux, tels l'objet graphe, la sérialisation, la durée de vie d'un objet et le polymorphisme. Historiquement, la plupart des jeux avaient été conçus d'une manière monolithique, avec des fonctionnalités majeures codées en dur et figée au niveau de l'objet, aussi de nombreux jeux, comme Doom et Quake ont prouvé qu'ils étaient très 'extensibles' au niveau de leur contenu. Il y a un bénéfice majeur au fait qu'Unreal soit orienté objet : des fonctionnalités majeures et des types d'objets peuvent être ajoutés à Unreal au moment du démarrage, et cette extension peut prendre la forme d'une sous-classe, plutôt que (par exemple) en modifiant un monceau de code existant. Cette forme d'extension est extrêmement puissante, aussi il encourage la communauté Unreal à créer des améliorations qui peuvent toutes inter-agir entre elles.
"Object" est la classe parente de tous les objets dans Unreal. Toutes les fonctions de la classe "Object" sont accessibles de n'importe où, car toute chose dérive de "Object". "Object" est une classe de base abstraite, en cela elle ne fait rien d'utile. Toute fonctionnalité est produite par les sous-classes, telles que texture (texture de niveau), TextBuffer (Tampon de textes : réservoir de textes) et "Class" (qui décrit la classe des autres objets)
"Actor" (extension d'Object") est la classes parente de tous les objets du jeu qui se suffisent a eux-même ('standalone') dans Unreal. La classe "Actor" contient toutes les fonctionnalités nécessaires à un acteur pour se mouvoir, inter-agir avec les autres acteurs, affecter l'environnement, et faire d'autres choses utiles pour le déroulement du jeu.
"Pawn" (extension d'"Actor"), est la classe parente de toutes les créatures et joueurs dans Unreal qui sont dotés d'un haut niveau d'Intelligence Artificielle et peuvent recevoir des ordres de contrôle de la part du joueur.
"Class" (extension d'"Object") est une sorte d'objet spécial qui décrit les classes d'objets. Cela peut paraître confus au premier abord : une classe est un objet, et une classe décrit certains objets. Il en est ainsi pour le son, et il y a de nombreux cas où vous traiterez des classes d'objets. Par exemple, quand vous faite apparaître un nouvel acteur dans un UnrealScript, vous pouvez spécifier la classe de ce nouvel acteur par une classe d'objet.
Avec UnrealScript, vous pouvez écrire du code pour chaque classe d'objet, mais 99% des fois, vous écrirez du code pour une classe dérivée de "Actor". La fonctionnalité la plus utile d'UnrealScript est relative au jeu et traite des acteurs.
Chaque script correspond exactement à une classe, et le script commence par la déclaration de la classe, la classe parente, et toute information additionnelle relative à la classe. La forme la plus simple est :
class MyClass expands MyParentClass;
Ici, je déclare une nouvelle classe nommée "MyClass", qui hérite les fonctionnalités de "MyParentClass". Additionnellement, la classe se trouve dans le "package" nommé "MyPackage".
Chaque classe hérite de toutes les variables, fonctions et états de la classe parente. On peut ensuite lui ajouter de nouvelles déclarations de variables, ajouter de nouvelles fonctions (où "passer outre" (override) des fonctions existantes), ajouter de nouveaux états (où ajouter des fonctionnalités à des états existants)
L'approche typique de la conception d'une classe dans UnrealScript, est de fabriquer une nouvelle classe (par exemple un monstre Minotaure) qui est un extension d'une classe déjà existante comportant la plupart des fonctionnalités dont vous avez besoin (par exemple la clase "Pawn", la classe de base des monstres) Avec une telle approche, vous n'aurez jamais à 'réinventer la roue'- il vous suffira d'ajouter les nouvelles fonctionnalités que vous voulez adapter, tout en conservant les fonctionnalités existantes que vous n'avez pas à modifier. Cette approche est spécialement puissante pour implémenter l'IA dans Unreal, car les 'built-in' du système d'IA fournissent un nombre fantastique de fonctionnalités de base que vous pouvez utiliser comme des blocs de constructions pour adapter votre créature à vos besoins.
La déclaration de classe, peut prendre différents spécificateurs qui affectent cette classe.
Voici quelques exemples de déclarations de variables de circonstances dans UnrealScript
var int a; // Déclare un entier nommé "a" var byte Table[64]; // Déclare une zone de 64 octets nommée "Table". var string[32] PlayerName; // Déclare une chaîne de 32 caractères max. var actor Other; // Déclare une variable référençant un acteur
Les variables peuvent apparaître à deux endroits dans UnrealSript : variables de circonstances qui s'appliquent à un objet entier, elles apparaissent immédiatement après la déclaration de la classe. Les variables locales apparaissent au sein d'une fonction, et ne sont actives que durant l'exécution de cette fonction. Les variables de circonstance sont déclarées avec le mot-clé "var". Les variables locales le sont avec le mot-clé "local".
Voici les types de variables élémentaires supportées dans UnrealScript :
Les variables peuvent aussi contenir des références additionnelles telles "const" qui décrira plus tard la variable. Actuellement, il existe un grand nombre de concepteurs qui souhaitent qu'UnrealScript supporte d'une manière native les concepts spécifiques de nombreux jeux - et environnements.
Les "Arrays" (zones) sont déclarées en utilisant la séquence suivante :
var int MyArray[20]; // Déclare une zone de 20 entier
UnrealScript supporte les zones à une seule dimension, mais vous pouvez simuler des zones multidimensionnelles en traitant mathématiquement vous-même les lignes et les colonnes.
Dans Unreal, vous pouvez créer une variable de circonstance "éditable", ainsi les utilisateurs pourront éditer la valeur de la variable dans UnrealEd. Ce mécanisme permet de dialoguer avec toutes les propriétés des acteurs à l'aide d'UnrealEd : tout ce que vous y voyez sont simplement des variables d'UnrealScript, qui ont été déclaré éditables.
La syntaxe pour déclarer une variable éditable ressemble à cela :
var() int MyInteger; // Déclare un entier éditable dans la catégorie par défaut. var(MyCategory) bool MyBool; // Déclare un entier éditable dans "MyCategory".
Variables de référence pour Objets et Acteurs
Vous pouvez déclarer une variable qui réfère un acteur ou un objet comme cela :
var actor A; // référence un acteur. var pawn P; // référence a un acteur dans la classe Pawn. var texture T; // Une référence a un objet texture
La variable "P" ci-dessus fait référence à un acteur dans la classe "Pawn". Une telle variable peu référer tout acteur appartenant à la sous-classe Pawn. Par exemple, P peut référer un Brute, un Skaarj ou une Manta. Il peut aire référence à n'importe quel type de Pawn. Par contre, P ne fera jamais référence à un acteur Trigger (du fait que Trigger n'est pas une sous-classe de Pawn)
Un exemple ou il est pratique d'avoir une variable référençant un acteur est la variable "Enemy" dans la classe Pawn, qui réfère les acteurs que la créature essaye d'attaquer.
Quand vous avez une variable qui fait référence à un acteur, vous pouvez accéder aux variables de cet acteur et appeler ses fonctions. Par exemple :
// Déclare deux variables qui font référence aux pawns.
var pawn P, Q;
// Voici une fonction qui mettra en jeu P.
// Elle affiche quelques informations relatives à P.
function MyFunction()
{
// Initialise les P des ennemis à Q.
P.Enemy = Q;
// Demande à P de lancer son animation (courir).
P.PlayRunning();
}
Les variables qui font référence aux acteurs doivent toujours, soit faire référence à un acteur valide (tout acteur existant actuellement dans le niveau), soit contenir la valeur "None". "None" est équivalent au pointeur "NULL" du C/C++. Cependant, dans UnrealScript, il est sain d'accéder les variables et d'appeler les fonctions avec une référence "None"; le résultat est toujours zéro.
Notez qu'une référence à un objet ou un acteur "pointe vers" un autre acteur ou objet, il ne "contient" pas un acteur ou un objet. L'équivalent C d'une référence à un acteur est un pointeur vers un objet dans la classe AActor (en C, vous diriez un AActor*) Par exemple, vous pourriez avoir deux monstres dans le monde, Ludo et Fred qui sont en train de se battre. La variable "Enemy" de Ludo pourrait "pointer vers" Fred, et la variable "Enemy" de Fred pourrait "pointer vers" Ludo.
A l'inverse des pointeurs C, les références aux objets dans UnrealScript sont toujours surs et infaillibles. Il est impossible pour une référence d'objet de référencer un objet qui n'existe pas ou qui soit invalide (sauf dans le cas spécial de la valeur "None"). Dans UnrealScript, quand un acteur ou un objet est détruit, toutes les références s'y rapportant sont automatiquement initialisées à "None".
Dans Unreal, les classes sont des objets de la même manière que les acteurs, les textures, et les sons sont des objets. L'objet Class appartient à la classe nommée "class". Maintenant, il y aura souvent des cas ou vous voudrez ranger une référence dans une classe d'objet, ainsi vous pourrez faire apparaître un acteur appartenant à cette classe (sans connaître de quelle classe il s'agit au moment de la compilation) Par exemple :
var() class C; var actor A; A = Spawn( C ); // Fait apparaître un acteur appartenant à une classe quelconque C.
Maintenant, assurez-vous de ne pas confondre le rôle de la classe C, et un objet O appartenant à cette classe. Pour donner une analogie marquante, une classe ressemble à un moulin à poivre, et un objet est représenté par le poivre. Vous pouvez utiliser le moulin à poivre (la classe) pour créer du poivre (objet de la classe) en tournant la manivelle (en appelant la fonction "Spawn")... Mais un moulin à poivre (une classe) n'est pas du poivre (un objet appartenant à la classe), aussi vous n'ESSAYEREZ PAS DE LE MANGER!
Quand vous déclarez des variables qui référencent les objets d'une classe, vous pouvez optionnellement utiliser une syntaxe spéciale <classlimitor> pour la classe, afin de limiter la variable aux seules références à la classe qui étend une sous classe donnée. Par exemple, dans la déclaration:
var class<actor> ActorClass;
La variable ActorClass ne peut référencer une classe qui étend la classe "actor". Ceci est utile pour améliorer les contrôles au moment de la compilation. Par exemple, la fonction Spawn se sert d'une classe comme paramètre, mais n'a de sens que lorsque la classe donnée est une sous-classe "d'Actor", et la syntaxe de classe <classlimitor> permet au compilateur de renforcer cette condition obligatoire.
De la même façon, pour le lancement dynamique d'objet, vous pouvez lancer dynamiquement les classes de la manière suivante :
class<actor>( SomeFunctionCall() )
Les énumérations sont dans UnrealScript, une manière pratique de déclarer des variables qui peuvent contenir "un parmi" une collection de mots-clés. Par exemple, la classe "actor" contient les énumérations EPhysics qui décrivent les données physiques qu'Unreal peut appliquer à l'acteur. Celui-ci peut prendre l'une des valeurs prédéfinies comme PHYS_None (pas d'action), PHYS_Walking (marchant), PHYS_Falling (tombant), et ainsi de suite.
En interne, les énumérations sont stockées en tant que variables octet. Dans le design d'UnrealScript, les énumérations ne sont pas vus comme une nécessité, mais elles rendent le code beaucoup plus facile à lire. Il est plus facile de voir que la caractéristique physique d'un acteur est initialisé à "PHYS_Swimming" (nageant), plutôt (par exemple) à "3"
Voici un exemple de code qui déclare des énumérations.
// Déclare l'énumération EColor à trois valeurs.
enum EColor
{
CO_Red,
CO_Green,
CO_Blue
};
// Maintenant, déclaration de deux variables du type EColor.
var EColor ShirtColor, HatColor;
// D'une autre manière, vous pouvez déclarer des variables et
// des énumérations comme ceci:
var enum EFruit
{
FRUIT_Apple,
FRUIT_Orange,
FRUIT_Bannana
} FirstFruit, SecondFruit;
Dans le source d'Unreal, nous avons toujours déclaré les valeurs d'énumération comme LT_Steady, PHYS_Falling, et ainsi de suite, plutôt que "Steady" ou "Falling". Ceci est juste une manière de programmer, non une condition requise par le langage.
UnrealScript ne reconnaît que des étiquettes d'énumération non qualifiées (comme FRUIT_Apple) dans les classes où les énumérations sont définies et dans leurs sous-classes. Si vous avez besoin de référencer une étiquette d'énumération ailleurs, dans la hiérarchie de classe, vous devez la "qualifier":
FRUIT_Apple // Si Unreal ne peut trouver cette étiquette de numération EFruit.FRUIT_Apple // Alors, qualifiez là comme ceci.
Une structure UnrealScript est une manière d'entasser un tas de variables dans une sorte de super variable appelée "struct" (structure) Les structures UnrealSript sont comme celles du C, en cela, elles peuvent contenir de simples variables ou zones (arrays).
Vous pouvez déclarer une structure de la manière suivante:
// Un point ou un vecteur directionnel dans l'espace 3D
struct Vector
{
var float X;
var float Y;
var float Z
};
Une fois que vous avez déclaré une structure, vous êtes prêt à déclarer les variables spécifiques de ce type de structure.
// Déclare a ensemble de variables de type Vector. var Vector Position; var Vector Destination;
Pour accéder un composant d'une structure, utilisez le code de la manière suivante:
function MyFunction()
{
Local Vector A, B, C;
// Ajoute des vecteurs
C = A + B;
// Ajoute juste le composant X de ces vecteurs.
C.X = A.X + B.X;
// Passe le vecteur C à une fonction.
SomeFunction( C );
// Passe certains comosants du vecteur à une fonction.
OtherFunction( A.X, C.Z );
}
Vous pouvez faire avec les variables d'une Structure tout ce que vous pouvez faire avec les autres variables: vous pouvez leur assigner des variables, vous pouvez les passer à une fonction, et vous pouvez accéder à leurs composants.
Il y a de nombreuses structures définies dans la classe "Object" qui sont utilisées tout au long d'Unreal. Vous devez vous familiariser avec les opérations les concernant, car elles sont fondamentales dans la construction des blocs de scripts.
Dans UnrealScript, vous pouvez spécifier des valeurs de constantes de n'importe quel type de données :
Vous pouvez utiliser le mot clé "const" pour déclarer des constantes que vous référencerez plus tard par un nom. Par exemple :
const LargeNumber=123456; const PI=3.14159; const MyName="Tim"; const Northeast=Vect(1.0,1.0,0.0);
Les constantes peuvent être définies à l'intérieur de classes ou de structures.
Pour accéder une constante qui était déclaré dans une autre classe, utilisez la syntaxe "classname.constname", par exemple:
Pawn.LargeNumber
Pour assigner une valeur à une variable, utiliser "=" comme ceci:
function Test()
{
local int i;
local string[80] s;
local vector v, q;
i = 10; // Assigne une valeur à la variable entière i.
s = "Hello!"; // Assigne une valeur à la variable chaîne s.
v = q; // Copie la valeur du vecteur q vers v.
}
Dans UnrealScript, quand une fonction ou une expression nécessite un certain type de données (par exemple, un flottant), et que vous spécifiez un type de données différente (par exemple un entier), le compilateur essayera de convertir la valeur que vous lui avez fourni dans le type qui lui était propre. Les conversions entre les données numériques (octet, entier et flottant) se passent automatiquement, sans action de votre part.
UnrealScript est également capable de manier d'autres 'built-in' de data vers d'autres types, si vous les convertissez explicitement dans le code. La syntaxe pour cela est la suivante:
function Test()
{
local int i;
local string[80] s;
local vector v, q;
local rotation r;
s = string(i); // Convertit l'entier i en une chaîne et l'assigne à s.
s = string(v); // Convertit le vecteur v en chaîne et l'assigne à s.
v = q + vector(r); // Convertit la rotation r en un vecteur et lui ajoute q.
}
Voici le jeu complet des conversions non automatiques que vous pouvez utiliser dans UnrealScript:
De la même manière que les fonctions de conversion ci-dessus, qui font des conversions entre type de données, dans UnrealScript, vous pouvez convertir des références d'objets et d'acteurs entre différents types. Par exemple, tous les acteurs ont une variable nommée "Target", qui référence un autre acteur. Disons que vous êtes entrain d'écrire un script dans lequel vous désirez contrôler et voir si votre "Target" (cible) appartient à la classe d'acteur "Pawn", et vous voulez faire quelque chose de spécial, qui n'a un sens que dans la cas ou la cible est un Pawn -- par exemple, vous désirez appeler une des fonctions relative aux Pawn. La caste opérateur acteur vous permet cela. Voici un exemple.
var actor Target;
//...
function TestActorConversions()
{
local Pawn P;
// Voir si ma cible est un Pawn.
P = Pawn(Target);
if( P != None )
{
// La cible st un Pawn, aussi initialisons Enemy à Self.
P.Enemy = Self;
}
else
{
// La cible n'est pas un pawn.
}
}
Pour réaliser une conversion d'acteur, écrivez le nom de classe suivi par l'expression de l'acteur dont vous désirez la conversion, entre parenthèses. Une telle conversion peut réussir ou échouer en fonction de la 'sensibilité' de la conversion. Dans l'exemple ci-dessus, si votre cible (Target) référence un objet "Trigger" plutôt qu'un Pawn, l'expression Pawn(Target) retournera "None", puisq'un "Trigger" ne peut être convertit en Pawn. Cependant, si votre cible référence un objet "Brute", la conversion réussira et retournera Brute, puisque Brute est une sous-classe de Pawn.
Ainsi, les conversions d'acteur ont deux propos: D'abord, vous pouvez les utiliser pour vous assurer qu'une certaine référence d'acteur appartient à une classe. Ensuite, vous pouvez les utiliser pour convertir une référence d'acteur d'une classe vers une autre classe plus spécifique. Notez que ces conversions n'affectent pas du tout les acteurs dont vous faites la conversion -- ils permettent juste à UnrealSript de traiter la référence d'acteur comme s'il s'agissait d'un type plus spécifique.
Un autre exemple de conversions est contenu dans le script "Inventory". Chaque acteur de l'inventaire appartient a Pawn, même si la variable Owner peut référencer n'importe quel acteur. Ainsi, un thème commun dans le code d'Inventory est d'attribuer Owner à un Pawn, par exemple.
// Appelé par le moteur quand il y a destruction.
function Destroyed()
{
// Enlever de l'inventaire du propriétaire.
if( Pawn(Owner)!=None )
Pawn(Owner).DeleteInventory( Self );
}
Dans UnrealScript, vous pouvez déclarer de nouvelles fonctions et écrire de nouvelles versions de fonctions existantes. Les fonctions peuvent avoir un ou plusieurs paramètres (de n'importe quelles variables qu'UnrealScript supporte), et peut, optionnellement retourner une valeur. Ainsi, la plupart des fonctions étant écrites directement en UnrealScript, vous pouvez également écrire des fonctions qui peuvent être appelées à partir d'UnrealScript, mais qui sont implémentées en C++ et résident en DLL. La technologie d'UnrealScript supporte toutes les combinaisons possibles de fonctions appelantes. Le moteur C++ peut appeler des fonctions scripts, des scripts peuvent appeler des fonctions C++, et des scripts peuvent appeler des scripts.
Voici une déclaration simple de fonction. Cette fonction prend un vecteur comme paramètre d'entrée, et retourne un nombre en virgule flottante.
// Fonction pour calculer la taille d'un vecteur.
function float VectorSize( vector V )
{
return sqrt( V.X * V.X + V.Y * V.Y + V.Z * V.Z );
}
Le mot "function" précède toujours une déclaration de fonction. Il est suivi par le type de retour optionnel de la fonction (dans ce cas "float"), puis le nom de la fonction, enfin la liste des paramètres de la fonction inclus dans des parenthèses.
Quand une fonction est appelée, le code entre accolades est exécuté. A l'intérieur de la fonction, vous pouvez déclarer des variables locales (en utilisant le mot clé "local"), et exécuter n'importe quel code UnrealScript. Le mot clé optionnel "return", indique que la fonction retournera une valeur immédiatement.
Vous pouvez passer tout type de variable UnrealScript (incluant les zones (arrays)), et une fonction peut retourner tout type de variable.
Par défaut, toute variable locale que vous déclarez est initialisé à zéro.
Les appels de fonctions peuvent être récursifs. Par exemple, la fonction suivante calcule le factoriel d'un nombre.
// Fonction pour calculer le factoriel d'un nombre.
function int Factorial( int Number )
{
if( Number <= 0 )
return 1;
else
return Number * Factorial( Number – 1 );
}
Quelques fonctions UnrealScript sont appelées par le moteur, quand certains évènements se produisent. Par exemple quand un acteur est touché par un autre acteur, le moteur appelle sa fonction "Touch" pour lui indiquer qui le touche. En écrivant une fonction utilisateur "Touch", vous pouvez faire une action spéciale résultant du fait que l'acteur a été touché.
// Appelé quand quelque chose touche cet acteur.
function Touch( actor Other )
{
Log( "I was touched!")
Other.Message( "You touched me!" );
}
La fonction ci-dessus illustre plusieurs choses. La première de toute, la fonction écrit un message dans le fichier log en utilisant la commande "Log" (qui est l'équivalent de la commande "print" du Basic et "printf" du C). Ensuite, il appelle la fonction "Message" résidante dans l'acteur "Other" (autre). Appeler des fonctions 'dans d'autres acteurs' est une action commune dans UnrealScript, et dans les langages orientés objet comme Java en général, car cela fournit une manière simple aux acteurs de communiquer entre eux.
Quand vous appelez normalement une fonction, UnrealScript fait une copie locale des paramètres que vous passez à la fonction. Si la fonction modifie quelques uns de ces paramètres, ceux-ci n'ont aucun effet sur les variables que vous avez passées. Par exemple, le programme suivant :
function int DoSomething( int x )
{
x = x * 2;
return x;
}
function int DoSomethingElse()
{
local int a, b;
a = 2;
log( "La valeur de a est " $ a );
b = DoSomething( a );
log( "La valeur de a est " $ a );
log( "La valeur de b est " $ b );
}
Produit les valeurs en sortie, quand DoSomethinElse est appelé:
La valeur de a est 2 La valeur de a est 2 La valeur de b est 4
En d'autres termes, la fonction DoSomethingElse était utilisée avec une copie locale de "a" qui lui a été passée, et n'est pas affecté par la variable réelle "a".
La sortie spécifiée vous permet de dire à une fonction qu'elle peut modifier la variable qui est passée, plutôt que faire une copie locale. Ceci est utile, par exemple, si vous avez une fonction qui nécessite de retourner plusieurs valeurs vers l'appelant. Vous pouvez vouloir que l'appelant passe plusieurs variables à la fonction qui ne sont que des valeurs "out". Par exemple:
// Calcule les composants minimum et maximum d'un vecteur.
function VectorRange( vector V, out float Min, out float Max )
{
// Clacul de la valeur minimum.
if ( V.X<V.Y && V.X<V.Z ) Min = V.X;
else if( V.Y<V.Z ) Min = V.Y;
else Min = V.Z;
// Calcul de la valeur maxumum.
if ( V.X>V.Y && V.X>V.Z ) Max = V.X;
else if( V.Y>V.Z ) Max = V.Y;
else Max = V.Z;
}
Sans le mot clé "out", il serait pénible d'essayer d'écrire une fonction qui devrait retourner plus d'une valeur.
Avec le mot-clé "optional", vous pouvez faire que certaines fonctions aient des paramètres optionnels, à la convenance de l'appelant. Pour les fonctions UnrealScript, les paramètres optionnels qui ne sont pas spécifiées par l'appelant, sont initialisées à zéro. Pour les fonctions natives, les valeurs par défaut des paramètres optionnels dépendent de la fonction. Par exemple, la fonction Spawn prend un emplacement optionnel et une rotation, qui correspondent aux location et rotation par défaut de l'acteur à faire apparaître.
Le mot clé "coerce" force les paramètres de l'appelant a être convertis dans le type spécifié (même si UnrealScript ne faisait normalement pas la conversion automatiquement). Ceci est utile pour les fonctions qui traitent des chaînes de caractères, de telle manière que les paramètres soient automatiquement convertis en chaînes pour vous.
Le fait d'outrepasser des fonctions, s'adresse à l'écriture d'une nouvelle version d'une fonction dans une sous-classe. Par exemple, disons que vous écrivez un script pour une nouvelle sorte de monstre appelé Demon. La classe Demon, que vous venez de créer, étend la classe Pawn (créature). Maintenant, quand une créature voit un joueur pour la première fois, la fonction "SeePlayer" est appelée, de telle façon que la créature puisse commencer à attaquer le joueur. C'est un bon concept, mais disons que vous voulez traiter la fonction "SeePlayer" différemment pour votre classe Demon. Comment faire cela? Outrepasser la fonction est la réponse.
Pour outrepasser une fonction, coupez et collez la définition de la classe parente dans votre nouvelle classe. Par exemple, pour "SeePlayer", vous pourriez ajouter ceci à votre classe de Demon:
// Nouvelle version de la fonction Touch pour la classe Demon.
function SeePlayer( actor SeenPlayer )
{
log( "Le démon voit un joueur" );
// Ajoutez les nouvelles fonctionnalités utilisateur, ici…
}
Outrepasser des fonctions est la clé pour créer de nouvelles classes efficaces dans UnrealScript. Vous pouvez créer une nouvelle classe qui étend une classe déjà existante. Ensuite, tout ce que vous avez à faire est d'outrepasser les fonctions que vous voulez manipuler différemment. Ceci vous permet de créer de nouveaux types d'objets sans écrire un code gigantesque.
Plusieurs fonctions, dans UnrealScript sont déclarées "final". Le mot clé "final" (qui apparaît immédiatement avant le mot "function") indique que cette fonction ne peut être outrepassée par les classes filles. Ceci peut être utilisé pour des fonctions dont vous savez que personne ne voudra les outrepasser, parce que cela produit un code de script plus rapide. Par exemple, disons que vous avez une fonction "VectorSize" qui calcule la taille d'un vecteur. Il n'y a absolument aucune raison pour que quelqu'un outrepasse cette fonction, aussi déclarez là "final". D'un autre coté, une fonction comme "Touch" est extrêmement dépendante du contexte et ne sera pas déclarée "final".
Static: Une fonction "static" agit comme une fonction globale du C, dans la mesure où elle peut être appelée sans avoir une référence à un objet de la classe. Les fonctions statiques peuvent appeler d'autres fonctions statiques, et peuvent accéder aux valeurs par défaut des variables. Les fonctions statiques ne peuvent pas appeler de fonctions non-statiques et ne peuvent accéder aux variables d'instance (puisqu'elles ne sont pas exécutées en respectant l'instance d'un objet) A la différence d'un langage comme C++, les fonctions statiques sont virtuelles et ne peuvent outrepasser une classe fille. Ceci est utile dans le cas ou vous désirez appeler une fonction statique dans une classe variable (une classe non connue au moment de la compilation, mais référencée par une variable ou une expression)
Singular: Le mot clé "singular", qui apparaît immédiatement avant la déclaration de fonction, empêche une fonction de s'appeler récursivement. La règle est celle-ci: Si un certain acteur, est déjà dans le milieu d'une fonction singulière, tous les appels subséquents aux fonctions singulières seront sautées. Ceci est utile pour éviter des erreurs de processus récursifs infinis, dans certains cas. Par exemple, si vous essayez de déplacer un acteur à l'intérieur de votre fonction "Bump", il y a de fortes chances que cet acteur cognera contre un autre acteur durant son mouvement, entraînant un nouvel appel à la fonction "Bump", et ainsi de suite. Vous devez être attentif à éviter une telle situation, mais si vous ne pouvez écrire un code sûr, qui vous assure d'éviter de telles situations potentiellement récursives, utilisez le mot clé "singular".
Native: Vous pouvez déclarer des fonctions UnrealSript en tant que "Native", ce qui signifie que la fonction est appelable à partie d'UnrealScript, mais est actuellement écrite (quelque part) en C++. Par exemple, la classe Actor contient un tas de définitions de fonctions natives, telles que :
native(266) final function bool Move( vector Delta );
Le nombre entre parenthèses après le mot clé "native" correspond au nombre de la fonction tel qu'il est déclaré en C++ (en utilisant la macro AUTOREGISTER_NATIVE). La fonction native doit résider dans une DLL nommée identiquement au package de la classe contenant la définition UnrealScript.
Latent: Déclare qu'une fonction native est latente, c'est à dire qu'elle peut être seulement appelée à partir d'un code d'état, et que l'on doit en revenir après quelques temps de jeu.
Iterator: Déclare qu'une fonction native est un compteur itératif, qui peut être utilisé pour boucler sur une liste d'acteurs en utilisant la commande "foreach" (pour chaque)
Simulated: Déclare qu'une fonction peut s'exécuter du coté client quand un acteur est soit sur un proxy simulé ou un proxy autonome. Toutes fonctions à la fois "native" et "final" sont également automatiquement simulées.
Operator, PreOperator, PostOperator: Ces mots clés servent à déclarer une sorte de fonction spéciale appelée opérateur (équivalent aux opérateurs du C++). C'est la manière dont UnrealSript s'y retrouve, entre toutes les "built-in" opérateurs comme "+", "-", "==", et "||". Je ne vais pas rentrer dans ce document, dans les détails de la manière dont les opérateurs marchent, mais, le concept d'opérateurs est similaire au C++, et vous pouvez déclarer de nouvelle fonctions opérateurs et de nouveaux mots clés en tant que fonction UnrealScript ou fonctions natives.
Event: Le mot clé "event" a la même signification que la "function" dans UnrealScript. Cependant quand vous 'exportez' un fichier en-tête C++ à partir d'Unreal en utilisant "unreal -make -h", UnrealEd, génère automatiquement un C++-> souche d'appel UnrealScript pour chaque "event" (évènement) Ceci est un remplacement plus propre que les anciennes structures "PMessageParms", car cela conserve automatiquement le code C++ synchronisé avec les fonctions UnrealScript et élimine la possibilité de passer des paramètres invalides à une fonction UnrealScript. Par exemple, ce bout de code UnrealScript :
event Touch( Actor Other )
{ ... }
Génère cette partie de code dans EngineClasses.h:
void Touch(class AActor* Other)
{
FName N("Touch",FNAME_Intrinsic);
struct {class AActor* Other; } Parms;
Parms.Other=Other;
ProcessEvent(N,&Parms);
}
Ceci vous permet d'appeler la fonction UnrealScript à partir du code en C++ comme ceci :
AActor *SomeActor, *OtherActor; Actor->Touch(OtherActor);
UnrealScript supporte toutes les opérations de contrôle de déroulement du C/C++/Java
Les boucles "For", vous permettent de 'tourner' à travers une boucle, tant qu'une certaine condition est vérifiée. Par exemple:
// Exemple d'une boucle "for".
function ForExample()
{
local int i;
log( "Démonstration de la boucle for" );
for( i=0; i<4; i++ )
{
log( "la valeur de i est " $ i );
}
log( "Terminé avec i=" $ i);
}
L'édition de cette boucle est :T
Demonstration de la boucle for la valeur de i est 0 la valeur de i est 0 la valeur de i est 0 la valeur de i est 0 Terminé avec i=4
Dans une boucle "for", vous devez spécifier trois expressions séparées par des points-virgules. La première expression sert à initialiser une variable à sa valeur de départ. La seconde donne la condition qui est contrôlée avant chaque itération de l'exécution de la boucle; si cette expression est vérifiée, la boucle s'exécute. Si elle est fausse, la boucle se termine. La troisième expression permet d'incrémenter le compteur de boucle.
Pensez que beaucoup d'expressions de boucles 'for' ne font que mettre à jour un compteur, vous pouvez également utiliser les boucles 'for' pour faire des choses plus 'avancées', comme parcourir des listes liées, en utilisant l'initialisation appropriée, la terminaison, et l'expression d'incrémentation.
Dans toutes les opérations de contrôle de déroulement de programme, vous pouvez aussi exécuter une opération simple, sans accolades, comme ceci :
for( i=0; i<4; i++ ) log( "The value of i is " $ i );
Ou, vous pouvez exécuter de multiples opérations, encadrées par des accolades, comme ceci.
for( i=0; i<4; i++ )
{
log( "The value of i is" );
log( i );
}
Les boucles "Do"-"Until" vous permettent de tourner à travers une boucle tant qu'une expression est vraie. Notez que la syntaxe "Do"-"Until" de Unreal est différente de celle de C/Java (qui utilisent do-while)
// Exemple de boucle "do".
function DoExample()
{
local int i;
log( "Demonstrating the do loop" );
do
{
log( "The value of i is " $ i );
i = i + 1;
} until( i == 4 );
log( "Completed with i=" $ i);
}
L'édition faite par cette boucle est :
Demonstrating the do loop The value of i is 0 The value of i is 1 The value of i is 2 The value of i is 3 Completed with i=4
Les boucles "While" permettent de faire tourner une boucle "tant" qu'une expression donnée au départ est vraie.
// Exemple de boucle "while".
function WhileExample()
{
local int i;
log( "Demonstrating the while loop" );
while( i < 4 )
{
log( "The value of i is " $ i );
i = i + 1;
}
log( "Completed with i=" $ i);
}
L'édition faite par cette boucle est :
Demonstrating the do loop The value of i is 0 The value of i is 1 The value of i is 2 The value of i is 3 Completed with i=4
La commande Break fait sortir de la boucle la plus proche ("For", "Do", ou "While").
// Exemple de boucle "while".
function WhileExample()
{
local int i;
log( "Demonstrating break" );
for( i=0; i<10; i++ )
{
if( i == 3 )
break;
log( "The value of i is " $ i );
}
log( "Completed with i=" $ i );
}
L'édition faite par cette boucle est :
Demonstrating break The value of i is 0 The value of i is 1 The value of i is 2 Completed with i=3
La commande "Goto" branche sur un label quelque part dans la fonction ou l'état courant.
// Exemple de "goto".
function GotoExample()
{
log( "Starting GotoExample" );
goto Hither;
Yon:
log( "At Yon" );
goto Elsewhere;
Hither:
log( "At Hither" );
goto Yon;
Elsewhere:
log( "At Elsewhere" );
}
L'édition faite par ce programme est :
Starting GotoExample At Hither At Yon At Elsewhere
"If", "Else If", et "Else" vous permettent d'exécuter un code si certaines conditions sont vérifiées.
// Exemple d'un simple "if".
if( LightBrightness < 20 )
log( "My light is dim" );
// Exemple de "if-else".
if( LightBrightness < 20 )
log( "My light is dim" );
else
log( "My light is bright" );
// Exemple de "if-else if-else".
if( LightBrightness < 20 )
log( ‘My light is dim" );
else if( LightBrightness < 40 )
log( "My light is medium" );
else if( LightBrightness < 60 )
log( "My light is kinda bright" );
else
log( "My light is very bright" );
// Exemple de "if" avec parenthèses .
if( LightType == LT_Steady )
{
log( "Light is steady" );
}
else
{
log( "Light is not steady" );
}
"Switch", "Case", "Default", et "Break" vous permettent de manipuler des listes conditionnelles aisément.
// Exemple de cas switch.
function TestSwitch()
{
// Exécute l'une des séquences ci-dessous en fonction
// de la valeur de LightType.
switch( LightType )
{
case LT_None:
log( "Il n'y a pas de lumière" );
break;
case LT_Steady:
log( "Il y a une lumière ténue" );
break;
case LT_Backdrop:
log( "Il y a une lumière d'arrière plan" );
break;
default:
log( "La lumière est dynamique" );
break;
}
}
Une déclaration "switch" est constituée d'un ou plusieurs déclaration "case" (cas), et d'une déclaration optionnelle "default". Après une déclaration "switch", on exécute le code qui correspond à la déclaration "case" si elle existe; sinon on exécute la séquence correspondant à la déclaration "default" si elle existe, sinon l'exécution se poursuit à la fin de la séquence "select".
Après l'écriture de votre code suivant un label "case", vous devez utiliser une déclaration "break" pour que l'exécution passe à la fin de la séquence "switch". Si vous n'utilisez pas de "break", l'exécution de poursuit pas le code du "case" suivant.
Historiquement, les programmeurs de jeux ont utilisé le concept d'états depuis les jeux développés après la phase "pong". Les états (et ce qui est connu sous le nom de "programmation machine des états") sont une façon naturelle de faire que le comportement d'objets complexes soit gérable. Cependant, avant UnrealScript, les états n'étaient pas supportés au niveau langage, nécessitant que les programmeurs créent des séquences "switch" en C/C++ pour traiter des états d'objets. Un tel code était difficile à écrire et à mettre à jour.
UnrealScript traite les états au niveau langage.
Dans UnrealScript, chaque acteur dans 'ce monde' se trouve dans un et un seul état. Son état correspond à l'action qu'il veut exécuter. Par exemple, les 'brushs' qui de déplacent (Ndt : entités géométrique du jeu du type porte ou plate-forme par exemple) ont plusieurs états comme 'StandOpenTimed' (actif si quelqu'un se tient dessus), et 'BumpOpenTimed' (actif si quelqu'un le touche) Les 'Pawns' ont plusieurs états tels que "Dying" (mourant), "Attacking" et "Wanderind" (rodant).
Dans UnrealScript, vous pouvez écrire des fonctions et du code qui sont utilisés dans un état particulier. Ces fonctions sont seulement appelées quand l'acteur se trouve dans cet état. Par exemple, disons que vous êtes entrain d'écrire un script de monstre, et vous êtes en train de vous demander comment manipuler la fonction "SeePlayer". Quand vous êtes en train de roder, vous désirez attaquer le joueur que vous voyez. Quand vous avez déjà attaqué le joueur, vous voulez continuer sans interruption.
La façon la plus facile de faire cela est de définir plusieurs états (Wanderind et Attacking), et d'écrire une version différente de "Touch" dans chaque état. UnrealScript le permet.
Avant de plonger plus profondément dans les 'états', vous devez savoir qu'il y a deux avantages majeurs aux 'états', et une complication.:
Voici un exemple d'état issu du script TriggerLight (dispositif d'éclairage)
// Le Trigger allume la lumière.
state() TriggerTurnsOn
{
function Trigger( actor Other, pawn EventInstigator )
{
Trigger = None;
Direction = 1.0;
Enable( 'Tick' );
}
}
// Le Trigger éteint la lumière
state() TriggerTurnsOff
{
function Trigger( actor Other, pawn EventInstigator )
{
Trigger = None;
Direction = -1.0;
Enable( 'Tick' );
}
}
Ici vous avez déclaré deux états différents (TriggerTurnsOn et TriggerTurnsOff), et vous écrivez une version différente pour chaque état de la fonction Trigger. Bien que vous puissiez traiter cette implémentation sans les états, l'utilisation des états rend le code plus modulaire et extensible; dans UnrealScript, vous pouvez facilement créer une sous-classe d'une classe existante, et ajouter de nouveaux états et de nouvelles fonctions. Si vous aviez essayé de faire cela sans utiliser les états, le code résultant serait plus difficile à étendre plus tard.
Un 'état' peut être déclaré éditable, ceci indique que l'utilisateur peut initialisé l'état d'un acteur à partir de UnrealEd, s'il le désire. Pour déclarer un état 'éditable' faites ceci:
state() MyState
{
//...
}
Pour déclarer un état non-éditable, faites ceci:
state MyState
{
//...
}
Vous pouvez également spécifier l'état automatique ou initial d'un acteur en utilisant le mot-clé "auto". Ceci permet à tout nouvel acteur d'être placé dans un certain état au moment ou il est activé.
auto state MyState
{
//...
}
En plus des fonctions, un état peut contenir un ou plusieurs labels suivi par du code UnrealScript. Par exemple:
auto state MyState
{
Begin:
Log( "MyState vient juste de commencer!" );
Sleep( 2.0 );
Log( "MyState à terminé de dormir" );
goto Begin;
}
Le code de l'état ci-dessus édite le message "MyState vient juste de commencer!", attend deux secondes, puis édite le message : "MyState à terminé de dormir". La chose intéressante dans cet exemple est l'appel à la fonction "Sleep": cette fonction ne se termine pas immédiatement, mais se termine après un certain nombre d'intervalles de temps machine. Les fonctions latentes peuvent être seulement appelées dans le code d'état, et non à partir du code des fonctions. Les fonctions latentes vous permettent de traiter des chaînes d'évènements complexes qui incluent des temps d'attente.
Tout code d'état commence par une définition de label; dans l'exemple ci-dessus le label est nommé "Begin". Le label fournit un point d'entrée facile dans le code des états. Vous pouvez utiliser n'importe quel nom de label dans un code d'état, mais le label "Begin" est spécial: il correspond au point de départ par défaut du code d'état.
Il y a trois fonctions latentes principales attachées à tout acteur.
La classe Pawn définit plusieurs fonctions latentes importantes pour les actions comme la navigation à travers 'le monde' et des mouvements à courts termes. Reportez-vous à la documentation séparée de l'IA, pour une description de leurs usages.
Trois fonctions natives de UnrealScript sont particulièrement utiles quand on écrit un code d'état:
Voici un exemple de concept d'état dont nous venons de parler.
// Ceci est l'état automatique à exécuter.
auto state Idle
{
// Quand touché par un autre acteur…
function Touch( actor Other )
{
log( "Je suis touché, aussi vais-je attaquer" );
GotoState( ‘Attacking’ );
Log( "Je vais dans l'état Attaque" );
}
Begin:
log( "Je n'ai rien à faire…" );
sleep( 10 );
goto ‘Begin’;
}
// Etat "Attaque".
state Attacking
{
Begin:
Log( "J'exécute le code d'état Attaque" );
//...
}
Quand vous exécutez ce programme puis vous touchez l'acteur, vous verrez :
Je n'ai rien à faire… Je n'ai rien à faire… Je n'ai rien à faire… Je suis touché, aussi vais-je attaquer Je vais dans l'état Attaque J'exécute le code d'état Attaque
Assurez-vous de bien comprendre cet important aspect de GotoState: Quand vous appelez GotoState à l'intérieur d'une fonction, il ne va pas à destination immédiatement, il y va une fois que l'exécution est revenue dans le code d'état.
Dans UnrealScript, quand vous créez une sous-classe d'une classe existante, votre nouvelle classe hérite de toutes les variables, fonctions et états de la classe parente. Ceci est bien compris.
Cependant, l'adjonction de la notion 'd'état' dans la programmation en UnrealScript des modèles, ajoute de nouveaux possibilités (qui peuvent compliquer la situation) à l'héritage et à l'étendue des règles. Les règles d'héritage sont :
Voici un exemple de règles de passage outre:
// Voici un exemple de classe parente.
class MyParentClass expands Actor;
// Une fonction non-état.
function MyInstanceFunction()
{
log( "Executing MyInstanceFunction" );
}
// Un état.
state MyState
{
// Une fonction état.
function MyStateFunction()
{
Log( "Exécution de MyStateFunction" );
}
// Le label "Begin".
Begin:
Log("Début de MyState");
}
// Voici un exemple de classe fille.
class MyChildClass expands MyParentClass;
// Ici, je passe outre une fonction non-état.
function MyInstanceFunction()
{
Log( "Execution de MyInstanceFunction dans une classe fille" );
}
// Ici je re-déclare MyState, aussi je peux outre-passer MyStateFunction.
state MyState
{
// Ici j'outre passe MyStateFunction.
function MyStateFunction()
{
Log( "Execution de MyStateFunction" );
}
// Ici j'outre passe le label"Begin".
Begin:
Log( "Début de MyState dans MyChildClass" );
}
Quand vous avez une fonction qui est globalement implémentée, dans un ou plusieurs états, et dans une ou plusieurs classes parentes, vous devez connaître la version de la fonction qui sera appelée dans un contexte donné. L'étendue des règles, qui solutionne ces situations complexes, est la suivante:
Si un état n'outre passe pas un état de même nom dans la classe parente, alors vous pouvez, optionnellement, utiliser le mot-clé "expands" pour faire que l'état étende un état existant dans la classe courante. Ceci est utile, par exemple, dans une situation ou vous avez un groupe d'états similaires ( tels que MeleeAttacking (attaque en mélée) et RangeAttacking (attaque en rang)) qui ont un lot de fonctionnalités communes. Dans ce cas, vous pouvez déclarer un état de base pour l'attaque, comme ce qui suit:
// Etat d'attaque de base
state Attacking
{
// Collez ici les fonctions de base...
}
// Attaque de près.
state MeleeAttacking expands Attacking
{
// Collez ici les fonctions spécialisées...
}
// Attaque à distance.
state RangeAttacking expands Attacking
{
// Collez ici les fonctions spécialisées...
}
Un état peut optionnellement utiliser le spécificateur "ignores", pour ignorer les fonctions à l'intérieur d'un état. La syntaxe est la suivante:
// Declaration d'un état.
state Retreating
{
// Ignore les fonctions suivantes...
ignores Touch, UnTouch, MyFunction;
// Collez ici les fonctions...
}
Vous pouvez indiquer dans quel état spécifique se trouve un acteur à partir de sa variable "state", un variable de type "name" (nom)
Il est possible pour un acteur d'être dans un "no state" (non état) en utilisant GotoState(''). Quand un acteur est dans un état "no state", seules ses fonctions globales (non état) sont appelées.
Si vous utilisez la commande GotoState pour initialiser l'état d'un acteur, le moteur peut appeler deux fonctions de notification spéciales, si vous les avez défini : EndState() et BeginState(). EndState() est appelé dans l'état courant, immédiatement avant que le nouvel état ne commence, et BeginState est appelé immédiatement après que le nouvel état ne commence. Ces fonctions procurent un endroit facile pour effectuer des initialisations spécifiques d'un état ou nettoyer ce qui doit l'être avant l'exécution d'un état.
UnrealScript fournit une large variété d'opérateurs du style C/C++/Java pour de telles opérations comme additionner des nombres, mise entre quotes, et incrémentation de variables. Le jeu complet d'opérateurs est défini dans Object.u, mais voici un récapitulatif. Vous avez ici les opérateurs standards, en ordre de préséance. Notez que les opérateurs de style C ont la même préséance qu'en C.
| Operateur | Types sur lequel ils s'appliquent | Signification |
| $ | string | Concaténation de chaînes |
| *= | byte, int, float, vector, rotation | Multiplie et assigne |
| /= | byte, int, float, vector, rotation | Divise et assigne |
| += | byte, int, float, vector | Ajoute et assigne |
| -= | byte, int, float, vector | Soustrait et assigne |
| || | bool | Ou logique |
| && | bool | Et logique |
| & | int | Et bit à bit |
| | | int | Ou bit à bit |
| ^ | int | Ou exclusif bit à bit |
| != | All | Compare pour une inégalité |
| == | All | Compare pour une égalité |
| < | byte, int, float, string | Plus petit que |
| > | byte, int, float, string | Plus grand que |
| <= | byte, int, float, string | Plus petit ou égal à |
| >= | byte, int, float, string | Plus grand ou égal à |
| ~= | float, string | Sensiblement égal (à prés de 0.0001), cas d'égalité insensitif. |
| << | int, vector | Décalage à gauche (entier), transformation vers l'avant d'un vecteur |
| >> | int, vector | Décalage à droite (entier), transformation vers l'arrière d'un vecteur |
| + | byte, int, float, vector | Addition |
| - | byte, int, float, vector | Soustraction |
| % | float | Modulo (reste après division) |
| * | byte, int, float, vector, rotation | Multiplication |
| / | byte, int, float, vector, rotation | Division |
| Dot | vector | Vector Dot product |
| Cross | vector | Vector cross product |
| ** | float | Exponentiation |
La table ci-dessus liste les opérateurs par ordre de préséance (avec les opérateurs de même préséance groupés ensembles). Quand vous écrivez une expression complexe comme "1*2+3*4", UnrealScript regroupe automatiquement les opérateurs par préséance. Du fait que la multiplication à une préséance plus haute que l'addition, l'expression est évaluée comme "(1*2)+(3*4)".
Les opérateurs "&&" (Et logique) et "||" (Ou logique) sont court-circuités : si le résultat de l'expression peut être déterminée seulement par la première expression (par exemple si le premier argument de && est faux), la seconde expression n'est pas évaluée.
En plus, UnrealScript supporte les opérateurs unaires suivants:
Fonctions sur les entiers:
Fonctions sur les flottants:
Les fonctions sur les chaînes des caractères d'Unreal sont différentes de celles du Basic:
Fonctions vectoriellesV:
La commande "foreach" de UnrealScript, permet de manipuler un grand groupe d'acteurs, par exemple, tous les acteurs d'un niveau , ou tous les acteurs à une certaine distance d'un autre acteur. "foreach" marche en liaison avec une autre sorte de fonction spéciale appelée une fonction "itérator" qui permet d'itérer une liste d'acteurs.
Voici un exemple simple de "foreach":
// Montrer une liste de toutes les lumières dans ce niveau.
function Something()
{
local actor A;
// Chercher tous les acteurs du niveau.
log( "Lumières:" );
foreach AllActors( class ‘Actor’, A )
{
if( A.LightType != LT_None )
log( A );
}
}
Le premier paramètre dans toute commande "foreach" est une constante de classe, qui spécifie le type d'acteurs à chercher. Vous pouvez utiliser ceci pour limiter la recherche, aux seuls Pawns par exemple.
Le second paramètre, dans toute command "foreach" est une variable qui est assignée à un acteur pour chaque itération tout au long de la boucle "foreach".
Voici toutes les fonctions d'itération qui fonctionnent avec "foreach":
Dans des phases de programmation complexe, vous devez souvent faire appel à une version spécifique d'une fonction, plutôt qu'a celle de portée courante. Pour traiter ces cas, UnrealScript fourni les mots clés suivants:
Il n'est pas valide de combiner des spécificateurs d'appel (par exemple Super(Actor).Global.Touch)
Voici quelques exemple de spécificateurs d'appels:
class MyClass expands Pawn;
function MyExample( actor Other )
{
Super(Pawn).Touch( Other );
Global.Touch( Other );
Super.Touch( Other );
}
A titre d'exemple supplémentaire, la fonction BeginPlay() est appelée quand un acteur est sur le point d'entrer dans le jeu. La fonction BeginPlay() est implémentée dans la classe Actor et contient quelques fonctionnalités importantes qui doivent être exécutées. Maintenant, disons que vous voulez outre passer BeginPlay() dans votre nouvelle classe MyClass, pour ajouter quelques fonctionnalités nouvelles. Pour faire cela proprement, vous devez appeler la version BeginPlay() dans la classe mère.
class MyClass expands Pawn;
function BeginPlay()
{
// Appel la version de BeginPlay dans la classe mère(important).
Super.BeginPlay();
// Maintenant faites votre implémentation.
//...
}
UnrealEd permet aux concepteurs de niveaux d'éditer les variables par "défaut" de l'objet d'une classe. Quand un nouvel acteur est introduit dans le jeu ("spawné"), toutes ses variables sont initialisées à ces valeurs par défaut. Quelques fois, il est utile de réinitialiser une variable à sa valeur par défaut. Par exemple quand un joueur de débarrasse d'un objet de son inventaire, le code de l'inventaire doit comporter la réinitialisation de quelques valeurs par défaut, pour cet acteur. Dans UnrealScript, vous pouvez accéder les valeurs par défaut des variables d'un classe en utilisant le mot-clé "Default". Par exemple:
var() float Health, Stamina;
//...
// Réinitialisation de quelques variables à leur valeur par défaut.
function ResetToDefaults()
{
// Réinitialisation health, et stamina.
Health = Default.Health;
Stamina = Default.Stamina;
}
Si vous avez une classe référence (une variable de classe ou de type "class<classlimitor>), vous pouvez accéder aux propriétés par défaut de la classe de référence, sans avoir un objet de cette classe. Cette syntaxe fonctionne avec toute expression qui évalue un type de classe.
var class C; var class<Pawn> PC; // Accède à la valeur par défaut de LightBrightness dans la classe Spotlight . Health = class'Spotlight'.default.LightBrightness; // Accède la valeur par défaut de Health dans une classe variable identifiée ar PC. Health = PC.default.Health; Accéde la valeur par défaut de Health dans une expression de classe (casted class) Health = class<Pawn>(C).default.Health; //
Les fonctions statiques dans une classe variable, peuvent être appelées en utilisant la syntaxe suivante:
var class C; var class<Pawn> PC; // appelle une fonction statique dans une classe spécifique class'SkaarjTrooper'.static.SomeFunction(); // appelle une fonction statique dans une classe variable PC.static.SomeFunction(); // // appelle une fonction statique dans une expression de calss (casted class) class<Pawn>(C).static.SomeFunction();
UnrealScript est conçu de telle manière que les classes, dans un package de fichiers, puisse évoluer sans que la compatibilité binaire soit interrompue. Ici, par compatibilité binaire, on entend "dépendant des fichiers binaires qui peuvent être chargés et passés à l'éditeur de liens sans erreur"; quel que soit le code des fonctions que vous avez modifié dans différentes sessions. Spécifiquement, les différentes modifications qui doivent être faites sans erreurs, sont:
D'autres transformations sont en générale non sans risques, incluant (mais pas limité à)
Collecte de résidus : Tous les objets et acteurs dans Unreal voient leurs résidus collectés (garbage-collected) dans un arbre de collecte similaire à celui de la machine virtuelle de Java. Le collecteur de résidus d'Unreal utilise la fonctionnalité de sérialisation de la classe UObject, pour résoudre quels objets sont référencés par l'objet actif. Pour cette raison, un objet ne doit pas être détruit explicitement, car le collecteur de résidus peut éventuellement les dénicher quand ils ne sont plus référencés. Cette approche possède en effet de bord, des destructions latentes d'un objet non référencé, mais est cependant plus efficace qu'un compte de référence de destruction non fréquentes.
Intégration des Composants de Modèles d'Objets (COM) dans Unreal : La classe de base UObject d'Unreal, dérive de IUnknown pour anticiper le fait qu'Unreal puisse inter-opérer avec les COM sans avoir besoin de modifier ses objets. Cependant Unreal n'intègre pas les COM actuellement, et les avantages de les intégrer n'est pas très clair, aussi le projet est actuellement gelé.
UnrealScript est basé sur un code octal: le code UnrealScript est compilé en une série de codes en octal similaires aux p-codes où codes en octal de Java. Ceci permet à UnrealScript d'être neutre vis à vis des plate-formes de développement; ceci permet de porter les composants du client et du serveur sur les autres plate-formes, c'est à dire Macintosh ou Unix, directement et toutes les versions peuvent inter-opérer facilement en exécutant les mêmes scripts.
Unreal est une machine virtuelle : Le moteur d'Unreal peut être considéré comme une machine virtuelle de jeu en 3D de la même manière que le langage Java et la hiérarchie des 'built-in' des classes Java, définissent une machine virtuelle pour le cryptage des pages Web. La machine virtuelle Unreal est portable par héritage (du fait du découpage de tout le code dépendant de plate-formes, dans des modules séparés) et peut être étendu (du fait de l'extension des classes hiérarchiques). Cependant, pour le moment, il n'existe pas de plans pour documenter la machine virtuelle d'Unreal à des extensions nécessaires aux autres, pour créer des implémentations indépendantes mais compatibles.
Le compilateur UnrealScript est à deux passes : A l'inverse de C++, UnrealScript est compilé en deux passes distinctes. Dans la première passe les définitions des, variables, états et fonctions sont analysées syntaxiquement et regroupées en membres. Dans la seconde passe, le code script est compilé en code octal. Ceci permet a des hiérarchies de scripts complexes, avec des références circulaires, d'être complètement compilées et linkées (édition de liens) en deux passes, sans phase d'édition de lien séparée.
État d'acteur persistant: Il est important de noter que dans Unreal, du fait que l'utilisateur peut sauvegarder son jeu à tout moment, l'état de tous les acteurs, incluant l'état d'exécution de leur script, peut être sauvegardé à tout moment, y compris quand tous les acteurs sont au plus bas niveau dans la pile. Cette nécessité, est la raison pour laquelle il existe une restriction aux fonctions latentes impliquant qu'elles ne peuvent être appelées qu'a partir d'un code d'état: le code d'état, est exécuté au plus bas niveau, et par ce fait peut être sérialisé facilement. Un code de fonction peut exister à tous niveau de la pile et peut comporter (par exemple) du code natif en C++ sous lui dans la pile, ce qui n'est pas franchement une situation dans laquelle on pourrît faire une sauvegarde sur disque, qui pourrait être restauré par la suite.
Fichiers Unreal : les fichiers Unreal sont des fichiers de format natif binaires. Les fichiers Unreal contiennent un index, des dumps sérialisés des objets dans un package particulier d'Unreal. Les fichiers Unreal sont similaires aux DLL, dans la mesure ou ils contiennent des références à d'autres objets stockés dans d'autres fichiers Unreal. Cette approche permet de distribuer le contenu d'Unreal dans différents packages pré-définis sur internet, afin de réduire le temps de chargement (en ne chargeant jamais un package particulier plus d'une fois).
Pourquoi UnrealScript ne supporte pas les variables statiques : Alors que C++ supporte les variables statiques (par processus de classe) avec de vraies bonnes raisons pour le langage de bas niveau de la racine, Java supporte les variables statiques pour des raisons qui n'apparaissent pas très clairement. De telles variables n'ont pas place dans UnrealScript du fait de l'ambiguïté de leur portée, respectant la sérialisation, la dérivation, et les niveaux multiples: des variables statiques auraient-elles une sémantique "globale", signifiant que toutes les variables statiques dans tous les niveaux actifs d'Unreal auraient la même valeur? Cela pourrait-il être vrai par package? Cela pourrait-il être vrai par niveau? S'il en est ainsi, comment seraient-elles sérialisées-- avec la classe dans son fichier .u, ou avec le niveau dans son fichier .unr? Seraient-elles uniques par classe de base, ou des versions dérivées de classes auraient leurs propres valeurs de variables statiques? Dans UnrealScript, nous avons laissé le problème de coté en ne définissant pas de variables statiques en tant que composantes du langage de programmation, et en laissant le programmeur gérer ses variables de type statique ou global, en créant des classes qui contiennent ces variables et en s'assurant qu'elles sont sérialisées avec le niveau. Cette manière de faire n'est pas ambiguë. Pour les exemples de classes qui se servent de ce genre de propos, voyez LevelInfo et GameInfo.
Ici je souhaite couvrir un petit aperçu de la manière d'écrire du code UnrealScript efficacement, et se servir avantageusement de la puissance d'UnrealScript tout en évitant des chausses-trappes.
...
...
Note du traducteur
J'ai fait de mon mieux pour rendre
cette traduction compréhensible. Si vous trouvez des erreurs communiquez-les moi,
je tenterai de maintenir la traduction de ce tutorial.