dan

Avant propos du traducteur

Conitec édite un magazine utilisateur (AUM) en moyenne une fois par mois.

Ce magazine existe en version anglaise et en version allemande.

J'ai entamé la traduction il y atrès lontemps mais je n'ai malheureusement pas toujpours du temps à consacrer à une traduction régulière de ce pourtant merveilleux support.

Donc plutôt que de vous laissez sans rien j'ai pris le parti de ne traduire que 2 rubriques parmi les 5 proposées sur la version originale, celles que je pense être les plus utiles pour vous.

Je vous invite donc à visiter le site officiel si vous souhaitez avoir l'intégralité de ces magazines.

Qui est l'auteur de ces magazines ? Voici ce qu'il écrivait dans le premier numéro :

"Finalement, voici quelque information sur moi, le rédacteur AUM. Mon nom est George Dan Pirvu, j'ai 30 ans et je travaille comme programmeur et ingénieur de matériel d'ordinateur. J'ai commencé à travailler avec les moteurs de Conitec en 1997; quelques mois plus tard J.C. Lotter a reconnu que je devais m'occuper de la section wdl dans le vieux Magazine d'Utilisateur Acknex. J'aime jouer des jeux, mais surtout j'aime les construire. J'ai travaillé à plusieurs projets de client et je suis une des preuves que l'on peut gagner sa vie en employant le moteur A4."

Avertissement : Le magazine ne donne pas l'intégralité du code utilisé. Il utilise uniquement les bouts de code servant à illustrer les différents propos. Pour les codes dans leur entier il faut charger les fichiers Zip qui se trouvent sur la page d'accueil de chacun des magazines officiels.

Les 2 rubriques retenues sont :

le coin des débutants

les bouts d'code


AUM 33

 planet survivor part III aum38 AUM 33

AUM 45 | AUM 46 | AUM 47 | AUM 48
Bout d’code fontaine planet_survivor X Coin des débutants boussole oiseaux volants | Bout d’code Coin des débutants | Bout d’code Coin des débutants fdfdfdfdfd | Bout d’code Coin des débutants
AUM 41 | AUM 42 | AUM 43 | AUM 44
Bout d’code explosions avancées planet_survivor VI Coin des débutants balle rebondissante musique aléatoire | Bout d’code lance flamme planet_survivor VII Coin des débutants caméra aventure pièges | Bout d’code torche planet_survivor VIII Coin des débutants escalator brouillard de guerre fdfdfdfdfd | Bout d’code comme sur des rails planet_survivor IX Coin des débutants niveau infini
AUM 37 | AUM 38 | AUM 39 | AUM 40
Bout d’code grenades planet_survivor II Coin des débutants fusée éclairante achetez maintenant! | Bout d’code texte éditable planet_survivor III Coin des débutants bannière commentaire musique intelligente | Bout d’code interface utilisateur personnalisable planet_survivor IV Coin des débutants scènes coupées scénarisées fdfdfdfdfd | Bout d’code araignées tueuses planet survivor V Coin des débutants peindre peaux et sprites effet aquatique
AUM 33 | AUM 34 | AUM 35 | AUM 36
Bout d’code mur cassable voiture avec IA partie II Coin des débutants code d'accès message électronique | Bout d’code mouvement RPG voiture avec IA partie III Coin des débutants eau réaliste champ d'étoiles | Bout d’code Menu contextuel voiture avec IA partie IV Coin des débutants Travailler avec "material" fdfdfdfdfd | Bout d’code ascenseur planet_survivor I Coin des débutants plusieurs entiés physiques fdfdfdfdfd
AUM 29 | AUM 30 | AUM 31 | AUM 32
Bout d’code yeux laser IA parfaite partie III Coin des débutants fondu enchaîné poussière | Bout d’code armes stationnaires IA parfaite partie IV Coin des débutants brouillard volumétrique shooter en écran partagé | Bout d’code faux miroirs IA parfaite making of Coin des débutants utilisation du SDK | Bout d’code sélection du joueur voiture avec IA partie I Coin des débutants panneau à la Splinter Cell texte au dessus des entités
AUM 25 | AUM 26 | AUM 27 | AUM 28
Bout d’code apprentissage de la frappe démo de physique Coin des débutants machine à brouillard mise au point instantanée | Bout d’code champ de force pong Coin des débutants effets sur les sommets texte à la star wars | Bout d’code décollage IA parfaite partie I Coin des débutants gouttes d'eau mini carte | Bout d’code missile à tête chercheuse IA parfaite partie II Coin des débutants champ de vision changement de niveau
AUM 21 | AUM 22 | AUM 23 | AUM 24
Bout d’code chars viseur précis Coin des débutants chute d'eau vitre brisée | Bout d’code caméras de sécurité visionneuse de graphiques Coin des débutants pianissimo raie de lumière | Bout d’code fléchettes Robby le robot Coin des débutants bain de sang caméra à distance | Bout d’code alerte lune roquette caméra Coin des débutants aspect champ de mines
AUM 17 | AUM 18 | AUM 19 | AUM 20
Bout d’code musique skeleton inc. Coin des débutants nouveau code de mise au point sprites animés | Bout d’code ascenseur intelligent inventaire Coin des débutants lampe torche vision de nuit | Bout d’code non traduit se reporter à l'excellent tutorial Coin des débutants de AB sur les jeux multijoueur | Bout d’code calculatrice visionneuse de modèles Coin des débutants coups à la tête réveil
AUM 13 | AUM 14 | AUM 15 | AUM 16
Bout d’code turbo à votre joueur virtua cup Coin des débutants maison de la terreur meilleur score | Bout d’code panneau défilant Stratego 3 - RTS Coin des débutants balle énergétique disco | Bout d’code combat aérien téléportation Coin des débutants code de triche pluie intelligente | Bout d’code générateur de code ombre douce Coin des débutants chronomètre miroirs verticaux
AUM 9 | AUM 10 | AUM 11 | AUM 12
Bout d’code John Wayne radar Coin des débutants cash and carry empreintes de pas | Bout d’code astéroïde monter et descendre de voiture Coin des débutants arc et flèche caméras de surveillance | Bout d’code Stratego 2 - RTS tourelles Coin des débutants bombe à retardement tireur d'élite | Bout d’code combat à l'épée forêt Coin des débutants démo temps vignette
AUM 5 | AUM 6 | AUM 7 | AUM 8
Bout d’code redéfinir les touches scènes coupées Coin des débutants mot de passe split ball | Bout d’code éclipse guerre de l'espace Coin des débutants caméra suit chemin démonstration de voiture | Bout d’code pacman tir sur interrupteur Coin des débutants couperet tranchant npc parle | Bout d’code déformation de mailles menu Coin des débutants bruit de pas explosion de barils
AUM 1 | AUM 2 | AUM 3 | AUM 4
Bout d’code non traduit car instructions obsolètes Coin des débutants Dfdfdfd fdfdfdfdfd | Bout d’code SCRAP - capture d'écran Stratego 1 - RTS Coin des débutants horloge panneaux textes et boutons | Bout d’code échelle de corde maniac miner Coin des débutants partir comme un voleur fdfdfdfdfd | Bout d’code caméras intelligentes tremblement de terre Coin des débutants différentes caméras IA première démo


Untitled

Alerte Lune

Ce jeu avait été développé pour le Sinclair Spectrum et Commodore 64 il y a déjà 10 ans. Je pense cependant que c'était un grand jeu (si vous regardez le concept initial et nonpas mes pauvres graphiques bien entendu...)

Avant que nous commencions jetons un oeil au jeu (les commentaires ont été ajoutés en jaune):

Vous contrôlez le char (j'ai besoin d'un modèle avec la tourelle) et vous pouvez le faire sauter par dessus plusieurs trous noirs en contrôlant sa vitesse : une vitesse plus grande vous aidera à sauter des trous plus grands. Pour corser le tout vous devez détruire les vaisseaux ennemis sinon ce sont eux qui vous élimineront.

Je veux commencer avec la définition du texte et celle du panneau; j'utilise une simple chaîne pour afficher "Score;" et "Lives:" (NDT: vous pouvez mettre "Vies:") et 2 digits pour leurs valeurs:

text moon_text
{
font = alert_font;
pos_x = 0;
pos_y = 0;
string = "Score:
Lives:";
flags = visible;
}

panel
moon_panel // displays the score using 3 digits and lives using 1 digit
{
pos_x = 0;
pos_y = 0;
digits = 190, 0, 3, alert_font,
1, score;
digits = 720, 0, 1, alert_font, 1, lives;
flags = refresh, d3d, visible;
}

La fonction principale (main) montre tous les triangles pour tous les modèles, limite le taux d'affichage à 30 fps et charge le niveau:

function main()
{
clip_size = 0;
fps_max = 30;
level_load (alert_wmb);
}

Le joueur contrôle le char avec cette action:

action
players_vehicle
{
player = me;
my.z = 20;
ent_create(barrel_mdl, nullvector,
attach_barrel);
vehicle_speed.y = 0;
vehicle_speed.z = 0;

Le char est placé 20 quants au-dessus du sol; de cette façon on est sur qu'il ne se coincera pas dans le sol après un grand saut. Nous créons le canon et nous l'attachons au char en utilisant la fonction attach_barrel(). Le char se déplace à la vitesse donnée par vehicle_speed; Il se déplace de la droite vers la gauche (sur l'axe des x) aussi nous mettons à 0 les 2 autres composantes de la vitesse (y et z).

     while (lives > 0)
{
if (key_cul ==
1 && speed_offset > 0) // use cursor left to slow down the vehicle
{
speed_offset -= 1 * time;
}
if (key_cur ==
1 && speed_offset < 5) // use cursor up to increase the speed of the
vehicle
{
speed_offset += 1 * time;
}
vehicle_speed.x
= 5 + speed_offset; // speed = 5...10
vehicle_speed *=
time;
if (key_space ==
1)
{
vehicle_jumps();
}

Tant que le joueur est en vie, il peut réduire ou augmenter la vitesse du char en utilisant les touches de direction gauche et droite. La vitesse se modifie de 5 en 5 car speed_offset peut se modifier de 0 à 5. Si nous pressons la barre espace, le char sautera; nous verrons cela plus en détail par la suite.

          move_mode = ignore_passable;
ent_move (vehicle_speed,
nullvector);
camera.x = my.x;
camera.y = -2500;
camera.z = 700;
camera.pan = 90;
score += 0.01 *
time; // add to the score as long as the player manages to survive
wait (1);
}
}

Le char ignore les entités passables pendant qu'il se déplace. La caméra suit le char à chaque cycle comme indiqué sur l'image ci-dessous:

Le score augmente (doucement) tout le temps car ce n'est pas facile de survivre dans le niveau, n'est-ce pas? Si le joueur "descend" un des ennemeis le score augmentera un peu. Rendons-nous à la fonction qui crée le canon:

function attach_barrel()
{
proc_late(); // prevent shaking
my.passable = on;
while(lives > 0) // as long as the player isn't dead
{
vec_set(my.x,you.x);
my.pan = you.pan;
my.tilt = barrel_offset;
if (key_cuu ==
1 && barrel_offset < 90) // use cursor up
{
barrel_offset += 3 * time;
}
if (key_cud ==
1 && barrel_offset > 0) // and cursor down to change the speed
{
barrel_offset -= 3 * time;
}

La première ligne se prémunit contre l'effet de lag entre le canon et le char (il est important que le canon soit placé tout à la fin c'est à dire une fois que le char a fait ce qu'il avait à faire). Aussi longtemps que le joueur est en vie, la tourelle garde la même position que le char, mieux encore, elle utilise le même angle pan. Nous voulons être capable de contrôler l'angle tilt pour notre tourelle, car nous voulons pouvoir tirer sur les ovnis et ils arrivent à différentes hauteur et positions. Nous changeons barrel_offset en utilisant les touches de direction haut et bas; cela change l'angle tilt de la tourelle dans un intervalle de : 0...90 degrés.

          vec_for_vertex(rocket_coords,
my, 33); // vertex for player's rocket
if (key_ctrl ==
1 && rocket_ptr == null && lives > 0) // ctrl fires a rocket
{
ent_create (rocket_mdl, rocket_coords, fire_rocket);
snd_play (rocket_wav, 100, 0);
}
if (you.invisible
== off) // used for blinking when the player loses a life
{
my.invisible = off;
}
else
{
my.invisible = on;
}
wait(1);
}
ent_remove(my);
}

Nous tirons à partir du 33ème sommet de la tourelle aussi nous stockons sa position dans rocket_coords. Si nous pressons la touche "Ctrl" et que nous n'avons aucune roquette à l'écran (rocket_ptr = null) et que le joueur n'est pas mort, nous créons une roquette puis nous jouons un son. La fonction fire_rocket() vous dit des choses à propos de rocket_ptr; nous verrons cela plus tard.

Le char perd une vie s'il tombe dans un trou ou s'il est touché par une des roquettes tirées par les ovnis; lorsqu'un de ces évènements se produit, le char clignote quelques instants pour montrer ce qui s'est passé.. J'ai pensé que ça ne serait pas terrible si le char clignotait et que la tourelle restait tout le temps visible aussi la tourelle sera visible seulement lorsque son créateur (vous) le char, est visible. Lorsque le nombre de vies est inférieur à 1 (le joueur est mort) la tourelle disparaît.

Regardons la fonction associée à la roquette:

function
fire_rocket()
{
while (rocket_ptr != null) {wait (1);} // disable auto fire
rocket_ptr = me;
wait (1);
my.pan = you.pan;
my.tilt = you.tilt;
my.enable_impact = on;
my.enable_block = on;
my.enable_entity = on;
my.event = rocket_explodes;
while ((my != null) && (vec_dist(my.x, player.x) < 2000))
{
rocket_speed.x
= 100;
rocket_speed.y
= 0;
rocket_speed.z
= 0;
rocket_speed *=
time;
move_mode = ignore_you
+ ignore_passable; // ignores the barrel -> can't collide with it
ent_move (rocket_speed,
nullvector);
wait (1);
}
ent_remove (me);
}

Vous vous posez peut-être des questions à propos du pointeur rocket_ptr; pourquoi en avons-nous besoin? J'aichoisi une nouvelle méthode pour interdire l'autofire (le tir automatique en rafale). Lorsque le joueur tire une roquette, une nouvelle entité avec le nom ocket_ptr est créée. Si nous voulons tirer une nouvelle roquette alors que l'ancienne n'a pas encore disparue, rocket_ptr n'est pas mis à null aussi la nouvelle roquette ne peut pas être tirée à cause de cette ligne.

while (rocket_ptr != null) {wait (1);} // disable auto fire

La roquette utilise les mêmes angles tilt et pan que son créateur (la tourelle); elle est sensible aux impacts, blocs de niveau et entités. Si la roquette se cogne avec quelque chose, son évènement rocket_explodes sera exécuté. Aussi longtemps que la roquette existe et que sa distance au joueur est inférieure à 2000 quants, la roquette se déplace en ignorant toutes les entités passables et son créateur (vous). De cette façon les roquettes n'entre pas en collison avec son créateur - la tourelle. Lorsque la roquette est à plus de 2000 quants du joueur, elle est enlevée car elle utilise des ressources pour rien.

Que se passe-til lorsqu'une roquette touche quelque chose? La réponse est donnée par la fonction suivante:

function
rocket_explodes()
{
wait (1);
snd_play (explosion_wav,
40, 0);
vec_set (temp, my.pos);
temp.y -= 50; // move the
explosion sprite a little closer to the player
ent_create(explosion_pcx, temp, animated_explosion);
ent_remove (me);
}

Nous attendons un cycle pour éviter d'avoir l'avertissement "dangerous event", nous jouons un son d'explosion et nous créons un sprite d'explosion 50 quants vers le joueur comme sur l'image ci-dessous:

Nous faisons cela car sinon une partie du sprite d'explosion serait couvert par l'entité touchée et ça ne serait pas joli, joli,n'est-ce pas? Le sprite exécutera sa fonction animated_explosion et la roquette sera enlevée. Jetons un oeil à la fonction qui anime le sprite:

function
animated_explosion()
{
wait (1);
my.passable = on;
my.flare = on;
my.bright = on;
my.ambient = 100;
while (my.frame < 7)
{
my.frame += 1 *
time;
wait (1);
}
ent_remove (me);
}

Nous attendons pour un autre cycle puis nous rendons le sprite passable, nous mettons ses drapeaux flare et bright et nous lui donnons de l'éclat en lui donnant une grande valeur pour ambient. Les frames d'animation pour l'explosion seront jouées avec la vitesse donnée par 1 * time. Dès que le sprite atteint sa dernière frame d'animation, il est enlevé.

Ladernière fonction assocée au joueur est celle qui permet le saut:

function vehicle_jumps()
{
if (my.skill40 == 1) {return;} // disable multiple jumps
snd_play (jump_wav, 30, 0);
my.skill40 = 1;
while (my.z < 300)
{
vehicle_speed.z
= 5 * time;
wait (1);
}
while (my.z > 20)
{
vehicle_speed.z
= -5 * time;
wait (1);
}
vehicle_speed.z = 0;
my.z = 20; // restore the exact vehicle height
my.skill40 = 0; // allow
a new jump
snd_play (landed_wav, 30,
0);
}

Vous vous souvenez que je vous avais dit que le char se déplaçait en utilisant uniquementsa composante vehicle_speed.x? J'ai menti; comment peut-on sauter sans changer vehicle_speed.z? Nous savons que les variables d'entités sont mises à 0 au démarrage du jeu et my.skill40 n'échappe pas à la règle. Nous jouons un son de saut et nous mettons skill40 à 1. De cette façon si vous pressez la barre espace 2 fois vous ne sauterez pas une deuxième fois car il est déjà en l'air. Aussi longtemps que la hauteur du char est inférieure à 300 quants, nousaugmentons sa hauteur en donnant à vehicle_speed.z une valeur positive. Lorsque le char dépasse les 300 quants en z, il sort de la boucle et entre dans la deuxieme boucle while, démarrant la chute car vehicle_speed.z a reçu une valeur negative. La chute se poursuit jusqu'à ce que sa hauteur soit en dessous de 20 quants et si vous vous demandez pourquoi j'ai choisi cette valeur, la réponse est simple: je ne veux pas que le char reste coivé dans le sol qui se trouve aux environs de 0 sur son axe z.

Nous quittons la deuxième boucle aussi nous remettons vehicle_speed.z à 0 et redonnons sa hauteur initiale au char (20 quants), nous remettons skill40 à 0 ce qui autorise un autre saut et nous jouons le son landed_wav.

Il est temps de jeter un oeil aux trous noirs. Vous devez être surpris qu'en fait il ne s'agit pas de vrai trou mais d'entités wmb noires. J'utilise des textures du standard.wad pour tous les projets d'AUM mais pour une fois je n'ai pas pu le faire parceque j'ai besoin d'une couleur unie. J'ai inclu le wad utilisé pour ce projet (colors.wad) donc assurez-vous de le mettre dans votre répertoire wads.

Nous voulons que notre char entre en collison avec les trous noir, n'est-ce pas? Cependant ce n'est pas une tâche si facile. Imaginez la situation:

Pensez-vous vraiment que le char parviendra à sortir de ce trou.? Moi, je ne pense pas! C'est pourquoi il a fallu inventer un système de trou différent c'est pourquoi j'ai décidé que ces trous seraient des entités wmb noires, comme sur l'image ci-dessous:

Maintenant essayez d'imaginer ce qui se passera si vous remplacez la texture blanche de l'entité wmb par une texture noire; cela donnera l'impression d'un trou noir car nous utilisons un fond noir. Le char peut entrer en collision avec l'entité noire et ce sera comme si il était entré en collision avec le trou. Le niveau a été construit avec un seul bloc et ces entités wmb, joli n'est-ce-pas?

Chaque "trou" a une simple action qui lui est attachée:

action obstacle
{
my.enable_impact = on;
my.enable_entity = on;
my.event = obstacle_hit;
}

Chaque entité trou est sensible à l'impact et aux autres entités; son évènement est obstacle_hit:

function
obstacle_hit()
{
my.passable = on; // don't trigger other events for this entity
lives -= 1;
player.invisible = on; //
make the player blink 3 times
sleep (0.3);
snd_play (lost_wav, 60, 0);
player.invisible = off;
sleep (0.3);
player.invisible = on;
sleep (0.3);
snd_play (lost_wav, 60, 0);
player.invisible = off;
sleep (0.3);
player.invisible = on;
sleep (0.3);
snd_play (lost_wav, 60, 0);
player.invisible = off;
}

Tant qu'un char touche un trou, ce trou est rendu passable. Laligne de code suivante décrémente le nombre de vie de 1 - maintenant vous pouvez voir pourquoi nous avons rendu le trou passable : nous ne voulons pas générer plusieurs déclenchements (perdre plusieurs vies) pour un seul trou. Le joueur clignotera 3 fois et un son sera joué 3 fois.

Le jeu serait facile si vos seuls ennemis étaient des trous. Regardons les vrais ennemis:

action ufo
{
while (player == null) {wait (1);}
my.y = player.y; // we don't have to align the enemies in Wed
my.enable_impact = on;
my.enable_entity = on;
my.event = destroy_ufo;
while (vec_dist (player.x,
my.x) > 1000) // if the player is far away
{
wait (1); // wait
}

L'ennemi attend que le joueur soit créé puis il change sa valeur y pour s'aligner sur celle du joueur comme illustré ci-dessous:

Le char tire ses roquettes en ligne droite aussi s'il n'est pas exactement aligné avec les ovnis dans WED, les roquettes n'atteindront jamais leur but. Cette simple ligne de code aligne les ovnis pour nous.

L'ovni est sensible à l'impact et aux autres entités (roquettes du joueur); son évènement destroy_ufo est exécuté lorsqu'il est touché par une roquette. L'ovni reste en attente si le joueur est à plus de 1000 quants de lui.

     while (my.x > player.x)
{
my.x -= 5 * time;
if (total_frames
% 90 == 1)
{
ent_create (rocket_mdl, my.pos, fire_bomb); // fire a rocket every 3 seconds
}
wait (1);
}
}

Cette nouvelle boucle while commence à s'exécuter dès que le char arrive à moins de 1000 quants. L'ovni se déplace de la droite vers la gauche en changeant ses coordonnées x (pas besoin d'utiliser une instruction ent_move ici). Nous voulons un déplacement doux pour l'ovni aussi nous changeons sa position à chaque cycle mais nous ne voulons pas qu'il puisse tirer à chaque cycle car dans ce cas le joueur n'aurait aucune chance. J'ai décidé d'utiliser total_frames, la variable qui contient le nombre total de cycles passés depuis le démarrage du jeu. Nous avons limité le taux d'affichage à 30 fps dans main aussi total_frames est incrémenté de 30 toues les secondes. L'expression (total_frames % 90 == 1) est vraie lorsque total_frames = 1, 91, 181, ... etc. De cette façon la roquette est tirée toutes les 3 secondes.

Ok, regardons la fonction associée au tir de la roquette par l'ovni:

function fire_bomb()
{
vec_set (temp.x, player.x);
vec_sub (temp.x, my.x);
vec_to_angle (my.pan, temp); // rotate the bomb towards the player
my.enable_impact = on;
my.enable_block = on;
my.enable_entity = on;
my.event = hit_player; //
same event here
while (my != null)
{
bomb_speed.x =
100;
bomb_speed.y =
0;
bomb_speed.z =
0;
bomb_speed *= time;
move_mode = ignore_you
+ ignore_passable; // ignores the barrel -> can't collide with it
ent_move (bomb_speed,
nullvector);
wait (1);
}
}

Cette fonction est similaire à la fonction fire_rocket, utilisée par le char. La seule différence significative se trouve dans les 3 premières lignes de code, elles oriententla roquette tirée par l'ovni en direction du char. Si le char touche l'ovni cette fonction sera exécutée:

function destroy_ufo()
{
score += 20;
ent_remove (me);
}

Cette fonction ajoute 20 points au score et enlève l'ovni. Si l'ovni touche le joueur,cette fonction sera exécutée:

function
hit_player()
{
snd_play (explosion_wav, 40, 0);
vec_set (temp, my.pos);
temp.y -= 50; // move the explosion sprite a little closer to the player
ent_create(explosion_pcx,
temp, animated_explosion);
ent_remove (me);
lives -= 1;
}

Nous jouons un son et nous créons l'animation du sprite d'explosion. La roquette ennemie est enlevée et "lives" est décrémenté.


Untitled

Apprentissage de la frappe

Ce n'est un secret pour personne que les utilisateurs d'Acknex peuvent développer toute sorte d'applications en utilisant le moteur; ce mois je veux vous apprendre à créer un "jeu" pour apprendre à taper au clavier. L'idée concernant ce logiciel d'apprentissage de la frappe est très simple : vous voyez une lettre tomber du ciel (oui, bon, du plafond si vous préférez) et vous essayez de presser la touche correspondante aussi vite que vous pouvez. Si vous êtes trop lent (petit scarabée), la lettre tombe sur le sol et vous perdez un point. Il y a plusieurs attaques par "vagues", avec des letrres qui tombent de plus en plus vite.

Lorsque nous exécutons le niveau, nous voyons cette image de l'écran:

Il y a pas mal d'éléments d'affichés à l'écran aussi nous allons les voir tous dans le détail:

text
start_txt
{
font = comic_font;
pos_x = 0;
pos_y = 200;
string = "   Prêt,
moussaillon!\n Pressez une touche pour commencer!";
flags = d3d, visible;
}

Le premier "text" affiche le texte au centre de l'écran. Notez que j'utilse \n pour indiquer qu'il faut aller à la ligne. J'utiliserai ce "text" pour afficher d'autres messages pendant le jeu.

text score_txt
{
font = comic_font;
pos_x = 10;
pos_y = 10;
string = "Total:\nCorrect:\nWave:";
flags = d3d, visible;
}

Score_txt affiche les 3 lignes de texte dans le coin supérieur gauche.

panel score_pan
{
pos_x = 0;
pos_y = 0;
digits = 250, 10, 3, comic_font,
1, total_chars;
digits = 250, 50, 3, comic_font, 1, correct_chars;
digits = 250, 90, 3, comic_font, 1, wave;
flags = refresh, d3d, visible;
}

Ce panneau affiche les 3 valeurs numériques qui apparaissent dans le coin supérieur gauche (0, 0, 1 sur notre image).

Je démarre avec la fonction qui crée les lettres :

function create_letters(number,
interval)
{
while (number > 0)
{
letter = 1 + int(random(26));
temp.x = 0;
temp.y = 350;
temp.z = 450;
str_for_asc(temp_str,
letter + 64);
str_cat(temp_str,
".wmb");
str_cpy(letter_str,
temp_str);
str_trunc(letter_str,
(str_len(letter_str) - 1));
ent_create(temp_str,
temp, falling_letters);
letter_falls =
1;
while (letter_falls)
{wait (1);}
str_cpy(temp_str,
"");
number -= 1;
sleep(interval);
}
}

Vous pouvez voir que la fonction prend 2 paramètres : le nombre de lettres qui doivent être créées et l'intervalle (en secondes) entre 2 lettres consécutives. Si j'exécute cette fonction de la façon suivante:

create_letters(4, 2);

Elle créera 4 lettres qui apparaîtront toutes les 2 secondes.will create four letters that will appear every two seconds. Regardons cette fonction encore une fois:

function create_letters(number, interval)
{
while (number > 0)
Aussi longtemps que le nombre de lettres devant être créées est plus grand que zéro
{
letter = 1 + int(random(26));
génère un entier de 1 à 26
temp.x = 0;
temp.y = 350;
temp.z = 450;
met les coordonnées pour la lettre devant et au-dessus de la tête du joeur
str_for_asc(temp_str, letter + 64);
convertit le nombre en caractère asciithe associé à ce nombre 65..90)
str_cat(temp_str, ".wmb");
ajoute ".wmb" pour créer quelque chose comme : p.wmb
str_cpy(letter_str, temp_str);
copie la chaine lettre.wmb dans letter_str
str_trunc(letter_str, (str_len(letter_str) - 1));
maintenant garde seulement la première lettre de la chaine c'est à dire LA lettre
ent_create(temp_str, temp, falling_letters);
maintenant nous créons la lettre
letter_falls = 1;
met cette variable à 1 -> elle sera utilisée par une autre fonction
while (letter_falls) {wait (1);}
attend jusqu'à ce que l'autre fonction mette letter_falls à zéro
str_cpy(temp_str, "");
remet temp_str à zéro
number -= 1;
décrémente le nombre de lettres
sleep(interval);
attend le nombre de secondes données par interval et répète les étapes ci-dessus.
}
}

Lorsque nous créons une lettre elle exécute sa fonction qui lui est associée - falling_letters():

function
falling_letters()
{
total_chars += 1;
my.enable_block = on;
my.event = destroy_letter;
falling_speed.x = 0;
falling_speed.y = 0;
falling_speed.z = -7;
falling_speed.z *= wave;
falling_speed.z *= time;

Nous augmentons total_chars, la valeur associée à "Total:" dans le jeu. la lettre est sensible aux blocs de niveau, qui déclenchera leur évènement destroy_letter. Falling_speed est négatif sur son z aussi la lettre tombe. Je vous ai parlé que le programme utilisait des "vagues"; en fait, nous utilisons une variable nommée wave qui est mis à 1 pour le premier niveau, à 2 pour le deuxième niveau etc. Finallement falling_speed est multiplié par time pour être sur d'avoir le même taux d'affichage quelque soit le pc utilisé.

     while ((my != null) && (my.event != null))
{
ent_move (falling_speed,
nullvector);
str_for_key(temp_str,
key_lastpressed);
if (str_cmpi(letter_str,
temp_str) && key_any)
{
correct_chars += 1;
snd_play(key_wav, 30, 0);
letter_fades();
str_cpy(letter_str, "");
while (key_any) {wait (1);}
}
else
{
if (key_any)
{
destroy_letter();
}
}
wait (1);
}
}

Aussi longtemps que la lettre existe et que son event n'est pas mis à null,nous déplaçons la lettre vers le bas, nous convertissons le code de balayage associée à la lettre frappée au clavier puis nous comparons la lettre qui tombe avec celle pressée par le joueur. Si ces 2 chaines sont identiques nous incrémentons correct_chars, correspondant à la valeur de "Correct:" sur notre écran puis nous jouons un petit son. Nous exécutons une petite fonction qui fait disparaitre la lettre avec un fondu puis nous remettons la chaine nommée letter_str à zéro, autrement nous pourrions augmenter correct_chars plusieurs fois pour une même lettre, ce qui ne serait pas correct, n'est-ce pas? Nous attendons jusqu'à ce que la lettre pressée (any key) soit relâchée.

Si l'égalité n'est pas vraie, nous avons tapé la mauvaise lettre aussi puisque vos petits doigts se sont trompé nous appelons la fonction nommée destroy_letter:

function destroy_letter()
{
my.event = null;
ent_remove (me);
letter_falls = 0; // the
letter has stopped
snd_play(error_wav, 50, 0);
}

La fonction met event à null, enlève la lettre, dit à la fonction create_letters que la lettre courante a disparue aussi on peut créer une nouvelle lettre puis nous jouons un son d'erreur. La fonction qui fait un fondu sur les lettres est simple:

function
letter_fades()
{
my.alpha = 100;
my.transparent = on;
while (my.alpha > 5)
{
my.alpha -= 10
* time;
wait (1);
}
ent_remove (me);
letter_falls = 0; // la lettre a disparu, on
peut en créer une autre
}

La lettre est rendue transparente mais si son alphaest mis à 100 elle apparaîtra opaque; son alpha sera décrémenté jusqu'à ce qu'il soit inférieur à 5. Quand cela se produit, la lettre est enlevée et letter_falls est remis à zéro aussi une nouvelle lettre peut être créée.

La dernière fonction est main(); Pour une fois elle paraît un peu compliquée mais tout le code à l'intérieur est très simple:

function main()
{
on_d = null;
randomize();
fps_max = 30;
level_load (tutor_wmb);
sleep (1);
camera.x = 0;
camera.y = -350;
camera.z = 150;
camera.pan = 90;
camera.ambient = 100;

Nous invalidons la touche debug car nous ne voulons pas montrer le panneau debug lorsque la lettre D tombera. Nous nous assuronsde générer un nombre aléatoire (lettres aléatoires) à chaque fois que nous démarrons le moteur, nous limitons le taux d'affichage à 30 fps puis nous chargeons le niveau. nous attendons une seconde puis nous paramétrons la position de la caméra et son angle pan. Je pense que la texture que j'utilise est un petit peu trop sombre aussi je mets camera.ambient à 100 afin de rendre le niveau plus lumineux.

     while (key_any == 0) {wait (1);}
start_txt.string = " First
wave - prepare!";
sleep (2);
start_txt.visible = off;
create_letters(10, 2);
while(total_chars < 10)
{wait (1);}

Vous souvenez-vous des définitions de "text" plus haut? Ces "texts" sont visibles au démarrage du jeu; vérifiez leur drapeau si vous ne me croyez pas. C'est pour cela que le texte "Get ready, captain!..." (que vous pouvez traduire par : prêt moussaillon...) est visible à l'écran dés le démarrage du jeu. Nous attendons que le joueur presse une touche puis nous changeons la chaîne utilisée pour le texte c'est pourquoi maintenant on peut lire "First wave - prepare" à l'écran (première vague se prépare). Nous attendons encore 2 secondes puis nous cachons le texte et nous créons 10 lettres, chaque 2 secondes, en utilisant notre fonction create_letters vue plus haut. Nous attend que toutes les lettres soient créées (total_chars = 10).

Les choses sont identiques à partir de maintenant mais ne vous inquiétez pas je commenterai également le code:

     sleep (2); // wait for 2 more seconds
start_txt.string = " Second
wave - prepare!";
start_txt.visible = on;
sleep (3); // for 3 seconds
start_txt.visible = off;
wave = 2;
create_letters (10, 1);
while(total_chars < 20)
{wait (1);}

Nous attendons secondes,nous changeons la chaîne de texte puis nous le rendons visible. Nous attendons 3 secondes puis nous cachons le texte, nous mettons wave à 2; ce qui augmentera la vitesse de chute. Nous créons 19 lettres qui tombe toutes les secondes puis nous attendrons que la deuxième vague soit terminée (total_chars = 20).

     sleep (2);
start_txt.string = " Third wave - prepare!";
start_txt.visible = on;
sleep (3);
start_txt.visible = off;
wave = 3;
create_letters (10, 0.5);
while(total_chars < 30)
{wait (1);}
sleep (5); // wait for 5
more seconds
start_txt.string = "
Impressive result!";
start_txt.visible = on;
sleep (5);
exit;
}

Nous attendons encore 2 secondes, nous nous changeons la chaîne de texte puis nous le rendons visible. Nous attendons 3 secondes puis nous cachons le texte, nous mettons wave à et nous créons encore 10 lettre; une mlettre tombera toutes les 1/2 secondes si vous avez une très grande vitesse de frappe. Nous attendons que la 3ème vague soit terminée (total_chars = 30) puis nous affichons "Impressive result!" (chapeau bas) ce qui n'est pas tout à fait vrai puisque le texte s'affiche même si vous avez loupé des lettres. Le texte apparaît pendant 5 secondes puis le moteur s'arrête. Et cet article également.


Untitled

Araignées tueuses

Ces créatures qui donnent le frisson se déplace à l'intérieur du niveau, évitant les obstacles et piquant toutes les entités qu'elles rencontrent (à l'xclusion des autres araignées bien entendu).

action
deadly_spider
{
var spider_speed;
var in_front;
my.enable_entity = on;
my.enable_impact = on;
my.event = hurt_them;
while(1)
{
spider_speed.x = 8 * time;
spider_speed.y = 0;
spider_speed.z = 0;
spider_speed *= time;
in_front.x = my.x + 40 * cos(my.pan);
in_front.y
= my.y + 40 * sin(my.pan);
in_front.z = my.z;
if (content(in_front) == content_solid)
{
my.skill40 = my.pan;
my.skill41 = 30 + random(90);
while (my.pan < my.skill40 + my.skill41)
// rotate 30..120 degrees
{
my.pan
+= 5 * time;
wait (1);
}
}

Les araignées sont sensibles aux autres entités, leur fonctiond'évènement est hurt_them. Les mouvements sont dans une boucle while(1), à la vitesse donnée par spider_speed.x. J'ai défini une variable locale in_front, et j'ai mis ses coordonnées 40 quants devant le joueur; si le contenu de in_front est solide (un mur, une entité carte), nous stockons l'angle pan dinitial de l'araignée puis nous lui ajoutons un angle aléatoire (30...120 degrees) utilisant une boucle qui augmente la valeur en douceur. Cette astuce est sensée faire tourner l'araignée dans une direction où elle n'est pas bloquée par un obstacle.

     else // free to move
{
ent_cycle("run", my.skill20);
// play the "run" animation
my.skill20 += 10 * time;
my.skill20 %= 100; // loop
move_mode = ignore_passable; // ignore passable entities
result = ent_move (spider_speed, nullvector);
if (result == 0) // got stuck?
{
spider_speed.x
*= -1; // then reverse the movement direction
my.skill40 = my.pan;
my.skill41 = 30 + random(90);
while (my.pan < my.skill40 + my.skill41) // rotate 30..120 degrees
{
my.pan += 5 * time;
ent_cycle("run", my.skill20); // play reversed "run" animation
my.skill20 -= 10 * time;
my.skill20 %= 100; // loop
move_mode = ignore_passable; // ignore passable entities
ent_move (spider_speed, nullvector);
wait (1);
}
spider_speed.x
*= -1; // restore the initial speed
}
}
wait (1);
}
wait
(1);
}

Si l'araignée peut se déplacer elle joue son animation de course ("run") ignorant les entités passables qu'elle pourrait rencontrer sur son chemin. Nous stockons le résultat de ses mouvements dans "result" puis nous vérifions ces valeurs; si l'araignée arrive dans de la matière "result" est mis à zéro (pas de déplacement) aussi nous multiplions spider_speed.x par -1, ce qui a pour effet d'inverser sa direction de mouvement. Nous générons un autre angle aléatoire qui sera ajouté à l'angle pan initial puis nous faisons reculer l'araignée utilisant son animation "run" inversée en ignorant les entités passables. Nous multiplions de nouveau spider_speed.x par -1 à la fin de la boucle et nous remettons la vitesse initiale.

La dernière chose dont nous avons besoin de discuter c'est la fonction d'évènement.:

function
hurt_them()
{
proc_kill(4);
if (you != null)
// collided with an entity (the player, a monster, etc)?
{
snd_play (gotcha_wav, 50, 0);
you.skill9 -= 10; // if "health" for that entity is stored in its skill9
}
}

Cette fonction s'exécute dès que l'araignée entre en collision avec quelque chose. Si elle cogne une entité (you != null) nous jouons le son gotcha_wavpuis nous diminuons skill9 pour l'entité qui a été piquée. Si votre joueur ou ennemis utilisent une autre variable skill pour sa santé vous devez le changer ici. L'action player_walk_fight utilise skill9 pour la santé aussi votre joueur se verra mourir dans mon niveau de démonstration.


Untitled

Traduction Olivier Burton - correction AB

Armes stationnaires

Cet article vous montrera comment créer des armes stationnaires : canons, grandes mitrailleuses, etc.

Le bout de code le plus compliqué est listé ci-dessous :

action
sweapon
{
while (player == null) {wait (1);}
player._health = 100;
my.passable = on;
while (player._health > 0)
{
if (player_immobile == 0)
{
while (vec_dist (player.x, my.x) > 100) {wait (1);}
vec_set (players_speeds, strength);
vec_set (players_angles, astrength);
vec_set (strength, nullvector);
vec_set (astrength, nullvector);
player_immobile = 1; // can't move
while (key_any == 1) {wait (1);}
player.pan = 0;
player.tilt = 0;
}

Nous attendons que le joueur soit créé et ensuite nous mettons sa santé à 100 (dans le cas où vous auriez oublié de le faire dans l'action associée au joueur). L'arme stationnaire (un canon, dans mon exemple) sera passable.

L'action sweapon continuera à s'exécuter tant que la santé du joueur sera supérieure à zéro; j'ai défini une variable nommée "player_immobile" qui est mis à 0 quand le joueur peut se déplacer et est mise à 1 quand le joueur est immobile. Si le joueur peut se déplacer, nous attendons qu'il soit à moins de 100 quants du canon et ensuite nous copions sa vitesse sur x, y, z et sa vitesse angulaire dans players_speed et players_angles. Les lignes suivantes de code mettent strength et astrength à zéro, donc, dorénavant, le joueur ne sera pas capable de se déplacer ou de tourner . Player_immobile est mis à 1, nous attendons que toutes les touches soient relâchées puis nous remettons les angles pan et tilt du joueur.

else // player_immobile == 1
{
player.x = -1250;
player.y = -728;
if ((key_cuu == 1) && (player.tilt < 30))
{
player.tilt += 2 * time;
}
if ((key_cud == 1) && (player.tilt > -10))
{
player.tilt -= 2 * time;
}
if ((key_cul == 1) && (player.pan < 30))
{
player.pan += 2 * time;
}
if ((key_cur == 1) && (player.pan > -30))
{
player.pan -= 2 * time;
}
my.pan = player.pan;
my.tilt = player.tilt;

Si le joueur ne peut pas se déplacer, nous mettons ses coordonnées x et y où nous en avons besoin ; j'ai repéré ces valeurs pendant l'exécution du programme en passant en mode de mise au point. Si nous pressons la touche de direction "haut" et si l'angle du joueur est au-dessous de 30 degrés, nous augmentons l'angle. Si nous pressonsla touche de direction "bas" et langle du joueur est au dessus de -10 degrés, nous diminuons l'angle. La même chose arrive avec l'angle pan; il change ses valeurs de-30 à 30 degrés employant les touches de direction "gauche" et "droite".

Les deux dernières lignes de code copient les angles pan et tilt du joueur et inclinent l'angle vers le canon.

if (key_ctrl == 1)
{
while (key_ctrl == 1) {wait (1);}
vec_for_vertex (rocket_coords, my, 190);
snd_play (fire_wav, 100, 0);
ent_create (rocket_mdl, rocket_coords, move_rocket);
}
if (key_space == 1)
{
vec_set (strength, players_speeds);
vec_set (astrength, players_angles);
player.pan = 0;
player.tilt = 0; // restore pan and tilt
player_immobile = 0; // can move
while (vec_dist (player.x, my.x) < 200) {wait (1);}
}
}
wait (1);
}
}

Si nous avons appuyé sur la touche "Ctrl", nous attendons qu'elle soit relâchéei (nous ne voulons pas de tir en rafale), nous obtenons les coordonnées de vertex pour la roquette (vertex #190 pour le modèle de canon), nous jouons un son et ensuite nous créons une roquette qui se déplace en utilisant la fonction nommée move_rocket.

Si nous pressons la touche "espace", nous restaurons les valeurs initiales de strength et d'astrength, permettant au joueur de se déplacer de nouveau. Nous mettons pan = 0, tilt = 0 et player_immobile à 0, le reste de l'action disant que le joueur peut de nouveau se déplacer. Nous permettons au joueur de partir du canon s'il n'a plus besoin du canon.

Revenons à la fonction qui déplace la roquette :

function
move_rocket()
{
my.enable_entity = on;
my.enable_block = on;
my.event
= remove_rocket;
my.pan = camera.pan;
my.tilt = camera.tilt;
rocket_speed.y = 0;
rocket_speed.z = 0;
while
(my != null)
{
rocket_speed.x = 100;
rocket_speed *= time;
move_mode = ignore_you + ignore_passable;
ent_move (rocket_speed, nullvector);
wait (1);
}
}

La roquette est sensible aux blocs du niveau et aux entités; quand elle entre en collision avec une des entités ou avec un bloc de niveau, sa fonction d'événement nommée remove_rocket s'exécutera. La fusée a les même angles pan et tilt avec la caméra et sa vitesse est donnée par rocket_speed.x. La roquette se déplace dans la boucle while, ignorant son créateur (le canon) et toutes les entités passables.

Curieux de voir l'événement fonctionner pour la roquette ? Ok, allons-y:

function
remove_rocket()
{
wait (1);
if (you != null)
{
you._health -= 150;
}
ent_create(explosion_pcx, my.pos, sprite_explosion);
my.event = null;
my.invisible = on;
wait (2);
ent_remove (me);
}

Si la roquette frappe une entité (you! = null) elle prend 150 points de santé de cette entité, crée un sprite d'explosion, met son événement à nul (donc elle ne réagira à rien d'autre dorénavant), devient invisible et est enlevé après deux cyccles. La fonction qui exécute le sprite d'explosion est listée ci-dessous :

function
sprite_explosion()
{
my.scale_x = 5;
my.scale_y = my.scale_x;
my.scale_z = my.scale_x;
my.passable = on;
my.flare = on;
my.bright = on;
my.ambient = 100;
ent_playsound (my, explosion_wav,
400);
while (my.frame < 7)
{
my.frame += 1 *
time;
wait (1);
}
ent_remove (me);
}

L'échelle du sprite est augmentée cinq fois sur x, y et z; cela pourrait être pratique si vous voulez remplacer le sprite d'un modèle. L'explosion est passable et les drapeaux "flare" "bright" sont mis; l'ambiant du sprite est mis à 100. Nous entendrons un son au point d'impact et les frames d'explosion seront jouées dans une boucle; aussitôt que la dernière frame d'explosion a été montrée, le sprite sera enlevé.


Untitled

Ascenseur

Cet article vous apprend à créer un ascenseur pour un immeuble de 3 étages; bien entendu vous pouvez adapter ce code si vous avez un immeuble plus grand. Commençons par la fonction principale (main):

function
main()
{
on_d = null;
level_load (elevator_wmb);
mouse_map = pointer_pcx;
mouse_mode = 0;
while (1)
{
mouse_pos.x =
pointer.x;
mouse_pos.y = pointer.y;
wait (1);
}
}

Nous invalidons la touche "D" (utile si vous êtes utilisateur A5) parce que nous allons utiliser les touches "Z", "S", "Q", "D" pour nous déplacer. Nous chargeons le niveau, nous paramétrons l'image bitmap qui sera utilisée pour le pointeur de la souris, mais nous la cachons pour l'instant. Pour terminer nousutilisons un code classique qui met à jour la position de la souris à chaque cycle.

J'ai dit que nous cachions la souris au démarrage du jeu mais nous pouvons la montrer en cliquant sur le bouton droit de la souris:

function
toggle_crosshair()
{
mouse_mode = (mouse_mode == 0) * 2;
}

on_mouse_right = toggle_crosshair;

Cette simple fonction met mouse_mode à 0 ou 2, dependant du nombre de clics; de cette façon le pointeur apparaît et disparaît de l'écran.

action
player1
{
player = my;
my.invisible = on;
// hide the player model
while (1)
{
vec_set (camera.pos, my.pos);
camera.z += 50;
camera.pan = my.pan;
camera.tilt += 10 * mouse_force.y * time;
my.pan -= 20 * mouse_force.x * time;
vec_set
(temp, my.x);
temp.z -= 3000;
trace_mode = ignore_me + ignore_passable + use_box;
temp.z = -trace (my.x, temp); // trace 3000 quants below player's feet
temp.x = 10 * (key_w - key_s) * time * ready_for_clicks;
temp.y = 8 * (key_a - key_d) * time * ready_for_clicks;
ent_move (temp, nullvector);
wait (1);
}
}

L' action attachée au joueur le rend invisible; la caméra à la même position x et y que le joueur mais son z est placé 50 quants au dessus de l'origine du joueur. Les angles pan et tilt peuvent être changés en utilisant la souris ce qui correspond à la façon dont le joueur regarde autour de lui. Nous exécutons une trace de 3000 quants sous les pieds du joueur et nous mettons sa hauteur en fonction du résultat de trace. pour terminer nous permttons au joueur de se déplacer en avant ou en arrière avec les touches "Z" and "S" et sur les côtés avec les touches "Q" et"D". Notez que les axes temp.x et temp.y sont multipliés avec ready_for_clicks; c'est une variable qui peut "geler" le joueur si elle est mise à 0. Ne vous inquiétez pas nous verrons bientôt cette variable.

L'entité ascenseur (levator) utilise une simple action:

var
floor1 = 39;
var floor2 = 520;
var floor3 = 1032;

action elevator
{
my_elevator = my;
my.z = floor1;
}

J'ai assigné le pointeur "my_elevator", défini auparavant, à l'entité ascenseur et j'ai mis sa hauteur à la coordonnée z qui a été choisie pour le premier étage. Les valeurs pour les 3 étages floor1...floor3 sont expérimentales et auront besoin d'être ajustée jusqu'à ce qu'elle fonctionne parfaitement dans votre niveau.

Vous ne serez pas surpris d'apprendre que l'ascenseur comprend 3 boutons; ce ne sont rien de plus que des petites entités wmb qui ont cette action qui leur est attachée:

entity*
button_red;
entity* button_green;
entity* button_blue;

action
button
{
my.light = on;
my.lightrange = 0;
my.unlit = on;
my.ambient = -100;
if
(my.skill1 == 1)
{
button_red
= my;
my.lightred = 255;
my.lightgreen = 0;
my.lightblue = 0;
}
if (my.skill1 == 2)
{
button_green = my;
my.lightred = 0;
my.lightgreen = 255;
my.lightblue = 0;
}
if (my.skill1 == 3)
{
button_blue = my;
my.lightred = 0;
my.lightgreen = 0;
my.lightblue = 255;
}
my.enable_click = on;
my.event = button_clicked;
}

J'ai utilisé une simple entité wmb pour tous les boutons et j'ai changé leur apparence en utilisant des couleurs différentes pour chaque bouton. Ne pas oublier de mettre skill1 à 1 pour le bouton rouge, skill1 = 2 pour le vert et skill1 = 3 pour le bleu. Tous les boutons sont sensibles au clic et exécutent leur évènement button_clicked dés qu'ils sont pressés par le joueur:

function
button_clicked()
{
if (ready_for_clicks == 0) {return;}
ready_for_clicks = 0;
if (my.skill1 == 1)
{
if (my_elevator.z == floor1)
{
ready_for_clicks = 1;
return;
}
if ((my_elevator.z == floor2) || (my_elevator.z == floor3))
{
while (my_elevator.z
> floor1)
{
my_elevator.z -= 5 * time;
player.z = my_elevator.z + 48;
button_red.z
= my_elevator.z + 81;
button_green.z
= my_elevator.z + 105;
button_blue.z
= my_elevator.z + 129;
wait (1);
}
}
my_elevator.z
= floor1;
}

J'ai défini une variable nommée ready_for_clicks et j'ai mis sa valeur à 1. Cette variable sera mise à jour dès qu'un bouton sera pressé et restera à zéro tant que l'ascenseur ne sera pas arrêté; de cette façon nous ne permettons pas au joueur de jouer avec l'ascenseur lorsqu'il est en mouvement.Si nous avons pressé le bouton rouge (skill1 = 1) et que l'ascenseur est déjà au premier étage, nous autorisons le clic de nouveau (ready_for_clicks = 1) et nous sortons de la fonction. Si l'ascenseur est au 2ème ou 3ème étage il descendra jusqu'au 1er étage c'est pourquoi la boucle while diminue sa hauteur jusqu'à atteindre la valeur du 1er étage.

Le joueur est mis 48 quants au dessus du plancher, c'est une valeur expérimentale dépendant de la hauteur de votre joueur. Une bonne chose nous déplaçons les 3 boutons en même temps que l'ascenseur sinon nous ne les verrions plus et le joueur ne pourrait plus choisi run nouvel étage s'il le désire. Une fois encore 81, 105 et 129 sont des valeurs expérimentales. La dernière ligne de code met la hauteur de l'ascenseur à la valeur donnée par floor1; nous avons besoin de corriger le décalage.

  if (my.skill1 == 2)
{
if (my_elevator.z == floor2)
{
ready_for_clicks = 1;
return; // do nothing
}
if (my_elevator.z
== floor1)
{
while (my_elevator.z < floor2)
{
my_elevator.z += 5 * time;
player.z = my_elevator.z + 48;
button_red.z
= my_elevator.z + 81;
button_green.z = my_elevator.z + 105;
button_blue.z = my_elevator.z + 129;
wait (1);
}
}
if (my_elevator.z == floor3)
{
while (my_elevator.z
> floor2)
{
my_elevator.z -=
5 * time;
player.z = my_elevator.z + 48;
button_red.z = my_elevator.z + 81;
button_green.z = my_elevator.z + 105;
button_blue.z = my_elevator.z + 129;
wait (1);
}
}
my_elevator.z = floor2;
}

Si le joueur presse le second bouton (vert) et que l'ascenseur est déjà au 2ème étage, la fonction retourne Par ailleurs si l'ascenseur est au 1er nous augmentons son z jusqu'à ce qu'il soit supérieur ou égal à floor2. Si l'ascenseur est au 3ème étage nous décrémentons sa valeur jusqu'à ce qu'elle soit inférieure ouégale à floor2. La dernière ligne de code met la hauteur de l'ascenseur à la valeur donnée par floor2.

if (my.skill1 == 3) // pressed the blue (3rd floor) button?
{
if (my_elevator.z == floor3) // if the elevator is at the third floor
already
{
ready_for_clicks = 1; // enable clicking again
return; // do nothing
}
if ((my_elevator.z
== floor1) || (my_elevator.z == floor2)) // if the elevator is at the first or
second floor
{
while (my_elevator.z < floor3)
{
my_elevator.z += 5 * time;
player.z = my_elevator.z + 48;
button_red.z = my_elevator.z + 81;
button_green.z = my_elevator.z + 105;
button_blue.z = my_elevator.z + 129;
wait (1);
}
}
my_elevator.z = floor3;
}
button_red.z
= my_elevator.z + 81;
button_green.z = my_elevator.z + 105;
button_blue.z = my_elevator.z + 129;
ready_for_clicks = 1;
}

Si le joueur presse le 3ème bouton (bleu) et que l'ascenseur est déjà au 3ème étage, la fonction retourne Par ailleurs si l'ascenseur est au 1er ou au 2ème nous augmentons son z jusqu'à ce qu'il soit supérieur ou égal à floor3. Les dernières lignes permettent de conserver une bonne position pour les boutons dans l'ascenseur et la dernière ligne permet qu'on reclique dessus. Jetons un oeil à ces lignes dans l'action utilisée par le joueur:

action
player1
{
................................................................................
temp.x = 10 * (key_w - key_s) * time * ready_for_clicks;
temp.y = 8 * (key_a - key_d) * time * ready_for_clicks;
ent_move (temp, nullvector);
wait (1);
}
}

Voyez-vous ce qui se passe ici? Lorsque l'ascenseur se déplace, ready_for_clicks est mis à zéro et le joueur ne peut pas se déplacer du tout parce que temp.x et temp.y sont à 0. Dés que l'ascenseur s'arrête, ready_for_clicks est mis à 1 et le joueur est de nouveau autorisé à se déplacer. C'est une sécurité pour maintenir le joueur dans l'ascenseur pendant son déplacement; la caméra peut changer ses angles pan et tilt, de cette façon le joueur peut regarder autour de lui si le pointeur de la souris est off mais ses pieds ne pourront pas bouger tant que l'ascenseur ne sera pas arrêté.


Untitled

Ascenseur intelligent

Je suis pratiquement sur qu'en lisant le titre de cet article vous vous êtes dit quelque chose du genre : " oui, bien...comment pouvez-vous donner de l'intelligence à un ascenseur?" Le code ascenseur de cet article commencera à s'exécuter si le joueur s'approche d'un interrupteur. L'idée de l'intelligence est que l'interrupteur et l'ascenseur qui lui correspond ne sont pas liés du tout! Vous pouvez utiliser plein d'ascenseurs et plein d'interrupteurs et vous saurez comment ils fonctionnent ensemble sans utiliser de pointeurs etc. Vous utiliserez la même action pour chaque bouton et pour chaque platteforme et le code fera le reste. Maintenant qu ej'ai retenu votre attention, laissez moi vous montrer comme cela est fait.

L'idée est comme toujours très simple : si vous créez un niveau normal, la distance entre les ascenseurs du niveau ne sera pas petite. J'utilise un bouton qui scane autour de lui et s'il trouve un ascenseur près de lui, il commencera à l'utiliser. Ici le code associé à chaque bouton d'ascenseur:

action elevator_button
{
while (1)
{
while (vec_dist
(player.x, my.x) > 50) {wait (1);}
temp.pan = 360;
// scan on a sphere
temp.tilt = 360;
// because the elevator can be anywhere around this button
temp.z = 500; //
the elevator should be closer than 500 quants all the time
scan_entity (my.x,
temp);
sleep (0.4); //
scan 2 times a second (sleep (0.4 + 0.1))
my.ambient = 100;
sleep (0.1);
my.ambient = 0;
}
}

Si le joueur approche du
bouton, le bouton commence à scanner autour de lui dans un rayon de 500
quants. Le bouton clignote car nous avons changé son drapeau ambient.

L'action associée à l'entité ascenseur est:

action elevator_entity
{
if (my.elevator_speed == 0) // if we forget to set skill1 in Wed
{
my.elevator_speed = 5; // set the default speed value
}
if (my.elevator_height == 0) // if we forget to set skill2 in Wed
{
beep; beep; exit; // shut down the engine
}
my.enable_scan = on;
my.event = move_elevator;
}

L'ascenseur a une valeur de vitesse par défaut; elevator_speed = 5 si nous avons oublié de lui en donner une dans skill1 dans Wed. Skill2 est le point de destination haut (elevator_height) et doit être rempli à chaque fois. J'ai cherché à m'assurer que vous n'oublierez pas de le faire aussi le moteur émettra 2 beeps puis s'arrêtera si vous oubliez de renseigner skill2. Si le point de destinationa vraiment besoin d'être à zéro, entrer une valeur pour skill2 comme 0.001 par exemple.

L'ascenseur est sensible balayage; lorsque cela se produit (le joueur est proche d'un bouton placé près de l'ascenseur) l'évènement sera déclenché:

function move_elevator()
{
proc_kill(1);
my.enable_scan = off;

Nous tuons toutes les autres instances de cette fonction lorsque nous rendons l'ascenseur insensible aux autres scans.

     if (my.z < you.z)
{
while (my.z <
(player.z - (player.max_z - player.min_z) / 2))
{
my.z += my.elevator_speed * time;
wait (1);
}
}

Si l'ascenseur est placé derrière le bouton, il doit monter jusqu'à ce qu'il atteigne la hauteur qui correspond aux pieds du joueur.

     if (my.z > you.z)
{
while (my.z >
(player.z - (player.max_z - player.min_z) / 2))
{
my.z -= my.elevator_speed * time;
wait (1);
}
}

Même chose ici; l'ascenseur descend jusqu'à ce que sa hauteur soit au-dessous des pieds du joueur.

my.z = player.z - (player.max_z - player.min_z) / 2;

Nous mettons la hauteur de l'ascenseur aux pieds du joueur (il était un peu plus petit ou plus grand).

while (vec_dist (player.x, my.x) > 50) {wait (1);}
my.enable_scan = on;
vec_set (player_strength, strength); // store player's speed on x, y, z
vec_set (strength, nullvector); // stop the player

Nous attendons jusqu'à ce que le joueur soit proche du centre de l'ascenseur puis nous autorisons de nouveau le balayage. Nous sauvegardons la vitesse du joueur (strength) puis nous mettons strength à 0 puisque nous voulons arrêter le joueur pour le moment (nous lui permettons de tourner).

     while (my.z < my.elevator_height)
{
my.z += my.elevator_speed
* time;
player.x = my.x;
player.y = my.y;
player.z = my.z
+ (player.max_z - player.min_z) / 2;
wait (1);
}

Aussi longtemps que la coordonnée z de l'ascenseur est en dessous de la valeur mise dans skill2, son z est augmenté et le joueur est gardé au milieu de l'ascenseur (et au dessus) de la platteforme en changeant ses coordonnées x et y.

     while (my.z > my.elevator_height)
{
my.z -= my.elevator_speed
* time;
player.x = my.x;
player.y = my.y;
player.z = my.z
+ (player.max_z - player.min_z) / 2;
wait (1);
}

Même chose ici si l'ascenseur a besoin de descendre.

vec_set (strength, player_strength);
}

Lorsque l'ascenseur atteint sa destination, nous autorisons le joueur à se déplacer de nouveau en lui redonnant strength.

J'ai fourni un niveau d'exemple avec 2 ascenseurs et 2 boutons. Naturellement vous pouvez ajouter autant d'ascenseurs et de boutons que vous le souhaitez, aussi longtemps qu'ils ne sont pas près les uns des autres. Jouez avec temp.z = 500 dans elevator_button pour mettre différentes étendues de balayage; la distance entre mes élévateurs est au dessus de 500 quants.


Untitled

Steroidz

Mike Bravo 23 appelle la base! Il y a un astéroide géant qui vient sur nous. NON, ce sont des centaines qui arrivent sur nos vaisseaux!

Voici le scénario que je vous propose de vivre avec Steroidz! Jetons un coup d'oeil à la fonction principale (main):

function main()
{
fps_max = 40;
level_load (steroidz_wmb); // niveau bidon
wait (2);
score = 0; // reset score
asteroid_generator();
display_lives();
}

Nous bloquons la vitesse d'affichage à 40 et nous chargeons le niveau (une énorme sphere mdl et un petit cube faute de quoi vous ne pourrez pas compiler) Lorsque le niveau est chargé, nous mettons score à 0, démarrons la génération des astéroïdes et affichons le nombre de vie. Nous verrons tout cela en détail un peu plus loin. Regardons pour l'instant l'action qui déplace le vaisseau du joueur:

action
players_ship
{
my.dead = 0;
my.metal
= on;
my.skill22 = 0; // reset le nombre de coup
my.enable_impact = on;
my.event = player_dies;
player = me;
while (camera == null) {wait (1);}
while (my.dead == 0) // aussi longtemps que le joueur n'est pas mort
{
player.tilt += 0.5 * (key_cuu
- key_cud) * time;
player.pan +=
2 * (key_cul - key_cur) * time;
ship_speed.x = 0.6 * key_space * time + max ((1 - time * 0.05), 0) * ship_speed.x;
ship_speed.y = 0;
ship_speed.z =
0;
ent_move(ship_speed, nullvector);

vec_set (temp, nullvector);
vec_sub
(temp, camera_distance);
vec_to_angle
(camera_angle, temp);
camera_angle.roll
= 0;

vec_set (camera.x, camera_distance);
vec_rotate (camera.x, my.pan);
vec_add
(camera.x, my.x);

vec_set (camera.pan,
my.pan);
ang_rotate (camera.pan,
camera_angle);

vec_for_vertex(particles_left,
my, 89);
vec_for_vertex(particles_right,
my, 83);

if (key_space == 1)
{
effect (ship_particles, 2, particles_left, normal);
effect (ship_particles, 2, particles_right, normal);
if (shipsound_handle == 0)
{
snd_loop (ship_snd, 50, 0);
shipsound_handle = result;
}
}
else
{
snd_stop (shipsound_handle);
shipsound_handle = 0;
}

if (key_ctrl == 1) // press ctrl to fire
{
if (my.skill35
== 0)
{
my.skill35 = 1; // disable autofire
ent_create (rocket_mdl, player.pos, ship_rocket);
snd_play (firerocket_snd, 80, 0);
}
}
else
{
my.skill35 = 0; // enable fire again
}

wait (1);
}
}

J'ai défini dead comme skill20 aussi my.dead = 0 signifie
my.skill20 = 0; skill20 doit être mis à 1 lorsque le joueur est mort.
Le joueur est sensible aux impacts et meurt après avoir reçu 5 coups;
le nombre de coup est stocké dans skill22. J'utilise les flèches
de directionI'm using the arrow keys (flèches haut et bas pour l'inclinaison
(tilt)et gauche et droite pour la rotation (pan)), espace pour la poussée
(incluant l'inertie) et ctrl pour le tir.

Nous faisons tourner la caméra vers le vaisseau, ajoutant la distance appropriée (indiquée dans var camera_distance) et ensuite nous faisons tourner la caméra en employant ang_rotate, capable de faire tourner un objet (la caméra dans Steroidz) selon la direction des angles du vaisseau.

Si nous pressons espace, les moteurs produisent des particules et joue un son en boucle. Les vertices appropriés des moteurs sont ceux avec les numéros 89 et 83 (on obtient ces numéros dans Med); les deux lignes vec_for_vertex mettent les deux variables (particle_left et particle_right) à ces coordonnées de vertex.

Si nous pressons Ctrl, nous créons une roquette à la position du joueur; la fonction qui gère la roquette est :

function
ship_rocket()
{
wait (1);
my.ambient
= 100;
my.skill30 = 0;
my.enable_entity
= on;
my.enable_block = on;
my.event
= remove_me;
my.passable = on;
my.pan
= player.pan;
my.tilt = player.tilt;

my.skill12 = 100 * time;
my.skill13 = 0;
my.skill14 = 0;

while (my != null) // aussi longtemps
que la roquette n'a pas explosé
{
my.skill12 = 100 * time;
my.skill30
+= 0.1 * time;
if (vec_dist (my.x,
player.x) < 100) {my.passable = on;}
// don't collide with the player
else {my.passable = off;}
if (my.skill30
< 100)
{
ent_move (my.skill12, nullvector);
}
else
{
ent_remove
(me);
}
wait (1);
}
}

Ce code est similaire à
celui utilisé dans le code arc et flèche dans ce magazine. La roquette
se déplace jusqu'à ce qu'elle touche quelque chose (un astéroïde
bien entendu) mais si elle ne touche rien elle sera malgré tout enlevée
au bout d'un certain temps.

J'ai utilisé un petit truc pour avoir un univers infini pour notre vaisseau : j'ai placé une sphère énorme dans le niveau et j'emploie une action simple pour tenir le joueur dans le centre de la sphère (ainsi quand le joueur bouge, la sphère se déplace avec lui dans la même direction):

action
sky_follows_player
{
my.passable = on;
while (player == null) {wait (1);}
while (1)
{
vec_set (my.pos, player.pos);
wait (1);
}
}

Le ciel doit être passable parce que les astéroïdes
doivent passer par lui et se déplacer vers le joueur. Revenons à
notre fonction asteroid_generator :

function
asteroid_generator()
{
while (1)
{
if (random(1) > 0.97)
{
asteroid_angle
= player.pan + 30 - random(60); // -30...+30 degrees
asteroid_pos.x = player.x + (5000 + random(5000)) * cos (asteroid_angle);
asteroid_pos.y = player.y + (5000 + random(5000)) * sin (asteroid_angle);
asteroid_pos.z = player.z + 300 - random(600);
ent_create (asteroid_mdl, asteroid_pos, move_asteroid);
}
waitt (3);
}
}

function move_asteroid()
{
my.ambient =
-50;
my.metal = on;
my.enable_entity
= on;
my.event = destroy_asteroid;
vec_set (temp.x, player.x);
vec_sub (temp.x, my.x);
vec_to_angle (my.pan, temp); // se tourne vers le joueur
my.pan += 3 - random(6); // manque le joueur de temps en temps
my.tilt += 1 - random(2);
while
(my != null)
{
my.roll += 5 * time;
my.skill3 =
20 * time;
my.skill4 = 0;
my.skill5 = 0;
ent_move(my.skill3,
nullvector);
wait (1);
}
}

Les astéroids sont générés de temps en temps, lorsque random(1) > 0.97, sur un anneau qui entoure le joueur, comme sur l'image ci-dessous.

Il ne serait pas juste de frapper le joueur par derrière, donc les astéroïdes sont produits seulement devant le joueur (player.pan + 30 - random (60) degrés). La fonction move_asteroid fera tourner l'astéroïde vers le joueur, mais nous changeons la direction un peu en ajoutant des petites valeurs aléatoires aux angles pan et tilt.

function
destroy_asteroid()
{
my.ambient = 100;
my.passable = on;
waitt (2);
ent_remove
(me);
snd_play (explode_snd, 70, 0);
}

Si l'astéroide est frappé par une roquette ou le joueur, sa valeur ambient est augmentée pendant 0.125 seconds puis l'astéroïde est enlevé. Si le joueur est touché par un astéroïde, il perd une vie. La fonction qui affiche les vies met le drapeau visible à 1 'ou 0) dépendant de la variable skill22 (nombre de coups).

function
display_lives()
{
while (1)
{
if (player.skill22 == 0)
{
first_pan.visible = on;
second_pan.visible = on;
third_pan.visible = on;
fourth_pan.visible = on;
fifth_pan.visible = on;
}
if (player.skill22 == 1)
{
first_pan.visible = on;
second_pan.visible = on;
third_pan.visible = on;
fourth_pan.visible = on;
fifth_pan.visible = off;
}
if (player.skill22 == 2)
{
first_pan.visible = on;
second_pan.visible = on;
third_pan.visible = on;
fourth_pan.visible = off;
fifth_pan.visible = off;
}
if (player.skill22 == 3)
{
first_pan.visible = on;
second_pan.visible = on;
third_pan.visible = off;
fourth_pan.visible = off;
fifth_pan.visible = off;
}
if (player.skill22 == 4)
{
first_pan.visible = on;
second_pan.visible = off;
third_pan.visible = off;
fourth_pan.visible = off;
fifth_pan.visible = off;
}
if (player.skill22 == 5)
{
first_pan.visible = off;
second_pan.visible = off;
third_pan.visible = off;
fourth_pan.visible = off;
fifth_pan.visible = off;
}
wait (1);
}
}

Les fonctions qui sont utilisées pour afficher les particules du moteur sont :

function ship_particles()
{
temp.x = random(2) - 1;
temp.y = random(10) + 2;
temp.z = random(2) - 1;
vec_add (my.vel_x, temp);
my.alpha = 30 + random(50);
my.bmap = particle_map;
my.size = 7 + random(4);
my.flare = on;
my.bright = on;
my.lifespan = 20;
my.function = fade_particle;
}

function fade_particle()
{
my.alpha -= 20 * time;
if (my.alpha < 0) {my.lifespan
= 0;}
}

Il n'y a rien de spécial avec ces fonctions; temp.y met la bonne direction pour la trainée et my.alpha -= 20 * time met sa longueur.

Après 5 coups le joueur perd ses vies et doit redémarrer le jeu ebn appuyant sur R qui appele tout simplement la fonction main de nouveau.


Untitled

Traduction Olivier Burton

Calculatrice

Cette calculatrice vous permet d'ajouter, soustraire, multiplier et diviser. J'ai ajouté le code qui s'occupe du dépassement comme exemple mais vous pouvez améliorer la calculatrice en ajoutant le code qui traite des désaccords par des divisions par zéro, priorités des opérations multiples (6 * 3 - 4 = 14 au lieu de 6 * 3 = 18, 18 - 4 = 14) et cetera.

Commençons par quelques définitions de string :

string result_string = " "; // pour 5 chiffres
string temp_string = " "; // pour 2 chiffres, nombre de bouton (1.. 15)

La fonction main mets la couleur de fond à RGB = 000 (noir), met l'indicateur de souris bitmap, copie "0" dans le string d'affichage parce que la calculatrice démarre pour la première fois et ensuite elle nous permet de déplacer la souris.

function
main()
{
screen_color.red = 0;
screen_color.green = 0;
screen_color.blue = 0;
mouse_map = mouse_pcx;
mouse_mode = 2;
str_cpy(result_string, "0"); //
démarre avec un zéro affiché à l'écran
while (1)
{
mouse_pos.x = pointer.x;
mouse_pos.y = pointer.y;
wait (1);
}
}

Nous emploierons un panneau pour tous les boutons et un texte pour l'affichage :

panel
main_pan
{
bmap = main_pcx;
pos_x = 0;
pos_y = 0;
layer = 10;

button = 292, 180, button1_pcx, button1_pcx, button1_pcx, null, key_pressed, null;
button = 332, 180, button2_pcx, button2_pcx, button2_pcx,
null, key_pressed, null;
button = 372, 180, button3_pcx,
button3_pcx, button3_pcx, null, key_pressed, null;
button = 292, 215, button4_pcx, button4_pcx, button4_pcx, null, key_pressed, null;
button = 332, 215, button5_pcx, button5_pcx, button5_pcx,
null, key_pressed, null;
button = 372, 215, button6_pcx,
button6_pcx, button6_pcx, null, key_pressed, null;
button = 292, 250, button7_pcx, button7_pcx, button7_pcx, null, key_pressed, null;
button = 332, 250, button8_pcx, button8_pcx, button8_pcx,
null, key_pressed, null;
button = 372, 250, button9_pcx,
button9_pcx, button9_pcx, null, key_pressed, null;
button = 292, 285, button0_pcx, button0_pcx, button0_pcx, null, key_pressed, null;
button = 332, 285, buttonplus_pcx, buttonplus_pcx,
buttonplus_pcx, null, key_pressed, null;
button
= 372, 285, buttonminus_pcx, buttonminus_pcx, buttonminus_pcx, null, key_pressed,
null;
button = 292, 320, buttonmultiply_pcx, buttonmultiply_pcx,
buttonmultiply_pcx, null, key_pressed, null;
button
= 332, 320, buttondivide_pcx, buttondivide_pcx, buttondivide_pcx, null, key_pressed,
null;
button = 372, 320, buttonequal_pcx, buttonequal_pcx,
buttonequal_pcx, null, key_pressed, null;

flags = refresh, d3d, visible;
}

text
display_text
{
layer = 20;
pos_x = 380;
pos_y = 145;
font = lcd_font;
string = result_string;
flags = visible;
}

La définition du panneau inclut 15 boutons qui apparaîtront sur l'écran. Quand un des boutons est cliqué, la fonction key_pressed courra et key_number sera mis à une valeur entre 1 (pour button1_pcx) et 15 (pour buttonequal_pcx), selon le bouton qui a été cliqué. Voyons ce que cette fonction key_pressed fait :

function
key_pressed(key_number)
{
snd_play(key_wav,
70, 0);
if (key_number >= 1 && key_number
<= 10)
{
str_for_num(temp_string, key_number);
if (str_len(temp_string) == 2)
{
str_clip(temp_string, 1);
}
if (str_cmp(result_string,
"0"))
{
str_cpy(result_string, temp_string);
}
else // we add a
digit to the existing number
{
if (str_len(result_string) < 5)
{
str_cat(result_string, temp_string);
align_result();
}
}
}

Chaque fois nous cliquons sur un bouton sur le panneau, un son est joué. Si nous avons appuyé 1, 2, 3..., 9 ou 0, nous convertissons la transmission key_number à un string. Si nous pressons le 10ème bouton (le zéro) nous devons changer 10 (c'est le 10ème bouton!) à sa valeur correcte (0). C'est une tâche facile parce que 10 est le seule chiffre qui a 2 chiffres si nous mettons simplement str_clip le premier travail de la journée de 10 et nous arrivons à 0.

Si l'exposition montre "0" et nous avons appuyé un bouton, "0" sera remplacé avec notre bouton. Si l'affichage montre quelque chose de différent notre bouton ajoutera un autre chiffre au numéro existant, c'est bon ? Vous pouvez lancer le projet pour voir ce que je veux dire. J'ai limité les nombres qui peuvent être entrés à 5 chiffres. Chaque fois nous ajoutons un chiffre, la fonction align_result ()... Aligne le résultat. Nous en parlerons plus tard.

if (key_number >= 11 && key_number <= 14)
{
display_text.pos_x
= 380; // réinitialise pos_x
str_cpy(result_string, "0");
number_one = str_to_num(result_string);
add = (key_number == 11);
substract = (key_number == 12);
multiply = (key_number == 13);
divide = (key_number == 14);
}

Si nous pressons "+", "-", "x" ou ":" display_text doit se déplacer à sa position initiale parce que l'affichage montrera "0". Le premier nombre apparaît sur l'affichage avant une pression sur "+", "-", "x" ou ":" et il est stocké dans number_one; le nombre apparaît sur l'affichage avant une pression sur "=" et il est stocké dans number_two. Nous avons appris que quand nous cliquons sur un des boutons, nous continuons à ajouter des chiffres sur l'affichage. En fait, nous continuons à ajouter des éléments à result_string et maintenant il est temps de convertir ce string en un nombre - > number_one.

Si nous avons cliqué "+", add sera mis à 1 et substract, multipliy et divide seront mis à 0.

if (key_number == 15) // nous avons pressé "="
{
number_two = str_to_num(result_string);
result = (number_one
+ number_two) * add + (number_one - number_two) * substract + (number_one * number_two)
* multiply + (number_one / number_two) * divide;
if (result < 99999)
{
str_for_num(result_string, result);
}
else // overflow
{
str_cpy(result_string, "ERROR");
}
align_result();
number_one = 0;
number_two = 0;
add = 0;
substract
= 0;
multiply = 0;
divide = 0;
while (key_any == 0) {wait (1);} // n'efface pas le résultat - attends
le prochain clique de souris
str_cpy(result_string, "0"); // réinitialise l'afficheur
display_text.pos_x = 380; // réinitialise pos_x
}
}

Quand nous pressons "=", result_string (le string montrée à l'écran avant pression sur "=") sera converti en un nombre et stocké dans number_two. Maintenant nous devons faire les maths réelles en employant une formule simple :

result = (number_one + number_two) * add + (number_one - number_two) * substract + (number_one * number_two) * multiply + (number_one / number_two) * divide;

Bien, c'est une formule simple si nous voulons ajouter que 2 nombres add = 1 et substract, multiply et divide = 0 donc notre formule change pour :

result = number_one + number_two

Et cela est mieux, n'est-ce pas ? La même chose arrive si nous choisissons "-", "x" ou ":".

Si le résultat de l'opération est en dessous de 99,999 (nous avons décidé d'employer 5 chiffres, vous vous souvenez?) le nombre sera converti en result_string donc il apparaîtra sur l'affichage. Si le résultat excède 99,999 l'exposition montrera "ERROR". Le résultat sera aligné employant la mystérieuse fonction align_result() et ensuite nous réinitialisons number_one, number_two, etc. Tant que nous n'avons pas pressé de touches / le bouton de la souris, le résultat sera montré à l'écran; quand nous pressons une touche ou nous cliquons sur un bouton de souris, l'affichage sera remis à zéro et result_string sera déplacé à sa position initiale.

Je ne peux pas supporter la pression! Je dois vous montrer la fonction suivante :

function align_result()
{
display_text.pos_x = 380 - (str_len(result_string) - 1) * 18; // déplace / aligne le texte à gauche quand plus de chiffres apparaissent / disparaissent
}

Cette fonction est simple mais elle fait son travail : elle aligne le texte à gauche quand plus de chiffres apparaissent ou disparaissent. Je sais qu'un caractère a une largeur de 18 pixels ( jetez un coup d'oeil dans la définition lcd_font si vous ne me croyez pas) ainsi je soustrais 18 * (number_of_digits - 1) de 380 pixels pour déplacer le texte qui montre result_string à gauche ou à droite.

Rappelez-vous s'il vous plaît que cette calculatrice a besoin de plus de code de gestion des erreur pour la division par zéro et cetera. À ce point les opérations doivent être exécutées comme cela :

32 x 2 = ( affichera 64) et ensuite vous devrez taper 64 + 7 = (affichera 71)

Vous ne pouvez pas employer 32 x 2 + 7 =... Mais si vous voulez améliorer le code vous pouvez remplacer number_one et number_two par un grand tableau et stocker les nombres provisoires à l'intérieur de celui_ci.


Untitled

Caméras de sécurité

Ce petit article vous montre combien il est facile d'implanter une caméra de sécurité. Nous utiliserons une caméra qui balaye le joeur mi auparavant je vous propose de regarder un schéma de scan_entity en action. Cette précieuse instruction peut scanner (balayer) toutes les entités dans un secteur donné. Nous pouvons gérer l'angle de balayage en vertical et en horizontal; nous pouvons également paramétrer la distance de balayage:

J'ai créé une caméra de surveillance en utilisant un modèle de microscope; n'y voyez aucune allusion avec un programmer à la recherche d'un modèle :)
L'action attachée à la caméra est security_camera:

action
security_camera
{
my.skill1 = my.pan;
init_alarm();
while (1)
{
while (my.pan <
my.skill1 + 60)
{
my.pan += 1 * time;
if (random(1) > 0.9)
{
start_scanning();
}
wait (1);
}
while (my.pan >
my.skill1 - 60)
{
my.pan -= 1 * time;
if (random(1) > 0.9)
{
start_scanning();
}
wait (1);
}
wait (1);
}
}

Nous stockons l'angle pan initial de la caméra dans son skill1, nous appelons la fonction init_alarm (nous la verrons plus tard) et nous tournons la caméra de 60 degrés autour de son anglepan de départ, vers la gauche et vers la droite - ce qui donne un angle de balayage de 120 degrés.. Scan_entity est une instruction qui consomme beaucoup de ressources systèmes aussi nous ne devons pas l'appeler à chaque cycle d'affichage. Une des solutions consiste à remplacer wait (1) par waitt (3) ou quelque chose d'approchant mais le mouvement de la caméra va être saccadé. Il y a plusieurs autres solutions pour régler le problème; la plus facile et de générer un nombre aléatoire et lorsqu'il est plus grand qu'une certaine valeur (0.9 dans mon exemple) nous appelons la fonction qui gère le balayage. Vous pouvez ajuster la vitesse de rotation en changeant 1 * time.

Ok, maintenant nous avons un nombre aléatoire à chaque cycle; lorsque sa valeur est supérieure à 0,9 (ce qui arrivera plusieurs fois par seconde) la fonction start_scanning est exécutée:

function
start_scanning()
{
temp.pan = 45;
temp.tilt = 90;
temp.z = 700;
scan_entity(my.x, temp);
if ((result > 0) &&
(result < 500))
{
my.light = on;
my.lightred = 250;
my.lightgreen =
0;
my.lightblue =
0;
my.lightrange =
0;
wait (2);
my.light = off;
alarm = 1;
}
}

Nous paramétrons l'angle de balayage horizontal à 45 degrés, angle de balayage vertical à 90 degré et la distance de balayage à 700 quants. Scan_entity donnera en retour la distance entre la caméra et l'entité proche, si l'entité est à moins de 500 quants. Si la caméra a détecté le joueur, elle change sa couleur en rouge pendant 2 cycles et, le plus important, la variable nommée alarm est mise à 1.

Je vous avais dit que nous reparlerions de la fonction init_alarm un peu plus tard, nous y voici:

function
init_alarm()
{
while (alarm == 0) {wait (1);}
snd_loop(alarm_wav, 100, 0);
}

La fonction est appelée au départ du jeu et elle attend que la variable alarm soit à 1. Dés que cela se produit, un son d'alarme est joué en boucle. C'est l'endroit ou vous devez alerter l'ennemi, fermer des portes, éteindre le moteur, que sais-je encore, c'est à vous de décider.


Untitled

Caméras Intelligentes

Vous êtes nombreux à me réclamer une caméra à la Resident Evil. Je vous propose une solution simple où il y a en permanence une caméra tournée vers le joueur et à la condition d'en mettre un nombre raisonnable dans votre niveau le héros sera vu en permanence.

Cet exemple a été créé pour 50 caméras mais vous pouvez cependant, en avoir autant que vous voulez. Pour tester le code, incluez recam.wdl et .fixed_cameras() dans votre Main. Placez quelques modèles factices dans votre niveau (j'ai employé ici fish.mdl) pour faire office de caméra mettez leurs angles pan, tilt et roll et attalchez leur l'action recam. N'oubliez pas de mettre skill1 pour ces modèles à 1 à 50 ; il n'y a pas d'ordre particulier ; assurez-vous juste que vous n'ayez pas deux caméras avec la même valeur de skill1 dans Wed. La valeur mise pour skill1 est employée par wdl pour choisir la caméra avec la meilleure vue à un certain moment. Si vous voulez, vous pouvez charger et exécuter mon office.wmp modifié pour voir un exemple.

Je ne veux pas modifier les calibres aussi je dois neutraliser la vue courante et permettre la nouvelle vue re_camera. Jetons d'abord un coup d'oeil à l'action de recam :

action
recam
{
my.invisible = on;
my.passable = on;
re_camera.tilt = my.tilt;
re_camera.roll = my.roll;
if (my.skill1 == 0) {remove me; beep; beep; return;}
while (1)
{
camera_position[my.skill1]
= vec_dist(my.x, player.x);
if (winner
> camera_position[my.skill1] - 50)
{
re_camera.x = my.x;
re_camera.y = my.y;
re_camera.z = my.z;

vec_set(temp, player.x); // re_camera's pan
vec_sub(temp, my.x);
vec_to_angle(re_camera.pan, temp);
}
wait (1);
}
}

Les modèles factices sont rendus invisibles et passables ; leur orientation (tilt et roll) est employée par re_camera. Pan est calculée àchaque cycle pour faire que la caméra regarder le joueur sans la manquer une seconde.

J'emploie le tableau camera_position[50 ] pour stocker la distance entre chaque caméra et le joueur ; camera_position[1 ] est la distance entre le joueur et la caméra qui a son skill1 réglé à 1 dans Wed, camera_position[35 ] est la distance entre le joueur et la caméra qui a son skill1 réglé à 35 et ainsi de suite. J'emploie vec_dist(my.x, player.x) pour calculer la distance entre le joueur et la caméra. J'ai déclaré une variable nommée winner - c'est la plus petite distance entre le joueur et n'importe quelle caméra. Si nous trouvons un winner, nous mettons re_camera aux nouvelles coordonnées (de poissons) et nous le tournons constamment vers le joueur en utilisant une instruction simple de vec_to_angle

function
fixed_cameras()
{
camera.visible = off;
re_camera.visible = on;
while (1)
{
temp_counter
+= 1;
if (temp_counter
== 50)
{
temp_counter -= 49;
}
if (camera_position[temp_counter]
< winner && camera_position[temp_counter] > 0)
{
winner = camera_position[temp_counter];
found_camera = temp_counter;
}
else
{
winner = camera_position[found_camera];
}
wait (1);
}
}

La fonction fixed_cameras inclut une boucle while qui recherche constamment des winner. Si la distance entre la caméra et le joueur est plus petite que winer, cette caméra sera activée. Nous devons nous assurer que camera_position[temp_counter] est plus grand que zéro parce que notre niveau pourrait utiliser moins de 50 caméras - toutes les valeurs inutilisées dans le tableau sont zéro mais ceci ne devrait pas leur faire des winner.


Untitled

Champ de force

J'adore champ de force! C'est un jeu qui fera fonctionner la pompe à adrénaline. Et ce n'est pas trop dur à créer par ailleurs.

Pour commencer avant tout, nous avons besoin de créer la partie du champ de force qui apparaît jolie.

action
force_field
{
var number_of_particles = my.skill1;
var particle_distance = my.skill2;
if (my.skill1 == 0) {number_of_particles = 600;}
if (my.skill2 == 0) {particle_distance = 0.2;}
vec_set (temp, my.pos);
while (number_of_particles
> 0) // don't use a wait(1) here
{
temp.x += particle_distance
* cos(my.pan);
temp.y += particle_distance
* sin(my.pan);
effect(particle_barrier,
1, temp, normal);
number_of_particles
-= 1;
}
}

Nous définissons 2 variables locales : une stockera le nombre de particules utilisées pour le champ de force (en l'occurence la longueur du champ de force) et la seconde stockera la distance entre 2 particules consécutives. Si vous oubliez de paramétrer ces valeurs en utilisant skill1 et skill2 nous mettrons des valeurs par défaut (600 et 0.2). Nous mettons temp à la position de l'entité champ de force puis nous démarrons la génération des particules dans une boucle while (sans wait) jusqu'à ce que toutes les particules (valeur skill1) soient générées.

Il est temps de voir la fonction relative aux particules:

function
particle_barrier()
{
my.bmap = redflare_pcx;
my.flare = on;
my.bright = on;
my.size = 10;
my.function = keep_particles;
}

function keep_particles()
{
my.lifespan = 10;
my.alpha = 30 + random(70);
}

La seule chose que nous pouvons observer est le fait que lifespan est mis à 10 tout le temps, afin de garder les particules en vie. Oh et nous mettons un facteur alpha aléatoire pour montrer quelques activités laser. Tout ceci est bien joli bien entendu mais comment nous tuons le joueur?

La méthode la plus simple et d'utiliser une entité invisible qui se supperposera au champ de force. Voici le code pour cela:

action
killer
{
my.invisible = on;
my.enable_impact = on;
my.enable_entity = on;
my.event = kill_them;
}

function
kill_them()
{
wait (1);
if (you != null)
{
you._health = -10;
}
}

L'entité tueuse est une simple entité wmb qui est sensible aux autres entités. Si elle se cogne à une autre entité (you != null) ilmet la santé de cette entité à -10, la tuant instantanément.

Comment allez-vous utiliser un champ de force, une barrière laser comme celle de mon exemple?
1) Placez une petite entité dans votre niveau et attachez-lui action force_field pour créer l'effet. Ce sera le générateur de particules, aussi vous devriez placer une autre entité identique à l'extrémité du faisceau de particules pour que ce soit plus convaincant pour le regard.
2) Créer une entité wmb qui couvre votre ou vos champ(s) de force, placez le dans votre niveau et attachez-lui l'action nommée killer. Cette entité aura le rôle de tueur.

Le niveau d'exemple inclu dans le fichier zip est fourni avec 3 générateurs de particules (3 champs de force) et une grosse entité tueuse.


Untitled

Chars

Le projet autonome suivant vous aidera à démarrer si vous projetez de créer un jeu de chars.

La fo nction main est très simple: elle désactive la touche "D" parce que nous l'utilisons pour déplacer le joueur (WSAD) et chargeons le niveau:

function
main()
{
on_d = null; // disable the standard "debug" key
level_load (level1_wmb);
}

Le char qui est controlé par le joueur est en 3 parties : le corps, la tourelle et le canon. La tourelle et le canon sont attachésau corps en utilisant 2 instructions ent_create dans l'action player_tank:

ent_create(challenger_turret, nullvector, attach_player_turret);
ent_create(challenger_barrel, nullvector, attach_player_barrel);

Jetons un oeil à la fonction qui attache ces parties du char au corps:

function attach_player_turret()
{
my.passable = on;
while(you) // as long as
the creator exists
{
vec_set(my.x, you.x);
my.pan = you.pan
+ turret_offset;
my.frame = you.frame;
my.next_frame =
you.next_frame;
wait(1);
}
ent_remove(my);
}

La tourelle est passable et n'existe que si le corps existe. La tourelle a tout le temps la même position et animation de frame que le corps. Il y a quelque chose d'intéressant avec cette ligne de code:

my.pan = you.pan + turret_offset;

Le corps et la tourelle peuvent tourner à différents angles, comme sur l'image ci-dessous; turret_offset est la différence entre leurs angles panà un moment donné.

Dans mon exemple, turret_offset = 60 degréss. La fonction attach_player_barrel() est identique:

function attach_player_barrel()
{
my.passable = on;
while(you) // as long as
the creator exists
{
vec_set(my.x,you.x);
my.pan = you.pan
+ turret_offset;
my.tilt = you.tilt
+ barrel_offset;
my.frame = you.frame;
my.next_frame =
you.next_frame;
vec_for_vertex(rocket_coords,
my, 33); // vertex for player's rocket
if (mouse_left
== 1 && procket_ptr == null && player.shield > 0)
{
ent_create (tank_rocket, rocket_coords, player_rocket);
snd_play (tankrocket_snd, 100, 0);
}
wait(1);
}
ent_remove(my);
}

Le canon doit avoir le même angle pan que qu'avec la tourelle aussi il utilise la même variable turret_offset mais il peut avoir un angle tilt différent et sa valeur est stockée dans barrel_offset. Le canon est le générateur de roquettes aussi nous utilisons son 33ème vertex comme point de départ pour toutes les roquettes tirées par le joueur.

J'ai voulu interdire le tir automatique (autofire) car ça ne serait par classe (et pas très réaliste) de tirer plusieurs roquettes à la seconde. Je fais cela en assignant un pointeur - procket_ptr - à la roquette lorsqu'elle est créée. Si la roquette explose et disparaît, procket_ptr est mis à null et nous pouvons en créer une autre. Si nous pressons le bouton gauche de la souris (LMB) et que la roquette créée précédemment a disparu et que le joueur char est toujours vivant, nous créons une roquette et nous jouons un son.

Jetons un oeil à l'action associée au joueur:

action
player_tank
{
player = me;
my.enable_impact = on;
my.enable_entity = on;
my.event = tank_damaged;
ent_create(challenger_turret,
nullvector, attach_player_turret);
ent_create(challenger_barrel, nullvector, attach_player_barrel);
my.shield = 250; // 250 shield points for player's tank

Je suis le joueur et je suis sensible aux impacts ou aux autres entités. Lorsque l'un de ces évènements intervient, la fonction tank_damaged() est exécutée. J'ai 250 points de bouclier (shield); shield est juste un autre nom pour skill20. La tourelle et le canon ont été discuté plus haut.

     while (my.shield > 0) // the 10th hit will
destroy player's tank
{
camera.pan = my.pan;
camera.x = my.x
- 500 * cos (my.pan);
camera.y = my.y
- 500 * sin (my.pan);
camera.z = my.z
+ 300;
camera.tilt = -20;
// the camera looks down

          if (mouse_force.x
!= 0) // if we move the mouse on x
{
if (turret_handle == 0)
{
turret_handle = snd_play (turret_snd, 70, 0);
}
turret_offset -= 2 * mouse_force.x * time;
}
else
{
turret_handle = 0;
}
if (mouse_force.y
!= 0) // if we move the mouse on y
{
barrel_offset += 2 * mouse_force.y * time;
}

Aussi longtemps que mon bouclier est plus grand que zero, la caméra me suit, étant placé 500 quants derrière moi et 300 quants au dessus de moi. La caméra pointant vers moi.

Si vous bougez la souris sur l'axe des x et que le son de la tourelle n'est pas entrain d'être joué, nous jouons le son et nous changeons turret_offset - ce qui change l'angle pan de la tourelle et du canon en même temps. Si on bouge la souris sur l'axe des y, nous changeons barrel_offset - l'angle tilt pour le canon.

          if (player_speed.x
!= 0) //
{
my.pan -= 0.1 * (key_d - key_a) * player_speed.x * time;
if (engine_handle == 0)
{
engine_handle = snd_loop (tank_snd, 50, 0);
snd_tune (engine_handle, 25, 100, 0); // original frequency (100%)
my.skill10 = 100;
}
}
else
{
my.skill10 -= 5 * time;
snd_tune (engine_handle, 25, my.skill10, 0);
if (my.skill10 < 20) // below 20% of the original frequency
{
snd_stop (engine_handle);
engine_handle = 0;
}
}

Le charge modifie son angle pan lorsque vous pressez les touches "Q" et"D". Notez que le char ne peut se tourner que s'il est en mouvement comme (pratiquement) tous les véhicules à roues. Si le son du moteur du char n'est pas en train d'être joué, nous démarrons le son à sa fréquence initiale. Si le char n'est pas en mouvement (les touches ("Z" ou"S" ne sont pas pressées) la fréquence du son est diminuée (en utilisant skill10 et snd_tune) jusqu'à sous 20% de la fréquence originale puis il est arrêté.

          player_speed.x
= 1.5 * (key_w - key_s) * time + max (1 - time * 0.15, 0) * player_speed.x;
player_speed.y
= 0;
player_speed.z
= 0;
move_mode = ignore_you
+ ignore_passable;
ent_move(player_speed,
nullvector);
wait (1);
}
}

Le joueur se déplace avec "Z" et"S" enutilisant une formule qui a déjà été commentée dans les éditions précédentes d'AUM; changez 1.5 pour modifier la vitesse et 0.15 pour modifier l'inertie.

Lorsque le joueur presse le bouton gauche de la souris (LMB), une roquette est crée:

function player_rocket()
{
procket_ptr = me;
my.skill40 = 1; // it is a rocket
my.pan = you.pan;
my.tilt = you.tilt;
my.enable_impact = on;
my.enable_block = on;
my.enable_entity = on;
my.event = explode_rocket;
my.skill5 = 0; // used for vertical movement
procket_speed.x = 100;
procket_speed.y = 0;
procket_speed.z = 0;
procket_speed *= time;
while (my != null)
{
my.skill5 += time;
if (my.skill5 >
20)
{
procket_speed.z -= fall_speed * time;
}
move_mode = ignore_you
+ ignore_passable; // ignores the barrel -> can't collide with it
ent_move (procket_speed,
nullvector);
wait (1);
}
}

Chaque roquette utilise le pointeur procket_ptr pour interdire le tir automatique, vous vous souvenez? En mettant skill40 à 1 nous pouvons identifier si une entité donnée est une roquette ou non - nous utiliserons cette fonctionnalité plus tard. La roquette à la même orientation (pan et tilt) que le canon est est sensible à l'impact, aux blocs de niveau et autres entités. Si un de ces évènements se produit, la fonction explode_rocket() est exécutée. La roquette se déplace à 100 * time quants à chaque cycle.

Tant que la roquette n'a pas explosé, skill5 est incrémenté dans une boucle et la roquette se déplace. Lorsque skill5 dépasse 20, la roquette commence à perdre de la hauteur (l'effet de la gravité) et si elle n'a rien touché jusque là elle touchera le sol. Jouez avec fall_speed et avec le 20 pour obtenir différentes trajectoires.

Lorsque la roquette touche le sol, un mur ou une autre entité elle explose en utilisant la fonction suivante:

function
explode_rocket()
{
my.skill10 = vec_dist (player.x,
my.x);
if (my.skill10 < 1000) {my.skill10 = 1000;}
my.skill10 = 100000 / my.skill10;
snd_play (tankexplo_snd,
my.skill10, 0);

     vec_set (temp, my.pos);
temp.z += 50; // create the
explosion sprite a little higher
ent_create(explosion_pcx, temp, animate_explosion);
ent_remove (me);
}

J'ai décidé de remplacer la fonction ent_playsound par une qui fonctionne mieux dans ce jeu particuliers. La roquette prend la distance entre elle et le joeur et stocke l'information dans son skill10. Si la distance est en dessous de 1000 quants, le son de l'explosion est à son volume maximal autrement le volume sera moins fort dépendant de la valeur stockée dans skill10.

Nous avons à créer une explosion au point d'impact, n'est-ce pas? Nous le faisons en utilisant un sprite explosion et la fonction animate_explosion; notez que le sprite est créé 50 quants au dessus du point d'impact ce qui permet de le voir entièrement:

function
animate_explosion()
{
my.passable = on;
my.ambient = 100;
while (my.frame < 7)
{
my.frame += 1 *
time;
wait (1);
}
ent_remove (me);
}

Le sprite est passable et lumineux (bright) et joue ses frames; lorsqu'il atteint sa dernière frame il est enlevé.

Que se passe-t-il si un char est atteint?

function
tank_damaged()
{
if (you.skill40 != 1) {return;}
wait (1);
my.shield -= 25;
if (my.shield <= 0) //
player's tank was hit 10 times or the enemy tank was hit twice
{
while (1)
{
effect (tank_smoke, 5, my.pos, normal);
sleep (0.2);
}
}
}

Le char peut se cogner avec une autre entité, cela peut-être une fontaine, un autre char etc. Si cette entité n'a pas son skill40 de mis à 1 la fonction retourne. Chaque roquette qui le touche lui fait perdre 25 points de bouclier aussi le joueur mourra après avoir été touché 10 fois (player.shield = 250, souvenez-vous?)

Si le joueur est touché 10 fois ou si le char ennemi est touché 2 fois (leur bouclier est à 50) nous démarrons une émission de fumée:

function tank_smoke()
{
temp.x = random(2) - 1;
temp.y = random(2) - 1;
temp.z = random(2) + 1;
vec_add (my.vel_x, temp);
my.alpha = 30 + random(30);
my.bmap = tanksmoke_pcx;
my.size = 100;
my.flare = on;
my.move = on;
my.lifespan = 100;
my.function = fade_smoke;
}

function
fade_smoke()
{
my.alpha -= 0.5 * time;
if (my.alpha < 0)
{
my.lifespan = 0;
}
}

La fumée à une vitesse aléatoire en x et en y et une vitesse positive en z ce qui lui permet de monter. Les particules de fumées décrémentent leur valeur alpha et lorsqu'elles sont devenues invisibles elles sont enlevées.

Maintenant il est temps de voir le char ennemi. J'utilise un modèle qui a sa tourelle et son canon d'attachés au coprs afin de rendre les choses plus simples pour vous.

action
enemy_tank
{
my.enable_impact = on;
my.enable_entity = on;
my.event = tank_damaged;
my.shield = 50;

Le char est sensible à l'impact et autres entités et partage la même fonction tank_damaged que le joueur. 2 roquettes tirées par le joueur le détruront.

while (my.shield > 0)
{
vec_set (temp.x, player.x);
vec_sub (temp.x, my.x);
vec_to_angle (my.skill4, temp); // turn towards the player

Tant que le char ennemi est vivant il tourne autour du joueur,essayant de tirer sur lui. Nous ne voulons pas une brusque rotaion mais quelque chose qui se fasse en douceur aussi nous ne tournons pas le char vers le joueur et nous utilisons skill4. Maintenant nous savons que skill4 contient l'angle correct pour se tourner vers le joueur mais nous voulons une rotaion douce comme dans la réalité.

          if (abs(my.skill4
- my.pan) < 1)
{
vec_for_vertex(my.skill1, my, 41); // vertex for the enemy rocket
ent_create (tank_rocket, my.skill1, enemy_rocket);
sleep (3);
}
else
{
my.pan += ang(my.skill4 - my.pan) * 0.03 * time; // smooth rotation code
enemy_speed.x = 5 * time;
enemy_speed.y = 0;
enemy_speed.z = 0;
move_mode = ignore_you + ignore_passable;
ent_move(enemy_speed, nullvector);
}
wait (1);
}
}

Si la différence entre l'angle stocké dans skill4 et l'angle pan actuel du char ennemi est inférieur à 1 degré, nous mettons le point de départ pour la roquette ennemi à son 41ème sommet sur le char et nous tirons une roquette toutes les 3 secondes. Si la différence entre les 2 angles est supérieure à 1 degré le char se tourne doucement et se déplace en mêm temps, comme un véhicule à roues.

La fonction pour la roquette ennemie est différente car l'ennemi tire une roquette toutes les 3 secondes:

function
enemy_rocket()
{
my.skill40 = 1; // it is a rocket
my.pan = you.pan;
my.tilt = you.tilt;
my.enable_impact = on;
my.enable_block = on;
my.enable_entity = on;
my.event = explode_rocket;
erocket_speed.x = 100;
erocket_speed.y = 0;
erocket_speed.z = 0;
erocket_speed *= time;
while (my != null)
{
move_mode = ignore_you
+ ignore_passable; // ignores the barrel -> can't collide with it
ent_move (erocket_speed,
nullvector);
wait (1);
}
}

Nous mettons skill40 à 1; de cette façon l'entité est détectée comme étant une roquette. La roquette ennemie à le même angle pan et tilt que le char ennemi, elle est sensible aux impacts, autres blocs et autres entités et elle a la même gestion d'évènement que les roquettes tirées par le joueur. Notez que sa trajectoire est rectiligne, sans gravité aussi elle atteint votre char assez souvent.

Avant de transformer cette ossature de jeu en un superbe jeu monnayable vous devez ajouter la gravité et une meilleure intelligence artificielle à vos ennemis. N'oubliez pas de m'envoyer une copie grauite de votre jeu :)


Untitled

Combat aérien

Cette fois nous avons (du moins je l'espère) un superbe jeu de combat joueur contre hélicoptère avec un certain nombre de particularités intéressante. En tant que projet autonome il a sa propre fonction main:

function
main()
{
sky_clip = -90; // rendre le ciel dans son entier
on_d = null; // interdire le panneau de mise au point
level_load (aircombat_wmb);
wait (2);
game_init();
}

La première ligne rend le ciel intégralement; c'est une zone ouverte aussi nous avons besoin de le voir sans coupure. Nous invalidons la touche "D" car nous allons l'utiliser pour nos déplacements, nous chargeons le niveau et lorsqu'il est chargé nous appelons la fonction game_init():

function
game_init()
{
while (player == null) {wait (1);}
while (1)
{
if ((mouse_left
== 1) && (player.health > 0)) // fire
{
snd_play (shoot_snd, 70, 0);
ent_create (rocket_mdl, player.pos, move_bullet);
recoil = 1;
waitt (2);
recoil = 0;
while (mouse_left == 1) {wait (1);} // disable autofire
}
temp_counter +=
1;
if (temp_counter
== 50)
{
temp_counter -= 49;
}
if (chopper_index[temp_counter]
< distance && chopper_index[temp_counter] > 0)
{
distance = chopper_index[temp_counter];
found_chopper = temp_counter;
}
else
{
distance = chopper_index[found_chopper];
}
if (remaining_choppers
== 0)
{
aiming_pan.visible = off;
}
wait (1);
}
}

Cette fonction attend que le joueur soit créé. Lorsque c'est fait une boucle while (1) est démarrée. Si nous pressons le bouton gauche de la souris (LMB) et si le joeur est toujours en vie, un son est joué et une roquette est créée à la position du joueur. Nous avons mis le recul à 1 pour environ 0.1 seconde et nous attendons que le bouton gauche soit relaché; nous verrons le recul un peu plus tard.

Le reste du code traite du système de ciblage automatique (pas besoin de viser). Si l'hélicoptère sélectionné est plus près que les autres et si c'est un hélicoptère valide, la distance qui le sépare du joueur est stockée dans une variable appelée distance, sinon nous conservons l'ancienne distance. Lorsque tous les hélicoptères sont descendus, nous cachons le système de visée.

Nous pouvons avoir jusqu'à 50 hélicoptères; nous mesurons la distance qui les séparence du joueur et l'hélicoptère le plus proche est selectionné comme cible à l'écran. La seule chose que vous ayez à faire c'est de tirer dans le centre de l'hélicoptère qui est selectionné.

Le code qui déplace le joueur et la caméra est inclus dans l'action suivante:

action
player_moves
{
player = me;
my.health = 100;
my.invisible = on;
my.enable_impact = on;
my.enable_entity = on;
my.event = player_event;
while (my.health > 0)
{
vec_set (camera.pos,
my.pos);
camera.tilt +=
20 * mouse_force.y * time;
camera.pan -= 20
* mouse_force.x * time;
my.pan = camera.pan;
my.tilt = camera.tilt;
player_speed.x
= 15 * (key_w - key_s) * time - 0.5 * recoil;
player_speed.y
= 10 * (key_a - key_d) * time;
vec_set (temp,
my.x);
temp.z -= 1000;
trace_mode = ignore_me
+ use_box;
player_speed.z
= -trace (my.x, temp);
move_mode = ignore_you
+ ignore_passable;
ent_move(player_speed,
nullvector);
wait (1);
}
}

Le joueur a 100 points de santé et est invisible puisque nous sommes en mode 1ère personne; il est sensible à l'impact avec les entités. Aussi lontemps que le joueur est vivant, il peut se déplacer en utilisant les touches ZSQD et la souris. Vous pouvez jouer avec les valeurs camera.pan et tilt, player_speed.x et y pour modifier leur sensibilité et leur vitesse. La vitesse pour le déplacement avant est stockée dans player_speed.x; vous souvenz-vous que nous avons mis le recul à 1 pour 0.1 seconde? De cette façon le joueur reculera à chaque fois qu'il tirera une roquette. Vous pouvez changer 0.5 pour avoir différents reculs. Le code mouvement inclus le code de la gravité aussi vous pouvez l'utiliser avec le niveau de votre choix (ici j'utilise un niveau plat).

Si le joueur est touché par une entité (les hélicoptères tirent des sprites) sa fonction player_event est exécutée:

function player_event()
{
wait (1);
exclusive_global;
if (my.health > 0)
{
my.health -= 0.5;
snd_play (playerhit_snd,
40, 0);
}
else
{
my.health = 0;
}
}

Si
le joueur est vivant, sa santé est décrémentée et
un son est joué. Si le joueur est mort, sa santé est mise à
0 car il n'est jamais bon de montrer une santé négative. Regardons
à présent la fonction qui déplace la roquette:

function move_bullet()
{
my.passable = on;
my.enable_entity = on;
my.enable_block = on;
my.event = remove_me;
my.pan = camera.pan;
my.tilt = camera.tilt;
my.skill1 = 0;
rocket_speed.x = 500 * time;
rocket_speed.y = 0;
rocket_speed.z = 0;
while (my.skill1 < 100)
{
if (vec_dist (my.x,
player.x) > 50)
{
my.passable = off;
}
my.roll += 25 *
time;
my.skill1 += 1
* time;
effect (create_smoke,
1, my.x, normal);
move_mode = ignore_you
+ ignore_passable;
ent_move (rocket_speed,
nullvector);
wait (1);
}
ent_remove (me);
}

function
remove_me()
{
waitt (2);
ent_remove (me);
}

La roquette est passable au moment de sa création mais son drapeau passable sera mis à 0 dès que la distance entre elle et le joueur est > 50 quants. La roquette tourne autour de son angle roll et crée un nuage de fumée. Si la roquette voyage trop lontemps sans rien toucher elle est supprimée. C'est également le cas si elle vient à toucher une entité ou un bloc du niveau.

Il
est temps de regarder la fonction qui crée la fumée de la roquette:

function
create_smoke
{
temp.x = random(4) - 2;
temp.y = random(4) - 2;
temp.z = random(3) + 3;
vec_add (my.vel_x, temp);
my.flare = on;
my.bright = on;
my.alpha = 50;
my.bmap = smoke_pcx;
my.size = 30;
my.move = on;
my.lifespan = 100;
my.function = fade_smoke;
}

function
fade_smoke()
{
my.alpha -= 2 * time;
my.size -= 2 * time;
if ((my.alpha < 0) || (my.size < 2)) {my.lifespan = 0;}
}

La fumée se déplace vers le haut; Les particules disparaissent dès que leur alpha atteint 0 ou que leur taille atteigne 2 (ceci bien entendu si vous utilisez mes valeurs, mais vous pouvez les changer).

Chaque hélicoptère est créé par un objet qui a l'action chopper_generator qui lui est attachée.

action
chopper_generator
{
my.invisible = on;
my.passable = on;
waitt (64 + random (64));
ent_create (chopper_mdl,
my.pos, move_chopper);
}

J'ai créé un cube pour chaque génération d'hélicoptère; le cube est invisible et passable. L'hélicoptère est créé 4..8 secondes après que le niveau soit chargé et la fonction qui lui est associé est effrayante :

function
move_chopper()
{
chopper_number += 1;
remaining_choppers += 1;
my.skill1 = chopper_number;
my.enable_impact = on;
my.enable_block = on;
my.enable_entity = on;
my.event = chopper_event;
my.skill40 = 1;

Chaque hélicoptère à un identifiant unique qui lui est associé; nous utilisons une variable nommée chopper_number et chaque fois que move_chopper() est exécutée, chopper_number est incrémentée et le résultat est copié dans skill1. L'hélicoptère réagira s'il entre en collision avec une autre entité ou avec un bloc du niveau. Puis nous mettons skill40 à 1 pour tous les hélicoptères - c'est notre méthode pour détecter si une entité donnée est un hélicoptère ou pas.

     while (my != null)
{
vec_for_vertex(my.skill18,
my, 55);
if (my.skill42
== 0 && abs(my.x + my.y - player.x - player.y) > 2000)
{
vec_set
(temp.x, player.x);
vec_sub
(temp.x, my.x);
vec_to_angle
(my.pan, temp);
}
my.tilt = 0;
ent_cycle("fly",
my.skill10);
my.skill10 += my.skill11 *
time;
if (my.skill11
< 10)
{
my.skill11
+= 0.2 * time; // augmente la vitesse de rotation
}
if (my.skill10
> 100)
{
my.skill10
= 0; // loop animation
}

Aussi longtemps qu'un hélicoptère est "vivant" nous stockons le vertex utilisé pour le tir dans skill18. Si la différence entre les coordonnées du joueur et celle de l'hélicoptère est au dessus de 2000 quants, l'hélicoptère tournera en direction du joueur. Nous n'avons pas besoin de pencher l'hélicoptère parce que vec_to_angle fait que nous mettons tilt à zero. L'hélicoptère tournera son rotor de plus en plus vote parce que skill11 accroit de 0 to 10.

if ((my.skill17 < 3 + random(3)) && (my.skill12 == 0))
{
my.skill15 = 0; // skills 15..17 are used a var here
my.skill16 = 0;
my.skill17 += 0.05 * time;
}
else
{
my.skill12 = 1;
if (my.skill15 < 5 * time)
{
my.skill15 += 2 * time;
}
my.skill16 = 0;
my.skill17 = 0;
if (500 < abs(my.x + my.y - player.x - player.y) < 1000) // près du joueur -> tire des balles
{
my.skill25 += 3 * time; // un simple compteur
my.skill26 = int (my.skill25);
if ((my.skill26 % 100 == 0) && (vec_dist (player.x, my.x) > vec_dist (player.x, my.skill18))) // changez "100" pour ajuster la fréquence du tir
{
ent_create (bullet_pcx, my.skill18, chopper_bullet); // don't fire until the chopper is up
}
}
}

Chaque hélicoptère a une altitude aléatoire; skill15..17 sont utilisées comme variable ici, avec skill15 qui stocke la vitesse en x, skill16 la vitesse en y et skill17 la vitesse en z. Vous pouvez noter que l'hélicoptère ne fait que monter puisque skill17 augmente sa valeur par palier de 3 + random(3); vous pouvez changer cette valeur si vous vouilez avoir d'autres hauteurs. Lorsque l'hélicoptère a atteint sa hauteur maximale, il cesse de monter et se déplace de plus en plus vite dans la direction donnée par son angle pan. Si la différence des coordonnées de l'hélicoptère et du joueur est comprise entre 500 et 1000, l'hélicoptère tirera des balles.

Maintenant nous devons résoudre un autre challenge: nous ne voulons oas que l'hélicoptère tire 50 à 100 balles à la seconde (ce serait pas sympa pour nous) mais nous voulons qu'il se déplace à 50-100 frames à la seconde dans la même boucle. Si nous utilisons un waitt(8) à la fin de notre boucle, l'hélicoptère tirera 2 balles à la secondes, et ce serait bien, mais il se déplacera en jouant son animation seulement 2 fois par seconde et ça c'est pas génial, OK?

La solution est simple: nous utilisons une variable skill et nous incrémentons sa valeur à chaque frame. Lorsque le contenu de cette variable atteint une certaine valeur, alors nous tirons une balle. J'utilise skill26 pour stocker la partie entière de skill25; si skill26 % 100 = 0 (ou si vous préferrez si skill26 = 0 or 100 or 200 or 300 ou multiple de 100..) nous tirons une balle. La bonne nouvelle c'est que vous pouvez changer le 100 par n'importe quelle valeur pour ajuster la fréquence du tir.

Attendez! Je vois dans le branchement if qu'il y a encore une autre condition ! Quelle est cette ligne?

if (.......vec_dist (player.x, my.x) > vec_dist (player.x, my.skill18)

Vous aimeriez le savoir, je ne sais pas si je vais vous expliquer. Mais si, allez.... Cette partie s'arrure que l'hélicoptère n epuisse pas tirer derrière lui. J'aurais pu contrôler les angles du joueur et de l'hélicoptère mais ces magazines sont aussi fait pour vous montrer d'autres façons de faire aussi je vous explique mon truc, regardez cette image :

Si la distance entre le joueur et l'arme de l'hélicoptère (stockée dans skill18) est plus petite que la distance entre le joueur et l'origine de l'hélicoptère alors l'hélicoptère peut tirer. is smaller than the distance between the player and chopper's origin, the chopper will fire. Si la distance entre le joueur et l'origine de l'hélicoptère est plus petite que la distance entre le joueur et l'arme de l'hélicoptère, nous ne pouvons pas tirer car cela signifie que le joueur est derrière l'hélicoptère.

        chopper_index[my.skill1] =
vec_dist(my.x, player.x);
if (distance > chopper_index[my.skill1]
- 4000)
{
vec_set
(temp, my.x);
if
(vec_to_screen (temp, camera)) // if the target is visible
{
aiming_pan.pos_x = temp.x - bmap_width(aiming_pcx) / 2;
aiming_pan.pos_y = temp.y - bmap_height(aiming_pcx) / 2;
aiming_pan.visible = on;
}
else
{
aiming_pan.visible = off;
}
}
move_mode
= ignore_you + ignore_passable;
ent_move(my.skill15, nullvector);
wait (1);
}
}

Le tableau chopper_index range la distance entre chaque hélicoptère et le joueur. Si un des hélicoptères est proche, la cible apparaît à sa position. J'ai diminué la distance de 4000 quants - pour s'assurer que la cible ne change pas rapidement d'une cible à une autre pendant qu'ils viennent de plus en plus près du joueur. Jouez avec cette valeur - elle dépend de la taille de votre niveau.

Si la cible est visible à l'écran, nous plaçons la bonne position pour le panneau viseur, sinon nous cachons le panneau. Finalement les hélicoptères se déplacent en utilisant skills15..17 et ignorent les entités passables.

Si un hélicoptère entre en collision avec une roquette, un mur ou un autre hélicoptère, sa fonction chopper_event() est exécutée:

function chopper_event()
{
if (((event_type == event_entity) || (event_type == event_impact)) &&
(you.skill40 == 1))
{
my.pan -= 180;
my.skill42 = 1;
waitt (64);
my.skill42 = 0;
}
else
{
chopper_index[my.skill1]
= 0;
distance = 20000;
remaining_choppers
-= 1;
ent_playsound (my,
chopperx_snd, 1000);
my.passable = on;
my.ambient = 50;
waitt (2);
ent_create (explo13_pcx,
my.pos, explode_me);
waitt (4);
my.transparent
= on;
ent_create (explo13_pcx,
my.pos, explode_me);
ent_remove (me);
}
}

Si l'hélicoptère se cogne avec une entité et que cette entité est un autre hélicoptère (nous le savons car nous avons mis skill40 à 1 pour tous les hélicoptères)If the chopper collides with an entity and this entity is another chopper (we've set skill40 to 1 for all the choppers, right?), l'hélicoptère disparaîtra pendant 4 secondes puis il réapparaîtra. Cela se produit parce que l'hélicoptère chasse le joueur uniquement si son skill42 est mis à zéro - vérifié au début de la boucle dans la fonction move_chopper.

Si l'hélicoptère touche une roquette, nous n'en voulons plus comme cible et nous lui donnons donc une très grande distance ce qui permet à un autre hélicoptère proche de devenir cible à son tour. Nous jouons un son d'explosion, nous créons 2 sprites qui se supperposent et exécutent la même fonction pour faire une explosion encore plus réaliste et nous enlevons l'hélicoptère.

function
explode_me()
{
my.oriented = on;
my.flare = on;
my.bright = on;
my.ambient = 100;
while (my.frame < 14)
{
if (my.scale_x
< 3)
{
my.scale_x += 0.5 * time;
my.scale_y = my.scale_x;
}
my.frame += 1 *
time;
vec_set (temp.x,
player.x);
vec_sub (temp.x,
my.x);
vec_to_angle (my.pan,
temp);
wait (1);
}
ent_remove (me);
}

Lorsque l'hélicoptère explose il créé 2 sprites; leur taille augmentera jusqu'à 3 et en même temps leur frame d'animation sera exécutée. Les sprites sont orientés vers le joueur pour être sur que l'explosion paraisse énorme; un simple sprite n'était pas suffisant pour l'explosion d'un hélicoptère au dessus de votre tête.

Vous devez commencer à être fatigué? Nous pourrions nous arrêter là mais il reste une fonction dont j'aimerai discuter:

function
chopper_bullet()
{
my.passable = on;
my.oriented = on;
my.flare = on;
my.bright = on;
my.scale_x = 0.5;
my.scale_y = my.scale_x;
my.enable_entity = on;
my.enable_block = on;
my.event = remove_me;

     vec_set (temp.x, player.x);
vec_sub (temp.x, my.x);
vec_to_angle (my.pan, temp);
// la balle se tourne vers le joueur

     my.skill1 = 0;
bullet_speed.x = 100 * time;
bullet_speed.y = 0;
bullet_speed.z = 0;
while (my != null)
{
if (my.roll >
200) {my.passable = off;} // it works :)
my.roll += 25 *
time;
my.skill1 += 1
* time;
move_mode = ignore_you
+ ignore_passable;
ent_move (bullet_speed,
nullvector);
wait (1);
}
}

Cette fonction déplace les balles (sprite) de l'hélicoptère. Les balles sont passables et elles sont tournées vers le joueur au moment de leur création; si vous mettiez ces 3 lignes dans la boucle et non en dehors, le joueur ne pourrait jamais esquiver les balles et il mordrait la poussière à chaque fois. Les balles tournent autour de leur angle roll et lorsque roll l > 200 leur drapeau passable est mis à 0.

J'espère que vous aurez plaisir à jouer avec ce projet !


Untitled

Combat à l'épée

Cette fois-ci j'ai créé un petit niveau où le joueur et un squelette combattent à l'épée. This time I have created a small level where the player and a skeleton fight using swords. Au delà, ce code peut (et doit) vous offrir de précieuses informations sur la façon de créer des combats rapprochés avec des armes tels que couteaux, épées, poing etc... Vous savez que je n'aime pas les fonctions main trop compliquées :

function
main()
{
level_load (sword_combat_wmb);
wait (2);
clip_size = 0;
on_d = null;
fps_max = 40;
}

Nous allons déplacer notre joueur en utilisant les touches ZSQD aussi j'ai inhibé le panneau debug qui apparaissait lors de l'appui sur la touche 'D'. L'action attachée au joueur est :

action
player_fight
{
player = me;
player.healthpoints = 100;
while (player.healthpoints
> 0)
{
camera.x = player.x
- 200 * cos(player.pan);
camera.y = player.y
- 200 * sin(player.pan);
camera.z = player.z
+ 200;
camera.pan = player.pan;
camera.tilt = -30;

Ces lignes disent: jesuis le joueur et j'ai 100 points de vie (healthpoints est simplement un autre nom pour skill18 dans mon exemple). Aussi longtemps que mes points de santé sont supérieurs à 0, la boucle while continuera d'être exécutée. Les lignes suivantes placent la caméra 200 quants derrière le joueur en permanence.

my.pan += 4 * (key_a - key_d) * time - 20 * mouse_force.x * time;
player_distance.x = 10 * (key_w - key_s) * time;
player_distance.y = 0;
player_distance.z = 0;

Le joueur peut tourner en pressant les touches Q et D ou en bougeant la souris. Vous pouvez avancer ou reculer en utilisant les touches Z et S. Player_distance est une variable utilisée dans l'instruction ent_move pour déplacer le joueur.

          if ((key_w == 1) || (key_s
== 1))
{
ent_cycle("walk", my.skill20);
my.skill20 += 4 * time;
if (my.skill20 > 100) {my.skill20 = 0;}
}
else
{
ent_cycle("stand", my.skill21);
my.skill21 += 2 * time;
if (my.skill21 > 100) {my.skill21 = 0;}
}
ent_move(player_distance,
nullvector);

Si le joueur marche (Z ou S de pressée) nous jouons son animation "walk". Si ni Z, ni S ne sont pressées le joueur est en position d'attente aussi nous jouons son animation "stand". Et au final nous utilisons player_distance pour déplacer le joueur.

if (mouse_left == 1)
{
while (my.skill22 < 100)
{
ent_vertex(my.sword_tip, 315); // obtenez la valeur dans MED
ent_vertex(my.sword_base, 293); // obtenez la valeur dans MED
trace_mode = ignore_me + ignore_passable;
trace (my.sword_base, my.sword_tip);
if (result != 0)
{
effect (particle_sparks, 10, target, normal);
if (you != null) {you.healthpoints -= 6 * time;}
ent_playsound (my, sword_snd, 50);
}
ent_cycle("attack", my.skill22);
my.skill22 += 8 * time;
wait (1);
}
while (mouse_left == 1) {wait (1);}
}
wait (1);
}

Si nous pressons le bouton gauche de la souris, nous entrons dans la boucle d'attaque. Cette boucle fonctionnera jusqu'à ce que le joueur ai joué toute son animation "attack", stockée dans skill22. Nous stockons la base de l'épée et son extrémité (juste un autre nom pour skill12 et skill15) et nous traçons entre ces 2 positions. Si result != 0 (le joueur a tapé quelque chose) nous générons des particules au point d'impact en utilisant le vecteur prédéfini "target" (cible). Si le joueur a touché une autre entité (pas un mur, etc) sa santé est décrémentée et un bruit d'épée est joué. Le joueur exécute toute son animation "attack" et après que nous ayons relâché le bouton gauche de la souris nous revenons à la normale.

     while (my.skill23
< 90)
{
ent_cycle("death",
my.skill23);
my.skill23 += 3
* time;
wait (1);
}
my.passable = on;
}

A ce point le joueur est mort, aussi l'animation "death" du joueur est jouée et il devient passable, ce qui fait qu'il ne peut plus être touché par l'épée de l'ennemi à partir de maintenant car trace est mis pour ignorer les entités passables.

L'action attaché à l'ennemi est similaire, j'ai utilisé un pointeur nommé "enemy" mais si vous prévoyez d'utiliser plusieurs ennemis remplacez "enemy" par "my":

action
enemy_fight
{
enemy = me;
enemy.healthpoints = 100;
while (my.healthpoints >
0)
{
if (vec_dist (my.x,
player.x) < 200 && player.healthpoints > 0)
{
vec_set(temp, player.x);
vec_sub(temp, my.x);
vec_to_angle(my.pan, temp);
my.tilt = 0;
enemy_distance.x = 5 * time;
enemy_distance.y = 0;
enemy_distance.z = 0;
ent_move(enemy_distance, nullvector);
ent_cycle("walk", my.skill19);
my.skill19 += 5 * time;
if (my.skill19 > 100) {my.skill19 = 0;}

L'ennemia 100 points de vie également; si le joueur est vivant et s'approche à moins de 200 quants, l'ennemi se tourne vers lui et commence à le poursuivre en jouant son animation "walk" dans une boucle.

               if
(vec_dist (my.x, player.x) < 50)
{
while (my.skill20 < 100)
{
ent_vertex(my.sword_tip, 291); // obtenez la valeur dans MED
ent_vertex(my.sword_base, 306); // obtenez la valeur dans MED
trace_mode = ignore_me + ignore_passable;
trace (my.sword_base, my.sword_tip);
if (result != 0)
{
effect (particle_blood, 2, target, normal);
if (you != null) {you.healthpoints -= 4 * time;}
ent_playsound (my, sword_snd, 50);
}
ent_cycle("attack", my.skill20);
my.skill20 += 5 * time;
wait (1);
}
waitt (6); // ralentit l'ennemi et réduit le nombre de trace par seconde
}
}

Si le joueur est à moins de 50 quants, l'ennemi l'attaque en utilisant un code similaire et une fonction de particules différente. L'ennemi cause moins de dommages - enemy will attack using the similar code and a different particle function. The enemy will cause less damage - le joueur passera de toute façon un mauvais 1/4 d'heure :).

          else // le joueur
est à plus de 200 quants
{
ent_cycle("stand", my.skill21); // joue l'animation d'attente
my.skill21 += 2 * time; // vitesse de l'animation "stand"
if (my.skill21 > 100) {my.skill21 = 0;} // boucle animation
}
wait (1);
}
while (my.skill22 < 80) // l'ennemi est mort
{
ent_cycle("death",
my.skill22); // nous jouons l'animation de mort de l'ennemi
my.skill22 += 1
* time; // vitesse de l'animation "death"
wait (1);
}
my.passable = on; // le corps ne peut plus être
touché par notre épée à partir de maintenant
}

Si le joueur est à plus de 200 quants, l'ennemi revient à son animation d'attente ( "stand"). L'ennemi meure en partageant le même code que le joueur.

Les fonctions qui générent les particules (sang du joueur et sang du squelette) sont similaires :

function
particle_sparks()
{
temp.x = random(2) - 1;
temp.y = random(2) - 1;
temp.z = random(1) - 1.5;
vec_add (my.vel_x, temp);
my.alpha = 30 + random(50);
my.bmap = spark_map;
my.size = 10;
my.flare = on;
my.bright = on;
my.move = on;
my.lifespan = 20;
my.function = fade_particle;
}

function
particle_blood()
{
temp.x = random(2) - 1;
temp.y = random(2) - 1;
temp.z = random(1) - 1.5;
vec_add (my.vel_x, temp);
my.alpha = 70 + random(30);
my.bmap = blood_map;
my.size = 6;
my.flare = on;
my.bright = on;
my.move = on;
my.lifespan = 20;
my.function = fade_particle;
}

function
fade_particle()
{
my.alpha -= 5 * time;
if (my.alpha < 0) {my.lifespan = 0;}
}

Les particules sont générées dans une zone située près du point d'impact et tombent (temp.z < 0). Jetons à présent un coup d'oeil au panneau et au texte:

string health_str = "Player Health: Enemy Health:";

panel health_panel
{
pos_x = 0;
pos_y = 0;
digits = 120, 575, 4, swc_font,
1, player.healthpoints;
digits = 550, 575, 4, swc_font, 1, enemy.healthpoints;
flags = refresh, visible;
}

text
health_text // affiche le texte
{
pos_x = 0;
pos_y = 550;
font = swc_font;
string = health_str;
flags = visible;
}

Vous pouvez voir que j'utilise un simple panneau avec l'affichage de 2 digits pour les valeurs et un simple texte avec une chaîne pour afficher les chaînes du joueur et de l'ennemi. Vous comprenez à présent pourquoi j'ai créé un pointeur pour l'ennemi, je voulais afficher ses points de vie également.

Que se passe-t-il si le squelette cogne l'épée du joueur? Dans mon exemple le joueur et l'épée ont été créés comme un modèle unique aussi si l'épée est cognée le joueur perd des points de vie également. Vous pouvez arranger cela en créant un modèle séparé pour le joueur et pour l'épée puis lui attacher ou en détectant le point d'impact et en comparant ses coordonnées avec les coordonnées de l'épée lues par ent_vertex.


Untitled

Comme sur des rails

le propos de cet article est simple . vous aider à créer un code pour une entité qui sait la façon de suivre une route. Vous porez utiliser ce code pour NPCs, piétons marchant dans la rue, trains, signaux lumineux etc. Voyons une brève explication de la méthode puis nous passerons au code pour voir ce qui se passe.

Nous avons un véhicule (bleu) et nous calculons les coordonnées pour les 2 points qui se trouvent devant lui : front_left et front_right. Nous traçons sous ces points à chaque cycle afin de connaître le nom de la texture qui se trouve dessous. Si le nom de la texture obtenu est le mêm que la texture de la piste, nous ne faisons rien; autrement nous tournons le véhicule jusqu'à ce que trace retourne les bonnes valeurs de texture dec ette façon le véhicule reste sur la piste sans utiliser de chemins prédéfinis. Si vous cherchiez à avoir des piètons utilisant les trotoirs correctement sans avoir à programmer une IA complexe alors ce code estfait pour vous.

var danger_zone = 0;

define
none 0;
define left 1;
define right 2;

function
main()
{
fps_max = 60;
level_load(level_wmb);
wait (3);
camera.x = -3500; // choose a convenient camera
position
camera.y = -2100;
camera.z = 1000;
camera.pan = 31; // and angles
camera.tilt = -20;
}

J'ai défini "none", "left" et"right" en tant que 0, 1 et 2. La fonction principale (main) charge le niveau et met la bonne position et angles pour la caméra.

action
train
{
var train_speed;
var front_left;
var front_right;
while (1)
{
train_speed.x = 20 * time;
train_speed.y = 0;
train_speed.z = 0;
ent_move (train_speed, nullvector);
front_right.x = my.x + 400 * cos(my.pan) + 300 * sin(my.pan);
front_right.y = my.y + 400 * sin(my.pan) - 300 * cos(my.pan);
front_right.z = my.z;
vec_set (temp.x, front_right.x);
temp.z -= 1000; // trace 1000 quants below
trace_mode = ignore_me + ignore_passable + ignore_models
+ ignore_sprites + scan_texture;
trace (my.x,
temp);
if ((str_cmpi ("metalplain4", tex_name)
== 0) && (danger_zone == none)) // if the texture below the train isn't
named "metalplain4"
{
danger_zone = right;
my.pan
+= 2 * time; // rotate the train toward left, play with this value
}
else
{
danger_zone = none;
}

Le train se déplace à la vitesse de train moves with 20 * time quants / frame.. Front_right est placé 400 quants devant le train et 300 sur le côté). Nous traçons 1000 quants sous front_right, vérifions si le nom de la texture retourné par scan_texture est correcte (metalplain4, utilisée pour la piste dans ma démo) et si danger_zone est à zéro ou pas. Danger_zone est une simple variable qui est contrôlée par la première antenne qui sert à indiquer si on est en dehors de la piste. Si danger_zone est à zéro (none) nous pouvons contrôler le train en changeant son angle pan et nous mettons danger_zone à right; jouez avec 2 * time si vous voulez ajuster la vitesse de rotation. Dès que le train (en fait front_right!) est revenu sur la piste, danger_zone est mis à none, permettant à l'un de front_left ou front_right de prendre le contrôle quand nécessaire.

     front_left.x = my.x + 400 * cos(my.pan) - 300 * sin(my.pan);
front_left.y = my.y + 400 * sin(my.pan) +
300 * cos(my.pan);
front_left.z = my.z;
vec_set (temp.x, front_left.x);
temp.z -= 1000; // trace 1000 quants below
trace_mode = ignore_me + ignore_passable + ignore_models + ignore_sprites + scan_texture;
trace (my.x, temp);
if ((str_cmpi ("metalplain4", tex_name) == 0) && (danger_zone == none))
// if the texture below the train isn't named "metalplain4"
{
danger_zone = right;
my.pan -= 2 * time; // rotate the train
toward right, play with this value
}
else
{
danger_zone = none;
}
wait (1);
}
}

Ceci se répète pour front_left; la seule chose qui pourrait vous tracasser est ceci : pourquoi utiliser la variable danger_zone? La réponse est simple nous ne voulons pas que front_left et front_right prennent la main en mêm temps et cette variable permet donc de s'assurer que le train est bien revenu sur la piste et seulement après il pourra procéder à d'autres rotations.

J'ai utilisé de grandes valeurs (400 et 300 quants) pour front_left et front_right; ce qui fait que le train sui t la piste d'une manière aléatoire. Si vous avez besoin de suivre un tracé plus pointu vous devez réduire ce svaleurs, principalement le 300.


Untitled

Décollage

Parfois nous devons interrompre le joueur, quoi qu'il fasse, parce que nous devons montrer un effet sympa de scène coupée. Si vous avez décidé d'écrire le code pour votre jeu à partir de zéro, vous ne devriez-vous pas avoir trop de problèmes mais que diriez-vous du reste des utilisateurs, ceux qui sont fidèleq au code des templates ?

Le code utilisé à l'intérieur de cet article crée un hélicoptère qui repose sur le sol, attendant que le joueur vienne près de lui ; quand ceci se produit, l'hélicoptère décolle par lui-même.

J'ai dû me débarasser de la caméra par défaut sans modifier les templates aussi j'ai défini une nouvelle vue :

view
cutscene_view
{
layer = 15;
pos_x = 0;
pos_y = 0;
}

L'action attachée à l'hélicoptère a son code listé ci-dessous:

action
chopper
{
my.passable = on;
while (vec_dist (my.x, player.x) > 150) // wait until the player has come closer
than 150 quants
{
ent_cycle("fly",
my.skill20); // play "fly" animation
my.skill20 += animation_speed
* time;
my.skill20 %= 100;
// loop the animation
if (!snd_playing(chopper_handle))
{
chopper_handle = ent_playsound (my, chopper_wav, 200);
}
wait (1);
}

L'hélicoptère est passable; si le joueur est éloigné de plus de 150 quants, il joue son animation "fly" dans une boucle et joue le son chopper_wav.

     cutscene_view.size_x = screen_size.x; // has
the same width
cutscene_view.size_y = screen_size.y;
// and height with "camera"
cutscene_view.x = -1685;
cutscene_view.y = -1240;
cutscene_view.z = 210;
cutscene_view.pan = 20;
cutscene_view.tilt = -15;
cutscene_view.roll = 0;
player.invisible = on; // hide the player
player.shadow = off; // and its shadow (if any)
camera.visible = off; // hide "camera"
cutscene_view.visible = on;
// show the new view

Si le joueur arrive à moins de 150 quants de l'élicoptère, nous employons la mêmes largeur et taille de taille d'écran pour cutscene_view, nous plaçons une position et des angles convenables por la nouvelle caméra, nous cachons le joueur et son ombre, nous cachons l'ancienne vue (caméra) puis nous montrons la nouvelle vue (cutscene_view). J'ai choisi ces valeurs pour le cutscene_view en volant à l'intérieur du niveau ; pressez zéro deux fois pour pouvoir voler, pressez "D" ou "F11" pour voir le panneau de mise au point (debug) et repérez ses valeurs de xyz et d'angles pour obtenir que vous avez besoin pour votre x, y, z, angles pan, roll et tilt pour votre jeu.

     while (my.z < 1000) // wait until the chopper
has reached a height of 1,000 quants
{
if (animation_speed
< 10)
{
animation_speed += 0.05 * time;
}
else // start flying
when animation_speed is bigger than 10
{
my.z += 10 * time;
my.x += 30 * time;
}
ent_cycle("fly",
my.skill20); // play "fly" animation
my.skill20 += animation_speed
* time;
my.skill20 %= 100;
// loop the animation
if (!snd_playing(chopper_handle))
{
chopper_handle = ent_playsound (my, chopper_wav, 500); // the sound is louder
this time
snd_tune (chopper_handle, 0, 25 * animation_speed, 0);
}

As long as the chopper hasn't reached the height of 1,000 quants, its animation speed grows from 4 to 10; when animation_speed is bigger than 10, the chopper starts to take off (increases its z) and moves forward (increases its x). We play the same "fly" animation at a bigger speed and chopper_wav is tuned accordingly.

Aussi longtemps que l' hélicoptère n'a pas atteint la hauteur de 1.000 quants, sa vitesse d'animation accroît de 4 à 10 ; quand animation_speed est plus grand que 10, l'hélicoptère commence à décoller (en augmentant son z) et se déplace vers l'avant (en augmentant son x). Nous jouons la même animation "fly" mais à une plus grande vitesse et chopper_wav est accordé en conséquence.

          vec_set(temp, my.x);
vec_sub(temp, cutscene_view.x);
vec_to_angle (cutscene_view.pan,temp);
// rotate cutscene_view towards the chopper

Ces 3 lignes de code tourne cutscene_view vers le joueur en permanence.

wait (1);
}
exit; // shut down the engine
}

L' hélicoptère a atteint une hauteur de plus de 1.000 quants aussi le moteur s'arrêtera. Vous pourriez charger un nouveau niveau ici, déclenchez peut-être quelques explosions dans le même niveau, etc... La seule chose qui importe est que le joueur soit en de bonnes mains maintenant (aussi longtemps que ça ne soit pas moi qui pilote l'hélicoptère).


Untitled

Déformation de Terrain

Vec_for_mesh et vec_to_mesh ont été annoncés comme particularités dans Aum3. Jetez un œil sur ce code qui génère des bombes qui tombent (je sais j’ai utilisé le modèle roquette) et qui crée des cratères lorsqu’elles touchent une maille terrain.

J’ai créé un autre projet autonome ; cet exemple s’exécute en 800x600x16bit, Vertex_dist[1090] est un tableau qui contient la distance entre chaque sommet et les coordonnées d’impact (hit_coords)
var video_mode = 7; // 800x600
var video_depth = 16; // 16 bit mode
var vertex_dist[1090];
var hit_coords;

La fonction main charge notre niveau et met une position de caméra bizarre (si vous n’aimez pas cette position, pressez 0 et déplacez la caméra où vous voulez)

function main()
{
level_load (terrain1_wmb);
wait (2);
camera.x = 345;
camera.y = -345;
camera.z = 300;
camera.pan = 135;
camera.tilt = -40;
}

J’ai ajouté un petit modèle dans Wed; Je lui ai associé cette action qui fait tomber les bombes aléatoirement:

action bomb_launcher
{
my.passable = on;
while (1)
{
temp.x = my.x + 200 - random(400);
temp.y = my.y + 200 - random(400);
temp.z = my.z;
bomb = ent_create (bomb_mdl, temp, move_bomb);
waitt (40); // crée une nouvelle bombe toutes les 2.5 secondes
}
}

La première ligne de cette action empêche la bombe d’être prisonnière lors de sa création. Une nouvelle bombe est créée toutes les 2.5 secondes, dans une zone de 400x400 quants. Chaque bombe se déplace en relationavec sa fonction move_bomb:

function move_bomb()
{
my.enable_entity = on;
my.enable_block = on;
my.event = explode_me;
bomb_speed.x = 0;
bomb_speed.y = 0;
bomb_speed.z = -5;
bomb_speed *= time;
while (my != null)
{
ent_move (nullvector, bomb_speed);
wait (1);
}
}

Vous pouvez voir que la bombe est sensible aux entités et blocs de niveaux, qu’elle a une vitesse Z négative et se déplace jusqu’à ce que l’évènement explode_me soit déclenché:

function explode_me()
{
waitt (2);
play_entsound (me, bumm_wav, 200);
ent_remove (me);
}

Nous avons besoin d’attendre un peu avant de stocker les coordonnées d’impact dans hit_coords. Nous jouons un son d’explosion et nous enlevons la bombe.

Le terrain fait tout le travail lorsque son évènement create_crate est déclenché:

action terrain
{
terrain1 = me;
my.enable_impact = on;
my.event = create_crate;
}

function create_crate()
{
vec_set (hit_coords, bomb.pos);

Avant tout, nous stockons les coordonnées de l’impact avant que la bombe ne disparaisse.

wait (1);
my.skill1 = 1;
my.skill2 = 1500;

Nous avons placé l'index de vertex_dist à 1 et skill2 à une distance énorme; cette distance sera beaucoup plus petite pour les sommets qui sont de plus en plus près du point d'impact.

while (my.skill1 < 1090) // vertices 1...1089 for
a 33x33 grid
{
vec_for_mesh (temp, terrain1, my.skill1);
vertex_dist [my.skill1] = vec_dist (hit_coords.x, temp.x);
if (vertex_dist[my.skill1] < my.skill2) // this vertex is closer than the others?
{
my.skill2 = vertex_dist[my.skill1]; // then it might
be the one!
}
my.skill1 += 1; // next vertex
}

Mon terrain a une grille de 33x33 avec 1089 sommets; my.skill1 sera mis à 1, 2, 3. .. 1089 dans la boucle while ci-dessus. Vec_for_mesh place temp au sommet 1, 2, 3. .. et nous stockons la distance entre vertex1 et hit_coords dans vertex_dist[1 ], la distance entre vertex2 et hit_coords dans vertex_dist[2 ] et ainsi de suite, comme dans l'image ci-dessous:

Je ne pourrais pas dessiner les flèches bleues pour tous les sommets, mais il semble que le vertex18 est le gagnant dans mon image, ainsi la distance entre vertex18 et hit_coords sera stockée dans skill2.

Maintenant nous devons découvrir l'index pour le gagnant, parce que cet index sera passé à la fonction deform(vertex_number).

my.skill1 = 1;
while (my.skill1 < 1090)
{
if (my.skill2 == vertex_dist[my.skill1])
{
my.skill3 = my.skill1;
}
my.skill1 += 1;
}
deform (my.skill3);
}

Cette fois nous recherchons le plus petit vec_dist stocké dans vertex_dist et quand nous le trouvons nous stockons l'index dans skill3 - c'est le sommet qui sera déplacé vers l'intérieur. Il est clair que les boucles while peuvent être optimisées mais elles fonctionnent assez vite (sans attente) et il n'est pas nécessaire de le faire.

La fonction finale est:

function deform(vertex_number)
{
vec_for_mesh(temp, terrain1, vertex_number);
vec_scale (temp, 0.7);
vec_to_mesh (temp, terrain1, vertex_number);
}

Nous plaçons vertex_number comme argument pour vec_for_mesh, ainsi temp sera mis aux coordonnées de vertex_number. Temp sera réduit à 70% de sa valeur et alors vec_to_mesh déformera le sommet qui était le plus proche des hit_coords.


Untitled

La corde pour la corde et l'echelle - vous en avez deux pour le prix d'un

Pendant que j'écrivais cet article je n'ai pas pu tester si mon code pour les cordes fonctionnait parfaitement avec une échelle, aussi laissez moi créer un petit niveau test et je reviens vers vous dès que possible...j'ai créé office.wmp - il y a une corde et une échelle.

Jetons un oeil sur ce code qui est vraiment très simple :

action rope //corde
{
gravity_temp = gravity;
my.enable_impact = on;
my.event = rope_action;
}

Si vous regardez movement.wdl dans le répertoire template, vous verrez que la gravité est contrôlée par 'gravity' - une variable qui change sa valeur si le joueur est sous l'eau, etc. C'est ce que j'utilise ici : si le joueur est très proche de la corde / échelle, gravity est mis à 0.01 (proche de 0) et ainsi le joueur est capable de monter et de descendre sans tomber. Notez que je sauvegarde la valeur initiale de 'gravity' dans notre variable 'gravity_temp'; vous pouvez utiliser une autre variable d'entité si vous souhaitez économiser un peu de mémoire. Regardons le reste du code :

function rope_action()
{
if (event_type == event_impact &&
you == player)
{
while
(my.skill10 < my.skill1)
{
my.skill10 = abs(my.x - player.x) + abs(my.y - player.y);
gravity = 0.01;
if (key_home == 1 && my.z + (my.max_z - my.min_z) / 2 > player.z -
(player.max_z - player.min_z) / 2)
{
move (player, rope_speed_up, nullvector);
}
if (key_end == 1) // go down
{
move (player, rope_speed_down, nullvector);
}
wait (1);
}
gravity
= gravity_temp;
}
my.skill10 = 0;
}

Si le joueur se cogne avec la corde, nous continuons à vérifier s'il reste près de la corde dans une boucle while. Au début du jeu, skill10 = 0 aussi la condition de la boucle while est vraie. Nous calculons la distance entre le joueur et la corde en x et en y et si elles sont suffisemment petites (plus petite que skill1 qui est mis dans WED), nous restons dans la boucle while, comme cela nous pouvons faire monter ou descendre notre joueur en utilsant les touches de direction haut et bas. Vous devez choisir la meilleure valeur pour skill1 lorsque vous testerez vos cordes et échelles - cette valeur dépendant de la taille du joueur et des dimensions de la corde / échelle etc. Si le joueur s'éloigne de la corde, gravity = gravity_temp redonne la valeur originale de gravity et le joueur peut de nouveau marcher, courrir ou nager.

Ne pas oublier cette ligne:

if (key_home == 1 && my.z + (my.max_z - my.min_z) / 2 > player.z - (player.max_z - player.min_z) / 2)

L'explication est très simple. La distance entre la corde / échelle et le joueur est calculée uniquement sur ses axes x et y (je vous laisse deviner pourquoi) ainsi le joueur pourra monter au dessus de la corde sans perdre son x et son y. Cette ligne effrayante s'assure que les pieds du joueur cessent de s'élever à l'extrémité de la corde/d'échelle. Vous savez que my se rapporte à la corde/à échelle. Si vous créez une entité avec son origine dans son centre (et moi c'est ce que je ferai toujours si j'étais vous), son x, y
et z sont situés au centre de l'entité.

Nous ne pouvons pas employer quelque chose comme cela :

if (key_home == 1 && my.z > player.z) parce que ceci ne permettra pas au joueur de s'élever à l'extrémité de la corde/d'échelle

Si vous jetez un coup d'oeil à la première image, vous voyez que nous devons découvrir la 'hauteur '(longueur) de la corde ; heureusement nous pouvons employer max_z - min_z pour le découvrir (2ème image). Notre but est de permettre au joueur de s'élever sur la corde/échelle (3ème image). J'espère que vous savez que la corde est rouge et le joueur est bleu ; leurs origines sont les points jaunes.

Si vous regardez la 1ère image encore, vous voyez que si nous ajoutons la moitié de la corde à son centre (qui est my.z) nous obtenons la coordonnée supérieure z de la corde ; c'est :

my.z + (my.max_z - my.min_z) / 2

Si le joueur a son origine trop au centre (nous supposons que c'est vrai), son z inférieur (les pieds du joueur) sera donné par :

player.z - (player.max_z - player.min_z) / 2

Nous soustrayons la moitié de son corps de son centre, ainsi le joueur peut s'élever aussi longtemps que ses pieds ne sont pas au-dessus de la corde. Par ailleurs, le joueur s'élève en utilisant une instruction simple de mouvement et deux vecteurs : l'un d'entre eux a une vitesse positive sur z, l'autre a une vitesse négative sur z.. Changez ces valeurs pour obtenir différentes vitesses d'élévation.

Ce code de corde / 'échelle est un peu trop simple pour être parfaitmais il vous aura permis de commencer.


Untitled

Code d'éclipse

Un des nouveaux dispositifs qui a été présenté dans AUM5 est la capacité de changer la couleur pour le ciel et la carte de scène. Ceci nous aide à obtenir des transitions douces de jour/nuit. J'ai pensé que si j'écrivaiss un code simple de jour/nuit, aucun de mes lecteurs ne se reposera dans sa chaise et attendra 24 heures pour examiner si cela fonctionne très bien. J'ai décidé d'aller chercher quelque chose peu un plus compliqué mais plus spectaculaire ; l'éclipse se produit toutes les deux minutes (les vitesses étant placé par moi au niveau d'essai) et dure pendant quelques secondes. Si vous manquez l'éclipse ou si vous ne voulez pas attendre encore deux minutes pour la voir, pressez la touche [tab] et tapez moon_speed = 1 à la console. Ceci fonctionne très bien quand vous commencez le niveau - si le soleil s'est déjà trop déplacé vous devrez trouver une autre valeur.

entity* moon_pnt;
entity* sun_pnt;
var sun_radius;
var sun_speed;
var moon_radius;
var moon_speed;

Nous devons placer deux pointeurs pour la lune et le soleil ; en fait nous n'avons pas besoin de pointeur pour le soleil (nous pouvons employer le pointeur my pour cela) mais il facilite les choses pour vous. Le rayon pour le soleil et la lune (leurs orbites) sont placés automatiquement quand vous placez les planètes dans Wed ; leurs vitesses sont placées avec leur skill1.

L' action pour le soleil est la plus complexe :

action
sun
{
sun_pnt = me;
qLensFlare = 0;
mouseview = 5;
camera.ambient = 100;
my.unlit = on;
my.ambient = 100;
my.bright = on;
my.flare = on;
while (player == null) {wait (1);}
while (moon_pnt == null) {wait (1);}
sun_radius = vec_dist (my.x, player.x);

Après que la première ligne soit exécutée, le soleil sera connu comme sun_pnt et sera accessible de n'importe quelle autre action ou fonction. Nous mettons les éclats de lumière du template à 0 (si lflare.wdl est inclus dans votre projet, autrement vous devrez mettre cette ligne en commentaire). La variable qui commande la sensibilité de souris est appelée mouseview - nous augmentons sa valeur.Une façon de contrôler l'éclat pour un niveau entier est de modifier camera.ambient - ceci fonctionnera avec toutes les surfaces, avec tous les modèles et entités et ainsi de suite. Nous plaçons camera.ambient à 100 pour l'instant (ainsi nous commençons par un niveau lumineux). Les drapeaux bright et flare utilisés conjointement mélangeront le soleil par dessus le fond. Nous attendons que le joueur et la lune soient créés et alors nous obtenons le rayon du soleil de sa position liée à la position du départ du joueur dans Wed.

    while (1)
{
my.x = player.x + sun_radius
* sin (sun_speed);
my.y = player.y + sun_radius * cos (sun_speed);
my.z = sun_radius * ((moon_pnt.z
- (player.max_z - player.min_z) * 0.7) / moon_radius);

vec_set (temp, player.x);
vec_sub (temp, sun_pnt.x);
vec_to_angle (sun_pnt.pan, temp);

my.skill20 = ang(sun_pnt.pan - moon_pnt.pan);

        if (my.skill20 > -6 &&
my.skill20 < 6)
{
camera.ambient = 16.6 * abs(my.skill20); // 100:6
scene_color.red = 42 * abs(my.skill20); // 255:6
scene_color.green = 42 * abs(my.skill20);
scene_color.blue = 42 * abs(my.skill20);
sky_color.red = 42 * abs(my.skill20);
sky_color.green = 42 * abs(my.skill20);
sky_color.blue = 42 * abs(my.skill20);
}
sun_speed += my.skill1 * time;
wait (1);
}
}

Le soleil se déplace sur un cercle qui a pour centre le joueur. Si le joueur (point bleu) se déplace, le soleil et la lune se déplaceront avec lui aussi. J'ai fait cela parce que nous ne voulons pas créer un énorme niveau et nous devons duper le joueur car la distance entre lui et les planètes est énorme. Si nous n'utilisions pas cette astuce, le joueur verrait que la distance entre lui et les planètes devient plus petite pendant qu'il les approche.

Regardons cette ligne:

sun.z = sun_radius * (moon.z - player.z) / moon_radius;

Vous ne trouverez pas la ligne ci-dessus dans daynight.wdl - je l'ai simplifié. Nous voulons nous assurer que la lune bloque la vue du soleil du regard indépendamment de la position du joueur, aussi je monte(ou abaisse) le soleil selon la position du joueur. Si vous jetez un regard à l'image de gauche vous pouvez imaginer ce qui se produit si le joueur vient plus près de la lune (regardez à la ligne pointillée) : l'éclipse ne peut pas se produire parce que le soleil devrait être levé beaucoup plus haut que ce que fait cette ligne . Nous n'avons pas fini avec ceci : vous pouvez voir que le joueur regarde avec ses yeux (image de droite) aussi nous devons calculer la taille approximative de l'oeil. J'ai employé un simple (player.max_z - player.min_z) * 0.7 qui calcule la taille du joueur et la multiplie par 0.7 - vous devriez jouer avec cette valeur. Voici le résultat (rappelez-vous que my est le soleil) :

my.z = sun_radius * ((moon_pnt.z - (player.max_z - player.min_z) * 0.7) / moon_radius);

Le soleil est tourné de sorte qu'il fasse toujours face au joueur en utilisant une instruction simple de vec_to_angle. Nous employons le skill20 du soleil pour maintenir la différence entre les angles pans du soleil et de la lune, comme dans l'image.

Si skill20 est plus grand que -6 et plus petit que 6c'est le moment de l'éclipse ! Naturellement vous devrez modifier ces valeurs dans votre niveau selon la taille de vos planètes, leur distance selon le joueur et ainsi de suite. Nous modifions scene_color, sky_color et camera.ambient pour obtenir une superbe éclipse.

Voici l'action associée à la lune :

action
moon
{
moon_pnt = me;
my.unlit = on;
my.bright = on;
while (player == null) {wait (1);}
moon_radius = vec_dist (my.x, player.x);
while (1)
{
my.x = player.x + moon_radius
* sin (moon_speed);
my.y = player.y + moon_radius * cos (moon_speed);

vec_set (temp, player.x);
vec_sub (temp, moon_pnt.x);
vec_to_angle (moon_pnt.pan, temp);

moon_speed += my.skill1 * time;
wait (1);
}
}

Toutes les lignes de cette action peuvent être trouvées (et elles sont déjà expliquées) dans l'action sun.


Untitled

Effets avancés d'explosion

Je préfère vous le dire tout de suite, plus vous travaillerez dur et plus vos effets seront superbes. En ce qui nous concerne nous allons faire des effets simples qui consistent à se faire chevaucher des effets de particules et des sprites:

1) Le coeur de l'explosion:

function
explosion_core()
{
if (random(1) > 0.5)
{
my.bmap = core1_tga;
}
else
{
my.bmap = core2_tga;
}
my.transparent = on;
my.flare = on;
my.bright = on;
my.size = random(30) + 20;
my.alpha = 100;
my.function = core_fade;
}

function
core_fade()
{
if (my.alpha > 50)
{
my.size += 2 * time;
}
else
{
my.size -= 2 * time;
}
my.alpha -= 3 * time;
if(my.alpha < 0)
{
my.lifespan = 0;
}
}

La première fonction prend une des 2 images (core1_tga ou core2_tga), la rendant transparente, lumineuse etc et lui donne une taille aléatoire. La deuxième fonction augmentera la taille de l'image jusqu'à ce que son alpha atteigne 50, puis elle commencera à la diminuer. Les particules sont enlevées dès que leur alpha atteint zéro.

2) L'anneau sprite (onde de choc...):

function
explosion_ring()
{
my.passable = on;
my.scale_x
= 0.1;
my.scale_y = 0.1;
my.tilt = 90;
my.oriented = on;
my.bright = on;
my.flare
= on;
my.transparent = on;
my.alpha = 100;
my.ambient = 100;
my.lightred = 255;
my.lightgreen = 200;
my.lightrange = 100;
while
(my.scale_x < 4)
{
my.alpha
-= 10 * time;
my.lightrange += 50 * time;
my.scale_x += 0.5 * time;
my.scale_y = my.scale_x;
wait(1);
}
ent_remove(my);
}

Le sprite démarre avec une petite échelle de taille et éclaire une zone de 100 quants au départ; la boucle "while" diminue son facteur de transparence et augmente sa taille et sa zone d'éclairage. L'anneau est supprimé dès que sa taille est supérieure ou égale à 4.

3) Le sprite tournant:

function
explosion_sprite1()
{
my.passable = on;
my.facing
= on;
my.bright = on;
my.flare = on;
my.alpha = 100;
my.ambient = 100;
while
(my.scale_x < 4)
{
my.scale_x
+= 1 * time;
my.scale_y = my.scale_x;
my.alpha -= 10 * time;
my.roll += 25 * time;
wait(1);
}
ent_remove(my);
}

Ce sprite fait face à la caméra tout le temps. Nous augmentons sa taille dans une boucle "while", diminuant son alpha et le faisant tourner dans le même temps. Lorsque sa taille est supérieure à 4 le sprite est enlevé.

4) Le sprite animé:

function
explosion_sprite2()
{
my.passable = on;
my.facing
= on;
my.bright = on;
my.flare = on;
my.ambient = 100;
my.red = 255;
my.lightrange
= 100;
while (my.frame < 20)
{
my.frame += 1 * time;
my.scale_x += 0.1 * time;
my.scale_y = my.scale_x;
my.lightrange = 300 - random(100);
wait(1);
}
ent_remove(my);
}

Le sprite déroule toutes les frames de son animation, augmentant sa taille et prenant un éclairage aléatoire. Le sprite est enlevé dès qu'il a fini de jouer sa dernière frame d'animation.

5) Le générateur de "débris" et leur trainée de particules:

function
move_gibbits()
{
var gib_coords;
var gib_speed;
my.passable = on;
my.scale_x = 0.5 + random(1);
my.scale_y = my.scale_x;
my.scale_z = my.scale_x;
vec_set (gib_coords, my.x);
gib_speed.x = (10 - random(20))
* time;
gib_speed.y = (10 - random(20)) * time;
gib_speed.z = (20 + random(10)) * time;
my.skill10 = 10;
while (my.skill10 > 0)
{
if (vec_dist (my.x, gib_coords.x) > 70)
{
my.passable = off;
}
ent_move (nullvector,
gib_speed);
effect (debris_trail, 1, my.x,
nullvector);
if (bounce.z != 0)
{
gib_speed.z = -(gib_speed.z * min(0.8, random(1)));
if (gib_speed.z < 0.1)
{
gib_speed.x = 0;
gib_speed.y = 0;
gib_speed.z = 0;
}
}
gib_speed.z -= 2
* time;
my.skill10 -= 0.3 * time;
wait (1);
}
my.transparent
= on;
my.alpha = 100;
while (my.alpha > 0)
{
my.alpha -= 3 * time;
wait (1);
}
ent_remove (my);
}

Les "débris" sont des petits modèles 3D avec une taille aléatoire (0.5 ... 1.5) et une vitesse aléatoire sur l'axe des x, y et z. Notez que la vitesse sur le z est positive ce qui permet au débris de s'élever au moment de sa création. Nous mettons skill10 à 10 puis nous exécutons une boucle while qui s'arrête dès que skill10 atteint zéro. Que se passe-t-il dans la boucle while? Avant tout les débris sont rendus impassables lorsqu'ils se trouvent près de leur point de création; ce truc a pour effet d'éviter que les débris se cognent au moment de leur génération. Jouez avec la valeur "70" - afin d'avoir la meilleure valeur pour votre projet. Le débris se déplace à la vitesse donnée par gib_speed et crée des particules à sa position à chaque frame utilisant la fonction debris_trail. Si le débris touche quelque chose (le sol, un mur etc.) il change sa vitesse, prenant une petite valeur (au dessus de 0.8 de la valeur initiale) et change son signe. Si gib_speed est trop petit, gib_speed est mis à zéro. Les dernières lignes de la boucle diminue gib_speed.z à chaque frame, simulant les effets de la gravité et diminuant la valeur de skill10 car il faut bien que cette boucle s'arrête à un moment donné. Que se passe-t-il ensuite? Le modèle du débris est rendu transparent et on fait un fondu jusqu'à ce qu'on alpha atteigne zéro auquel cas il est supprimé.

function
debris_trail()
{
my.bmap = debristrail_tga;
my.flare = on;
my.bright = on;
my.size = 2;
my.alpha = 30;
my.function = fade_debtrail;
}

function
fade_debtrail()
{
my.size += 0.8 * time;
my.alpha
-= 1.5 * time;
if (my.alpha < 0)
{
my.lifespan = 0;
}
}

Ces simples particules augmentent leur taille et diminuent leur alpha puis sont enlevées dès que leur facteur de transparence atteint zéro.

6) La fumée:

function
particle_smoke()
{
my.bmap = smoke_tga;
my.vel_x
= 2 - random(4);
my.vel_y = 2 - random(4);
my.vel_z
= 2 + random(1);
my.lifespan = 200;
my.flare
= on;
my.transparent = on;
my.alpha = 50;
my.move = on;
my.size = 50;
my.function
= fade_smoke;
}

function
fade_smoke()
{
my.size += 0.5 * time;
my.alpha
-= 0.4 * time;
if (my.alpha < 0)
{
my.lifespan = 0;
}
}

C'est le générateur de fumée le plus classique avec une vitesse aléatoire sur l'axe des x et des y et une vitesse positive sur l'axe des z. La particule augmente sa taille au fur et à mesure qu'elle monte et diminue sa transparence.

Ce sont toutes les fonctions qui sont utilisées pour les effets, voyons à présent comment les assembler entre elles:

function
start_explosion(coordinates)
{
var sound_volume;
var number_of_gibbits;
sound_volume = 30000 / (vec_dist(coordinates.x,
camera.x) + 100);
snd_play (explostart_wav, sound_volume, 0);
effect (explosion_core, 10 + random(10), coordinates, nullvector);
sleep (0.5);
ent_create (exploring_tga, coordinates, explosion_ring);
snd_play (explomid_wav, sound_volume, 0);
sleep (0.1);
ent_create (exploflare_pcx, coordinates, explosion_sprite1);
sleep (0.1);
ent_create (explosion_pcx, coordinates, explosion_sprite2);
sleep (0.1);
number_of_gibbits = random(15) + 5;
while (number_of_gibbits > 0) // while without wait!
{
ent_create (gibbit_mdl, coordinates, move_gibbits);
number_of_gibbits -= 1;
}
snd_play (exploend_wav, sound_volume, 0);
sleep (0.5);
effect (particle_smoke, random(20) + 10, coordinates, nullvector);
}

J'ai calculé la valeur pour sound_volume en utilisant la distance entre les coordonnées de l'explosion et la caméra; de cette façon nous sommes capables de joueur le son de l'explosion n'importe où dans le niveau (pas nécessairement à la position d'une entité). Nous commençons par exécuter la fonction explosion_core, avec un nombre aléatoire (10...20) de particules à la position donnée par "coordinates", qui est le vecteur passé à la fonction start_explosion(). Nous attendons 0.5 seconde et nous créons l'explosion anneau. Nous attendons 0.1 seconde, nous créons l'effet de rotation, nous attendons encore 0.1 seconde et nous créons l'effet du sprite animé. Nous générons un nombre aléatoire (15...20) de débris et nous les créons dans une boucle while rapide, c'est à dire sans instruction wait. Pour terminer nous attendons 0.5 seconde et nous créons la fumée.

Libre à vous de faire des essais avec un ordre différent, d'ajouter plusieurs lignes identiques qui créeront le même effet, de joueur avec les intervalles sleep etc. Je vous vois bientôt!

Qu'est-ce que vous dîtes jeune homme! Je ne vous ai pas montré comment utiliser ces merveilleux effets dans notre projet?

Ok, voici 2 exemples:

1) Crée l'explosion dans une zone particulère de votre niveaul:

function explo_test()
{
start_explosion (vector(0, 0, 100));
}

on_e = explo_test;

Lorsque vous pressez la touche "E" vous générez une explosion à la position x = 0, y = 0, z = 100 quants dans votre niveau. Appelez simplement la fonction explo_test() lorsque vous avez besoin de déclencher l'explosion.

2) Crée des barils explosifs:

function destroy_me()
{
start_explosion (vector(my.x,
my.y, my.z + 20));
sleep (0.5);
my.invisible
= on;
my.passable = on;
}

action explodable_barrel
{
my.enable_impact
= on;
my.enable_entity = on;
my.enable_shoot
= on;
my.event = destroy_me;
}

Le baril est sensible à l'impact avec les autres entités aussi bien qu'avec une instruction "trace" ou "c_trace"; sa fonction d'évènement déclenche l'explosion du baril à sa position (plus exactement 20 quants au dessus) attend un peu puis cache le baril. Vous trouverez ces 2 exemples dans le fichier office.wmp, donc n'hésitez pas à faire des expériences. N'oubliez pas que si vous n'avez pas de projectiles pour les barils vous pouvez toujours jouer les kamikazes et entrer vous-même directement en contact avec l'un d'eux :)


Untitled

Faux miroirs - traduction Olivier Burton - correction AB

La capacité de créer des miroirs est une particularité disponible seulement aux utilisateurs du moteur "PRO"; cependant, vous pouvez créer des effets de miroir convenables à regarder même si vous employez A5 ou A6 standard.

Comment créons-nous un effet de miroir qui peut être employé sur le plancher d' une pièce ? Voici une explication point par point :

1) Ajouter un grand cube Wed, redimensionnez-le et texturez le;

2) Creusez-le (Hollow) puis supprimez son "plancher" :

3) Enregistrez le niveau et créez ensuite une entité séparée wmb qui sera employée comme plancher pour votre pièce; texturez la en employant "checker1" dans standard.wad :

4) Charger le niveau de nouveau et ajoutez ensuite l' entité employée pour le plancher :

5) Sélectionner toutes les géométries de niveau sauf l' entité de plancher et groupez les; employez les vues "Back" ou "Side" pour cela :

6) Faire un double de la pièce, faites-la tourner et déplacez-la pour la mettre à l'envers (tête bêche ou en miroir si vous préférez ah, ah) :

7) Vous pouvez ajouter du mobilier en double à chaque fois (normal et inversé - tête bêche) . C' est l' image de la pièce employée dans ma démonstration :

8) Ajoutez un modèle de joueur et attachez-lui l'action "player1". Attachez l'action "transparent_floor" à l' entité de plancher. Voilà!

Vous l' avez fait !: le joueur peut voir par l' entité de plancher transparente et il voit les mêmes choses des deux côtés du plancher. Mais comment cela se passe-til avec le modèle de joueur? Lui qui change de positions, d'animations, etc tout le temps, donc il doit y avoir quelque code attaché!

action
player1
{
player = me;
while (1)
{
camera.x = player.x
- 100 * cos(player.pan);
camera.y = player.y
- 100 * sin(player.pan);
camera.z = player.z
+ 80;
camera.pan = player.pan;
camera.tilt +=
10 * mouse_force.y * time;
player.pan += 4
* (key_a - key_d) * time;
player_distance.x
= 10 * (key_w - key_s) * time;
player_distance.y
= 0;
player_distance.z
= 0;

La caméra est placée 100 quants derrière le joueur et 80 quants au-dessus de son origine; elle a le même angle pan avec le joueur et change son angle tilt selon le mouvement de la souris sur l' axe des ordonnées. Le joueur peut changer son angle pan en employant "Q" et "D" et les mouvements en avant et en arrière avec "Z" et "S".

          if ((key_w == 1)
|| (key_s == 1)) // the player is walking
{
ent_cycle("walk", my.skill20); // play "walk" frames animation
my.skill20 += 4 * time; // "walk" animation speed
my.skill20 %= 100; // loop the animation
}
else // the player
is standing
{
ent_cycle("stand", my.skill20); // play "stand" frames animation
my.skill20 += 2 * time; // "stand" animation speed
my.skill20 %= 100; // loop the animation
}
move_mode = ignore_passable;
ent_move (player_distance,
nullvector);
wait (1);
}
}

Si le joueur marche ("Z" ou "S" est appuyé) le joueur montrera son animation "walk" dans une boucle; autrement, si le joueur est debout, il jouera son animation "stand" dans une boucle. Le joueur sera déplacé avec la vitesse donnée par player_distance, passant par tous les objets passables.

Je ne vois pas de code de miroir ici; vérifions une nouvelle fonction :

starter
init_mirrors()
{
while (player == null) {wait (1);}
ent_create (fakeplayer_mdl, nullvector, mirror_player);
}

Cette fonction starter attend que l' entité de joueur soit chargée dans le niveau et ensuite elle crée le modèle de réflexion du joueur (fakeplayer_mdl) à l' origine et l' associe à la fonction nommée mirror_player () :

function
mirror_player
{
my.passable = on;
while (1)
{
vec_set(temp, player.x);
temp.z -= 500;
// trace 500 quants below the player
trace_mode = ignore_me
+ ignore_passable + ignore_models + ignore_sprites + scan_texture;
trace (player.x,
temp); // get tex_name
my.frame = player.frame;
my.next_frame =
player.next_frame;
if (str_cmpi ("checker1",
tex_name))
{
my.invisible = off; // show the fake player image
my.pan = player.pan;
my.roll = player.roll + 180; // flip it upside down
my.x = player.x;
my.y = player.y;
my.z = player.z - 60;
}
else
{
my.invisible = on;
}
wait (1);
}
}

Le modèle employé comme réflexion du joueur dans le miroir est passable; nous traçons 500 quants au-dessous du joueur "réel" pour obtenir le nom de la texture employée pour le plancher et ensuite nous mettons la même frame et next_frame pour le joueur et sa réflexion. Si "trace" voit la texture nommée "checker1" au-dessous des pieds du joueur, elle montre le modèle de réflexion, employant le même angle pan et un angle roll différent (180 degrés permet de mettre le modèle de réflexion tête b^che, ce qui est bon pour nous). La réflexion aura les mêmesx et y que le joueur et elle sera placée 60 quants au-dessous de l' origine du joueur (jouez avec cette valeur en fonction de l'épaisseur de votre plancher).

Si "trace" ne peut pas trouver la texture nommée "checker1" au-dessous des pieds du joueur, le modèle de réflexion est caché. De cette façon vous pouvez créer des pièces normales, avec les planchers qui ne reflètent rien et les pièces qui ont des planchers qui reflètent. J' ai créé une pièce de chaque sorte dans ma démonstration.

L' action employée pour le plancher transparent est vraiment simple; changez "85" pour changer le facteur de transparence pour le miroir.

action
transparent_floor
{
my.transparent = on;
my.alpha = 85;
}

Si vous projetez d'avoir des ennemis dans une pièce qui inclut un plancher miroir, vous devrez créer des pointeurs et des fonctions qui ressemblent à mirror_player () pour eux.

Vous pouvez employer une méthode semblable pour créer des miroirs verticaux; si vous voulez que j' en écrive un article, postez un message au forum de Conitec, sous "User Contributions" à l' intérieur du post Aum31. Heureux reflets!


Untitled

flèchettes

Si vos mains ne tremblent pas trop vous obtiendrez un score honnête et si vous ne les contrôlez pas vous aurez un score d'enfer. Exécutez ce projet et voyez par vous-même...

Jetons un oeil à la fonction principale (main) - elle fait un certain nombre de choses:

function
main()
{
camera.arc = 60;
fps_max = 30;
level_load (board_wmb);
wait (2);
mouse_map = mouse_pcx; //
2d dart picture
mouse_mode = 2;
mouse_pos.x = 372;
mouse_pos.y = 272;

Nous mettons notre champ de vue à 60 degrés, nous limitons le taux d'affichage à 30 fps puis nous chargeons le niveau. Nous utilisons mouse_pcx comme pointeur de souris (c'est l'image d'une flèchette vue de l'arrière) puis nous plaçons le pointeur de souris au centre de l'écran, nous soustrayons 28 pixels en x et en y car l'image bitmaps du curseur fait 56x56 pixels.

     while (1)
{
if (key_t == 1)
{
mouse_pos.x = pointer.x;
mouse_pos.y = pointer.y;
}
else
{
counter += 1;
if (counter == 1)
{
random_x = 20 - int(random(40));
random_y = 20 - int(random(40));
}
if (mouse_pos.x < pointer.x + random_x) {mouse_pos.x += 1;}
if (mouse_pos.x > pointer.x + random_x) {mouse_pos.x -= 1;}
if (mouse_pos.y < pointer.y + random_y) {mouse_pos.y += 1;}
if (mouse_pos.y > pointer.y + random_y) {mouse_pos.y -= 1;}
if ((mouse_pos.x == pointer.x + random_x) && (mouse_pos.y == pointer.y
+ random_y))
{
counter = 0;
}
}

Si nous pressons et maintenons la touche "T" le pointeur de souris peut être déplacé sans effort sur la totalité de l'écran. Néanmoins ce mode de mouvement doit être utilisé uniquement à des fins de calibration car c'est un peu facile de placer la souris sur le centre de la cible et de lancer ses 5 flèches au centre. Nous avons fait le jeu un peu plus difficile - c'est ce que le reste du code fait pour nous.

J'utilise une simple variable nommée counter; sa valeur initiale est zro mais counter += 1 lui ajoute quelque chose. Si counter = 1 nous générons une cible aléatoire qui peut se trouver à -20...20 pixels de la position de la souris. La souris peut être déplacée vers la cible aléatoire et lorsqu'elle l'atteint une nouvelle cible aléatoire est générée etc. Ceci ressemble à une secousse élégante et propre du pointeur (la flèchette) à l'écran et rend le jeu un peu plus difficile et (bien entendu) plus sympa.. Counter est remis à zéro lorsque le pointeur a atteint sa cible aléatoire.

          if (mouse_left
== 1 && number_of_darts < 5)
{
while (mouse_left == 1) {wait (1);}
dart_coords.x = mouse_pos.x + 28;
dart_coords.y = mouse_pos.y + 28;
dart_coords.z = 100;
vec_for_screen(dart_coords, camera);
ent_create(dart_mdl, dart_coords, move_dart);
}
wait (1);
}
}

Si nous pressons le bouton gauche de la souris (LMB) et que nous n'ayopns pas envoyé la totalité de nos 5 flèchettes, nous attendons que le bouton soit relâché, afin d'éviter l'autofire (le tir en rafale), et nous mettons la variable dart_coords aux coordonnées du pointeur de la souris. L'image utilisée pour le pointeur a 56 x 56 pixels aussi j'ai ajouté 28 pixels en x et en y pour obtenir les bonnes coordonnées. La flèchette 3D, le modèle réel, est placé 100 quants devant l'écran et vec_for_screen fait que mon rêve devient réalité : changer une position 2D à l'écran en des coordonnées 3D dans le monde "réel". La dernière ligne crée la flèchette à la position (même position que le curseur de la souris pour x et y, 100 quants devant l'écran- ceci pour ceux qui aurait loupé le début du film).

Maintenant nous avons une vraie flèchette dans notre niveau, qu'allons-nous en faire? Regardons la fonction qui la gère, move_dart:

function
move_dart()
{
wait (1);
mouse_map = null;
my.enable_entity = on;
my.enable_block = on;
my.event = stop_dart;
my.skill1 = 0;

Le curseur de la souris disparait (nous avons une flèchette 3D à présent, nous n'avons donc plus besoin de la version 2D); la flèchette (dart) est rendue sensible aux autres entités ou blocs de nouveau. Si la flèchette entre en collision avec une autre entité ou avec un des blocs du niveau, son évènement stop_dart est exécuté. Je remts à zéro skill1 pour cette flèchette mais si elle cogne quelque chose, skill1 sera mis à 1.

C'est l'heure des devinettes:) les composants de la vitesse de la flèchette. Vous pourriez penser qu'une flèchette vole tout droit mais cette phase dangereuse doit être traitée avec beaucoup plus de soins. Pourquoi, vous demandez-vous? Imaginez ce scénario : vous êtes assis devant votre ordinateur, buvant une boisson gazeuse et jouant à ce petit jeu de flèchette. Il se fait tard et vous avez déjà joué durant des heures et pourtant vous marquez 250 points sur 250 points à chaque coup. Pourquoi cela? Bien, si vous manquez le centre de l'écran (qui se trouve être le centre de la cible également) de quelques pixels, disons que vous le manquez de 3 pixels - la flèchette arrivera donc à 3 pixels du centre de la cible (pour 50 points) mais cela n'a aucune importance car l'entité utilisée pour le centre de la cible a un diamètre de 8 pixels. Vous devriez être "maladroit" de 80 pixels pour rater la cible complètement - et ceci ne peut pas se produire dans le monde réel.

Voic donc le problème posé. Regardons la solution. La flèchette avance tout droit vers la cible à cause de dart_speed.x. Lorsque nous loupons le centre, nous changeons la vitesse de la flèchette en y et en z en fonction de la distance entre le centre (le tir parfait) et la destination actuelle. L'image ci-dessous sera peut-être plus explicte:

Je suis dans le brouillard... mais je peux prendre la main à la console et taper camera.fog = 0 et nous pourrons continuer (humour Acknex). La mauvaise evrsion est mauvaise parce que même si le joueur loupe le centre de la cible la flèchette à toutes les chances d'atteindre quand même le centre de la cible. La bonne version contrôle si le joueur a loupé le centre de la cible et si c'est le cas il modifie la vitesse en y et en z pour que la flèche s'éloigne du centre de l'écran.

     dart_speed.x = 100;
dart_speed.y = (372 - mouse_pos.x) / 15;
dart_speed.z = (272 - mouse_pos.y)
/ 15;
dart_speed *= time;
while (my.skill1 == 0)
{
move_mode = ignore_passable;
// ignore passable entities
ent_move (nullvector,
dart_speed);
wait (1);
}
// the dart has hit something here
mouse_map = mouse_pcx;
}

Est-ce que je vous ai dit que dart_speed.x est le composant principal de la vitesse de la flèchette? ItC'est ce qui permet à la flèche d'aller vers la cible. Les 2 autres composants de dart_speed ont une histoire différente : dart_speed.y déplacera la flèchette vers la gauche ou vers la droite tandis que dart_speed.z pourra déplacer la flèchette vers le haut ou le bas. Quand le centre de la souris est au centre de l'écran (400, 300) le coin supérieur gauche est placé à (372, 272) parce que nous avons soustrait 28 pixels en x et y. Les valeurs pour dart_speed.y et dart_speed.z sont calculés en soustrayant la position réelle de la souris du centre virtuel de l'écran placé à (372, 272); la valeur résultante de cette opération sera divisé par un nombre (15) qui peut être utilisé pour la calibration.

Vous vous souvenz que si nous pressons "T" la soruis devient libre? Cela va nous permettre de calibrer le jeu; pressez et mainetenez la touche "T", tirez quelques flèchettes comme montré par les cercles rouges sur l'image ci-dessous et ils indiqueront l'extrimité de la cible. Si elles touchent trop près du centre ou loupe carrément la cible, changez la valeur 15 jusqu'à ce qure tout soit OK.Souvenez-vous que si vous changez dart_speed.x, vous devrez recalibrer de nouveau.

Dart_speed est multiplié par time afin d'assurer une même vitesse quelque soit le taux d'affichage. Si votre pc ne peut pas délivrer 30 fps, ces lignes de code pourront vous aider. La flèchette se déplace à l'intérieur de la boucle while jusqu'à ce qu'elle touche quelque chose : quand cela se produit, nous montrons de nouveau le pointeur de la souris.

Que se passe-t-il lorsque la flèchette touche quelque chose? Jetons un oeil sur la fonction ci-dessous:

function
stop_dart()
{
my.skill1 = 1;
snd_play (hit_snd, 50, 0);
my.passable = on;
number_of_darts += 1;
while (number_of_darts <
5) {wait (1);}
while (camera.arc > 20)
// zoom in
{
camera.arc -= 0.2
* time;
wait (1);
}
while (key_any == 0) {wait (1);}
ent_remove (me);
while (camera.arc < 60)
// zoom out
{
camera.arc += 0.2
* time;
wait (1);
}
number_of_darts = 0;
score = 0;
}

En tout premier nous mettons skill1 à 1; cela aura pour effet d'arrêter le mouvement dans la fonction move_dart(), n'est-ce past? Nous jouons un son puis nous rendons la flèchette passable car une autre flèchette peut atteindre le même endroit. Nous incrémentons le nombre de flèchette et nous attendons que les 5 flèchettes soient tirées. A présent pourquoi voulons-nous faire quelque chose comme cela? Ce sera plus facile (et superbe) de créer le code qui relance le jeu si nous le faisons de cette façon. Lorsque les 5 flèchettes sont tirées nous faisons unzoom avec 5 fonctions qui décrémentent camera.arc en même temps. Nous nous sommes rapprochés de la cible comme cela nous pouvons admirer notre talent et dès que nous presserons une touche ou un bouton de la souris, les vielles flèchettes seront enlevées, la camera remise à son origine, le nombre de flèchettes et le score sont remis à zéro.

Vous vous souvenez que je vous ai dit de ne pas tirer sur le tableau des scores? OK, tirez dessus et voyez ce qui se passe:

action
score_board
{
my.oriented = on;
my.enable_impact = on;
my.event = fall;
}

function
fall()
{
while (my.z > -500)
{
my.z -= 50 * time;
wait (1);
}
snd_play (fall_snd, 50, 0);
}

J'utilise simplement unsprite orienté qui est sensible à l'impact; s'il est touché par une flèchette, il exécutera la fonction fall(). Le tableau des scores décrémente sa hauteur jusqu'à atteindre -500 quants sur z. Le joueur entendra un son lorsque le tableau arrêtera sa chute: il ou elle pensera que le tableau a atteint le sol...ce qui est ok car il ne peut pas voir le sol :)

Il est temps de voir le code de la cible - c'est vraiment simple:

action got_50
{
my.skill1 = 50;
my.enable_impact = on;
my.event = compute_score;
}

action
got_25
{
my.skill1 = 25;
my.enable_impact = on;
my.event = compute_score;
}

action
got_10
{
my.skill1 = 10;
my.enable_impact = on;
my.event = compute_score;
}

function
compute_score()
{
score += my.skill1;
}

Ces actions sont attachés aux entités placés au dessus du grand cercle de la cible. Le score pour chaque entité est stocké dans son skill1; lorsqu'une des flèchettes touche une de ces entités, la valeur contenue dans skill1 est ajouté au score total.


Untitled

Fontaine

Cette fontaine utilise des particules qui s'arrêtent (ne passent pas à travers) lorsqu'elle rencontre une entité wmb ou un bloc de niveau et crée une brume agréable à l'oeil lorsqu'elle touche la surface de l'eau.

action
fountain
{
my.invisible = on;
my.passable
= on;
while(1)
{
effect(particle_fountain, 15 * time, my.x, nullvector);
wait(1);
}
}

L'action ci-dessus doit être attachée à un petit modèle; il le met invisible et passable et génère des particules en utilisant la fonction particle_fountain. Le nombre de particules générés est coorigé par time.

function
particle_fountain()
{
temp.x = random(6) - 3;
temp.y = random(6) - 3;
temp.z = random(10) + 10; // play with
this value
vec_set(my.vel_x,temp);
my.bmap =
drop_tga;
my.size = 1.5;
my.flare = on;
my.bright = on;
my.move = on;
my.streak
= on; // if your engine version supports it
my.gravity = 2;
my.alpha = 80;
my.lifespan = 50;
my.function
= fade_particles;
}

Les particules ont une vitesse aléatoire en x et en y (-3 à 3) et une vitesse aléatoire positive en z (10 à 20). Notez que j'utilise le drapeau "streak" qui donne de meilleures particules mais qui n'est peut-être pas utlisable sur votre version de moteur; essayez d'utiliser différentes images de particules si vous ne pouvez pas utiliser streak. Les particules ont une gravité de 2 et une durée de vie de 50; la fonction qui les gère s'appelle fade_particles.

function
fade_particles()
{
my.alpha -= 2 * time;
if(content(my.x) == content_solid) // the particle is inside a solid?
{
effect(fountain_mist, 1, my.x, nullvector);
// generate mist
my.lifespan = 0; // don't
allow the particles to continue their movement if they have hit something solid
}
if(my.alpha < 0)
{
my.lifespan = 0;
}
}

la première ligne de code diminue la transparence des particules. Si ine des particules termine dans un solide (une entité wmb ou un bloc de niveau) elle génère une particule de brume en utilisant la fonction fountain_mist puis disparaît. Pour de bons résultats vous devriez utilser des fontaines avec des murs épais parce que les gouttes d'eau tombent à grande vitesse et pourraient pénétrer dans les murs de la fontaine si elle était trop mince. Les dernières lignes de la fonction détruise les particules lorque le facteur de transparence arrive à zéro.

function
fountain_mist()
{
temp.x = random(2) - 1;
temp.y = random(2) - 1;
temp.z = random(1) + 1; // play with
this value
vec_set(my.vel_x,temp);
my.bmap =
smoke123_tga;
my.size = 5;
my.lifespan = 50;
my.flare = on;
my.bright = on;
my.move
= on;
my.gravity = 0;
my.alpha = 70;
my.function = fade_mist;
}

La brume a une vitesse aléatoire en x et y (-1 à 1) et une vitesse positive en z (1 à 2). La fonction qui la gère s'appelle fade_mist:

function
fade_mist()
{
my.alpha -= 3 * time;
my.size
+= 0.5 * time;
if(my.alpha < 0)
{
my.lifespan = 0;
}
}

Cette fonction diminue la transparence de la particule de brume tout en diminuant sa taille. Lorsque son alpha atteint zéro la particule est enlevée. Vous pouvez jouer avec la valeur temp.z à l'intérieur de fountain_mist() et avec alpha et size (taille) dans fade_mist() jusqu'à ce que vous obteniez une brume parfaite. Je ne suis pas un fan de particules excessivesmais voici un exemple de ce que vous pouvez obtenir avec un jeu de valeurs légèrement différentes:


Untitled

Forêt

Ce code génère une forêt dans une zone carré de notre niveau. Cela vous épargnera beaucoup de travail et mieux encore, les forêts seront différentes à chaque exécution de votre niveau.

define num_trees = 100;

var max_x = 1000;
var min_x = -1000;
var max_y = 1000;
var min_y = -1000;

La forêt aura 100 arbres; dans mon exemple les arbres sont placés à l'intérieur d'un rectangle (-1000 quants ... 1000 quants en x et -1000 ... 1000 quants en y mais nous pouvons utiliser n'importe quelles autres valeurs).

La forêt est générée en exécutant cette fonction dans main:

function generate_forest()
{
randomize(); // générera
toujours une forêt différente
while (tree_index < num_trees)
{
tree_pos.x = sign(min_x)
* random(abs(min_x)) + sign(max_x) * random(abs(max_x));
tree_pos.y = sign(min_y)
* random(abs(min_y)) + sign(max_y) * random(abs(max_y));
tree_pos.z = 1000;
// 3000 quants au dessus du sol du niveau
tree_index += 1;
if (random(1) >
0.5)
{
ent_create (tree1_mdl, tree_pos, create_tree);
}
else
{
ent_create (tree2_mdl, tree_pos, create_tree);
}
ifdef
fun;
waitt (8);
endif;
}
}

Pour commencer nous appelons la fonction prédéfinie randomize(); De cette façon nous nous assurons d'avoir une forêt différentye à chaque exécution du niveau. Les nombres aléatoires de la fonction random ne sont pas aléatoires; elles sont généralemenent calculées en utilisant des fonctions compliquées mais à chaque fois que vous redémarrez un niveau la fonction "random" redonne les mêmes séries de nombres. Randomize nous assure donc que tout cela n'arrivera pas et que nos nombres aléatoires le seront bien, OK?

Tant que nous n'avons pas généré tous nos arbres (tree_index < num_trees), nous générons une position d'arbre aléatoire à l'intérieur de notre zone rectangulaire, 1000 quants au dessus du sol (si l'axe z du sol est à 0). Nous ajoutons 1 à tree_index, indiquant que nous avons généré un nouvel arbre et en fonction d'une autre valeur aléatoire nous créons un des 2 modèles d'arbres utilisés dans notre forêt.

Si nous exécutons le niveau de cette façon: "office.wdl -d fun" nous plaçons 2 arbres à la seconde, aussi nous assisterons à leur création et à leur placement dans notre niveau. Si nous exécutons sans -d fun tous les arbres seront placés trop vite, mais c'est ce que nous voulons, n'est-ce pas?

La fonction qui s'exécute pour chaque arbre est:

function
create_tree()
{
wait (1);
my.pan = random(360);
vec_set (temp, my.pos);
temp.z -= 3000;
trace_mode = ignore_me +
ignore_sprites + ignore_models + use_box;
my.z -= trace (my.pos, temp) + 20;
}

Vous pouvez voir que l'arbre à un angle pan aléatoire. Puis nous traçons 3000 quants sous la position initiale et nous décrémentons z jusqu'à ce qu'il soit placé sur le sol. Nous ajoutons 20 quants pour être bien sur que le tronc soit bien enfiché dans le sol surtout s'il est sur une pente.

Assurez-vous d'avoir un modèle avec peu de polygones ou mieux, utilisez des sprites arbres Si votre arbre a 500 polygones et que vous voulez 500 arbres dans votre forêt, vous ne trouverz pas une carte 3d capable de rendre 250,000 polygones à un taux d'affichage décent.


Untitled

Flickgen

Ne serait-ce pas merveilleux d'avoir un générateur de code WDL? Certains utilisateurs ont créé de superbes générateurs de particules il y a quelques temps déjà. Vous donnez quelques valeurs, glissez un curseur ou deux ensuite vous cliquez sur le bouton new et hop, le fichier wdl est créé. Il ne vous reste plus qu'à l'inclure dans votre projet et il fonctionnera merveilleusement bien. Mais comment cela est-il possible?

Avant tout vous devez créer une pièce et positionner une caméra. Vous créez quelques curseurs / boutons / peu importe, ils changeront ce que vous voyez. lorsque vous êtes satisfait du résultat, vous pressez un bouton et un fichier wdl est créé. Le fichier peut inclure des commentaires etc mais vous pouvez être sur qu'il incluera les action(s) / function(s) nécessaires pour votre effet. Cela semble facile, non ? Voyons voir comment c'est fait.

Flickgen vient de flickering light generator - ce projet génère des codes pour faire des lumières qui clignotent. Vous pouvez jouer sur les couleurs (Rouge, Vert, Bleu) , la portée, le temps d'allumage et le temps d'extinction. Avant que nous commencions, regardez le fichier test.wdl dans le répertoire \flickgen - son contenu a été généré par Flickgen.

Chaque projet autonome a sa propre fonction main:

function
main()
{
d3d_lightres = 1; // les lumières dynamiques sont meilleures
level_load (flickgen_wmb);
wait (2);
mouse_init();
}

La première ligne permet une meilleure qualité pour les lumières dynamiques; le niveau test est chargé et nous montrons le curseur de la souris:

function
mouse_init()
{
mouse_map = arrow_pcx;
mouse_mode = 2;
while (1)
{
mouse_pos.x = pointer.x;
mouse_pos.y = pointer.y;
wait (1);
}
}

Mon niveau test inclut un modèle qui a cette action qui lui est attachée:

action camera_init
{
my.invisible = on;
my.passable = on;
vec_set (camera.pos, my.pos);
camera.pan = my.pan;
camera.tilt = my.tilt;
}

J'ai choisi comme modèle une flèche; changez sa position et ses angles pour avoir une meilleure vue parce que la caméra est liée à ce modèle de flèche. L'objet qui génère la lumière clignotante est un modèle cube qui exécute l'action suivante:

action flicker
{
my.passable = on;
while (1)
{
my.z = height;
my.lightred = red;
my.lightgreen =
green;
my.lightblue =
blue;
my.lightrange =
distance;
waitt (interval1);
my.lightrange =
0;
waitt (interval2);
}
}

Cette
action est très simple, mais toutes les variables qu'elle utilise sont
contrôlées par des curseurs; height est la hauteur de la source lumineuse
clignotante, red (rouge) green (vert) et blue (bleu) sont les valeurs pour lightred,
lightgreen et lightblue, distance est la valeur pour lightrange, interval1 et
interval2 sont les valeurs de durée pour allumé / éteint
pour la lumière. Regardons comment fonctionne les curseurs:

panel adjust_pan // main
panel
{
bmap = panel_pcx;
layer = 20;
pos_x = 0;
pos_y = 0;
vslider = 2, 10, 100, slider_pcx,
0, 255, red;
vslider = 22, 10, 100, slider_pcx, 0, 255, green;
vslider = 42, 10, 100, slider_pcx, 0, 255, blue;
vslider = 62, 10, 100, slider_pcx,
1, 100, interval1;
vslider = 82, 10, 100, slider_pcx, 1, 100, interval2;
vslider = 102, 10, 100, slider_pcx, 0, 500, distance;
vslider = 122, 10, 100, slider_pcx,
10, 500, height;
flags = d3d, overlay, refresh, visible;
}

La 1ère définition place le curseur à (2,10) pixels, met sa taille verticale à 100 pixels, utilise l'image slider_pcx pour le curseur et modifie la valeur de la red entre 0 et 255. Les autres curseurs fonctionnent sur le même principe.

Si
vous avez eu la curiosité de "jouer" avec Flickgen vous avez
noté que j'affiche les vraies valeur RGB, etc à l'écran.
J'utilise une simple définition de panneau pour cela:

panel digits_pan // affiche les valeurs rgb..etc
{
layer = 21; // apparaît au dessus de adjust_pan
pos_x = 0;
pos_y = 0;
digits = 0, 130, 3, univers_font,
1, red;
digits = 20, 140, 3, univers_font, 1, green;
digits = 40, 130, 3, univers_font, 1, blue;
digits = 60, 140, 3, univers_font,
1, interval1;
digits = 80, 130, 3, univers_font, 1, interval2;
digits = 100, 140, 3, univers_font, 1, distance;
digits = 120, 130, 3, univers_font,
1, height;
flags = d3d, overlay, refresh, visible;
}

La valeur pour "red" apparaît à (0,130) pixels, sur 3 digits, avec univers_font, et sa valeur est multipliée par 1. Flickgen a 2 autres boutons : save and quit. En fait ces boutons sont 2 petits panneaux et leur définition sont aussi simple que possible:

panel quit_pan
// montre le bouton quit
{
bmap = quit_pcx;
layer = 21; // apparaît au dessus
de adjust_pan
pos_x = 10;
pos_y = 190;
flags = d3d, overlay, refresh,
visible;
on_click quit;
}

panel
save_pan // montre le bouton save
{
bmap = save_pcx;
layer = 21; // apparaît au dessus
de adjust_pan
pos_x = 10;
pos_y = 160;
flags = d3d, overlay, refresh,
visible;
on_click save_file;
}

function quit()
{
exit; // exit the engine
}

Lorsque vous cliquez le bouton "Quit", la fonction quit qui lui est associée est exécutée. Vous pouvez voir que cette fonction coupe tout simplement le moteur, c'est ce que nous voulions, non ? Lançons-nous dans la définition de 2 textes et quelques chaînes de caractères:

text action_txt
{
layer = 21;
pos_x = 55;
pos_y = 176;
font = albertus_font;
string = action_str;
}

text
filename_txt
{
layer = 21;
pos_x = 55;
pos_y = 204;
font = albertus_font;
string = filename_str;
}

string action_str = " "; // pour le nom de l'action
string filename_str = " "; // pour le nom du fichier
string empty_str = ""; // aune chaîne vide
string content_str; // pour le contenu du fichier
string indent_str = " "; // 4 espaces mais vous pouvez utiliser une autre valeur d'indentation
string temp_str = " "; // juste une chaîne temporaire pour la conversion des nombres en chaines

Si vous cliquez sur le bouton "Save", la fonction save_file est exécutée:

function
save_file()
{
action_txt.visible = on; // montre le texte
str_cpy (action_str, empty_str);
while (str_cmpi (action_str, empty_str) == 1) // on s'assure que le joueur a bien
tapé quelque chose
{
inkey action_str;
// montre le curseur -> stocke les caractères saisis dans filename_str
wait (1);
}
filename_txt.visible = on;
// montre le texte
str_cpy (filename_str, empty_str); // remet à zéro l'entrée
précédente
while (str_cmpi (filename_str,
empty_str) == 1) // on s'assure que le joueur
a bien tapé quelque chose
{
inkey filename_str;
// montre le curseur -> stocke les caractères saisis dans filename_str
wait (1);
}
waitt (16); // attend une seconde
action_txt.visible = off;
// cache le texte
filename_txt.visible = off; // cache le texte

Le texte action_txt est rendu visible et action_str est remise à zéro. Nous voulons stocker le nom de notre nouvelle action dans action_str; si nous utilisions inkey ici, le joueur pourrait presser la touche entrée sans rien saisir et ça ne serez pas bien. Nous pouvons nous prémunir contre cela en mettant l'instruction inkey dans une boucle while; si action_str = "" (rien), inkey continue de s'exécuter jusqu'à ce quelque chose soit entré dans le champ "Action name" . La même chose se produit avec le reste du code qui stocke le nom du fichier dans filename_str. Les textes restent visibles encore une seconde puis disparaîssent.

str_cpy (content_str, empty_str); // vide la chaîne
str_cat (content_str, "// incluez ce fichier dans votre fichier c-script principal\n// placez n'importe quel modèle dans votre niveau et attachez lui l'action ");
str_cat (content_str, action_str);
// str_cat (content_str, " action"); ndt cette ligne n'est pas utilisée dans la version française, le mot action se trouvant avant le nom de l'action
str_cat (content_str, "\n\naction ");
str_cat (content_str, action_str);
str_cat (content_str, "\n{\n");
str_cat (content_str, indent_str);
str_cat (content_str, "while (1)\n");

La chaîne content_str est utilisée pour stocker tout le texte qui doit être écrit dans notre fichier WDL. Ouvrez le fichier test.wdl qui est livré avec Flickgen ou un autre fichier créé par vous avec Flickgen, sinon je vais vous perdre en chemin :) La chaîne est vidée de son contenu et les lignes de commentaires sont ajotées à la chaîne. Nos commentaires doivent inclure le nom de notre action, aussi nous ajoutons le nom stocké dans action_str à cet effet. (dans la version anglaise on ajoute ensuite le mot action). J'espère que vous vous souvenez: \n équivaut à un retour à la ligne comme si vous aviez pressé la touche Entrée dans un traitement de texte. A près 2 retour à la ligne nous commençons à écrire "action", suivi par un espace et le nom stocké dans action_str. Retour à la ligne, le signe "{" et un autre retour à la ligne. Maintenant il est temps d'ajouter notre indent_str, une chaîne qui contient 4 espaces puis le texte "while (1)" et un autre retour à la ligne. Voyez-vous tout cela dans votre fichier WDL généré? Voici la suite:

    str_cat (content_str, indent_str);
str_cat (content_str, "{\n");
str_cat (content_str, indent_str);
str_cat (content_str, indent_str);
str_cat (content_str, "my.lightred
= ");
red
= int(red);
str_for_num (temp_str, red);
str_cat (content_str, temp_str);

Nous ajoutons un autre indent_str à content_str, une accolade et un retour à la ligne, 2 chaînes indent_str et nous ajoutons le texte : "my.lightred = ". La valeur choisie pour le rouge par le curseur est convertie en un nombre entier, convertie en chaîne et ajoutée à content_str. On fait la même chose pour le vert, le bleu et la distance, faites moi confiance la dessus.

    str_cat (content_str, ";\n");
str_cat (content_str, indent_str);
str_cat (content_str, indent_str);
str_cat (content_str, "my.lightgreen
= ");
green
= int(green);
str_for_num (temp_str, green);
str_cat (content_str, temp_str);
str_cat (content_str, ";\n");
str_cat (content_str, indent_str);
str_cat (content_str, indent_str);
str_cat (content_str, "my.lightblue
= ");
blue
= int(blue);
str_for_num (temp_str, blue);
str_cat (content_str, temp_str);
str_cat (content_str, ";\n");
str_cat (content_str, indent_str);
str_cat (content_str, indent_str);
str_cat (content_str, "my.lightrange
= ");
distance
= int(distance);
str_for_num
(temp_str, distance);
str_cat
(content_str, temp_str);

Maintenant il faut ajouter les lignes waitt(x); nous avons un retour à la ligne, 2 * indent_str, la chaîne "waitt (" , la valeur entière de interval1 est convertie en chaîne et ajoutée à content_str, nous ajoutons ")" et allons à la prochaine ligne etc etc.

    str_cat (content_str, ";\n");
str_cat (content_str, indent_str);
str_cat (content_str, indent_str);
str_cat (content_str, "waitt (");
interval1 = int(interval1);
str_for_num (temp_str, interval1);
str_cat (content_str, temp_str);
str_cat (content_str, ");\n");
str_cat (content_str, indent_str);
str_cat (content_str, indent_str);
str_cat (content_str, "my.lightrange
= 0;\n");
str_cat
(content_str, indent_str);
str_cat
(content_str, indent_str);
str_cat
(content_str, "waitt (");
interval2
= int(interval2);
str_for_num
(temp_str, interval2);
str_cat
(content_str, temp_str);
str_cat
(content_str, ");\n");
str_cat
(content_str, indent_str);
str_cat
(content_str, "}\n");
str_cat
(content_str, "}");

Si vous êtes observateur vous noterez que les lignse suivantes ajoutent .wdl à notre nom de fichier stocké dans filename_str, ouvre le fichier pour écriture, écrit content_str dans le fichier et le ferme. Ouf le cauchemar est enfin terminé, mais en êtes-vous surt?

str_cat (filename_str, ".wdl");
filehandle = file_open_write (filename_str);
file_str_write (filehandle, content_str);
file_close (filehandle);
}

Le curseur nommé P (position) déplace la lumière vers le haut ou vers le bas. Si vous prévoyez de mettre une lampe clignotante au plafond de votre pièce, 300 quants au dessus du sol, vous pouvez mettre P à 300 et voir ce que ça donne.
En prenant ce code comme base de travail vous pouvez créer n'importe quel assistant pour c-script: générateur de particules, éditeur de script, etc.


Untitled

Grenades

Pmusieurs d'entre vous m'ont posé la question sur le forum : où puis-je trouver un bon code pour l'explosion d'une grenade? Ne cherchez plus, cet article va vous apprendre à créer le code qui fera exploser une grenade sur un impact, détruisant toutes les entités alentour. Intéressé pour voir une explosion en chaîne? J'ai réalisé un petit niveau de démo exprès pour vous!

Nous commençons par une fonction starter:

starter init_grenades()
{
while (player == null) {wait
(1);}
while (1)
{
if (mouse_left == 1)
{
ent_create (grenade_mdl, player.x, move_grenade);
while (mouse_left == 1) {wait (1);} // wait until the LMB is released
}
sleep (0.1);
}
}

Cette fonction attend que le joueur soit créé et démarre une boucle while (1). A chaque fois que nous pressons le bouton gauche, une grenade est créée et elle commence son déplacement comme on le voit dans la fonction move_grenade:

function
move_grenade()
{
my.passable = on;
my.pan
= camera.pan;
my.tilt = camera.tilt;
my.enable_entity
= on;
my.enable_impact = on;
my.enable_block
= on;
my.event = grenade_event;
while (my !=
null)
{
if (vec_dist (my.x,
player.x) > 50) {my.passable = off;}
my.skill1
= 50 * time; // movement speed
my.skill2 =
0; // speed on y
if (my.skill40 < 100) //
covered at least 100 quants?
{
my.skill3 = 0; // if not, don't apply gravity yet
}
else // moved away from the player
{
my.skill3 = -5 * (my.skill40 / 100) * time;
}
move_mode = ignore_you + ignore_passable;
my.skill30 = ent_move (my.skill1, nullvector); // get
the distance that was covered this frame in skill30
my.skill40 += my.skill30; // store the distance covered so far in skill40
wait (1);
}
}

Cette fonction est identique à celle utilisée pour les éclats dans le coin des débutants du même numéro d'aum; la grenade est sensible aux entités non passables et aux blocs de niveau et elle exécute sa fonction "grenade_event" dès qu'il se produit une collision. La grenade deviendra non passable lorsque la distance entre elle et le joueur sera supérieure à 50 quants. Nous appliquons la gravité à la grenande si elle couvert une distance supérieure à 100 quants, comme sur l'image ci-dessous, jouez avec la valeur "-5" si vous voulez ajuster la gravité. Oh, et nous utilisons skill40 pour stocker la distance totale parcourue par la grenade, skill30 étant mis à la distance parcurue à chaque cycle.

Que sepasse-t-il lorsque la grenade heurte quelque chose? regardons sa fonction évènement:

function
grenade_event()
{
vec_set (temp, my.pos);
temp.z += 10; // create the explosion sprite 10 quants above the ground
ent_create(explosion_pcx, temp, animated_explosion);
ent_remove (me);
}

Nous stockons la position actuelle de la grenade dans temp, nous ajoutons 10 quants à la coordonnée z, puis nous créons un sprite nommé explosion_pcx à cette position, en l'attachant à la fonction nommée animated_explosion. La dernière ligne de code enlève la grenade.

function
animated_explosion()
{
my.passable = on;
my.flare = on;
my.ambient = 100;
ent_playsound
(my, explosion_wav, 500);
while (my.frame < 7)
{
my.frame += 1 * time;
my.scale_x += 2 * time;
my.scale_y = my.scale_x;
my.scale_z = my.scale_x;
player.roll = 2 - random(4); // shake the player a bit
wait (1);
}
player.roll = 0;
my.invisible = on;
temp.pan = 360;
temp.tilt
= 180;
temp.z = 400;
scan_entity (my.x, temp);
sleep (2);
ent_remove (me);
}

Le sprite explosion est passable, a son drapeau flare de mis et son ambient est 100. Nous jouons le son explosion_wav aux coordonnées de l'impact, et nous jouons toutes les frames de l'animation pour notre sprite dans une boucle while.

La même boucle while augmente l'échelle du sprite de l'explosion à chaque cycle pour un effet plus dynamique et secouer un peu le joueur, en fait il choisit simplement une valeur aléatoire entre -2 à 2 pour l'angle roll de la caméra. Dès que nous quittons la boucle while nous remettons à 0 l'angle roll de la caméra, nous cachons le sprite de l'explosion, nous ne pouvons pas encore l'enlever car le son de l'explosion qui lui est associé continue à jouer.

Il est temps pour nous de parler des dommages causés dans la zone, aussi nous balayons dans un rayon de 360 degrés à l'horizontal, 180 degrés à la verticale et sur 400 quants, et nous exécutons scan_entity. Toutes les entités ayant leur drapeau enable_scan de mis déclencheront leur évènement. Les dernières lignes de code s'assurent que le son est bien terminé pour enlever le sprite explosion.

Que dire à propos des entités pouvant être détruites par notre grenade? Regardez par vous même cvomme c'est simple d'en créer:

action barrel
{
my.enable_scan = on;
my.event = barrel_explodes;
}

Ces barils sont sensibles au balayage et exécute leur fonction d'évènement barrel_explodes dès qu'ils sont scannés.

function
barrel_explodes()
{
my.event = null;
wait
(1 + random(20)); // wait up to 20 frames
ent_morph (my, barrel_pcx);
my.flare = on;
my.ambient = 100;
ent_playsound (my, explosion_wav, 200);

La première ligne met la fonction évènement à null; de cette façon un même baril ne peut pas exécuter son évènement plus d'une fois. Nous attendons d'une façon aléatoire (1..20) afin de rendre l'effet plus réaliste puis nous transformons le baril en sprite explosion avec son drapeau flare de mis. Nous avons besoin d'une explosion brillante aussi nous mettons le drapeau ambient du sprite à 100. Finalement nous juons un son d'explosion.

   while (my.frame < 10)
{
my.frame += 1 * time;
wait (1);
}
my.invisible = on;
temp.pan = 360;
temp.tilt = 180;
temp.z =
150;
scan_entity (my.x, temp);
sleep (2);
ent_remove (me);
}

Le sprite explosion joue son animation dans une boucle xhile puis il devient invisible et génère un balayge sur un petit rayon (150 quants) parce que un baril peut (et doit) causer des turbulences dans son environnement ;) en balayant dont en déclenchant les évènements d'autres entités proches ayant leur enable_scan de mis. Nous attendons 2 secondes car nous voulons être sur que le son de l'explosion est terminé puis nous enlevons le sprite (invisible) de l'explosion.

Chargez mon niveau de démo office.wmp, jetez une grenade, reculez sur votre siège, prenez une poignée de popcorn et amusez-vous en regardant les explosions en chaîne.


Untitled

Guerre de l'espace 3D

N'avez-vous jamais joué à Demon Star ? La guerre de l'espace 3D n'a rien à voir avec les graphique de Demon Star 'mais vous voulez le code, pas les effets, none ? Vers la fin de cet article vous aurez un niveau jouable avec des canons, ennemis... n'est-ce pas là l'essentiel ?.

Cette fois j'utilise la fonction Main elle-même pour redémarrer le jeu

on_r main();

function main()
{
while (key_r == 1) {wait (1);}
waitt (8); // wait 0.5 seconds before restarting
load_level <space01.wmb>;
score = 0; // reset the score at game start
}

Nous ne souhaitons pas déclencher plusieurs redémarrage lorsque nous pressons R (restart) pendant trop longtemps aussi j'ai mis une boucle while dans main.

Toutes les entités déclencheront la fonction explode_me() lorsqu'elle seront tuées (joueur, ennemis, rockets, etc)

function
explode_me()
{
exclusive_global;
play_sound explo_snd, 100;
if (you.skill2 == 1) {score += 10 + random(10);}
remove me;
}

Si cette fonction est déclenchée par un méchant (skill2 = 1) le score du joueur augmentera.

Le joueur contrôle le vaisseau utilisant cette action:

action
ship
{
player = me;
my.enable_entity = on;
my.enable_push = on;
my.enable_impact = on;
my.event = explode_me;
camera.diameter = 0;
camera.tilt = -90;
camera.pan = 90;
camera.z = my.z + 700;
while (1)
{
if (my.y > 2975) {return;}

       ship_speed.x = 0;
ship_speed.y = 10 * time;
ship_speed.z = 0;
if (key_cuu == 1)
{
ship_speed.y
+= 20 * time;
}
if (key_cud == 1)
{
ship_speed.y
-= 5 * time;
}
if (key_cur == 1 && my.x
< 450)
{
ship_speed.x
= 30 * time;
}
if (key_cul == 1 && my.x
> -450)
{
ship_speed.x = -30 * time;
}
ent_move (nullvector, ship_speed);
camera.x = my.x;
camera.y = my.y + 200;

if (key_space == 1)
{
fire_rocket();
}
if (key_ctrl == 1)
{
fire_bomb();
}
wait (1);
}
}

La caméra peut passer à travers les murs, est incliné vers le bas et tourné de 90 degrés de sorte qu'elle se dirige dans la bonne direction, 700 quants au-dessus du joueur. J'ai mesuré y = 2975 (où le niveau se termine) dans Wed. Vous devriez mettre une entité invisible et passable (ou pas) dans Wed et comparer son y au y du joueur. Le vaisseau (commandé par le joueur) se déplace vers le haut tout le temps parce que son ship_speed.y est positif tout le temps. Si nous pressons les touches de direction la vitesse du joueur est doublée (touche haut) ou divisée par deux (touche bas). Le vaisseau peut se déplacer à gauche et à droite aussi longtemps que son x est plus grand que -450 et plus petit que 450.

Si vous pressez la barre espace le vaisseau tirera des rockets; pressez ctrl pour lâcher des bombes :

function
fire_rocket()
{
exclusive_global;
while (key_space == 1) {wait (1);}
if (player == null) {return;}
vec_set (temp, player.pos);
temp.y += 40;
create (<rocket.mdl>, temp, rocket_moves);
}

Nous ne permettons pas le feu automatique (ça ne serait pas juste) et naturellement nous ne pouvons pas tirer des rockets si le joueur est mort (player = null). Les roquettes sont créées 40 quants au-dessus du joueur (nous stockons la position du joueur dans temp, nous ajoutons 40 quants au y du joueur et alors nous créons la roquette ici). Jetons un coup d'oeil à la fonction qui déplace les roquettes du joueur :

function
rocket_moves()
{
my.enable_entity = on;
my.enable_push = on;
my.enable_impact = on;
my.event = explode_me;
my.skill1 = 0;
rocket_speed.x = 0;
rocket_speed.y = 80 * time;
rocket_speed.z = 0;
while (my.skill1 < 10)
{
my.skill1 += time;
ent_move (nullvector, rocket_speed);
wait (1);
}
remove me;
}

La roquette se déplace dans la direction du joueur, beaucoup plus rapidement que le joueur (elle se heurterait au joueur si sa vitesse était plus petite que la vitesse du joueur). Une bonne chose à faire est de détruire la roquette après une certaine distance. Nous ne voulons pas permettre le tir dés le début du niveau et avoir tous les ennemis morts pour le reste du niveau entier parce que nos roquettes traversent le niveau dans son entier - et nous ne voulons pas perdre en performance en laissant nos roquettes voler toutes seules dans le niveau. C'est pourquoi nous plaçons skill1 à 0 pour chaque roquette et puis nous l'augmentons en fonction du taux d'affichage. Quand skill1 > 10, la roquette disparaîtra.

function
fire_bomb()
{
exclusive_global;
while (key_ctrl == 1) {wait (1);}
if (player
== null) {return;}
create
(<bomb.mdl>, my.pos, bomb_moves);
}

La fonction fire_bomb est plus simple que fire_rocket aussi je ne l'expliquerai pas ici. Vous rappelez-vous que j'ai créé la roquette 40 quants au-dessus du joueur ? Je pourrais avoir créé la bombe 100 quants au-dessous du joueur mais j'ai voulu vous montrer une technique différente qui empêche la bombe de se heurter à son créateur au moment de sa création. Jetons un coup d'oeil à la fonction qui déplace la bombe du joueur :

function
bomb_moves()
{
my.enable_entity = on;
my.enable_push = on;
my.enable_impact = on;
my.event = explode_me;
my.skill1 = 0;
bomb_speed.x = 0;
bomb_speed.y = ship_speed.y;
bomb_speed.z = -10;
while (my.skill1 < 100)
{
if (my.skill1 < 5)
{
my.passable
= on;
}
else
{
my.passable = off;
}
my.skill1 += time;
ent_move (nullvector, bomb_speed);
wait (1);
}
remove me;
}

La bombe est détruite quand son skill1 est plus grand que 100. Vous voyez que bomb_speed.z = -10 ; cela semble logique puisque la bombe descend mais pourquoi bomb_speed.y = ship_speed.y ? Si le joueurse déplace à la vitesse de la lumière ( touche haut pressée) les bombes à l'intérieur du vaisseau se déplacent à la même vitesse aussi leur inertie devrait les déplacer devant leur position de lancement. Si le joueur appuie sur la touche bas, la bombe devrait toucher la terre près de sa position de lancement. Naturellement qu'il n'y a aucune bombe à l'intérieur du vaisseau mais seulement vous et moi le savons- correct ? Cette ligne de code minuscule prend soin de cela.

if
(my.skill1 < 5)
{
my.passable = on;
}
else
{
my.passable = off;
}

Les bombes étant créées à la position du joueur elles se heurteraient à leur créateur (le joueur). Cette fois je rends les bombes passables pendant une période courte ainsi elles peuvent sortir du joueur et ensuite je place leur drapeau passable à off.

Jetons un coup d'oeil aux méchants :

action
enemy1
{
my.z = player.z; // make sure
that it collides with the player and its rockets
my.skill2 = 1;
my.enable_entity = on;
my.enable_push = on;
my.enable_impact = on;
my.event = explode_me;

Nous voulons nous assurer qu'enemy1 (et surtout, ses roquettes) peuvent heurter le joueur aussi nous plaçons sa position z à la position z du joueur. Ceci signifie que nous pouvons placer le vaisseau enemy1 à n'importe quelle hauteur dans Wed - il sera aligné à la hauteur du joueur. Enemy1 est un méchant aussi nous plaçons son skill2 à 1 (nous vérifions ceci dans la fonction explode_me() souvenez-vous ?)

    while (player != null)
{
if (abs(vec_dist (my.x, player.x))
< 500)
{
vec_set (my.skill10, player.x);
vec_sub (my.skill10, my.x);
vec_to_angle (my.pan, my.skill10);

my.pan += 90;
if (my.y > player.y)
{
create (<bullet1.mdl>, my.pos, bullet1_moves);
}
}
waitt (16);
}
}

L'enemy1 se déplacera seulement si le joueur est encore vivant (player != null). Quand le joueur vient plus près que 500 quants de enemy1, l'ennemi tournera (changeant son angle pan) et tireraune balle par seconde - le waitt (16). Nous ajoutons 90 degrés au vaisseau pour nous assurer que nous tirons les balles dans la bonne direction - nous employons un niveau et une caméra qui sont tournés de 90 degrés. Enemy1 tirera seulement si le joueur est au-dessous de lui. La fonction qui déplace la balle d'enemy1 est :

function
bullet1_moves()
{
my.pan = your.pan;
my.enable_entity = on;
my.enable_push = on;
my.enable_impact = on;
my.event = explode_me;
my.skill1 = 0;
e1bullet_speed.x = 0;
e1bullet_speed.y = -30 *
time;
e1bullet_speed.z = 0;
while (my.skill1 < 20)
{
if (my.skill1 < 2)
{
my.passable
= on;
}
else
{
my.passable
= off;
}
my.skill1 += time;
ent_move (e1bullet_speed, nullvector);
wait (1);
}
remove me;
}

La première ligne place l'angle pan de la balle avec l'angle pan d'enemy1's (son créateur). La balle descend et elle s'enlèvera après une certaine période. Nous rendons la balle passable d'abord pour l'empêcher de se heurter à son créateur. J'ai utilisé ce truc il y a environ une année pour me débarasser d'un bogue ennuyeux dans les calibres ; si vous créiez un ennemi qui tirait des roquettes, il se tuait lui-même parce que les roquettes se heurtaient son corps. La balle se déplace dans la direction mise par son propre angle pan ; nous n'employons pas ent_move (nullvector, e1bullet_speed) mais ent_move (e1bullet_speed, nullvector).

Pour finir , enemy2 comme canon :

action
enemy2
{
my.z = -270;
my.skill2 = 1;
my.enable_entity = on;
my.enable_push = on;
my.enable_impact = on;
my.event = explode_me;
while (player != null)
{
if (abs(my.x - player.x)+
abs(my.y - player.y) < 400) // player comes close to enemy1
{
create (<bullet2.mdl>, my.pos, bullet2_moves);
}
waitt (16);
}
}

J'emploie un tour simple pour aligner tous les canons au niveau du plancher (j'ai z = -270 dans Wed pour ce niveau) et je place skill2 à 1 (ce sont des méchants également). Enemy2 commence à tirer quand le joueur est près ; Je ne pourrais pas employer vec_dist ici parce que la distance entre enemy2 et le joueur est assez grande - les canons sont placés sur la terre et le joueur est placé vers le haut dans le ciel. Les canons tireront toutes les secondes, aussi.

Voici la dernière fonction - elle déplace la balle d'enemy2 mais il n'y a rien spécial avec elle :

function
bullet2_moves()
{
my.enable_entity = on;
my.enable_push = on;
my.enable_impact = on;
my.event = explode_me;
my.skill1 = 0;
e2bullet_speed.x = 0;
e2bullet_speed.y = 0;
e2bullet_speed.z = 40 * time;
while (my.skill1 < 100)
{
if (my.skill1 < 5)
{
my.passable
= on;
}
else
{
my.passable
= off;
}
my.skill1 += time;
ent_move (nullvector, e2bullet_speed);
wait (1);
}
remove me;
}

Ce jeu peut (et doit) être amélioré : vous pouvez faire éclater les bombes quand elles frappent la terre, calculer les "vrais" angles entre le joueur et les balles ennemies pour s'assurer que chaque projectile peut être mortel :) et ainsi de suite.


Untitled

Lance flamme

Si vous avez besoin d'une arme qui crée un jet de particules au lieu de tirer, vous êtes au bon endroit.

action
flame_thrower
{
my.metal = on;
while (player
== null) {wait (1);} // wait until the player is loaded
while
(vec_dist (my.x, player.x) > 60) // wait until the player has come close enough
{
my.pan += 5 * time;
wait (1);
}
snd_play(gotflames_wav,
60, 0);
my.passable = on;
while (1)
{
my.x = player.x + 20 * cos(player.pan)
+ 13 * sin(player.pan);
my.y = player.y
+ 20 * sin(player.pan) - 13 * cos(player.pan);
my.z = player.z + 12;
my.pan = camera.pan;
my.tilt = camera.tilt;
vec_for_vertex (flame_start, my, 14); // the flames will start at the coordinates
given by the 14th vertex on the gun
vec_set(flame_end,
nullvector); // reset flame_end
flame_end.x
= flame_range.x; // add 400 quants to it
vec_rotate(flame_end, camera.pan); // rotate it depending on player's angles
vec_add(flame_end.x, camera.x); // now set the
correct flame_end position in 1st person mode (use player.x instead of camera.x
for 3rd person)
if(key_ctrl == on) //
Ctrl was pressed?
{
create_flames(); // fire!
}
wait (1);
}
}

L'arme attend que le joueur soit à moins de 60 quants d'elle. Dès que cela se produit, l'arme devient passable et elle est placée 20 quants devant le joueur, 13 quants au dessus de l'origine du joueur, prenant dans la même direction et les mêmes angles que le joueur. Jouez avec les valeurs 20, 13 et 12 pour mettre le jet de flamme à une autre position dans un mode à la première personne. L'arme aura les mêmes angles pan et tilt que la caméra.

J'ai défini une variable nommée "flame_start" et j'utilise vec_for_vertex pour mettre le jet de flamme à la position du 14ème sommet à chaque cycle. ce sera l'origine de notre flamme. Nous mettons à zéro la 2ème variable nommée "flame_end",nous lui ajoutons le contenu de "flame_range" (une variable pour l'étendue de la flamme, mise à 400 quants), puis nous tournons "flame_end", créent ainsi un vecteur qui a une magnitude de 400 quants et une orientation donnée par les angles du joueur. Pour terminer, nous ajoutons la position de la caméra au vecteur, de cette façon nous obtenons la bonne valeur pour le point flame_end qui est (sauf surprise) le point où la flamme s'arrête.

A chaque fois que nous pressons la touche "Ctrl",la fonction create_flames() est exécutée:

function
create_flames()
{
vec_sub(flame_end.x, flame_start.x);
temp = vec_length(flame_end);
flame_end.x = (flame_end.x
* 3) / temp;
flame_end.y = (flame_end.y * 3) / temp;
flame_end.z = (flame_end.z * 3) / temp;
while(temp >
0)
{
effect(particle_flames,
1, flame_start.x, nullvector);
vec_add(flame_start.x,
flame_end.x);
temp -= 3;
}
}

Cette fonction calcule la distance entre flame_end et flame_start,puis la découpe en toute petite partie, créant des particules jusqu'à ce que flame_start ait la valeur de flame_end. Si vous pouviez voir de côté notre effet de particules, ce qui de nos jours est tout à fait possible, voici ce que vous verriez:

J'utilise beaucoup plus de particules pour cet effet. L'écran ci-dessus montre l'effet avec 20 à la place de 3 dans la fonction "create_flames", ce qui fait que l'effet utilise 7 fois plus de particules que l'effet original.

function
particle_flames()
{
temp.x = random(2) - 1;
temp.y = random(2) - 1;
temp.z = random(2) - 1;
vec_set(my.vel_x, temp);
my.move = on;
my.bmap
= fire_tga;
my.flare = on;
my.bright = on;
my.alpha = 100;
my.lifespan = 100;
my.size
= 15;
my.function = fade_flames;
}

function
fade_flames()
{
my.alpha -= 75 * time;
my.size
+= 10 * time;
if(my.alpha < 0) {my.lifespan = 0;}
}

Le reste du code est l'effet de particules standard; si votre effet d'effet de feu traine un peu la jambe vous pouvez réduire la durée de vie des particules. Oh et si vous preniez l'arme de votre ennemi pour votre flame_start et notre joueur pour flame_end vous verrez ce qui se passe lorsqu'onest de l'autre côté de l'effet de flamme. Pour ceux d'entre vous, trop fainénats, si, si, j'en connais, pour essayer ce petit code, j'ai réalisé un film, basse qualité, ici. Vous avez besoin de Media Player 9 ou plus pour le visualiser.


Untitled

IA parfaite

Ce mois-ci j'ai créé le premier article d'une série qui parlera d'IA (Intelligence Artificelle) avancée. Si vous pensez qu'intelligence artificielle parfaite sonne ttrop beau pour être vrai, voici ce que j'ai préparé pour vous : un ennemi qui connait la position du joueur en permanence et qui sait comment le rejoindre en prenant le chemin le plus court possible.

Il est évident que chaque jeu a ses besoins particuliers d'IA aussi jmon propos sera pour un niveau débutant - intermédiaire ; de cette façon tout le monde devrait pouvoir modifier le code existant et lui ajouter de nouvelles fonctionnalités. N'oubliez pas la rubrique AUM sur le forum officel : signalez-y vos idées et certaines d'entre elles viendront à la vie. Par exemple, tous les articles en ce magazine ont été demandés par vous sur ce forum.

Ok, maintenant voyons ce que nous pouvons apprendre ce mois-ci. J'ai écrit quelques articles concernant l'IA dans mes magazines précédents, la plupart du temps en utilisant la méthode "de tests et d'erreur". L'ennemi essaye d'arriver au joueur en se déplaçant vers lui ; quand il touche un obstacle, il tourne vers la gauche ou vers la droite en se déplaçant sur une certaine distance dans la nouvelle direction, et alors il essaye de nouveau d'arriver au joueur.

Si vous pensez que c'est de l'IA primaire, et bien vous avez raison ! Cependant, il i fait assez bien son travail pour des petits obstacles et il est extrêmement facile à coder. L'IA avançée doit employer des noeuds ou des points de destination (mais je ne vais pas employer ce terme parce que vous pourriez le confondre avec les points sur les chemins qui peuvent être créés dans Wed). Un noeud est un point d'intérêt placé dans le niveau ; jetez un coup d'oeil à tous les carrés colorés dans la première image : ce sont les noeuds créés par moi pour ce niveau de labyrinthe.

Chaque noeud doit pouvoir discuter avec un autre noeud. Vous pouvez ajouter autant de noeuds que vous voulez dans vos niveaux ; mais attention à ne pas employer de noeuds inutiles parce que vous ralentirez le jeu. Voici un exemple d'un mauvais placement de noeud :

Vous pouvez voir que "1" est isolé; il ne peut voir aucun autre noeud. La même chose est valable pour "3" et "8": ils peuvent se voir l'un l'autre mais aucun d'eux ne peut voir les autres noeuds. Maintenant voici la bonne version:

J'ai ajouté les noeuds "9" et "10" et à présent tous les noeuds peuvent discuter les uns avec les autres. Imaginons la situation suivante:

L'ennemi veut trouver le joueur aussi il doit se passer les choses suivantes :
- Poser cette question : lequel de vous, noeud, est le plus près du joueur et peut le voir?"
- "C'est moi, prends moi "dit le noeud 2.
- "Oh, je vois.." répond l'ennemi, "mais comment y aller?"
- "Bien", dit node 2, "je ne sais pas ce que vous devez faire mais je vous dit que je peux également voir le noeud 10".
- "Parfait", répond l'ennemi. "Hey, noeud 10, que vois-tu?"
- "je peux voir le noeud 5" répond noeud 10.
- "et vous, noeud 5?" demande l'ennemi.
- "je peux voir noeud 4 et noeud 7".
- "je peux voir noeud 7 également! Maintenant je sais comment rejoindre le joueur" dit l'ennemi en se frottant les mains.Tout ce que j'ai à faire c'est d'aller vers le noeud 7 puis le 5, puis le 10 puis le 2 et je trouverai (et tuerai) le joueur.

C'est comme cela que fonctionne une recherche avancée de chemin. Il y a un certain nombre d'algorithmes de recherche de chemin qui sont bien connus : A *, Dijkstra, largeur - premiere recherche, etc... Je pense que j'emploierai une combinaison des deux premiers algorithmes.

Maintenant que vous en savez un peu plus au sujet de l'IA voyons cen détail le code utilisé pour l'article sur l'IA de ce mois :
- un réseau de noeuds ;
- seules les noeuds qui sont près du joueur utilisent de précieuses ressources CPU ;
- les noeuds qui sont près du joueur et qui peuvent voir le joueur sont détectés ;
- le noeud le plus près du joueur qui peut voir le joueur est connu, ainsi que sa distance au joueur.

Je dirais que c'est un bon début, aussi laissez-nous voir le code qui a rendu toutes ces choses possibles. Je commencerai par vous présenter les tableaux qui sont employés dans la démo :

var
node_distance[50];
var see_player[50];
var nodex[50];
var nodey[50];

Node_distance[50 ] contient la distance d'un noeud spécifique au joueur ; ses éléments sont node_distance[0 ]... node_distance[49 ] mais nous n'allons pas employer le premier élément (nous voulons maintenir les choses aussi simples que possible) aussi ce tableau peut être employé par 49 noeuds. Mon niveau de demo emploie 30 noeuds mais si vous avez besoin de plus de noeuds vous pouvez augmenter la taille des tableaux ci-dessus.

See_player[50 ] est placé à un si le noeud peut voir le joueur et à zéro si le noeud ne peut pas voir le joueur. Jetez un autre coup d'oeil à l'image ci-dessus : le noeud 2 peut voir le joueur ainsi see_player[2 ] = 1 et pour noeud 10 qui ne peut pas voir le joueur see_player[10 ] = 0.

Nodex[50 ] et nodey[50 contiennent les coordonnées des noeuds en x et y ; nous avons besoin de ces tableaux parce que si nous voulons que notre ennemi aille au noeud 5 (par exemple) nous trouverons les coordonnées pour le noeud 5 dans ces tableaux.

Il est temps de voir un texte et un panneau :

text
ai_txt // displays the texts on the screen
{
font = ai_font;
pos_x = 0;
pos_y = 0;
string = "Target node:\nD.to
target:";
flags = visible;
}

panel
ai_pan // displays the numerical values on the screen
{
pos_x = 0;
pos_y = 0;
layer = 10;
digits = 240, 0, 3, ai_font,
1, target_node;
digits = 240, 22, 3, ai_font, 1, closest_distance;
flags = overlay, refresh, visible;
}

J'ai ai_txt pour montrer les deux rangées du texte à l'écran et ai_pan pour montrer les deux figures. Le noeud cible est le noeud le plus près du joueur qui peut voir le joueur et le "D. target" est la distance du joueur au noeud cible. Jetons un coup d'oeil à la fonction principale (main):

function
main()
{
fps_max = 60;
clip_size = 0;
level_load(level1_wmb);
wait (3);
camera.z = 2400;
camera.tilt = -90;

Nous limitons le taux d'affichage à 60 fps, nous nous assurons que nous montrons toutes les triangles pour tous les modèles puis nous chargeons le niveau. Nous attendons le niveau à charger et nous nous assurons que nous obtenons une bonne vue du niveau entier en choisissant une taille et un angle convenables pour la caméra.

     while (1)
{
index %= 49; //
limit the "index" value
index += 1; //
from 1 to 49

La boucle while déterminera le noeud le plus près qui peut voir le joueur. Nous employons une variable nommée index comme compteur ; sa valeur s'étend de 1 à 49 parce que nous pouvons employer jusqu'à 49 noeuds si ces tableaux ont 50 éléments.

if ((node_distance[index] < closest_distance) && (see_player[index] == 1))

Si la distance d'un nouveau noeud au joueur est plus petite que l'ancienne distance au joueur et que nous avons testé un node pouvant voir le joueur (see_player[index] = 1) alors nous avons un nouveau gagnant. Nous avons un nouveau noeud cible pour notre ennemi!

{
closest_distance = node_distance[index];
target_node = index;
}

Si ce n'est pas vrai, nous utilisons le gagnant précédent, l'ancien noeud qui était le prus près du joueur.

          else // haven't
found a new winner?
{
closest_distance = node_distance[target_node];
}
wait (1);
}
}

Nous utilisons un modèle qui a au moins 2 animations (marche = "walk" et attente = "stand") pour le joueur; regardons le code pour cela:

action
player1
{
player = me; // I'm the player
while (1)
{
my.pan += 4 * (key_cul
- key_cur) * time; // rotates with the left / right arrow keys
player_speed.x
= 10 * (key_cuu - key_cud) * time; // moves forward / backward with the up / down
arrow keys
if (key_cuu + key_cud
> 0) // if the player is walking
{
ent_cycle("walk", my.skill46); // play "walk" frames animation
my.skill46 += 7 * time; // "walk" animation speed
my.skill46 %= 100; // loop animation
}
else // the player
is standing
{
ent_cycle("idle", my.skill46); // play "stand" frames animation
my.skill46 += 2 * time; // "stand" animation speed
my.skill46 %= 100; // loop animation
}
move_mode = ignore_passable
+ glide;
ent_move(player_speed,
nullvector);
wait (1);
}
}

Le joueur tourne lorsque vous utilisez les touches de direction gauche ou droite et se déplace en avant / arrière avec les touches de direction haut et bas. Changez "4" et "10" pour changer la vitesse de rotation et de déplacement. Si le joueur est en train de marcher, il joue son animation de marche ("walk") et s'il est en attente il joue son animation d'attente ("stand"). Le joueur se déplace et glisse le long des murs s'il les cognent.

La dernière partie du code est celui associé aux noeuds:

action node
// using 30 nodes in this demo
{
my.skill47 = 1234;
my.passable = on;
my.skin = 1; // red
while (player == null) {wait
(1);}
node_number += 1; // get a unique number
my.skill48 = node_number; // and store it in skill48
nodex[my.skill48] = my.x; // store x and y
nodey[my.skill48] = my.y;
// for this node

Nous plaçons une valeur étrange pour que skill47 donne aux noeuds une identification unique, nous rendons le noeud passable parce qu'ils ne devraient pas bloquer le joueur ou ses ennemis puis nous choisissons la première peau pour le noeud (rouge). Le modèle de noeud a trois peaux : rouge, vert et bleu. Le rouge est la valeur par défaut, le vert est employé par les noeuds qui sont près du joueur et le bleu est employé par les noeuds qui peuvent "voir" le joueur.

Nous attendons jusqu'à ce que le joueur soit créé et alors nous assignons à chaque noeud un nombre unique, qui sera stocké dans skill48. Peut-être vous vous demandez comment ce système fonctionne aussit voici une explication détaillée : toute les entités placées dans notre niveau se charge une par une. La première entité de noeud qui apparaît dans le niveau additionnera un à node_number puis stockera la valeur (un) dans skill48. Le deuxième noeud additionnera un à la valeur existante (un) puis stockera le résultat (deux) dans son skill48. Le processus se répéte jusqu'à ce que toutes les entités de noeud soient chargées ; quand ceci se produit, chaque noeud a stocké son nombre à l'intérieur de son skill48.

Les deux dernières lignes du code stockent la position du noeud (x et y) dans le niveau.

while (1)
{
node_distance[my.skill48] = vec_dist(player.x, my.x);

Je vous ai dit que node_distance stocke la distance du noeud au joueur, n'est-ce pas ? La ligne du code ci-dessus fait cela. Si le joueur est à moins de 500 quants du noeud :

          if (node_distance[my.skill48]
< 500)
{
trace_mode = ignore_me + ignore_models + ignore_passents;
if (trace (my.pos, player.pos) == 0) // if this node can "see" the player
{
my.skin = 3; // the node changes its color to blue
see_player[my.skill48] = 1; // the node can see the player
}
else
{
see_player[my.skill48] = 0; // this node can't see the player
my.skin = 2; // can't see the player? keep the green color
}
}

Nous "traçons" du noeud au joueur ; si le résultat est zéro, le noeud peut voir le joueur aussi nous changeons la peau du noeud en bleu et nous plaçons see_player à 1 pour ce noeud. Si le noeud ne peut pas voir le joueur nous employons le vert comme couleur pour le noeud : le joueur est près mais ne peut pas être vu par le noeud.

else // far from the player? keep the red color
{
my.skin = 1;
}
sleep (0.1); // trace 10 times a second
}
}

Si le joueur est loin du noeud, nous gardons la couleur rouge pour le noeud (défaut). Toutes ces opérations se produisent 10 fois par seconde parce que la dernière instruction à l'intérieur de la boucle while est sleep(0.1).

Exécutons le projet : ouvrez level1.wmp, compilez-le et exécutez-le en tapant ai1.wdl dans la ligne d'option:

Vous pouvez voir l'image suivante:

Les carrés verts sont les noeuds qui sont à moins de 500 quants du joueur. Vous pouvez déplacer le joueur autour et voir comment les noeuds verts changent. Maintenant quittez le moteur et exécutez de nouveau le niveau mais cette fois-ci en employant -d traceplayer , comme dans l'image ci-dessous :

Vous pourrez voir l'image suivante:

Certains des noeuds verts se sont changés en bleu ; ce sont les noeuds qui sont à moins de 500 quants du joueur et qui "voient" le joueur. Il ne serait pas sage d'avoir des instructions de "trace" pour les noeuds qui sont lointains loin du joueur, n'est-ce pas ?

Je vous encourage à créer un ou plusieurs niveaux de test et à apprendre comment placer votre réseau de noeuds à l'intérieur de ce/ces niveaux. Voici un mauvais exemple pour un réseau correct de noeuds :

L'ennemi veut arriver au joueur, ainsi il doit utiliser ce chemin : 1. 5, 3, 2, joueur. Si j'avais ajouté le noeud absent (carré noir entouré de rouge) le chemin de l'ennemi au joueur aurait été beaucoup plus court, n'est-ce pas ? N'oubliez pas que l'IA parfaite commence par la planification parfaite de chemin.

La fois prochaine nous apprendrons à calculer le chemin le plus court entre deux noeuds quelconques dans le niveau : le point de départ (l'ennemi) et le point d'arrivée (le joueur).


Untitled

Traduction Olivier Burton - correction AB

IA parfaite - partie II

Le mois dernier j' ai commencé à écrire le code pour une IA (Intelligence Artificielle) parfaite, celui qui choisit le chemin le plus court d' un point de départ (l' ennemi) à un point de destination (le joueur). Ce mois-ci vous verrez l' algorithme dans l' action : ouvrez level2.wmp, compilez-le et exécutez-le :

Les captures d' écran "Touched" (le dernier noeud que la la souris a touché), "Start" (le noeud de départ) et "Target" (le noeud de destination). Déplacez maintenant la souris sur le noeud 27 et cliquez dessus; vous verrez que les noeuds qui décrivent le chemin le plus court de ce noeud au joueur changeront leur couleur de blanc à rouge, comme dans l' image ci-dessous :

Vous pouvez déplacer le joueur n' importe où; le noeud cible sera le noeud le plus proche du joueur qui peut voir le joueur. Vous pouvez cliquer sur n' importe quel noeud dans le niveau et vous obtiendrez le chemin le plus court jusqu' au joueur à chaque fois. Les numéros des noeuds qui décrivent le chemin le plus court apparaissent au sommet de l' écran. Il est clair que l' ennemi pourchassera le joueur comme un chien même si mon niveau d' exemple ressemble à un labyrinthe compliqué.

J' ai entendu beaucoup d' histoires sur la dificulté qu' il y a à écrire un bon code d' IA parce que la vitesse du jeu diminuera beaucoup, etc. Je suis heureux de vous dire qu' IA parfaite s'exécute vraiment vite; le chemin le plus court est calculé en moins d' un cycle d'affichage à chaque fois. J' ai employé l' algorithme de Floyd - Dijkstra - George :) parce qu' il est simple à mettre en oeuvre et il travaille extrêmement vite.

Je vous conseille de lire l' article dans Aum27; je n' expliquerai pas de nouveauce que j' ai déjà fait .

function
main()
{
mouse_range = 5000;
fps_max = 60;
clip_size = 0;
level_load(level2_wmb);
wait (3);
camera.z = 2400;
camera.tilt = -90;
init_mouse();
while (distances_computed == 0) {wait
(1);}
run_algorithm();

Nous employons la souris pour "toucher" les noeuds; déplacez la souris sur un noeud spécifique et vous verrez son numéro sur l' écran. La première ligne de code s' assure que nous pouvons toucher n' importe quel noeud même s' il est placé jusqu' à 5000 quants de distance. Nous limitons le taux d'affichage à 60 fps, nous montrons tous les triangles pour tous les modèles, nous chargeons le niveau et ensuite nous mettons une position de caméra convenable et inclinons l' angle. La fonction nommée init_mouse est votre fonction typique de souris ; j' inscrirai son code dans un moment. J' ai voulu calculer la distance entre les noeuds qui peuvent se voir directement (pas la distance la plus courte, ni le chemin réel!) au début de jeu si l' algorithme ne s'exécute pas distance_computed est mis à 1 par une autre action.

while (1)
{
index %= max_nodes;
if ((node_to_player[index] < closest_distance) && (see_player[index]
== 1))
{
closest_distance = node_to_player[index];
target_node = index;
}
else
{
closest_distance = node_to_player[target_node];
}
index += 1;
wait (1);
}
}

Le reste du code contrôle lequel des noeuds (0... 29) est le noeud le plus proche du joueur qui peut voir le joueur. Ce sera le noeud cible, le but pour notre ennemi. Lisez l' article dans Aum27 pour plus d' explications.

action
player1
{
player = me;
while (1)
{
my.pan += 4 * (key_cul - key_cur) * time;
my.skill1 = 15 * (key_cuu - key_cud) * time;
if (key_cuu + key_cud > 0)
{
ent_cycle("walk", my.skill46);
my.skill46 += 10 * time;
my.skill46 %= 100;
}
else
{
ent_cycle("idle", my.skill46);
my.skill46 += 2 * time;
my.skill46 %= 100;
}
move_mode = ignore_passable
+ glide;
ent_move(my.skill1,
nullvector);
wait
(1);
}
}

Je n' ai pas changé l' action attachée au modèle de joueur; ok, j' ai augmenté sa vitesse et je me suis débarrassé de la variable nommée player_speed (j' emploie quelques skills maintenant) mais le reste du code est comme celui employé dans Aum27.

Laissez-moi vous montrer les tableaux employés pour cette démonstration :

var
node_to_player[30];
var see_player[30];
var nodex[30];
var nodey[30];
var node_to_node[900];
var visited[900];
var path[30];

Node_to_player stockera la distance de chaque noeud au joueur; see_player sera mis à 1 si ce noeud peut "voir" le joueur et à 0 si le noeud ne peut pas voir le joueur. Nodex et nodey stockent les coordonnées de x et de y pour les 30 noeuds; ces valeurs seront employées par l' ennemi pour naviguer d' un noeud à un autre. Node_to_node stocke la distance entre n' importe quels deux noeuds et note comme une matrice de 30x30 les éléments. Les noeuds visités le chemin d' un noeud à un autre noeud et le chemin stockent le chemin réel du début à la cible. N' ayez pas peur, nous emploierons beaucoup d' images au long de ce cours.

Préparez-vous à l' action!

action
node
{
my.enable_scan = on;
my.enable_touch = on;
my.enable_click = on;
my.event = trace_back;
my.skill47 = 1234;
my.passable = on;

Chaque noeud est sensible au scan, touché et cliqué; si un de ces événements a lieu, le noeud dirigera sa fonction de trace_back associée. Skill47 a été mis à une valeur mystérieuse (pour une utilisation future) et le noeud a été rendu passable.

number_of_nodes += 1;
my.skill48 = number_of_nodes;
nodex[my.skill48] = my.x;
nodey[my.skill48] = my.y;
while (number_of_nodes
< (max_nodes - 1)) {wait (1);}

Chaque noeud doit obtenir un numéro d' ID (0... 29); les noeuds apparaissent dans le niveau dans un ordre aléatoire si number_of_nodes = 0 pour le premier noeud, number_of_nodes = 1 pour le deuxième noeud, etc... parce que number_of_nodes =-1 au démarrage du jeu. Chaque noeud stocke son numéro d' ID dans son propre skill48; les coordonnées (x et y) pour le noeud sont stockées dans nodex et nodey. Nous attendons que tous les noeuds soient placés dans le niveau.

while (current_node <= number_of_nodes)
{
if (current_node == my.skill48)
{
temp.x = 360;
temp.y = 30;
temp.z = 1000;
scan_entity (my.x, temp);
current_node += 1;
}
wait (1);
}
distances_computed = 1;

Le premier noeud (0) cherchera tous les noeuds qui sont près; si son scan trouve quelques noeuds il déclenchera leurs événements. Le balayage a lieu dans toutes les directions horizontalement (360 degrés) et jusqu' à 1000 quants. Nous attendons un cycle et ensuite le deuxième noeud (1) fera la même chose, etc. De cette façon nous n' avons pas 30 instructions scan_entity qui s'exécutent en même temps donc le taux d'affichage sera correct même sur des PC faibles. Aussitôt que tous les noeuds ont parcouru leurs voisins nous mettons distances_computed à 1, permettant à la fonction run_algorithm dans main à... de fonctionner. Nous parlerons plus de cette fonction un peu plus tard.

while (player == null) {wait (1);}
while (1)
{
node_to_player[my.skill48] = vec_dist(player.x, my.x);
if (node_to_player[my.skill48] < 500)
{
trace_mode = ignore_me + ignore_models + ignore_passents;
if (trace (my.pos, player.pos) == 0)
{
see_player[my.skill48] = 1;
}
else
{
see_player[my.skill48] = 0;
}
}
sleep (0.1);
}
}

Nous attendons que le joueur soit créé et ensuite nous stockons la distance de chaque noeud au joueur dans node_to_player. Si le joueur est venu tout près à moins de 500 quants du noeud, le noeud tracera le joueur, mettant see_player à 1 si le noeud peut voir le joueur et see_player à 0 si le noeud ne peut pas voir le joueur. Les noeuds tracent 10 fois par seconde; c' est la seule partie d' AI parfaite qui emploie de précieuses ressources CPU. Notez s' il vous plaît que seuls les noeuds qui sont près du joueur essayent de le tracer ainsi si six noeuds peuvent voir le joueur nous employons (dans la théorie) moins d' une trace à chaque cycle et c' est très bon pour nous!

C' est le code employé pour les noeuds et cela ne semble pas trop compliqué. Avant que je me déplace dessus, laissez moi vous montrer comment ces noeuds travaillent ensemble :

Noeud 1 scanne sur un rayon de 1000 quants; il détecte les noeuds 2, 3, 4, 5, 6, 7. Tous ces noeuds traceront en arrière au noeud 1; vous pouvez voir que 2 et 3 sont capable de voir 1 (le noeud qui les a parcourus) mais les autre noeuds ne peuvent pas le voir. Le processus se répète pour tous les noeuds.

Voyons la fonction d' événement associée aux noeuds :

function
trace_back()
{
if (event_type == event_scan)
{
my.skill45 = handle(you);
trace_mode = ignore_me + ignore_models + ignore_passents;
if (trace (my.x, you.x) == 0)
{
you = ptr_for_handle(my.skill45);
index = you.skill48 + my.skill48 * max_nodes;
node_to_node[index] = vec_dist(my.x, you.x);
}
}
if (event_type
== event_touch)
{
touched_node = my.skill48;
}
if (event_type == event_click)
{
start_node = my.skill48;
}
}

Si le noeud est parcouru il stockera le "you" de l' entité dans skill45. Qui est "you" ? C' est le noeud qui a parcouru ce noeud, déclenchant son événement (le noeud 1 dans l' image ci-dessus). N' importe quelle instruction de trace détruirait l' indicateur "you" donc nous le stockons dans skill45. Si le noeud peut "voir" le noeud qui l' a parcouru, nous reconstituons l' indicateur "you" et nous stockons la distance entre "me" (le noeud qui a été parcouru) et "you" (le noeud qui a parcouru) dans node_to_node. J' emploie "index" comme un index à l' intérieur du tableau qui crée la matrice virtuelle.

Node_to_node a 900 éléments donc vous devez le voir comme une matrice qui a 30 lignes et 30 colonnes.

Revenons à la fonction d' événement; si la souris touche le noeud, touched_node (une des variables affichées à l' écran) est mis à la valeur stockée dans skill40 pour ce noeud, qui est le numéro de ce noeud. De cette façon nous pouvons déplacer l' indicateur de souris sur un noeud et nous verrons son numéro. Finalement, si le noeud est cliqué avec le bouton gauche de la souris, start_node, le premier point sur le chemin le plus court, est mis au numéro de ce noeud.

Voici une fonction facile, celui qui traite avec le mouvement de souris :

function
init_mouse()
{
mouse_map = pointer_pcx;
mouse_mode = 2;
while (1)
{
if (mouse_left == 1) {display_path();}
mouse_pos.x = pointer.x;
mouse_pos.y = pointer.y;
wait (1);
}
}

La seule chose qui doit être mentionnée ici est que le même clic gauche dirige display_path, la fonction qui montre le chemin le plus court entre start_node et target_node.

Ok, nous arrêtons pour un instant. Nous avons quelques noeuds en scannant d' autres, traçant en arrière... Il semble que nous avons traité beaucoup de données, mais à quoi cela ressemble-t-il ?
C' est une excellente question! :)

C' est comme çà que node_to_node cherche les 10 premiers noeuds; le noeud 0 et 1 peuvent se voir et la distance entre eux est de 384 quants, le noeud 2 et 4 ne peuvent pas se voir (la distance entre eux est zéro), etc. S' il vous plaît soyez conscient que les noeuds obtiennent des numéros différents quand vous lancez le niveau de démonstration prenez cette table comme un simple exemple.

Il est temps de faire face à la fonction la plus horrible que vous ayez jamais vu :

function
run_algorithm()
{
i = 0;
j = 0;
while (i < max_nodes)
{
while (j < max_nodes)
{
index = j + i * max_nodes;
if (node_to_node[index] == 0)
{
node_to_node[index] = 999999;
}
j += 1;
}
i += 1;
j = 0;
}

J' ai bien regardé le code et il ne semble pas si effrayant que cela! J' ai pris l' habitude d' avoir plusieurs variables avec des noms courts (i, j, k, l) pour rendre le code plus lisible. Le morceau de code ci-dessus calcule l' index correct pour le tableau et remplace tous les zéros par 999,999. Vous rappelez-vous la table précédente ? Si deux noeuds ne pouvaient pas se voir la distance entre eux était zéro. Nous voulons calculer le chemin le plus court entre n' importe quels deux noeuds s' ils ne peuvent pas se voir nous devrions mieux mettre une distance énorme entre eux. La table doit ressembler à cela maintenant :

Ok, ainsi si un noeud peut voir un autre noeud je peux voir sa distance à ce noeud dans la table; s' il ne peut pas voir l' autre noeud la distance entre eux est de 999,999. Il est temps de calculer la distance entre n' importe quels deux noeuds, même s' ils ne peuvent pas se voir :

i = 0;
j = 0;
k =
0;
while (i < max_nodes)
{
while (j < max_nodes)
{
while (k < max_nodes)
{
if (node_to_node[j + i * max_nodes] + node_to_node[i + k * max_nodes]) < (node_to_node[j
+ k * max_nodes])
{
node_to_node[j + k * max_nodes] = node_to_node[j + i * max_nodes] + node_to_node[i
+ k * max_nodes];
visited[j + k * max_nodes] = i;
}

Je vais employer un exemple numérique pour vous rendre les choses plus faciles. Nous passons tous les noeuds et si la distance (2, 10) + la distance (10, 15) < la distance (2, 15) alors 10 est un noeud sur un chemin plus court, donc nous faisons le changement : nous n' emploierons pas l' itinéraire précédent, plus long dorénavant. Nous stockons aussi le noeud nouvellement visité (10 dans mon exemple).

Jetez un coup d' oeil à l' image ci-dessus : le noeud 2 savait que le chemin le plus court au noeud 15 était à 4, 5, 6, 7, 8, 9 et 11 mais maintenant il a appris qu' il y a un chemin plus court par le noeud 10. Le processus se répète pour tous les noeuds.

if (j == k)
{
node_to_node[j + k * max_nodes] = 0;
visited[j + k * max_nodes] = 0;
}
k += 1;
}
k = 0;
j += 1;
}
k = 0;
j = 0;
i += 1;
}
}

Quand j = k nous mettons la distance entre j et k : distance (0, 0).... Distance (29, 29) à zéro. Un noeud doit être de zéro Quants loin de lui, n' est-ce pas ? Nous faisons la même chose avec le noeud visité. Ces variables sont incrémentées, initialisées, etc s' assurer de trois choses tandis que les boucles continuent à diriger le passage par tous les éléments de node_to_node. Vous êtes intéressés par l' observation d' une nouvelle table node_to_node, étes-vous près?

C' est une belle table! Je peux voir la distance la plus courte entre n' importe quels deux noeuds, même s' ils ne peuvent pas se voir! La distance entre le noeud 2 et le noeud 6 est de 768 quants et cetera. Maintenant nous devons calculer le chemin réel (nous ne le savons pas encore) et nous sommes prêts à y aller!

Vous rappelez-vous la minuscule fonction employée pour le mouvement de la souris ? Quand vous cliquez sur le bouton de souris gauche vous dirigez cette fonction - function display_path :

function
display_path()
{
proc_kill(4);
while (mouse_left == 1) {wait (1);}
k = 0;
while (k < 30)
{
path[k] = 0;
k += 1;
}

Cette fonction ne devant s'exécuter qu'une seule fois; nous attendons que le bouton gauche de la souris soit relaché et ensuite nous remettons le tableau qui tient le chemin réel, nommé... path. Nous le faisons parce qu' une partie d' un chemin précédent, plus long continuerait à être stockée à l' intérieur du tableau de chemin :

k = 0;
path[k] = target_node;
find_path(start_node, target_node);
sleep (1);
i = k;
while (i >= 0)
{
temp.x = nodex[path[i]];
temp.y = nodey[path[i]];
temp.z = 50;
ent_create
(marker_mdl, temp, dynamic_light);
i -= 1;
sleep (0.5);
// every 0.5 seconds
}
}

Nous écrivons le premier élément de chemin (target_node) et ensuite nous exécutons la fonction qui trouve le chemin le plus court entre start_node et target_node. Cette fonction est extrêmement rapide donc vous ne devez pas attendre pendant une seconde; wait (1) travaille aussi. J' ai voulu attendre pendant une seconde parce que je vais employer quelques lumières dynamiques et je veux que vous les voyiez dans leur pleine splendeur. La variable nommée k stocke le numéro de noeud sur le chemin le plus court; nous verrons comment elle le fait . Tant que nous n' avons pas créé toutes les lumières qui décrivent le chemin le plus court entre start_node et target_node, nous obtenons les coordonnées précédemment stockées des noeuds appropriés de nodex et nodey et nous créons les entités invisibles qui produisent la lumière rouge environ 50 quants au-dessus du niveau du sol. De cette façon nous ne devons pas agir réciproquement avec les noeuds mais nous les ferons sembler rougeâtres.

Ces lumières rouges sont créées chaques 0.5 secondes et leur fonction associée fait inscrire son code ci-dessous :

function
dynamic_light()
{
my.invisible = on;
my.passable =
on;
my.lightrange = 100;
my.lightred = 255;
my.lightgreen
= 0;
my.lightblue = 0;
sleep (3);
ent_remove(me);
}

J' ai employé le même modèle de marqueur que pour ceux des entités rouges mais il est rendu invisible et passable aussi qui s' en soucie ? Les lumières rouges existeront pendant 3 secondes et ensuite elles seront enlevées; de cette façon nous pouvons avoir jusqu' à 6-7 lumières rougeoyantes sur notre chemin en même temps.

Un petit coup de fatigue ? Tient, moi aussi, subitement. seulement Il y a une fonction qui doit encore être commentée:

function
find_path(l, j)
{
i = 0;
while (i < max_nodes)
{
if ((i != j) && (visited[i + max_nodes * j] == 0))
{
if (node_to_node[l + max_nodes * j] == node_to_node[l + max_nodes * i] + node_to_node[i
+ max_nodes * j])
{
k += 1;
path[k] = i;
next_node = i;
i = max_nodes - 1;
if (path[k] == l)
{
return;
}
}
}
i += 1;
}
j
= next_node;
find_path(l, j);
}

Cette fonction écrit le chemin réel à l' intérieur du tableau "path". Voyons comment il a été appelé de la fonction display_path :

find_path(start_node, target_node);

Cela signifie que si je dirige find_path (14, 5) la fonction trouvera les noeuds qui créent le chemin le plus court entre 14 et 5, par exemple : 14 2 7 19 5. Voyons comment elle fait.

La variable nommée i commence de 0 et nous rechercherons ce noeud (0) dans le reste des 29 noeuds (max_nodes = 30). Si le noeud actuel peut "voir" un des autres noeuds (sauf lui, me! = j) et si c' est un noeud sur le chemin le plus court (parce que le chemin le plus court l' inclut) alors nous avons trouvé un nouvel élément de chemin; nous incrémentons k et nous stockons le nouveau numéro de noeud à l' intérieur du tableau "path". Nous copions le nouveau noeud sur le chemin le plus court dans next_node parce qu' il perdra sa valeur tout de suite; nous mettons i = 29 pour sortir de la boucle while parce que nous voulons éliminer d' autres chemins qui ont la même longueur, comme dans l' image ci-dessous :

Si le chemin [k] = l nous avons atteint le point de départ (l = start_node) donc nous sortons de la fonction, autrement nous mettons le noeud suivant sur le chemin de la cible (j = next_node) et appelons la fonction elle-même de nouveau (c' est comme le travail de fonctions récursives). Si votre appel de fonction précédent a recherché dans le chemin entre les noeuds 2 et 10, maintenant il cherchera le chemin entre 2 et 7, si le noeud 7 est le noeud suivant sur le chemin le plus court. Le processus se répète ainsi autant de fois qu' il est nécessaire et à la fin le chemin entier est stocké dans le tableau nommé path.

C' était tout! Je ne veux pas que vous vous estimiez trop mauvais si vous ne comprenez pas tout de suite comment ce code fonctionne; un bon système d' IA est assez difficile à coder, après tout. Le dernier article dans cette série vous apprendra comment fonder un niveau jouable sans toucher une ligne de code! IA parfaite a été écrit avec ces buts en mémoire :
-Installation extrêmement amicale pour le designer de niveau;
-Utilisation simple pour le programmeur.

Le moteur IA a été construit aussi le mois prochain nous nous amuserons avec un niveau jouable. Je vous verrai alors!

Que dites-vous ? Vous avez trouvé un défaut dans mon code d' AI parfaite ?

Je vois... Donc vous avez cliqué sur le noeud 1 et au lieu de l' observation 1 - 2 - 3 - 4 - 5 sur l' écran vous voyez seulement 1 - 3 - 4 - 5! Ce n' est pas un défaut; le noeud 1 peut voir le noeud 3 directement donc il n' a pas besoin du noeud 2 pour créer le chemin le plus court au joueur. Si vous aimez avoir plus de points sur le chemin le plus court (pourquoi le voudriez-vous ?) emploiyee une étendue scan_entity plus petite et placez plus de noeuds dans votre niveau.

Conseil final : évaluez vos chemins soigneusement. J' ai dû déplacer quelques-uns des noeuds dans ma démonstration et niveller un peu parce qu' ils se voyaient directement, comme dans l' image ci-dessous :

Ces changements mineurs ne changent pas le gameplay du tout donc ne soyez pas timide et faites la même chose dans vos niveaux si besoin est.


Untitled

Traduction Olivier Burton correction AB

IA parfaite - partie III

Ce mois-ci nous serons témoin de la panique du joueur quand il est attaqué par un drone qui emploie le code d' IA Parfaite que nous avons expliqué jusqu' ici. Nous discuterons de beaucoup de nouvelles choses, mais ne nous inquiétez pas, la partie la plus dure est finie.

Votre objectif est simple : courrez tout le temps pour sauver votre peau. Si le drone vous touche, il vous tue immédiatement. Si le drone vous voit, il commence à tirer des fusées; chaque fusée prend 10 points de votre santé. Le drone vous poursuivra jusqu 'à votre mort mais vous avez une vitesse de déplacement plus grande donc vous devez être capable de rester en vie pendant un peu de temps :)

Cette série d' IA Parfaite a commencée il y a deux mois; donc, je vous encourage à lire les articles dans Aum27 et Aum28 avant de lire la suite. Cet article décrira seulement les fonctions et les actions qui ont été ajoutées ou ont été changées. Le fichier de script - ai3.wdl - est scindé en deux parties; le nouveau code supplémentaire de la démonstration de ce mois est placé au-dessous de ces lignes dans ai3.wdl :

////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// Nouvelles actions et fonctions pour le 3ème épisode d' IA Parfait
////////////////////////////////////////////////////////////////////////////////////////////////////////////////

Examinons le nouveau code.

Var dist_to_drone [30];
Var drone_start [12] = 830, 700, 100,-830, 700, 100, 830,-700, 100,-830,-700, 100;

Le premier tableau tient la distance du drone à ses noeuds d' encerclement (ou zéro si les noeuds ne peuvent pas le voir). Drone_start tient les coordonnées de départ pour le modèle du drone.

J' ai choisi quatre positions de départ pour le drone et drone_start [12] tient leurs coordonnées. Le drone choisira un point de départ aléatoire à chaque fois.

text
ai_txt // affiche le texte à l' écran
{
font = system_font;
pos_x = 0;
pos_y = 0;
string = "Current path:   \n\nHealth:\nTarget:\nStart:";
flags = visible;
}

panel
ai_pan // affiche les valeurs numériques à l' écran
{
pos_x = 0;
pos_y = 0;
layer = 10;
digits
= 80, 24, 2, system_font, 1, player.health;
digits = 80, 37, 2, system_font, 1, target_node;
digits = 80, 50, 2, system_font,
1, start_node;
digits
= 120, 0, 4, system_font, 1, path[0];
digits = 150, 0, 4, system_font, 1, path[1];
digits = 180, 0, 4, system_font, 1, path[2];
digits = 210, 0, 4, system_font, 1, path[3];
digits = 240, 0, 4, system_font, 1, path[4];
digits = 270, 0, 4, system_font, 1, path[5];
digits = 300, 0, 4, system_font, 1, path[6];
digits = 330, 0, 4, system_font, 1, path[7];
digits = 360, 0, 4, system_font, 1, path[8];
digits = 390, 0, 4, system_font, 1, path[9];
digits = 420, 0, 4, system_font, 1, path[10];
digits = 450, 0, 4, system_font,
1, path[11];
digits
= 480, 0, 4, system_font, 1, path[12];
digits = 510, 0, 4, system_font, 1, path[13];
digits = 540, 0, 4, system_font, 1, path[14];
flags = overlay, refresh,
visible;
}

Nous employons un texte et un panneau pour montrer le chemin actuel du drone au joueur, start_node, target_node et la santé du joueur.

L' action associée aux noeuds a changé un peu donc nous la discuterons de nouveau brièvement :

action
node
{
my.invisible = on;
my.enable_scan = on;
my.event = trace_back;
my.skill47 = 1234;
my.passable = on;
number_of_nodes += 1;
my.skill48 = number_of_nodes;
nodex[my.skill48] = my.x; // stock x et y
nodey[my.skill48] = my.y; // pour ce noeud
while (number_of_nodes < (max_nodes - 1)) {wait (1);}

J' ai voulu cacher les noeuds dans cette démonstration mais vous pouvez les rendre visibles de nouveau en mettant un commentaire sur la première ligne. Les noeuds sont sensibles au balayage; s' ils sont parcourus, la fonction trace_back () s'exécutera. Nous donnons une valeur mystérieuse à skill47 (1234) pour nous assurer que nous pouvons détecter si une certaine entité est un noeud ou pas et nous rendons les noeuds passables parce qu' ils ne doivent pas bloquer le joueur ou le drone. Chaque noeud obtient un numéro d' ID unique (0... 29) qui est stocké dans son skill48. Nous stockons aussi les coordonnées de x y pour chaque noeud dans nodex et nodey. Finalement, nous attendons que tous les noeuds soient placés dans le niveau.

while (current_node <= number_of_nodes)
{
if (current_node == my.skill48)
{
temp.x = 360; // angle de balayage horizontal
temp.y = 30; // angle de balayage vertical
temp.z = 1000; // balayage de la gamme
scan_entity (my.x, temp); // scanne les noeuds tout près
wait (1);
current_node += 1; // vas au prochain noeud
}
wait (1);
}
distances_computed = 1;

Chaque noeud parcourra les noeuds qui sont leurs sont plus proches de 1000 quants ; quand tous les noeuds ont parcouru leurs voisins la variable nommée distances_computed est mise à 1.

while (player == null) {wait (1);}
while (1)
{
node_to_player[my.skill48] = vec_dist(player.x, my.x);
if (node_to_player[my.skill48]
< 500)
{
trace_mode = ignore_me + ignore_models + ignore_passents;
if (trace (my.pos, player.pos) == 0)
{
see_player[my.skill48] = 1;
}
else
{
see_player[my.skill48] = 0;
}
}
sleep (0.1);
}
}

Nous attendons que le joueur soit créé et ensuite nous stockons la distance de chaque noeud au joueur à l' 'intérieur du tableau nommé node_to_player. Si le joueur vient plus près de 500 quants à un noeud nous traçons du noeud au joueur; si le noeud peut "voir" le joueur, il met see_player à 1, autrement see_player sera mis à 0. Chaque noeud trace 10 fois par seconde.

Il est temps de jeter un coup d' oeil dans la fonction d' événement associée aux noeuds :

function
trace_back()
{
if (event_type ==
event_scan)
{
if (you.skill47 == 1234) // scanné par un noeud
{
my.skill45 = handle(you);
trace_mode = ignore_me + ignore_models + ignore_passents;
if (trace (my.x, you.x) == 0)
{
you = ptr_for_handle(my.skill45); // restaure l' indicateur "you"
index = you.skill48 + my.skill48 * max_nodes;
node_to_node[index] = vec_dist(my.x, you.x);
}
}
else // scanné par le drone (skill47 = 5678)
{
my.skill45 = handle(you);
trace_mode = ignore_me + ignore_models + ignore_passents;
if (trace (my.x, you.x) == 0)
{
you = ptr_for_handle(my.skill45);
dist_to_drone[my.skill48] = vec_dist (my.x, you.x);
if ((dist_to_drone[my.skill48] < min_drone) && (dist_to_drone[my.skill48]
!= 0))
{
min_drone = dist_to_drone[my.skill48];
}
wait (2);
if (dist_to_drone[my.skill48] == min_drone) // est-ce le noeud le plus proche
du drone?
{
start_node = my.skill48;
}
}
else
{
dist_to_drone[my.skill48] = 0;
}
}
}
}

Si le noeud est parcouru par une autre entité, il y a deux possibilités :
1) Le noeud a été parcouru par un autre noeud (skill47 à 1234). Si c' est vrai, nous stockons l' indicateur sur l' entité qui a exécuté le balayage (vous) et ensuite nous traçons de ce noeud à vous. Si le noeud peut voir l' entité qui la parcouru, nous stockons la distance entre le noeud et l' entité dans node_to_node;
2) Le noeud a été parcouru par le drone (skill47 à 5678). Nous stockons l' indicateur "you" et ensuite nous traçons du noeud au drone. Si ce noeud peut voir le drone, nous stockons la distance entre le drone et le noeud dans dist_to_drone. La variable nommée min_drone stockera la distance du noeud le plus proche au drone; start_node seras le noeud le plus proche du drone si son dist_to_drone note une valeur égale à min_drone.

Vous pouvez voir que le drone a parcouru 5 noeuds, mais seuls le noeud 2 et le noeud 4 peuvent voir le drone; start_mode seras mis à 2 parce que c' est le noeud le plus proche du drone qui peut voir le drone.

Le code associé au joueur a changé aussi :

action
player1
{
player = me;
my.health = 100;
my.enable_entity = on;
my.enable_impact = on;
my.event = decrease_health; // je perds de la santé
quand j' entre en collision avec des entités

Le joueur a 100 points de santé (health = skill40) et est sensible à d' autres entités (le drone et ses fusées). L' événement associé à l' action du joueur est decrease_health.

while (my.health > 0)
{
my.pan += 4 * (key_cul - key_cur) * time;
my.skill1 = 15 * (key_cuu - key_cud) * time;
if (key_cuu + key_cud >
0)
{
ent_cycle("walk", my.skill46); // joue l' animation "walk"
my.skill46 += 10 * time; // vitesse d' animation "walk"
my.skill46 %= 100; // boucle d' animation
}
else // the player is standing
{
ent_cycle("idle", my.skill46); //joue
l' animation "stand"
my.skill46 += 2 * time; // vitesse d' animation "stand"
my.skill46 %= 100; // boucle d' animation
}
move_mode = ignore_passable + glide;
ent_move(my.skill1, nullvector);
wait (1);
}

Tant que le joueur est vivant, il peut se déplacer et tourner par l' utilisation des touches de curseur. Si le joueur marche, il joue son animation "walk"; autrement, il joue son animation "stand". Le joueur se déplace en ignorant les entités passables (les noeuds) et glisse le long des murs s' il entre en collision avec eux.

my.skill46 = 0;
while (my.skill46 < 80)
{
ent_cycle("death",
my.skill46); // joue l' animation "death"
my.skill46 += 2 * time;
wait (1);
}
}

Quand le joueur est mort, il joue son animation "death". Jetons maintenant un coup d' oeil au code associé au drone :

starter
drone
{
while (total_frames < 5) {wait (1);}
ent_create (drone_mdl, nullvector, init_drone);
}

Le drone est créé par une fonction "starter" qui attend 5 frames et crée ensuite le drone dans le niveau et lui associe la fonction init_drone :

function
init_drone()
{
while (player ==
null) {wait (1);}
var
corner;
my.skill47
= 5678;
corner = sys_seconds
% 4;
if (corner ==
0)
{
my.x = drone_start[0];
my.y = drone_start[1];
my.z = drone_start[2];
}
if (corner == 1)
{
my.x = drone_start[3];
my.y = drone_start[4];
my.z = drone_start[5];
}
if (corner == 2)
{
my.x = drone_start[6];
my.y = drone_start[7];
my.z = drone_start[8];
}
if (corner == 3)
{
my.x = drone_start[9];
my.y = drone_start[10];
my.z = drone_start[11];
}

La fonction attend que le joueur soit créé et crée ensuite une variable locale nommée corner. Nous mettons skill47 à 5678 comme ID unique pour le drone et ensuite nous produisons un numéro aléatoire de 0 à 3 en utilisant sys_seconds. Nous choisissons une des quatre positions de départ pour le drone selon la valeur aléatoire stockée dans "corner".

while (player.health > 0)
{
my.lightrange = 400; // émet la lumière
my.lightred = 0;
my.lightgreen = 255; // lumière verte
my.lightblue = 0;
trace_mode = ignore_me + ignore_models + ignore_passents;
if (trace (my.pos, player.pos)
== 0)
{
shoot_rocket();
sleep (1);
}

Tant que le joueur est vivant, le drone émet une lueur verte sur un diamètre de 400 quants. Si le drone peut voir le joueur (trace = 0) il commence à tirer des fusées à chaque seconde.

else
{
min_drone = 999999;
temp.x = 360; // angle de balayage horizontal
temp.y = 360; // angle de balayage vertical
temp.z = 800; // scanne la gamme
scan_entity (my.x, temp);
sleep (1);

k = 0;
while (k < max_nodes)
{
path[k] = 0; // réinitialise le tableau, se débarrasse du chemin
précédemment stocké
k += 1;
}
k = 0; // commence par le premier élément du chemin[30]
path[k] = target_node; // écrit le premier élément du chemin

Si le drone ne peut pas voir le joueur, min_drone est mis à une distance énorme et le drone commence à scanner sur une gamme de 800 quants pour obtenir le noeud le plus proche. Nous remettons le tableau nommé "path" (qui est le tableau qui stocke le chemin actuel) et ensuite nous écrivons le premier élément du chemin = target_node.

snd_play (getpath_wav, 70, 0);
if (start_node != target_node)
{
find_path(start_node, target_node);
wait (2);
i = k;
while (i >= 0)
{
temp.x = nodex[path[i]];
temp.y = nodey[path[i]];
temp.z = my.z; // utilise la hauteur du drone pour z
d1_marker = ent_create (marker_mdl, temp, follow_path); // crée une entité
invisible qui guide le drone
wait (1);
while (vec_dist (d1_marker.x, my.x) > 10)
{
my.skill1 = 10 * time;
my.skill2 = 0;
my.skill3 = 0;
move_mode = ignore_passable + glide;
ent_move(my.skill1, nullvector);
wait (1);
}
i -= 1;
wait (1);
}
}
}
}
}

Nous jouons un son qui annonce une
nouvelle recherche de chemin et ensuite, si start_node et target_node sont différents,
nous dirigeons la fonction find_path d'Aum28; cette fonction récursive
constate que le chemin le plus court entre les deux noeuds passe par ses paramètres.
Le résultat de la fonction est stocké dans le tableau nommé
path; nous en avons parlé dans le paragraphe précédent.

Nous attendons 2 cycles et ensuite nous commençons à déplacer le drone d' un point du chemin à un autre. Nous créons une entité nommée d1_marker au-dessus de chaque noeud le long du chemin le plus court et ensuite nous déplaçons le drone vers l' entité tant que la distance entre le drone et l' entité invisible d1_marker n' est pas en dessous de 10 quants; quand cela arrive, nous créons d1_marker au-dessus du noeud suivant sur le chemin le plus court et ensuite nous déplaçons le drone vers l' entité. Les répétitions de processus du drone ont atteint le dernier noeud stocké à l' intérieur du tableau nommé path.

L' entité nommée d1_marker dirige cette fonction simple :

function
follow_path()
{
my.invisible = on;
my.passable = on;
vec_set (temp.x, my.x);
vec_sub (temp.x, you.x);
vec_to_angle (you.pan, temp);
while (d1_marker == me) {wait (1);}
ent_remove (me);
}

La fonction rend le marqueur invisible et passable et fait avancer le drone vers lui. Les marqueurs plus vieux inutilisés seront enlevés comme de nouveaux marqueurs sont créés.

Jetons un coup d' oeil dans les fonctions qui créent les fusées :

function shoot_rocket()
{
vec_set (temp.x, player.x);
vec_sub (temp.x, my.x);
vec_to_angle (my.pan, temp); // tourne le drone vers le joueur
if (player.health > 0)
{
ent_create (rocket_mdl, my.pos, move_rocket); // crée une fusée
}
}

Laissez-moi vous rappeler que la fonction nommée shoot_rocket () s'exécute quand le drone peut voir le joueur. La fonction tourne le drone vers le joueur et si le joueur n' est pas mort il crée des fusées et les dirige en employant la fonction ci-dessous :

function
move_rocket()
{
my.pan = you.pan;
my.enable_entity = on;
my.enable_impact = on;
my.enable_block = on;
my.event = remove_rocket;
while (my != null)
{
my.skill1 = 15 * time;
my.skill2 = 0;
my.skill3 = 0;
move_mode = ignore_you
+ ignore_passable; // ignore le drone (le créateur = you)
ent_move(my.skill1, nullvector);
wait (1);
}
}

Les fusées et leur parent (le drone) ont le même angle pan et ils sont sensibles à l' impact avec d' autres entités ou avec les blocs de niveau. La fusée se déplace avec la vitesse donnée par son skill1 et elle ignore son créateur, le drone (you) en se déplaçant. Si la fusée entre en collision avec quelque chose, son événement remove_rocket () s'exécutera :

function
remove_rocket()
{
snd_play (hit_wav,
100, 0);
wait (1);
ent_remove (me);
}

C' est une fonction facile donc je vous laisserai calculer ce qu' elle fait. Je vais m' occuper de la dernière fonction :

function
decrease_health()
{
if (you.skill47 ==
5678) // colision avec le drone?
{
my.health = 0; // mort instantanée
}
else // par une fusée?
{
my.health -= 10; // perds
10 points de santé
}
if (my.health <= 0)
{
my.event = null;
snd_play (death_wav, 40, 0);
}
}

Cette fonction est la fonction d' événement associée au joueur. Si le joueur entre en collision avec le drone (skill47 à 5678) le joueur mourra immédiatement; autrement, le joueur a été frappé par une fusée et il perdra 10 points de santé. Si ses diminutions de santé arrivent à 0, le joueur arrêtera de répondre aux événements et il jouera le son de mort.

C' était la première démonstration jouable qui emploie le coeur d' IA Parfaite. Le drone cherchera le joueur et le trouvera à chaque fois; le seul inconvénient consiste en ce que la boucle while continue à s'exécuter jusqu' à ce que tous les points sur le chemin soient visités ainsi si le joueur change sa position rapidement le drone ne sera pas capable de détecter son nouvel emplacement avant qu' il n' atteigne le dernier point du chemin précédent. J' ai codé le drone de cette façon parce que vous pouvez voir et comprendre comment les chemins changent d' un target_node à un autre; cependant, cette limitation peut être surmontée facilement en insérant une nouvelle trace dans la boucle while qui déplace le drone.

Le mois prochain nous sortirons du niveau de labyrinthe (sniff...) et nous nous déplacerons dans un niveau de shooter 3D typique avec des ennemis humains. Je promets de donner une arme à feu au joueur afin qu'il ait toutes ses chances!

Oh et ne me blâmez pas si le modèle du joueur pénètre dans les murs pendant son ordre d' animation "dead" :

Ce problème apparaît à cause de l' étrange animation "dead" du modèle et n' a aucun rapport avec mon code.


Untitled

Traduction Olivier Burton correction AB

IA parfaite - partie IV

Le 4ème épisode d'IA parfait ajoute un peu plus de particularités importantes au code que nous avons vu jusqu'ici. N'oubliez pas de lire les articles dans Aum27 28 et 29 pour voir comment tout a commencé.

Ouvrez le level 4, compilez-le et exécutez-le en employant ai4.wdl :

Respirez à fond et commencez à courir; les quatre petits types essayeront de vous pourchasser. Employez "Z", "S", "Q", "D" pour vous déplacer, utilisez le bouton droit de la souris pour tirer. Essayez d'en réchapper aussi longtemps que possible; jetez un coup d'oeil aux quatre chemins montrés sur l'écran et regardez comment ils changent selon votre position. J'aurai pu employer beaucoup plus d'ennemis dans cette démonstration, mais ayez confiance en moi, quatre sont bien assez dans ce niveau. Oh, vous pouvez tuer tous les ennemis; j'ai réussi à le faire.

J'ai modifié une partie du code d'Aum29 et j'en ai ajouté; nous discuterons de toutes les actions nouvelles ou modifiées et des fonctions, placées au-dessous de l'en-tête dans ai4.wdl :

////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// new stuff for the 4th Perfect AI episode
////////////////////////////////////////////////////////////////////////////////////////////////////////////////

define health skill42;
define start_node skill43;
define closest_node skill44;

Nous employons un panneau simple pour montrer les chemins pour les 4 ennemis, la santé du joueur et le noeud cible (la destination pour les ennemis) :

panel ai_pan // affiche les valeurs numériques à l'écran
{
pos_x = 0;
pos_y = 0;
layer = 10;
digits = 80, 72, 2, system_font, 1, player.health;
digits = 80, 85, 2, system_font, 1, target_node;

digits = 120, 0, 4, system_font, 1, path[0]; // montrent les 15 premiers points sur le chemin le plus court
digits = 150, 0, 4, system_font, 1, path[1]; // doit être assez pour chacun
digits = 180, 0, 4, system_font, 1, path[2];
// many more digits here
............................................................

flags = overlay, refresh, visible;
}

Cette démonstration emploie 30 noeudsqui s'échange des informations; jetons un coup d'oeil dans l'action qui les gère :

action
node // utilisation de 30 noeuds dans cette démonstration
{
my.invisible = on;
my.enable_scan = on;
my.event = trace_back;
my.skill47 = 1234;
my.passable = on;
node_id += 1;
my.skill48 = node_id;
nodex[my.skill48] = my.x;
nodey[my.skill48] = my.y;
while (node_id < (max_nodes - 1)) {wait (1);}

Chaque noeud est invisible et sensible au balayage; quand un noeud est balayé, il exécute son événement trace_back. Nous mettons skill47 à 1234 pour chaque noeud et ensuite nous le rendons passable, parce que le joueur et ses ennemis doivent être capables de passer par au travers de lui. Chaque noeud obtient un numéro d'id unique, qui est stocké dans son propre skill48. Nous stockons aussi les coordonnées x et y du noeud dans deux tableaux nommés nodex et nodey et ensuite nous attendons que tous les noeuds soient placés dans le niveau.

while (current_node <= node_id)
{
if (current_node == my.skill48)
{
temp.x = 360;
temp.y = 30;
temp.z = 1000;
scan_entity (my.x, temp);
wait (1);
current_node += 1;
}
wait (1);
}
distances_computed = 1;

Les noeuds commencent à balayer tous les ordres dans l'ordre : node0 balaye node1... Node29, puis node1 balaye node0... Node29, etc. Nous employons une étendue de balayage horizontale de 360 degrés, un angle de balayage vertical de 30 degrés et une étendue de balayage de 1000 quants. Aussitôt que le dernier noeud (29) a balayé node0... Node28, une variable nommé distances_computed est mise à 1.

while (1)
{
node_to_player[my.skill48] = vec_dist(player.x, my.x);
if (node_to_player[my.skill48] < 500)
{
trace_mode = ignore_me + ignore_models + ignore_passents;
if (trace (my.pos, player.pos) == 0)
{
see_player[my.skill48] = 1;
}
else
{
see_player[my.skill48] = 0;
}
}
sleep (0.1);
}
}

Chaque noeud stocke la distance au joueur dans un tableau nommé node_to_player; si le joueur s'approche à moins de 500 quants du noeud, le noeud trace de de sa position à cele du joueur. Si le noeud peut "voir" le joueur, il met see_player à 1, autrement see_player est mis à 0. Tout ceci se produit 10 fois par seconde.

Voyons ce qui arrive quand un noeud particuliers est balayé par un autre noeud :

function
trace_back()
{
if (event_type == event_scan)
{
if (you.skill47 == 1234) // scanned by a node
{
my.skill45 = handle(you);
trace_mode = ignore_me + ignore_models + ignore_passents;
if (trace (my.x, you.x) == 0)
{
you = ptr_for_handle(my.skill45);
index = you.skill48 + my.skill48 * max_nodes;
node_to_node[index] = vec_dist(my.x, you.x);
}
}

Lorsque un noeud est balayé, nous vérifions si l'entité qui l'a balayé (l'émetteur) a skill47 à 1234; si c'est vrai, nous savons que cette entité émettrice est un noeud. Nous stockons le "you" de l'entité (la trace le détruirait) dans skill45 et nous traçons en arrière du noeud balayé au noeud émetteur. Si le noeud peut "voir" le noeud émetteur, il rétablit le pointeur "you" et stocke la distance entre les deux noeuds dans le vecteur nommé node_to_node (un tableau bidimensionel qui a été simulé en employant un tableau simple).

else
{
my.skill45 = handle(you); // stocke "you" parce que la trace le détruirais
trace_mode = ignore_me + ignore_models + ignore_passents;
if (trace (my.x, you.x) == 0) // si ce noeud peut "voir" l'ennemi qui
l'a parcouru
{
you = ptr_for_handle(my.skill45); // rétablit le pointeur "you"
dist_to_enemy[my.skill48 + max_enemies * you.skill48] = vec_dist (my.x, you.x);
if ((dist_to_enemy[my.skill48 + max_enemies * you.skill48] < you.closest_node)
&& (dist_to_enemy[my.skill48 + max_enemies * you.skill48] != 0))
{
you.closest_node = dist_to_enemy[my.skill48 + max_enemies * you.skill48];
}
wait (2);
if (dist_to_enemy[my.skill48 + max_enemies * you.skill48] == you.closest_node)
{
you.start_node = my.skill48;
}
}
else
{
you = ptr_for_handle(my.skill45);
dist_to_enemy[my.skill48 + max_enemies * you.skill48] = 0;
}
}
}
}

La branche "else" s'exécute quand un certain noeud est balayé par un des ennemis; nous stockons "you" et nous traçons du noeud à l'ennemi qui a balayé le noeud, stockant la distance du noeud à l'ennemi dans le tableau nommé dist_to_enemy. Si la distance entre le noeud et l'ennemi est plus petite que la distance précédement stockée par l'ennemi (closest_node = skill44) et est plus grande que zéro, ce noeud sera mis comme le noeud le plus proche de l'ennemi. Nous donnons au reste des noeuds la possibilité de changer cela pendant deux cyccles (dans le cas où un d'entre eux est plus proche de l'ennemi) et ensuite nous mettons comme noeud de départ de l'ennemi le noeud qui est le plus proche de l'ennemi. Si le noeud ne peut pas voir l'ennemi, son dist_to_enemy sera mis au zéro.

Toutes ces choses arrivent parce que l'ennemi doit savoir quel noeud est le plus proche et doit le voir. L'ennemi se déplacera vers le noeud le plus proche et ensuite il lancera la fonction qui produira le chemin le plus court (plus de détails un peu plus tard).

Discutons de mon action préférée :

action
player1
{
player = me;
my.health = 100;
my.push = -1;
my.enable_entity = on;
my.enable_impact = on;
my.event = decrease_health;
while (my.health > 0)
{
my.pan += 8 * (key_a - key_d) * time;
my.skill1 = 15 * (key_w - key_s) * time;

Le joueur a 100 points de santé; sa valeur "push" est -1, donc il ne passera pas à travers les murs (leur push est nul). Le joueur est sensible à d'autres entités et exécute son événement decrease_health quand quelque chose le frappe. Tant que le joueur est vivant, il peut tourner en utilisant "Q" et "D" et se déplacer en avant / en arrière avec "Z" et "S".

if (key_w + key_s > 0)
{
ent_cycle("walk", my.skill46);
my.skill46 += 10 * time;
my.skill46 %= 100;
}
else
{
ent_cycle("idle", my.skill46);
my.skill46 += 2 * time;
my.skill46 %= 100;
}
if (mouse_left ==
1)
{
players_bullet();
}
move_mode = ignore_passable
+ glide;
ent_move(my.skill1,
nullvector);
wait
(1);
}

Si le joueur se déplace ("Z" ou "S" est appuyé), le modèle joue son animation de marche ("walk") dans une boucle, autrement il joue son animation d'attente ("idle") dans une boucle. Si le joueur presse le bouton de souris gauche, la fonction nommée players_bullet sera exécutée. Les déplacements du mdl du joueur ignorent les entités passables et glissent le long des murs.

my.skill46 = 0;
while (my.skill46 < 80)
{
ent_cycle("death", my.skill46);
my.skill46 += 2 * time;
wait (1);
}
}

Les lignes ci-dessus sont exécutées lorsque le joueur est mort : elles exécutent l'animation de mort ("dead") du joueur une seule fois. Voyons la fonction la plus compliquée - celle attachée aux ennemis :

action
enemy
{
my.push = -2;
my.enable_entity = on;
my.enable_impact = on;
my.health = 100;
my.event
= enemy_event;
enemy_id += 1;
my.skill48 = enemy_id;
my.skill47 = 5678;
while ((player.health > 0) && (my.health > 0))
{
trace_mode = ignore_me
+ ignore_models + ignore_passents;
if (trace (my.pos, player.pos) == 0)
{
my.skill46 = 0;
shoot_bullet();
while (my.skill46 < 100)
{
ent_cycle("standshoot", my.skill46);
my.skill46 += 15 * time;
wait (1);
}
}

L'ennemi a sa valeur "push" à -2; de cette façon les ennemis ne peuvent pas se tuer parce que leurs balles ont push = -1. L'ennemi est sensible à d'autres entités et à l'impact; sa santé est mise à 100 et son événement est enemy_event. Chaque ennemi obtient un numéro d'id unique (0... 3 dans cette démonstration) qui est stocké dans son propre skill48. Nous mettons skill47 à 5678 - c'est comme cela que nous pouvons identifier si une entité donnée est un ennemi ou non.

La boucle while continue à s'exécuter si le joueur et l'ennemi sont vivants. La boucle trace de l'ennemi au joueur; si l'ennemi peut voir le joueur, il commence à tirer en employant la fonction nommée shoot_bullet et en jouant son animation "standshoot" dans une boucle.

else
{
my.closest_node = 999999;
temp.x = 360;
temp.y = 360;
temp.z = 800;
scan_entity (my.x, temp);
if (vec_dist (my.x, player.x) > 1000)
{
my.skill46 = 0;
while (my.skill46 < 100)
{
ent_cycle("alert", my.skill46);
my.skill46 += 2 * time;
wait (1);
}
}
else
{
wait (2);
}
while (function_busy == 1) {wait (1);}
function_busy = 1;

Si l'ennemi ne peut pas voir le joueur, il met sa distance closest_node à une valeur énorme et ensuite il parcourt les noeuds tout près parce qu'il doit y détecter le noeud le plus proche. Si le joueur est plus éloigné que 1000 quants, l'ennemi joue son animation "alert", faisant semblant de réfléchir avant le choix d'un nouveau chemin qui mène au joueur. Si la distance entre l'ennemi et le joueur est plus petite que 1000 quants, l'ennemi attendra 2 cycles avant le choix d'un nouveau chemin.

J'ai employé une fonction simple qui calcule les chemins pour tous les ennemis. Quand la fonction est occupée, la variable nommé function_busy sera mise à 1; autrement elle sera mise à 0. L'ennemi attend jusqu'à ce que la function_busy soit à 0 et ensuite il en prend le contrôle, mettant function_busy à 1.

path_index = 0;
while (path_index < max_nodes)
{
path[path_index + max_enemies * my.skill48] = 0;
path_index += 1;
}
path_index = 0;
path[path_index + max_enemies * my.skill48] = target_node;
snd_play (getpath_wav, 100, 0);
if (my.start_node != target_node)
{
node_index = find_path(my.start_node, target_node, my.skill48, path_index);
wait (2);
function_busy = 0;

D'abord, nous réinitialisons le tableau nommé "path", parce que nous devons nous débarrasser du chemin précédemment stocké de l'ennemi au joueur. Target_node, le noeud le plus proche du joueur qui peut voir le joueur, est le premier élément de chemin. Le moteur jouera un son court, nous faisant savoir qu'un des ennemis doit obtenir le chemin le plus court de lui au joueur. Si le noeud de départ et le noeud cible sont différents, nous exécutons la fonction nommée find_path qui prend quatre paramètres et rend le résultat node_index. Nous attendons que le chemin soit calculé et libérons ensuite la fonction en mettant function_busy à zéro.

while (node_index >= 0)
{
temp.x = nodex[path[node_index + max_enemies * my.skill48]];
temp.y = nodey[path[node_index + max_enemies * my.skill48]];
temp.z = my.z;
vec_set (destination_node, temp);
vec_set (temp.x, destination_node.x);
vec_sub (temp.x, my.x);
vec_to_angle (my.pan, temp);
while ((vec_dist (destination_node.x, my.x) > 10) && (relax == 0))
{
ent_cycle("run", my.skill46);
my.skill46 += 10 * time;
my.skill46 %= 100;
my.skill1 = 10 * time;
my.skill2 = 0;
my.skill3 = 0;
move_mode = ignore_passable + glide;
ent_move(my.skill1, nullvector);
if (trace (my.pos, player.pos) == 0)
{
node_index = -1;
}
wait (1);
}
node_index -= 1;
wait (1);
}
}
}
}

Le chemin le plus court au joueur a été rendu par la fonction find_path, maintenant nous devons déplacer l'ennemi sur ce chemin! Nous récupérons les coordonnées x et y pour les noeuds sur le chemin, en employant la hauteur de l'ennemi pour z et ensuite nous faisons tourner l'ennemi vers le noeud de destination. Tant que l'ennemi ne s'est pas approché à moins de 10 quants du joueur et que le joueur n'est pas encore mort (relax = 0), l'ennemi joue son animation "run" dans la boucle while et se déplace en ignorant les entités passables et en glissant le long des murs.

Si l'ennemi peut voir le joueur (trace retourne zéro), node_index est mis à -1, permettant à l'ennemi de sortir de la boucle while, pour calculer un nouveau chemin vers le joueur, parce que le chemin le plus court vers le joueur a changé. Finalement, node_index est diminué, permettant à l'ennemi de se déplacer vers un nouveau noeud dès qu'il est à moins de 10 quants du noeud précédent.

// le joueur est mort, ainsi le commutateur de "stand" si vous étes vivant
while (my.health > 0)
{
ent_cycle("stand",
my.skill46); // joue les frames de l'animation "stand"
my.skill46 += 4 * time; // vitesse de l'animation "stand"
my.skill46 %= 100;
wait (1);
}
}

Les lignes de code ci-dessus s'exécuteront lorsque le joueur sera mort; l'ennemi commutera pour l'animation "stand" si le joueur est vivant et jouera cette animation dans une boucle. La fonction ci-dessous calcule et écrit le chemin réel pour chaque ennemi qui l'emploie :

function
find_path(sn, tn, id, px)
{
var p_i = 0;
while (p_i < max_nodes)
{
if ((p_i != tn)
&& (visited[p_i + max_nodes * tn] == 0))
{
if (node_to_node[sn + max_nodes * tn] == node_to_node[sn + max_nodes * p_i] +
node_to_node[p_i + max_nodes * tn])
{
px += 1;
path[px + max_enemies * id] = p_i;
next_node = p_i;
p_i = max_nodes - 1;
if (path[px + max_enemies * id] == sn)
{
return(px);
}
}
}
p_i += 1;
}
tn = next_node;
find_path(sn, tn, id, px);
}

La fonction prend quatre paramètres :
-Le noeud de début (sn), le noeud qui est le noeud le plus proche de l'ennemi et que peut voir l'ennemi;
-Le noeud cible (tn), le noeud qui est le noeud le plus proche du joueur et que peut voir le joueur;
-Id ennemi (id), un numéro de la fonction que l'ennemi a besoin pour obtenir le chemin au joueur;
-Index de chemin (px), employé comme un index pour le chemin le plus court jusqu'au joueur.

La fonction cherchera du noeud actuel au reste des noeuds; cette démonstration emploie 30 noeuds, donc chaque noeud doit vérifier encore 29 noeuds. Si le noeud actuel peut voir uns ou plus de ces noeuds et si nous avons un noeud qui est une partie du chemin le plus court (le chemin le plus court l'inclut), nous déplaçons l'élément de tableau au chemin suivant et nous stockons le numéro de noeud à l'intérieur du tableau du chemin [] (un tableau bidimensionel qui a été créé en employant un tableau simple). Nous stockons le nouveau noeud sur le chemin le plus court dans next_node, parce qu'il perdra sa valeur tout de suite et ensuite nous arrêtons d'évaluer d'autres valeurs (p_i = max_nodes - 1) parce que nous voulons éliminer d'autres chemins qui pourraient avoir la même longueur.

Si nous avons atteint le noeud qui est le point de départ pour l'ennemi, nous sortons de la fonction, retournant la valeur stockée dans px. Si la fonction continue à s'exécuter, p_i est incrémenté; si le noeud actuel ne peut pas voir un autre noeud, tn = next_node met le noeud suivant au chemin comme cible et la fonction (récursive) est exécutée de nouveau, cherchant le chemin le plus court, mais étant une étape plus près du résultat final.

La partie difficile du programme est terminée; le reste des fonctions est simple :

function
shoot_bullet()
{
vec_set (temp.x, player.x);
vec_sub (temp.x, my.x);
vec_to_angle (my.pan, temp);
if (player.health >
0)
{
ent_create (bullet_mdl, my.pos, move_bullet);
}
}

C'est la fonction qui s'exécute si l'ennemi peut voir le joueur; elle fait tourner l'ennemi vers le joueur et si la santé du joueur est supèrieure à zéro, elle commence à créer les balles qui emploient la fonction move_bullet () :

function
move_bullet()
{
my.skill47 = 5;
my.pan = you.pan;
my.push = -1;
my.enable_entity = on;
my.enable_impact = on;
my.enable_block = on;
my.event
= remove_bullet;
while (my != null)
{
my.skill1 = 15 *
time;
my.skill2 =
0;
my.skill3 = 0;
move_mode = ignore_you
+ ignore_passable + ignore_push;
ent_move(my.skill1, nullvector);
wait (1);
}
}

J'ai mis skill47 à 5 pour chaque balle tirée par les ennemis; la balle et l'ennemi qui la crée ont le même angle pan. J'ai mis push =-1 pour la balle, parce que cette méthode perrmet à la balle de passer à travers les ennemis sans les endommager (leur push a été mise à -2) mais frappe le joueur (son push a été mise à -1). La balle est sensible à d'autres entités et aux blocs du niveau; sa fonction d'événement est nommée remove_bullet. Tant que la balle continue à exister, elle se déplace avec la vitesse donnée par skill1, ignorant son créateur, les entités passables et d'autres entités qui ont leur valeur de push au-dessous de-1.

function
remove_bullet()
{
if ((my.skill47 == 5) &&
(you.skill47 == 5))
{
my.passable = on;
wait (2);
ent_remove
(me);
return;
}
my.event = null;
ent_playsound (my, hit_wav, 1000);
sleep (0.1);
ent_remove (me);
}

La fonction ci-dessus s'exécute quand la balle frappe quelque chose. Si deux balles ennemies sont entrées en collision, une d'entre elles est rendu passable et après deux cycles elle est enlevée; si la balle frappe quelque chose d'autre, elle arrête de répondre à d'autre événement, joue un son hit_was et disparaît après 0.1 secondes.

function
decrease_health()
{
if (you.skill47 == 5678)
{
my.health = 0;
}
else
// hit by a bullet?
{
my.health -= 1;
}
if (my.health <= 0)
{
my.event = null;
snd_play
(death_wav, 40, 0);
}
}

La fonction decrease_health est celle qui s'exécute quand le joueur est frappé par quelque chose; si le joueur est entré en collision avec un des ennemis (you.skill47 à 5678), le joueur meurt immédiatement parce que l'ennemi le poignarde aussitôt (plus de détails un peu plus tard). Si le joueur est frappé par une balle, il perd 1 point de santé pendant chaque tick (soit à chaque 1/16 de seconde); la balle sera active pendant 0.1 seconde donc je vous laisse faire les calculs. Si la santé du joueur va en dessous de zéro, le joueur arrêtera de répondre aux événements et il jouera le son death_wav.

Voyons la fonction d'événement associée aux ennemis :

function
enemy_event()
{
if (you.skill47 == 5678)
{
if ((vec_dist (my.x, player.x)) > (vec_dist (you.x, player.x)))
{
my.passable = on;
sleep (0.5);
my.passable = off;
}
}

Si l'ennemi est entré en collision avec un autre ennemi et si "you" est plus proches que "my" du joueur, "my" doit devenir passable, permettant à "you" de passer d'abord; cette situation dure pendant 0.5 secondes.

if (you == player)
{
my.event = null;
player.health
= 0;
vec_set (temp.x,
player.x);
vec_sub
(temp.x, my.x);
vec_to_angle
(my.pan, temp);
my.skill46
= 0;
while (my.skill46
< 90)
{
ent_cycle("point", my.skill46);
my.skill46 += 1 * time;
wait (1);
}
relax = 1;
}

Si l'ennemi entre en collision avec le joueur, il arrêtera de réagir à d'autres événements. Le joueur perdra sa santé et l'ennemi se tournera vers le joueur; l'ennemi jouera son animation "point" ( coup) une fois et ensuite la variable nommé "relax" seras mise à 1, disant à tous les ennemis de se détendre (pour arrêter et commuter leurs animations à "stand").

if (you.skill47 == 1)
{
my.health -= 1;
if
(my.health <= 0)
{
my.event = null;
my.invisible = on;
my.passable = on;
}
}
}

Si l'ennemi est frappé par la balle du joueur (you.skill47 à 1), il perdra 1 point de santé à chaque tick. Si l'ennemi est mort, il arrêtera de réagir à d'autres événements et ensuite il deviendra invisible et passable.

function
players_bullet()
{
proc_kill(4);
while (mouse_left == 1) {wait (1);}
snd_play (bullet_wav,
50, 0);
ent_create (bullet_mdl, player.pos, move_players_bullet);
}

La fonction ci-dessus s'exécute quand le joueur presse le bouton gauche de la souris ; proc_kill (4) arrêtera tous les autres cas de la fonction actuelle. Nous attendons que le bouton gauche de la sourise soit relaché (nous ne voulons pas de tir en rafale), nous jouons un son et ensuite nous créons une balle qui exécutera la fonction move_players_bullet () :

function
move_players_bullet()
{
my.skill47 = 1;
my.pan = you.pan;
my.tilt
= you.tilt;
my.passable = on;
my.enable_entity = on;
my.enable_impact = on;
my.enable_block = on;
my.event
= remove_bullet;
my.ambient = 100;

Chaque balle tirée par le joueur a son skill47 mit à 1; la balle et le joueur ont le même pan et angle tilt. La balle est passable au début; elle est sensible à d'autres entités et des blocs de niveau. Les balles tirées par le joueur et par les ennemis partagent la même fonction d'événement - remove_bullet ().

while (my != null)
{
if (vec_dist (player.x, my.x) > 30) {my.passable = off;}
my.skill1 = 50 * time;
my.skill2 = 0;
my.skill3
= 0;
move_mode = ignore_you
+ ignore_passable;
ent_move (my.skill1, nullvector);
wait (1);
}
}

La balle s'arrêtera d'être passable quand sa distance au joueur sera supérieure à 30 quants. La balle se déplace avec la vitesse donnée par skill1, ignorant son créateur et toutes les entités passables.

"Le mois prochain nous sortirons du niveau de labyrinthe (sniff...) et nous nous déplacerons dans un niveau de shooter 3D typique avec des ennemis humains. Je promets de donner une arme à feu au joueur afin qu'il ait toutes ses chances! " C'est ce que j'ai écrit à la fin de l'article qui a paru le mois dernier, donc certains d'entre vous pourraient s'estimer un peu lésés. La vérité est que j'e reste très attaché à ce niveau du labyrinthe qui nous a accompagné pendant tous ces cours :) et j'ai voulu que vous soyez capable de voir les ennemis poursuivre le joueur partout sur le niveau.

Bien, mais comme je n'ai qu'une parole, j'ai ajouté une nouvelle action de joueur , simplement nommée "player2" à la fin du fichier ai4.wdl. Ouvrez le niveau et remplacez l'action attachée au joueur (player1) avec la nouvelle action (player2) pour changer la caméra en une expérience de tir à la première personne. Utilisez "Z", "S", "Q", "D" pour vous déplacer et la souris pour tourner et tirer.

Qu'est-ce qui vient le mois prochain ? Le nouveau code et un niveau jouable avec un grand nombre de noeuds et d'ennemis, des monstres qui attendent que vous arriviez tout près d'eux et vous attaquent ensuite et plus!


Untitled

"Le making of" d'une IA parfaite

Ce mois-ci j' ai décidé de parler moins du code et plus de comment l' IA Parfaite fonctionne; de cette façon vous devez être capables d' éditer mon code facilement. Évaluons d' abord la démonstration ; ouvrez level5.wmp, compilez-le et exécutez-le employant le script nommé ai5 :

Je commencerais à courir si j' étais à votre place! Employez "Z", "S", "Q", "D" et la souris pour vous déplacer et tirer. Il y a deux ennemis qui commencent à vous poursuivre tout de suite et un autre (à droite) qui attend que vous l' approchiez à moins de 1000 quants avant de vous attaquer. Si vous tirez sur le type, il oubliera ces 1000 quants et il commencera à vous poursuivre aussi.

Les ennemis vous tireront dessus aussitôt qu' ils vous voient à moins que vous ne leur demandiez d' être plus gentils avec vous, voyons donc les options tout de suite :

Nous pouvons mettre "Alert_dist" et nous pouvons vérifier (ou pas) le drapeau "Gentle". Si nous mettons Alert_dist à zéro, les ennemis commenceront à poursuivre le joueur aussitôt que le niveau est chargé, autrement ils attendront que le joueur soit venu tout près des valeurs indiquées pour Alert_dist en quants (1000 quants dans mon exemple). Cochez le drapeau gentle et les ennemis essayeront de venir près du joueur et ensuite ils commenceront à tirer dessus.Décochez le drapeau et ils tueront le joueur aussitôt qu' ils seront capables de le voir; cela rend les choses beaucoup plus réelles et amusantes.

Vous pouvez voir que le niveau est une combinaison de secteurs d' intérieur et d'extérieurs, qui n' importent pas vraiment, tant que vous placez les noeuds à l' intérieur et à l'extérieur. Je vous ai dit qu' une IA parfaite commence par la planification de chemins parfaits; cependant, les ennemis doivent être capables d' arriver au joueur même si les noeuds ne sont pas placés parfaitement dans le niveau. Le niveau que j' emploie ce mois-ci est en désordre - ici un exemple :

L' ennemi veut trouver le joueur, donc il doit employer le chemin mis en évidence par les flèches rouges; cependant, vous pouvez voir que quelques autres noeuds peuvent se voir (suivez la flèche verte) mais l' ennemi se heurterait au mur s' il la suivrait, parce que l' ennemi est plus épais qu' une balle! L' algorithme essaye de trouver la distance la plus courte de l'ennemi au joueur chaque fois, donc il chargera l' ennemi de suivre l' itinéraire qui inclut la flèche verte! Le code à employer ce mois-ci permet aux ennemis de glisser le long des murs; s' ils sont coincés, ils arrêtent et demandent un nouveau chemin vers le joueur. Maintenant vous pouvez finalement placer vos noeuds dans le niveau sans être extrêmement précautionneux, mais n' oubliez pas que chaque noeud utilise des ressources complémentaires.

Cette démonstration peut travailler avec jusqu' à 100 noeuds et jusqu' à 40-50 ennemis dans un niveau. Avant que vous ne commenciez à penser que je suis d' une façon ou d' une autre aussi menteur que Pinocchio, laissez-moi vous montrer une copie d' écran qui a été prise tandis que j' évaluais le code avec environ 40 ennemis; je suis celui se cachant derrière ces boîtes mais je suis assez sûr que je serai mort bientôt.

J' ai assumé que n' importe quel chemin entre ces 100 noeuds (dans le cas où vous les employez tous) a un nombre maximal de 50 points, je pense que c' est ok pour n' importe quel niveau régulier. Si vous avez besoin de plus de noeuds, d' ennemis, etc vous pouvez les avoir en augmentant la taille des tableaux. Bien, je pense que 100 noeuds doivent être assez pour chacun, mais que sais-je ? Je ne suis pas chacun...

L' algorithme employé pour l' IA parfaite travaille correctement, mais il peut ralentir un peu le jeu. Comment çà un peu ? Mes essais montrent que la fonction de find_path qui a été employée dans mes articles précédents met la limite à environ 400 ennemis courant en même temps dans le niveau. Je peux vous entendre rire... 400 ennemis sont plus qu' assez pour un niveau simple, mais ces 400 ennemis emploieraient TOUTES les ressources, si vous deviez ajouter plus d' entités, de code, des panneaux, etc à votre niveau, vous termineriez avec peut-être 50 ennemis dans un niveau, ce qui n' est pas trop si vous créez un jeu de stratégie.

La solution est simple : j' ai décidé de créer un fichier de script séparé qui est exécuté seulement une fois et seulement par le développeur (pas par vos clients). Ce script nommé compute.wdl détectera et écrira tous les chemins entre n' importe quels deux noeuds dans votre niveau; les chemins réels entre n' importe quels deux noeuds sont stockés dans le fichier nommé paths.txt (ils doivent être nommés paths1.txt pour le premier niveau, paths2.txt pour le deuxième niveau et cetera). Quel profit tirons-nous d'e ceci ? Les ennemis n'ont plus besoin de chercher leur chemin correct, parce qu' il est déjà calculé pour eux et cela donne des augmentations de vitesse d' environ 800 %. Je vous laisserai faire les maths...

Voici une petite partie du paths.txt file:

1 999 2 1 999 3 1 999 4 1 999 5 1 999 6 3 1 999 7 8 1 999 8 1 999 9 8 1 999 10 5 1 999 11 10 5 1 999 12 13 16 1 999 13 16 1 999 14 1 999 15 16 1 999 16 1 999 17 1 999 .....

J' ai utilisé "999" comme séparateur, pour gagner quelque vitesse supplémentaire avec file_var_read, mais l' exemple ci-dessus ressemblerait à cela :

1
2 1
3 1
4 1
5 1
6 3 1
7 8 1
8 1
9 8 1
10 5 1
11 10 5 1
12 13 16 1
13 16 1
14 1
15 16 1
16 1
17 1
...............

Le chemin de 1 à 1 est évidemment à 1; les noeuds 2, 3, 4 et 5 peuvent voir 1 directement. Le chemin du noeud 6 au noeud 1 inclut le noeud 3 et cetera. Tous les chemins possibles entre n' importe quelle paire de noeuds sont écrits à l' intérieur de ce fichier. Notre scénario jouable (ai5.wdl) charge toutes ces valeurs dans un tableau énorme nommé paths; les ennemis choisiront simplement le chemin le plus court de ce tableau chaque fois qu' ils en auront besoin, donc ils ne devront pas calculer le chemin du tout. Si vous voulez voir comment ces chemins sont calculés dans compute.wdl, vous trouverez une instruction "wait (1);" commentée; enlevez les guillemets et vous verrez la fonction de find_path passer par chaque combinaison possible.

Ok, retournons au "making of". Comment est fait tout ce travail ?

Nous plaçons nos noeuds dans le niveau et ils obtiennent un numéro d' id unique automatiquement. J' ai pu mettre skill1 à 1 pour le premier noeud, skill1 à 2 pour le deuxième noeud, etc mais j' ai pensé que cette méthode m' aidera à gagner encore plus d' amis parmi les designers de niveaux :) Ces noeuds commencent à se balayer les uns les autres : noeud 1 balaye noeuds 2, 3, 4..., 40 (si notre niveau a 40 noeuds), puis le noeud 2 balaye noeuds 1, 3, 4..., 40, noeud 3 balaye noeuds 1, 2, 4..., 40 et cetera. Quand tous les noeuds se sont balayés, une variable nommée distances_computed est mise à 1 et le chemin trouvant l' algorithme peut s'exécuter.

Mais pourquoi ces noeuds se balayent-ils ? Jetez un coup d' oeil à l' image ci-dessous.

Supposons que le noeud 1 scanne les noeuds autour de lui (l' utilisation de ces cercles jaune) qui sont 2, 5 et 7. Vous savez qu' ent_scan passe les murs aussi, donc nous devons trouver une méthode qui nous permet de ne détecter que les noeuds qui peuvent "se voir" directement. La réponse est simple : nous traçons en arrière de chaque noeud qui a été balayé vers le noeud balayeur, ainsi le noeud 2 trace de sa position au noeud 1, le noeud 5 trace au noeud 1 et le noeud 7 trace au noeud 1. Le noeud 2 et le noeud 5 peuvent voir le noeud 1, mais le noeud 7 ne peut pas le voir; la fonction trace_back mettra le tableau node_to_node [2, 1] = 300 et node_to_node [5,1] à 340, qui sont les distances des noeuds scannés au noeud scanneur. La valeur pour node_to_node [7, 1] restera à zéro, parce que le noeud 7 ne peut pas voir le noeud 1.

J' ai commencé à comprendre quelques choses... Ainsi quand tous les noeuds ont fini de se balayer, nous avons un tableau (nommé node_to_node) qui sait quels noeuds peuvent se voir et la distance entre eux, ok ? Qu' est-ce qui vient ensuite ?

Bien, je vous ai dit que distances_computed est mis à 1 et l' algorithme de recherche de chemin peut s'exécuter; l' algorithme remplace les zéros dans node_to_node avec une valeur énorme : 999,999. Pourquoi l' ai-je fait ? L' algorithme veut détecter le chemin le plus court d' un noeud à un autre, donc il cherche à employer une distance qui est égale à zéro; cependant, nous savons que node_to_node [x, y] = 0 signifie que x et y sont deux noeuds qui ne peuvent pas se voir, aussi la distance entre eux est énorme!

L' algorithme passe tous les noeuds, trouvant la distance la plus courte entre n' importe quels deux noeuds; quand c' est fini, node_to_node [x, y] contiendra la distance la plus courte entre n' importe quels deux noeuds, même si ces noeuds ne peuvent pas se voir. Dans l' image ci-dessus; node_to_node [1, 7] serait mis à 500 , node_to_node [1, 2] serait mis à 200 et node_to_node [2, 7] serait mis à 300.

Nous avons la distance la plus courte entre n' importe quels deux noeuds sur la carte, et nous n' avons encore pas le chemin réel - c' est ce que la fonction find_path fait. C' est une fonction récursive qui choisit la distance la plus courte entre n' importe quelle paire de noeuds.

L' image ci-dessus me montre le chemin le plus court dentre A et I : ABCI, avec un coût (ou une distance) de 350. Il est clair qu' ADEC a un coût plus grand que ABC et c'est justement ce que fait la fonction find_path : elle remplace ADEC avec ABC s' il nécessaire, parce que 100 + 150 < 90 + 110 + 80. La fonction find_path rend les points réels sur le chemin le plus court de l' ennemi au joueur et ces valeurs sont écrites à l' intérieur de paths.txt.

Il y a seulement une chose qui n' a pas été discutée : comment savons nous où sont le noeud de départ et le noeud d'arrivée; autrement dit, comment détectons-nous le noeud le plus proche du joueur et le noeud le plus proche d'un certain ennemi ? Chaque noeud attend que le joueur soit à moins de 500 quants et ensuite il trace de sa position au joueur. Si le noeud 3 peut voir le joueur, il met see_player [3] à 1; s' il ne peut pas voir le joueur, il met see_player [3] à 0. Tous les noeuds qui peuvent voir le joueur à un certain moment sont comparés et le noeud le plus proche au joueur gagne la compétition : ce sera le noeud cible, le noeud qui sera mis comme une destination pour tous les ennemis. D' autre part, les ennemis parcourent les noeuds et chaque noeud trace en arrière de sa position à l' ennemi. Le noeud le plus proche del' ennemi qui peut voir l' ennemi sera le noeud de départ pour cet ennemi.

J' ai mis ent_create pour le joueur dans cette démonstration parce que je n' ai pas voulu qu' il soit balayé par les noeuds au début du jeu; mettez player_pos [3] à l' intérieur d' ai5.wdl aux valeurs qui s' adaptent à vos besoins.

C' est comme çà que l' IA parfaite fonctionne! Je pense que vous avez un système convenable d' IA entre vos mains maintenant; ce n' est pas un produit entièrement fini, mais la partie IA est vraiment finie. Vous devrez ajouter quelques lignes de code qui fait que les ennemis prennent des tournures plus lisses, peut-être quelques cris de douleur, quelques animations de mort, etc, mais ces codes ne sont pas liés à l' IA. Ne vous inquiétez pas, je ne projette pas d' abandonner l' IA parfaite; j' y ai passé environ un mois de travail, après tout. Postez vos suggestions sur le forum et si vous voulez que je continue à ajouter des particularités pour perfectionner l' IA, je le ferai, maintenant ou dans un proche avenir.


Untitled

Interface utilisateur personnisable

Je dois reconnaître que je suis une personne assez conservatrice. Les touches de direction du clavier sont bien pratiques pour configurer d'une certaine façon un panneau qui doit avoir une taille particulière à une position particulière. Cet article va vous apprendre à créer un panneau qui pourra être déplacé à la souris vers un des 4 coins de l'écran et "collé" automatiquement à sa bonne position.

panel hud_pan
{
bmap = hud_pcx;
layer = 20;
pos_x = 0;
pos_y = 0;
on_click = drag_panel;
flags = overlay, refresh, visible;
}

C'est la définition classique d'un panneau; la seule chose qui doit attirer votre attention c'est la fonction qui s'exécute chaque fois que l'on clique sur le panneau:

function
drag_panel()
{
while (mouse_left == on)
{
hud_pan.pos_x = pointer.x - bmap_width(hud_pcx) / 2;
hud_pan.pos_y = pointer.y - bmap_height(hud_pcx) / 2;
wait (1);
}

La boucle "while" ci-dessus s'exécutera aussi lontemps que vous maintiendrez le bouton gauche de la souris pressé; il positionne les coordonnées du centre du panneau aux coordonnées de la souris données par pointer.x et pointer.y

  if ((hud_pan.pos_x + bmap_width(hud_pcx) / 2) > (screen_size.x / 2))
// nous utilisons le côté droit de l'écran
{
if ((hud_pan.pos_y + bmap_height(hud_pcx)
/ 2) > (screen_size.y / 2)) // coin inférieur droit?
{
hud_pan.pos_x = screen_size.x
- bmap_width(hud_pcx);
hud_pan.pos_y
= screen_size.y - bmap_height(hud_pcx);
}
else // coin supérieur droit
{
hud_pan.pos_x = screen_size.x
- bmap_width(hud_pcx);
hud_pan.pos_y
= 0;
}
}

Si le centre du panneau a son pos_x that qui est supérieur à la moitié de la résolution de l'écran en x, nous faisons le même test sur le y et nous plaçons hud_pan en haut ou en bas à droite en fonction du résultat.

  else // nous utilisons le côté gauche de l'écran
{
if ((hud_pan.pos_y + bmap_height(hud_pcx)
/ 2) > (screen_size.y / 2)) // coin inférieur gauche?
{
hud_pan.pos_x = 0;
hud_pan.pos_y = screen_size.y - bmap_height(hud_pcx);
}
else // coin supérieur gauche
{
hud_pan.pos_x = 0;
hud_pan.pos_y = 0;
}
}
}

la valeur pos_x est inférieure à la moitié de l'écran, nous plaçons donc le panneau à gauche et en bas ou en haut en fonction de sa valeur pos_y. Nous utilisons également une autre petite fonction:

starter
init_mouse()
{
mouse_map = pointer_pcx;
while
(1)
{
if (key_space == on)
{
mouse_mode = 2;
}
else
{
mouse_mode = 0;
}
mouse_pos.x = pointer.x;
mouse_pos.y = pointer.y;
wait (1);
}
}

Cette
fonction starter montre le pointeur de la souris si nous pressons et relachons
la touche "espace" nous permettant de déplacer le panneau lorsque
nous le voulons. Vous pouvez voir que le code est très souple, vous devez
juste définir le panneau et le code fera le reste du boulot pour vous.


Untitled

Inventaire

Ce projet autonome vous apprend à créer un inventaire. Le joueur sera capable de prendre une épée, un bouclier, une armure et le cercle de feu. Il peut les laisser dans l'inventaire ou les utiliser afin d'améliorer ses capacités. Jetons un coup d'oeil à la fonction principale:

function main()
{
level_load (inventory_wmb);
wait (2);
clip_size = 0;
on_d = null; // disable the
debug panel (key "D") because it is used for movement
fps_max = 40;
check_inventory();
}

J'ai désactivé la touche "D" parce que j'utilise because I'm using WSAD pour déplacer le joueur. Avant de nous lancer dans l'effrayante fonction check_inventory, voyons quelques définitions:

define items skill30;
define armour skill41;
define attack skill42;
define strength skill43;

define shield 1; //bouclier
define mace 2; //épée
define armor 4; //armure
define ring 8; //anneau

Lorsque le joueur prend un article, sa valeur correspondante est ajoutée à player.items (simplement un autre nom pour skill30); Si lejoueur collecte un bouclier et un anneau, player.items vaudra 1 (bouclier = shield) + 8 (anneau = ring) = 9, n'est-ce pas? Regardons la fonction:

function
check_inventory()
{
while (1)
{
if (player.items
> 0)
{

Si le joueur a au moins un article (player.items > 0)

if (player.items == 1) // 1 + 0 + 0 + 0
{
shield_pan.visible = on;
mace_pan.visible = off;
armor_pan.visible = off;
ring_pan.visible = off;
}

Si player.items = 1, le joueur a le bouclier; seul shield_pan est rendu visible.

if (player.items == 2) // 0 + 2 + 0 + 0
{
shield_pan.visible = off;
mace_pan.visible = on;
armor_pan.visible = off;
ring_pan.visible = off;
}

Si player.items = 2, le joueur a l'épée; seul mace_pan est rendu visible.

if (player.items == 3) // 1 + 2 + 0 + 0
{
shield_pan.visible = on;
mace_pan.visible = on;
armor_pan.visible = off;
ring_pan.visible = off;
}

Si player.items = 3, le joueur a le bouclier et l'épée; shield_pan et mace_pan sont rendus visibles.

...................................

Les choses se répètent pour player.items = 1...15

if (player.items == 15) // 1 + 2 + 4 + 8
{
shield_pan.visible = on;
mace_pan.visible = on;
armor_pan.visible = on;
ring_pan.visible = on;
}
}

Si player.items = 15, le joueur a tous les articles et tout les panneaux sont rendus visibles.

else
{
shield_pan.visible = off;
mace_pan.visible = off;
armor_pan.visible = off;
ring_pan.visible = off;
}
mouse_map = cursor_pcx;
mouse_pos.x = pointer.x;
mouse_pos.y = pointer.y;

Si player.items = 0, tous les panneaux sont invisibles; nous mettons le pointeur de la souris à cursor_pcx; la position de la souris est calculée à chaque cycle.

          if (mouse_right
== 1)
{
mouse_mode = (mouse_mode + 2) % 4;
while (mouse_right == 1) {wait (1);}
}
wait (1);
}
}

Si nous cliquons le bouton droit de la souris, mouse mode sera mis de 0 à 2, dépendant du nombre de fois qu'on l'aura pressé. Cela montre / cache le pointeur de la souris, nous permettant de cliquer les articles dans l'inventaire afin de les utiliser. La boucle while attend jusqu'à ce que le bouton droit soit relaché sinon le pointeur appara^tra / disparaîtra à chaque cycle.

J'utilise 5 panneaux: le panneau principal de l'inventaire et 4 autres panneaux pour les articles.

panel
main_pan // the main panel for the items in the inventory
{
bmap = main_pcx;
pos_x = 700;
pos_y = 0;
layer = 10;
hbar = 7, 488, 85, blue_pcx,
1, player.armour;
hbar = 7, 518, 85, red_pcx, 1, player.attack;
hbar = 7, 550, 85, green_pcx, 1, player.strength;
flags = refresh, d3d, visible;
}

Le panneau principal inclut 3 barres horizontales ( hbars) qui montrent l'armure du joueur (j'ai utilisé armour parce que armor est déjà utilisé), attack et strength.

panel
shield_pan // displays the shield on the panel
{
bmap = shield_pcx;
pos_x = 715;
pos_y = 30;
layer = 11;
flags = overlay, refresh,
d3d;
on_click use_shield;
}

panel
mace_pan // displays the mace on the panel
{
bmap = mace_pcx;
pos_x = 730;
pos_y = 145;
layer = 11;
flags = overlay, refresh,
d3d;
on_click use_mace;
}

panel
armor_pan // displays the armor on the panel
{
bmap = armor_pcx;
pos_x = 710;
pos_y = 255;
layer = 11;
flags = overlay, refresh,
d3d;
on_click use_armor;
}

panel
ring_pan // displays the ring on the panel
{
bmap = ring_pcx;
pos_x = 710;
pos_y = 365;
layer = 11;
flags = overlay, refresh,
d3d;
on_click use_ring;
}

Les panneaux utilisés pour les articles apparaissent au dessus du panneau principal s'ils sont pris; avant que j'oublie,j'ai créé l'image bitmap pour ces panneaux en les plaçant dans une pièce noir et en faisant des copies d'écran. Chaque panneau d'article a une simple fonction qui sera exécutée si nous cliquons le panneau:

function use_shield()
{
player.items -= shield; // remove the shield from the panel
ent_create(shield_mdl, player.pos, attach_item);
player.strength += 30; // bigger hbar
}

Nous soustrayons shield (1) de player.items; la fonction check_inventory enlève son panneau associé de l'inventaire. Nous créons le "vrai" bouclier et nous le donnons au joueur et nous augmentons strength; la barre horizontale correspondante sur le panneau principal s'agrandira.

function
use_mace()
{
player.items -= mace; // remove the mace from the panel
ent_create(mace_mdl, player.pos, attach_item);
player.attack += 50; // bigger
hbar
}

Nous soustrayons mace (2) de player.items; la fonction check_inventory s'occupera du reste. Nous créons l'épée et la donnons au joueur, la barre attaque s'agrandira.

function
use_armor()
{
player.items -= armor; // remove the armor from the panel
ent_create(armor_mdl, player.pos,
attach_item);
player.armour += 40; // bigger hbar
}

Nous soustrayons armor (4) de player.items; la fonction check_inventory enlève le panneau de l'inventaire. Nous créons l'armure et la donnons au joueur; la barre armour s'agrandira.

function attach_item()
{
proc_late();
my.passable = on;
my.metal = on;
my.albedo = 0;
while(player != null)
{
vec_set(my.x, player.x);
vec_set(my.pan,
player.pan);
my.frame = player.frame;
my.next_frame
= player.next_frame;
wait(1);
}
}

Ces 3 articles sont collés au joueur à cause de cette fonction:

function attach_item()
{
proc_late();
my.passable = on;
my.metal = on;
my.albedo = 0;
while(player != null)
{
vec_set(my.x, player.x);
vec_set(my.pan,
player.pan);
my.frame = player.frame;
my.next_frame =
player.next_frame;
wait(1);
}
}

L'épée, le bouclier et l'armure sont animés ensemble avec le joueur et sauvegardés en tant que modèles séparés, ainsi ils utilisent les mêmes frames d'animation tout le temps. J'ai créé le modèle d'armure en effaçant tous les triangles que nous n'avions pas besoin; Je l'ai agrandi légèrement pour être sur qu'ilaille bien sur le joueur.

Tant que le joueur existe, ces articles sont placés à la position dujoueur en utilisant ses angles et frames tout le temps.

L'anneau est un article spécial dans l'inventaire, regardons le code.

function
use_ring()
{
player.items -= ring;
while (1)
{
if (mouse_left
== 1 && mouse_mode == 0 && number_of_flames == 0) // the mouse
pointer is invisible and not too many entities are visible
{
temp_angle = 0;
while (temp_angle < 360)
{
temp_angle += 3;
temp.x = player.x + 100 * cos(temp_angle);
temp.y = player.y + 100 * sin(temp_angle);
temp.z = player.z;
ent_create (fire_pcx, temp, fire_action);
}
}
while (mouse_left
== 1) {wait (1);}
wait(1);
}
}

Nous soustrayons ring (8) de player.items puis nous entrons dans une boucle while (1). Si nous pressons le bouton gauche et que le pointeur de la souris estinvisible et le nombre de frame à 0, nous créons 120 flammes autour du joueur, à distance de 100 quants du joueur puis nous attendons que le bouton gauche soit relâché.

function
fire_action()
{
number_of_flames += 1; // add another flame entity to this var
my.passable = on;
my.flare = on;
my.bright = on;
my.oriented = on;
my.pan = random(360);
while (my.pan < 1500)
{
my.pan += (5 +
random(3)) * time;
my.z -= 0.4 * time;
wait (1);
}
number_of_flames -= 1;
ent_remove (me);
}

Nous ne voulons pas employer l'autofire sur l'anneau du feu aussi number_of_flames est augmenté avec chaque flamme qui est créée ; de cette façon nous nous assurons qu'un anneau simple du feu peut être produit et nous gardons un taux décent d'affichage. Les flammes sont passables et orientées par un angle pan aléatoire ; elles tourneront jusqu'à ce que leursoit à 1500 (ça fonctionne:) et elles diminueront leur z en même temps. Quand toutes les flammes ont été enlevées, number_fo_flames est à zéro et la fonction use_ring peut créer un autre anneau du feu.

Il est temps de regarder les actions associées aux articles qui peuvent être pris:

action
shield_init
{
my.passable = on;
while (player == null) {wait (1);}
while (vec_dist (my.x, player.x) > 50)
{
ent_cycle("idle",
my.skill20); // play stand frames animation
my.skill20 += 3
* time; // "idle" animation speed
my.skill20 %= 100;
wait (1);
}
player.items += shield;
ent_remove (me);
}

Le bouclier est passable et joue son animation "idle" jusqu'à ce que le joueur arrive à moins de 50 quants pour le prendre. Quand c'est fait, player.items est augmenté avec shield (1) et le bouclier est enlevé. Toutes les autres actions sont similaires aussi je vous laisse jeter un oeil:

action mace_init
{
my.passable = on;
while (player == null) {wait (1);}
while (vec_dist (my.x, player.x) > 50)
{
ent_cycle("idle",
my.skill20); // play stand frames animation
my.skill20 += 3
* time; // "idle" animation speed
my.skill20 %= 100;
wait (1);
}
player.items += mace;
ent_remove (me);
}

action
armor_init
{
my.passable = on;
while (player == null) {wait (1);}
while (vec_dist (my.x, player.x) > 50)
{
ent_cycle("stand",
my.skill20); // play stand frames animation
my.skill20 += 2
* time; // "stand" animation speed
my.skill20 %= 100;
wait (1);
}
player.items += armor;
ent_remove (me);
}

action
ring_init
{
my.passable = on;
while (player == null) {wait (1);}
while (vec_dist (my.x, player.x) > 50)
{
ent_cycle("frame",
my.skill20); // play "frame" animation
my.skill20 += 2
* time; // "frame" animation speed
my.skill20 %= 100;
wait (1);
}
player.items += ring;
ent_remove (me);
}

Prêt pour l'action finale? Nous parlons de player_moves, l'action associée au joueur:

action
player_moves // attached to the player
{
player = me; // I'm the player
player.items = 0;
player.armour = 20;
player.attack = 30;
player.strength = 35;

Le joueur démarre le jeu sans aucun article et des valeurs pour armour, attack et strength.

     while (1)
{
camera.x = player.x
- 200 * cos(player.pan);
camera.y = player.y
- 200 * sin(player.pan);
camera.z = player.z
+ 200;
camera.pan = player.pan;
camera.tilt = -30;

La caméra est placée à 200 quants derrière et au dessus du joueur regardant vers lui.

my.pan += 4 * (key_a - key_d) * time - 20 * mouse_force.x * time;
player_speed.x = 10 * (key_w - key_s) * time;
player_speed.y = 0;
player_speed.z = 0;

Le joueur peut tourner en utilisant les touches "Q" et"D" ou la souris; il peut se déplacer en avant / arrière avec les touches"Z" et "S"

          if ((key_w + key_s)
!= 0)
{
ent_cycle("walk", my.skill20);
my.skill20 += 4 * (key_w - key_s) * time;
my.skill20 %= 100;
}

Si le joueur marche ,le modèle joue son animation de marche ("walk") ; l'animation est inversée si le joueur recule.

else
{
if (mouse_left != 1)
{
ent_cycle("stand", my.skill20); // play stand frames animation
my.skill20 += 2 * time; // "stand" animation speed
my.skill20 %= 100;
}

Si le bouton gauche de la souris (LMB) n'est pas pressé, le joeur attend aussi il joue son animation d'attente ("stand").

else // the player has pressed the left mouse button
{
if (ring_pan.visible != on && mouse_mode == 0 && number_of_flames
== 0) // the ring has been used
{
my.skill20 = 0;
ent_cycle("attack", my.skill20); // play stand frames animation
if (my.skill20 < 99)
{
my.skill20 += 0.5 * time; // "attack" animation played once (no loop)
}
}
while (mouse_left == 1) {wait (1);}
}
}

Si LMB est pressé et que l'anneau de feu est utilisé et que le pointeur de la souris est invisible et que number_of_flames = 0, le modèle joue son animation d'attaque ("attack") une fois puis se fige à la dernière frame jusqu'à ce que le bouton gauche soit relâché.

move_mode = ignore_passable;
ent_move(player_speed, nullvector);
wait (1);
}
}

Finalement, move_mode = ignore_passable autorise le joueur à porter les articles sans être ralenti par eux et l'instruction ent_move produit le mouvement.

Bien mais si je veux tuer mes ennemis en utilisant l'anneau de feu, comment je fais?
La méthode la plus simple voudrait que l'on fasse que le joueur scanne dans un rayon de 100 quants autour de sa position courante aussi longtemps que number_of_flames != 0. Utilisez l'évènement ennemys pour décrémenter leur vie lorsque event_type = event_scan. De cette façon vous n'utilisez pas beaucoup de temps en utilisant l'instruction ent_scan pour chaque flamme.


Untitled

John Wayne

Qui n’a jamais voulu être comme John Wayne? Je suis sûr qu'il a appris à utiliser si bien le pistolet par le tir à petites distances sur des assiettes bon marché. Avec ce projet autonome vous pourrez faire cela aussi, en utilisant deux pistolets avec le son stéréo!

La fonction Main est toujours simple; veuillez noter que j'ai neutralisé le panneau de mise au point (debug panel) parce que j'emploie les touches WSAD pour le mouvement.

function main()
{
on_d = null;
level_load (johnwayne_wmb);
wait (2);
game_init();
}

Mais que fait la fonction game_init?

function game_init()

{
crosshair_pan.pos_x = screen_size.x / 2 - 8;
crosshair_pan.pos_y = screen_size.y / 2 - 8;
while (1)
{
if (mouse_left == 1)
{
crosshair_pan.visible = on;
if (got_gun1 == 1 && got_gun2 == 0)
{
animate_right();
snd_play (shoot_snd, 70, -100);
ent_create (bullet_mdl, player.pos, move_bullet);
}
if (got_gun1 == 0 && got_gun2 == 1)
{
animate_left();
snd_play (shoot_snd, 70, 100);
ent_create (bullet_mdl, player.pos, move_bullet);
}
if (got_gun1 == 1 && got_gun2 == 1)
{
animate_right();
snd_play (shoot_snd, 70, -100); // haut parleur droit
ent_create (bullet_mdl, player.pos, move_bullet);
waitt (3);
animate_left();
snd_play (shoot_snd, 70, 100); // haut parleur gauche
ent_create (bullet_mdl, player.pos, move_bullet);
}
while (mouse_left == 1) {wait (1);}
}
wait (1);
}
}

Tout d'abord, game_init calcule la position du réticule; la carte binaire pour lui est de 16x16 aussi nous soustrayons 8 Pixel du centre de l'écran. Si le bouton de souris gauche (LMB) est cliqué, le réticule est rendu visible. J'ai associé 2 variables aux 2 pistolets: got_gun1 = 1 signifie que j'ai le pistolet droit et got_gun2 = 1 signifie que j'ai le pistolet gauche. Ce qui se produit est assez évident quand vous tirez avec une des armes; si vous avez pris les deux pistolets, les pistolets tirent en utilisant la nouvelle instruction snd_play qui produit un effet stéréophonique sympa.

Nous ne voulons pas que le joueur puisse employer l'autofire aussi nous attendons que lebouton gauche (LMB) soit libéré.

C'est le code de camera autonome le plus court du monde que je suis fier de vous présenter pour ce projet:

action
john_moves
{
player = me;
my.invisible = on;
while (1)
{
vec_set (camera.pos, my.pos);
camera.tilt += 20 * mouse_force.y * time;
my.pan += 4 * (key_a - key_d) * time - 20 * mouse_force.x * time;// NDT la touche
a correspond pour un clavier français à appuyer sur la touche q  donc gauche = q, droite = d
camera.pan = my.pan;
player_dist.x = 10 * (key_w - key_s) * time; / NDT la touche w correspond
pour un clavier français à appuyer sur la touche Z donc haut = z, bas = s
player_dist.y = 0;
player_dist.z = 0;
ent_move(player_dist, nullvector);
wait (1);
}
}

John Wayne est joué par (qui autrement?) guard.mdl aussi j’ai décidé de le rendre invisible. Employez les touches ZSAD pour déplacer le joueur et la souris pour regarder autour. Je sais que ce code de caméra n'a pas l'inertie et le mouvement est assez saccadé mais je vous ai dit que c'est le code de caméra le plus court du monde et plus que cela - qui vous dit que John Wayne avait de l’inertie? Jjetons un coup d'oeil au code qui est attaché aux modèles de pistolet; J'expliquerai ce qui se produit avec le pistolet du côté droit parce que le code pour le pistolet gauche est identique:

action pistol1
{
my.enable_impact = me;
my.event = get_pistol1;
while (my != null) // the gun wasn't picked up yet
{
my.pan += 3 * time;
wait (1);
}
}

function
get_pistol1()
{
wait (1);
got_gun1 = 1;
snd_play (gotgun_snd, 80, 0);
ent_remove (me);
right_pistol.visible = on;
}

Le pistolet est sensible à l'impact et tournera aussi longtemps qu'il n'a pas été pris. Quand nous touchons le pistolet, sa fonction get_pistol1 place got_gun1, joue un bruit, enlève le pistolet du sol et rend visible l'entité right_pistol:

entity right_pistol
{
type = <eagled.mdl>;
layer = 10;
view = camera;
x = 25;
y = -10;
z = -10;
}

C'est une définition typique d'entité; left_pistol a y = 10 au lieu de y = -10 ainsi il apparaîtra de l'autre côté de l'écran. Les pistolets sont animés en utilisant deux simples boucles while:

function animate_right()

{
while (right_pistol.tilt < 5)
{
right_pistol.tilt += 5 * time;
wait (1);
}
while (right_pistol.tilt > 0)
{
right_pistol.tilt -= 5 * time;
wait (1);
}
right_pistol.tilt = 0;
}

Quand nous tirons un coup de feu avec un des pistolets, la balle commence à partir de l'origine du joueur et se déplace vers la cible en utilisant la fonction move_bullet:

function
move_bullet()
{
wait (1);
my.invisible = on;
my.enable_entity = on;
my.enable_block = on;
my.event = remove_me;
my.passable = on;
my.pan = camera.pan;
my.tilt = camera.tilt;
my.skill1 = 0;
bullet_speed.x = 200;
bullet_speed.y = 0;
bullet_speed.z = 0;
bullet_speed *= time;
while (my.skill1 < 50)
{
if (my == null) {return;}
if (my.skill1 < 0.1)  // pas de collision avec le joueur
{
my.passable = on;
}
else
{
my.passable = off;
}
my.skill1 += 0.1 * time;
ent_move (bullet_speed, nullvector);
wait (1);
}
remove me;
}

La balle peut se heurter à d'autres entités ou avec les murs et elle sera enlevée si elle frappe quelque chose. La balle est passable à l'heure actuelle dès sa création aussi elle ne frappera pas le corps de John, mais après que quelques frame son drapeau passable est remis à zéro. Ce bébé voyage à la vitesse de 200 quants/tick!

En conclusion, le code pour ce fourneau étrange - le plate_generator:

action plate_generator
{
my.passable = on;
while (1)
{
if (random(1) > 0.3)
{
ent_create (plate_mdl, my.pos, move_plate);
}
waitt (32);
}
}

Si random(1) > 0. 3, une nouvelle assiette est produite toutes les deux secondes. Jetons un coup d'oeil à la fonction qui déplace l’assiette:

function
move_plate()
{
wait (1);
my.enable_impact = on;
my.enable_block = on;
my.event = destroy_plate;
my.skill12 = (1 - random(2)) / 5;
my.skill13 = (1 - random(2)) / 5;
my.skill14 = 2 + random(1);
plate_speed *= time;
while (me != null)
{
my.roll += 1 * time; // uncomment this line to see some weird stuff
ent_move (my.skill12, nullvector);
my.skill14 -= 0.05 * time;
wait (1);
}
}

Vous pouvez voir que l’assiette sera enlevée quand elle est frappée par une balle ou se heurteà un mur. J'emploie une technique spéciale ici qui justifie mon salaire: my.skill12. .14 sont employés comme si ils étaient une variable simple avec ses composants xyz. Tout que vous devez faire est de vous assurer que vous commencez par la bonne variable skill (0, 3, 6, 9, 12, etc..) et que vous vous référez à la même variable quand vous l'employez comme si c'étaient une variable standard avec 3 composants (comme j'ai fait avec l'instruction ent_move). C'est extrêmement utile parce que les variables sont ici et elles emploient de la mémoire même si vous ne les employez pas. Plus que cela, si vous avez 100 plats à faire fonctionner dans votre niveau, vous n'aimerez pas avoir 100 variables séparées et 100 actions légèrement modifiées.

function destroy_plate()

{
wait (1);
if (event_type == event_impact) // hit by a bullet
{
my.transparent = on;
my.alpha = 100;
while (my.roll < 1440) // 4 rotations
{
my.roll += 10 * time;
my.alpha -= 0.7 * time;
wait (1);
}
}
ent_remove (me); // remove the plate (without tilting) if it hit the ground (event_block)
}

Si l’assiette frappe un mur ou le sol, elle est simplement enlevée. Si l’assiette est frappée par une balle, son angle de roulement est augmenté tandis que sa transparence est diminuée jusqu'à ce que l’assiette disparaisse.


Untitled

Mani(a)c Miner - Le petit homme revient en 3D

Je me souviens avoir personnellement joué à cet excellent vieux jeu sur mon ordinateur Spectrum Sinclair ; où 32Kb de Ram étaient largement suffisant pour écrire un très grand jeu. Un de mes favoris était Manic Miner un jeu où vous jouez le petit homme qui doit rassembler une certaine substance afin d'ouvrir la porte au prochain niveau. C'est un classique, mais je l'ai simplement aimé.

Vers la fin de cet article, vous apprendrez comment créer n'importe quel type de jeu de plateforme/arcade 3D, y compris le mineur maniaque. Mais d'abord, donnons-leur un accueil chaleureux:

guard.mdl alias 'Maniac Miner'
oldone.mdl alias 'Rock'
curtain.mdl alias 'Curtain'
cola.mdl aliasa 'Cola'

et pour finir

evilplnt.mdl également connu sous le nom de "The Evil Plant"! Est-ce le méchant ou bien?

La fonction Main est si simple - il n'y a rien à expliquer ici. Chaque objet a sa propre action et nous savons que ces actions commencent quand nous démarrons le niveau sans rien faire de spécial. Avant que j'oublie: J'emploie des actions/fonctions simplifiées de temps en temps afin de vous faciliter la compréhension - regardez les dossiers de wdl pour avoir la totalité du code.

L'action player_start est l'action correspondante de guard.mdl:

action player_start
{
start_coords.x = my.x;
start_coords.y = my.y;
start_coords.z = my.z;
my.enable_entity = on;
my.enable_push = on;
my.enable_impact = on;
my.event = maniac_event;
points = 0;
maniac_moves();
}

Tout d'abord, nous stockons les coordonnées du maniac (joueur) parce
qu'il sera remis en cette position s'il meurt.   Si le maniac se heurte à n'importe quelle entité,
son maniac_event sera déclenché.   En conclusion,
nous plaçons des points à 0 et nous appelons la fonction qui prend soin du mouvement
du maniac.

function maniac_moves()
{
while (1)
{
maniac_gravity;
if (key_force.x != 0)
{
if (key_force.x > 0)
{
player.pan = 0;
}
else
{
player.pan = 180;
}
walk_speed.x = maniac_speed;
ent_cycle("walk", my.skill9);
my.skill9 += animation_speed * time;
if (my.skill9 > 100) {my.skill9 = 0;}
}
else
{
ent_cycle("stand", my.skill10);
my.skill10 += stand_speed * time;
if (my.skill10 > 100) {my.skill10 = 0;}
walk_speed.x = 0;
}
if (key_space
== 1)
{
maniac_jump();
}
move_mode
= ignore_you + ignore_passable;
ent_move
(walk_speed, nullvector);
wait
(1);
}
}

Il y a un appel à maniac_gravity et puis à un étrange key_force.x. Quand vous pressez les touches de déplacement du curseur gauche ou droite sur votre clavier, key_force.x change de 0 à -1 ou à 1, selon quelle touche vous avez appuyée. L'instruction ent_move (walk_speed, nullvector); déplacera le maniac dans la direction vers laquelle il fait face ainsi si nous faisons au maniaque un changement son angle pan, il ira dans cette direction. Puisque son move_mode = ignore_you + ignore_passable, le maniac peut passer par les entités passables.

ent_cycle("walk", my.skill9);
my.skill9 += animation_speed * time;
if (my.skill9 > 100) {my.skill9 = 0;}

J'ai expliqué comment l'ent_cycle fonctionne dans Aum2 - voici une version courte: au début de jeu, my.skill9 = 0, ainsi ent_cycle("walk ", my. skill9); met la frame du garde à sa première frame de marche (si garde.mdl a une animation encadre appelée « walk » ); my.skill9 augmente en raison de la 2ème ligne du code - donc les frames de marche changent. Si nous atteignons la dernière armature de marche (ceci signifie que my.skill9 = 100), nous faisons une boucle sur l'animation de marche en utilisant la dernière ligne du code.

Maintenant revenons de nouveau à maniac_moves(). Si nous ne pressons pas une des touches de déplacement du curseur, maniac est mis en position "stand" et sa vitesse sur x sera remise à zéro. Si nous pressons la barre espace, le maniac sautera:

function maniac_jump()
{
while (jump_time > -1)
{
if (key_space == 0) {return;}
walk_speed.z = jump_height * time * jump_time;
jump_time -= 0.04 * time;
wait (1);
}
while (key_space == 1) {wait (1);}
jump_time = 1;
}

Quand le maniac est sur la terre, jump_time = 1. L'idée est de donner au maniac une valeur positive de walk_speed.z pendant un temps limité, puis de la faire revenir en négatif - autrement le maniac sauterait vers le haut dans le ciel et nous ne voulons pas cela! Quand nous pressons la barre 'espace, le jump_time diminue jusqu'à ce qu'il soit plus petit que -1 (la ligne rouge dans l'image ci-dessus). Quand jump_time = -1 le saut est fini; nous attendons que key_space soit libéré nous permettre au maniac de sauter de nouveau.

Si nous pressons la barre 'espace et que nous la libérons tandis que nous sommes toujours dans la boucle while(jump_time >-1), l'instruction suivante sera exécutée if (key_space == 0) {return;}
Ceci nous permet d'avoir des sauts plus longs ou plus courts, selon le temps où la barre "espace" a été appuyée parce qu'aussi longtemps que jump_time > 0, le saut sera plus haut. Si nous libérons la barre espace rapidement et le jump_time a seulement diminué à 0. 8 le saut est plus court que celui qui est exécuté pour jump_time = 0.1

La fonction maniac_gravity() fait attention à ce que le maniac se tienne toujours sur le sol (lorsqu'il ne saute pas):

function maniac_gravity()
{
vec_set (temp, my.x);
temp.z -= 2000;
trace_mode = ignore_me
+ ignore_sprites + ignore_models + use_box;
my.skill11 = trace(my.x,
temp);
if (my.skill11 > 2)
{
walk_speed.z -= 2 * time;
}
else
{
walk_speed.z = 0;
jump_time = 1;
}
if (my.skill11 < 0)
{
walk_speed.z += 5 * time;
}
}

Nous employons la trace pour obtenir la hauteur du maniac au-dessus de la terre et nous la stockons dans skill11. Tandis que nous sommes dans le ciel (skill11 > 2) nous employons une vitesse négative sur z pour abaisser le maniac. Si skill11 < = 2, nous supposons que le maniac est sur la terre, ainsi nous remettent à zéro sa vitesse sur z et nous lui permettons de sauter encore en plaçant jump_time à 1. Il est possible de voir le maniac se coincer avec ses pieds dans la terre, aussi nous le tirons de là en lui donnant une poussée positive de vitesse de z si my.skill11 < 0.

Si le maniac se heurte à quelque chose, son événement est déclenché - vous savez déjà cela. Voici ce qui se produit:

function maniac_event()
{
if (event_type == event_entity
|| event_type == event_push || event_type == event_impact)
{
wait (1);
if (you.skill9 == 1)
{
player.x = start_coords.x;
player.y = start_coords.y;
player.z = start_coords.z;
points = 0; // reset score
beep;
}
}
}

Cette fonction contrôle si l'entité qui s'est heurtée
au maniac a son skill9 de réglé à 1 (ceci est correct pour toutes les "mauvaises"
entités).   Si c'est correct, le maniac
doit mourir, ainsi il sera déformé dans sa position de départ (rappelez-vous que
nous l'avons stockée au début de jeu?),  les points seront remis à zéro et ainsi de suite.

Le maniac doit avoir peur de deux entités: la roche (rock) et la plante du diable (evil plant). La roche a une action simple:

action rock
{
my.y = player.y;
my.skill9 = 1;
}

La première ligne s'assure que même si nous plaçons la roche (dans wed) dans une mauvaise position, elle sera alignée sur le y du joueur au début de jeu – c’est de cette façon que nous nous assurons que la collision peut se produire. La roche est un "mauvais", aussi nous mettons son skill9 à 1. La plante du diable a une plus grande action:

action
evil_plant
{
plant_speed.y = 0;
plant_speed.z = 0;
my.y = player.y;
my.skill9 = 1;
while (1)
{
move_mode = ignore_you + ignore_passable;
ent_move (plant_speed, nullvector);
my.skill20 += plant_speed.x;
if (my.skill20 == my.skill1 || my.skill20 == my.skill1 * -1)
{
plant_speed.x *= -1;
}
wait (1);
}
}

La
plante se déplace seulement sur l’axe des x (de gauche à droite et vice
versa), ainsi nous plaçons sa vitesse sur y et z à 0;
La plante déplace tout le temps en utilisant ent_move dans une boucle while;  nous stockons la distance couverte par la plante
dans skill20.   D'autre part, nous avons
placé la distance maximum dans wed (skill1).   Si la distance est plus petite que - skill1
ou plus grande que skill1, la plante changera sa direction.   Cette action a quelques lignes de code en plus;  l’une d'elles est

my.skill1 *= plant_speed;

Pouvez-vous deviner ce qui se produit ici?

En conclusion le bon code:

action eat_dots
{
my.y = player.y;
my.enable_impact = on;
my.event = dot_action;
}

Vous pouvez manger des points de cola - ils réagissent à l'impact, déclenchant la fonction suivante:

function dot_action()
{
if (event_type == event_impact)
{
my.invisible
= on;
my.passable
= on;
play_sound
eatdot_snd,70;
points
+= 10;
}
}

Je ne veux pas enlever ( "remove") les points car d'autres actions peuvent utiliser le synonime my. Je préfère rendre ces points invisibles et passables – ils n'existeront plus pour le joueur.

Si le maniac termine le niveau, le rideau l’attend pour l’impact. S'il y arrivet, un joli florilège d'effets est appliqué, nous permettant de voir le niveau entier.

Il y a encore quelque chose à expliquer : le panneau des scores. Nous commençons par définir une fonte et une chaîne de carctère (pour « crire « score » en bas de l’écran) :

font comic_font, <comic20.pcx>, 34, 38;
string score_string, "Score:";

Nous définissons un panneau qui est toujours visible:

panel score_panel
{
pos_x = 0;
pos_y = 0;
digits 650,550,4,comic_font,1,points;
flags = refresh, visible;
}

digits 650,550,4,comic_font,1,points; signifie que nous affichons les “points” en utilisant la fonte comic_font, sur 4 chiffre avec un multiplicateur de 1 à la position 650,650 pixels du coin supérieur gauche de l’écran.

Nous définissons un texte toujours visible pour montrer "Score:"

text
score_text
{
pos_x = 470;
pos_y = 550;
font = comic_font;
string = score_string;
flags = visible;
}

Avant que j'oublie, la version PC de Manic Miner peut être chargée librement à l'adresse suivante

Manic Miner's Homepage:
http://www.andyn.demon.co.uk

Si vous trouvez que Maniac Miner est trop facile à jouer, attendez de rencontrer la bête Kong !


Untitled

Code Menu

Beaucoup de vous ont réclamé du code pour un menu aussi j'ai décidé de créer un simple mais complet exemple de menu. Ce menu a les boutons suivants : New Game (nouveau jeu), Sound Volume (réglage volume), Video Resolution (résolution vidéo), Quit (quitter). Je sais que vous auriez également voulu Sauvegarde, chargement etc. mais toutes ces focntions sont déjà implantées dans le calibre Menu et vous pouvez les ajouter (ainsi que leur boton respectif) à mon code.

J'ai cherché à vous donner une chance de jouer avec le menu dans un projet de jeu tout seul, j'ai donc créé un petit jeu - un niveau tout bête qui n'a d'autre raison que de vous permettre de démarrer le jeu, d'ajuster le volume et d'entendre la différence, de changer la résolution et de voir la différence et de pouvoir quitter le jeu.

Le jeu démarre dans la résolution 800x600 pixels, en mode 16 bits.

var video_mode = 7; // 800x600
var video_depth = 16; // 16 bit mode

Avant tout, nous avons besoin de définir quelques bitmaps. Nous utilisons 2 images pour chaque bouton et une seule image pour chaque panneau. Les noms sont suffisammenet suggestifs pour ne pas nécessiter de commentaires ici.

bmap main_pcx = <main.pcx>;
bmap new1_pcx = <new1.pcx>;
bmap new2_pcx = <new2.pcx>;
bmap sound1_pcx = <sound1.pcx>;
bmap sound2_pcx = <sound2.pcx>;
bmap video1_pcx = <video1.pcx>;
bmap video2_pcx = <video2.pcx>;
bmap quit1_pcx = <quit1.pcx>;
bmap quit2_pcx = <quit2.pcx>;
bmap maincursor_pcx = <maincurs.pcx>;
bmap sound_pcx = <sound.pcx>;
bmap video_pcx = <video.pcx>;
bmap slider_pcx = <slider.pcx>;

J'utilise 2 niveauxs: introlev.wmb est un niveau bidon, un simple bloc évidé avec quelques lumières colorées à l'intérieur et level1.wmb qui est le premier et le seul niveau du jeu.

string introlevel_wmb = <introlev.wmb>;
string level1_wmb = <level1.wmb>;

La définition des panneaux est similaire; main_pan a 4 boutons, sound_pan et video_pan non pas de bouton mais des curseurs.

panel
main_pan
{
bmap = main_pcx;
layer = 20;
pos_x = 220;
pos_y = 200;
button = 50, 65, new1_pcx, new1_pcx, new2_pcx, null, start_game, mouse_over;
button = 50, 115, sound1_pcx, sound1_pcx, sound2_pcx, null, set_volume, mouse_over;
button = 50, 165, video1_pcx, video1_pcx, video2_pcx, null, set_resolution, mouse_over;
button = 50, 215, quit1_pcx, quit1_pcx, quit2_pcx, null, quit_game, mouse_over;
flags = d3d, overlay, refresh;
}

panel sound_pan
{
bmap = sound_pcx;
layer = 20;
pos_x = 160;
pos_y = 240;
hslider = 60, 95, 200, slider_pcx, 0, 100, sound_vol;
flags = d3d, overlay, refresh;
}

panel video_pan
{
bmap = video_pcx;
layer = 20;
pos_x = 160;
pos_y = 240;
hslider = 60, 95, 200, slider_pcx, 4, 8, video_value;
flags = d3d, overlay, refresh;
}

Si vous ne comprenez pas ce que j'ai fait avec les boutons et les curseurs, lisez les définitions dans le manuel et comparez avec ce que j'ai fait.

Voyons la suitet:

function
main()
{
on_esc = null;
level_load (introlevel_wmb);
wait (2);
game_init(); // contrôle la touche échapet change le panneau si nécessaire
main_pan.visible = on; // montre le panneau principal
mainmenu_mouse(); // et me donne un curseur pour jouer avec
vec_set (camera.pos, nullvector); // déplace la caméra à l'origine
while (game_started == 0) // nous n'avons rien pressé pour l'instant
{
camera.pan += 0.3 * time; // tourne la caméra doucement
camera.tilt += 0.1 * time;
camera.roll += 0.05 * time;
wait (1);
}
}

Si vous démarrez un nouveau projet à partir de zéro, Echap sort du moteur. Nous, nous voulons utilisez cette touche échap pour faire apparaître le menu, aussi notre première ligne désactive la fonction originale de sortie. Nous chargeons le niveau bidon, nous attendons 2 frames pour être sur qu'il soit chargé et nous exécutons la fonction game_init() (que nous verrons plus en détail plus tard). Main_pan est rendu visible, mainmenu_mouse() affiche un curseur de souris, la camera est placée à l'origine et nous démarrons la rotation en changeant ses angles pan, tilt et roll.

function mainmenu_mouse ()
{
mouse_mode = 2;
mouse_map = maincursor_pcx;
while (game_started == 0)
{
mouse_pos.x = pointer.x;
mouse_pos.y = pointer.y;
wait (1);
}
}

La première ligne dans mainmenu_mouse() montre le curseur et le change avec notre maincursor_pcx. Nous pouvons bouger ce curseur aussi lontemps que le jeu n'est pas démarré en utilisant les pointeurs prédéfinis pointer.x et pointer.y.

Petit retour arrière :

function game_init()
{
while (1)
{
main_pan.pos_x = (screen_size.x - bmap_width(main_pcx)) / 2;
main_pan.pos_y = (screen_size.y - bmap_height(main_pcx)) / 2;
sound_pan.pos_x = (screen_size.x - bmap_width(sound_pcx)) / 2;
sound_pan.pos_y = (screen_size.y - bmap_height(sound_pcx)) / 2;
video_pan.pos_x = (screen_size.x - bmap_width(video_pcx)) / 2;
video_pan.pos_y = (screen_size.y - bmap_height(video_pcx)) / 2;

Chaque panneau peut-être déplacé en utilisant ses paramètres pos_x et pos_y. Nous avons mis quelques valeurs initiales pos_x and pos_y à nos panneaux mais le panneau à besoin de changer sa position pour être sur d'être positionné au milieu de l'écran quelque soit la résolution. Regardez l'image suivante pour comprendre comment ça marche:

if (key_esc == 1)
{
while (key_esc == 1) {wait (1);}
if (main_pan.visible == on)
{
main_pan.visible = off;
mouse_mode = 0;
}
else
{
if (sound_pan.visible == off && video_pan.visible == off)
{
main_pan.visible = on;
game_started = 0;
mainmenu_mouse();
}
}
if (sound_pan.visible == on)
{
sound_pan.visible = off;
main_pan.visible = on;
}
if (video_pan.visible == on)
{
video_pan.visible = off;
main_pan.visible = on;
if (int (video_value != video_mode))
{
switch_video (video_value, 16, 1);
}
}
}
wait (1);
}
}

Si nous pressons la touche échap nous devons attendre qu’elle soit relâchée (boucle while) car nous ne voulons pas que le panneau appara^sse et disparaîsse 50 à 100 par seconde (le changement déclenché par échap serait exécuté à chaque cycle d’affichage. Si main_pan est visible, nous le rendons invisible et cachons le curseur de la souris. SI main_pan n’est pas visible nous vérifions si sound_pan ou video_pan sont visibles. Si aucun des deux n’est visible (il n’y a aucun panneau à l’écran) nous rendons main_pan visible, nous arrêtons le jeu et montrons le curseur de la souris. Si sound_pan est visible, nous le cachons et montrons main_pan; Si video_pan est visible, nous faisons la même chose avec lui MAIS si la résolution a changée nous commutons le mode vidéo. Notre curseur coulissant pouvant donner une valeur non entière nous utilisons int (video_value).

J’utilise une petite action :eye_checker pour maintenir le sprite orienté et une petite fonction mouse_over() pour jouer un son lorsque la souris est au dessus d’un bouton:

action eye_checker
{
my.oriented = on;
}

function mouse_over()
{
play_sound (mouse_snd, 70);
}

Le long moment tant attendu est enfin arrivé: nous allons regarder la fonction associée aux boutons du menu:

function start_game()
{
main_pan.visible
= off;
game_started = 1;
mouse_mode = 0;
level_load (level1_wmb);
wait (2);
camera.z = 220;
camera.tilt = -90;
}

La fonction start_game() sera exécutée à chaque fois que vous presserez le bouton New. Cette fonction cache main_pan, met la variable game_started à 1, montre le curseur, charge le niveau du jeu et met la position camera.z et l’angle tilt.

function set_volume()
{
main_pan.visible
= off;
sound_pan.visible = on;
}

La fonction set_volume() cache main_pan et montre sound_pan. Si vous regardez de plus près la définition du curseur coulissant horizontal (hslider) dans sound_pan, vous pouvez voir qu’en changeant la position du curseur vous modifiez sound_vol; c’est une variable prédéfinie qui change le volume pour tous les sons wav du jeu. Déplacez le curseur, pressez échap et vous devriez entendre la différence lorsque vous déplacez la souris au dessus des boutons de main_pan.

function set_resolution()
{
main_pan.visible
= off;
video_pan.visible = on;
}

La fonction set_resolution cache main_pan t montre video_pan. Lorsque vous déplacez le curseur, video_value change et lorsque vous pressez échap la résolution change – voir le code dans game_init()

function quit_game()
{
exit;
}

Cette fonction fait quitter le moteur une bonne fois pour toute.


Untitled

Menu contextuel

Cet article va vous apprendre comment utiliser le bouton droit de la souris pour faire apparaître des menus. L'exemple change la résolution de l'écran, mais vous pouvez utiliser cette trame et en faire un produit complet de menus contextuels, avec des options qui appara^ssent en fonction des circonstances etc.

Nous avons besoin de définir un tableau contenant 3 boutons:

panel
context_pan
{
bmap = menu_pcx;
pos_x = 0;
pos_y = 0;
layer = 10;
button = 2, 2,
b1_640_pcx, b2_640_pcx, b1_640_pcx, change_resolution, null, null;
button = 2, 22, b1_800_pcx, b2_800_pcx, b1_800_pcx, change_resolution,
null, null;
button = 2, 42, b1_1024_pcx, b2_1024_pcx, b1_1024_pcx,
change_resolution, null, null;
flags = overlay, refresh;
}

Notez que le panneau n'est pas visible pour le moment, les 3 boutons utilisent la même fonction change_resolution. La fonction principale (main) charge le niveau, met l'image qui sera utilisée pour le curseur de la souris et permet à la souris de se déplacer dans une boucle while.

function
main()
{
level_load (context_wmb);
mouse_map
= pointer_pcx;
while (1)
{
mouse_pos.x = pointer.x;
mouse_pos.y
= pointer.y;
wait (1);
}
}

Regardons la fonction qui s'exécute lorsqu'on clique sur un des 3 boutons dans le menu:

function change_resolution(button_number)
{
if (button_number == 1) // 640 x 480
{
video_switch (6, 0, 0);
}
if (button_number == 2) // 800 x 600
{
video_switch (7, 0, 0);
}
if (button_number == 3) // 1024 x 768
{
video_switch (8, 0, 0);
}
mouse_mode = 0; // now hide the pointer
}

Si le joueur presse le premier bouton, le moteur commute en 640 x 480 pixels, conservant le même nombre de couleurs. la même chose se produit en pressant le 2ème ou le 3ème bouton (la résolution étant différente bien entendu). La dernière ligne de code cache le curseur de la souris dès qu'un bouton est cliqué.

Le menu est rendu visible lorsque nous pressons le bouton droit de la souris:

function
show_menu()
{
mouse_mode = 2;
context_pan.pos_x
= pointer.x;
context_pan.pos_y = pointer.y;
context_pan.visible = on;
}

on_mouse_right = show_menu;

Vous pouvez voir que la fonction show_menu est exécutée dès que le joueur presse le bouton droit de la souris; la fonction rend le curseur de la souris visible, met les bonnes paramètres pos_x et pos_y pour le panneau et rend le panneau visible.

Comment nous débarassons-nous du menu ? Jetez un coup d'oeil à la fonction ci-dessous:

function
hide_menu()
{
context_pan.visible = off;
wait (1);
mouse_mode = 0;
}

on_mouse_left = hide_menu;

La dernière fonction cache le panneau, attend un cycle puis cache également le curseur de la souris.


Untitled

Turbo

Queques fois vous
auriez envie que votre joueur passe à la vitesse turbo. Peut-être
aimeriez-vous avoir plus de points de santé et que diriez vous d'un pack
de munitions qui fonctionne avec toutes vos armes?

action power_health
{
show_panels(); // affiche le panneau
my.metal = on;
my.passable = on;
while (vec_dist(my.x,
player.x) > 50)
{
my.pan += 3 * time;
wait (1);
}
my.invisible
= on;
player.light = on;
player.lightrange = 300; // le joueur
vire au rouge
player.lightred = 100;
snd_play (gotitem_snd, 70, 0);
while (my.skill1 < 500) // ~ 30 secondes
{
if (player._armor < 100)
{
player._armor
+= 0.8 * time;
}
else
{
if (player._health <
200)
{
player._health
+= 0.5 * time;
}
}
my.skill1 += 1 * time;
wait (1);
}
ent_remove (me);
player.light
= off; // remettons les choses en ordre !
player.lightrange
= 0;
player.lightred
= 0;
}

La première ligne affiche le panneau munition / santé / armure qui est fourni avec les templates. La turbo-santé tourne sur elle même jusqu'à ce que le joueur vienne près d'elle. S'il l'a prend, l'objet devient invisible et le joueur diffuse une lumière rouge autour de lui. Aussi longtemps que skill1 < 500 (pour 30 secondes environ) l'armure monte jusqu'à 100 et la santé peut grimper jusqu'à 200. Lorsque le temps est expiré (skill1 est plus grand que 500) la turbo santé invisible disparait; nous l'avions gardé en vie jusqu'à présent car nous avons utilisé son skill1. Le joueur cesse d'émettre la lumière rouge et tout redevient normal.

Jetons un coup d'oeil à l'action power_ammo:

action
power_ammo
{
my.metal =
on;
my.light = on;
my.lightrange = 0;
my.lightgreen
= 200;
my.transparent
= on;
my.passable
= on;
while (vec_dist(my.x, player.x) >
50)
{
my.pan += 1 * time;
my.tilt += 1 * time;
my.roll += 1 * time;
wait (1);
}
ent_remove
(me);
snd_play (gotitem_snd, 70, 0);

// 50 balles pour chaque armes - utilisez les valeurs que vous voulez ici
ammo1 = 50;
ammo2 = 50;
ammo3 = 50;
ammo4 = 50;
ammo5 = 50;
ammo6 = 50;
ammo7 = 50;
}

Cette surpuissance tourne sur tous ces axes (pan, tilt, roll). Si le joueur vient à le toucher, elle disparaît et ammo1...ammo7 (les variables qui indiquent le nombre de balles dans le template) est augmenté. Vous pouvez bien entendu donner un nombre de munition différent selon le type d'arme ou ne donner des munitions que pour certaines armes.

La dernière surpuissance :

action
power_speed
{
my.metal =
on;
my.light = on;
my.lightrange = 0;
my.lightblue
= 200;
my.transparent = on;
my.passable = on;
while (vec_dist(my.x, player.x) >
50)
{
my.pan += 3 * time;
my.tilt += 2 * time;
my.roll += 1 * time;
wait (1);
}
my.invisible
= on;
player.light = on;
player.lightrange = 300;
player.lightblue = 250;
while (my.skill1 < 300) // ~ 20 seconds
{
strength.x = 10; // strength.x = 5 in templates
strength.y = 8; //
strength.y = 4 in templates
my.skill1 += 1 * time;
wait (1);
}
ent_remove
(me);
snd_play (gotitem_snd, 70, 0);

// restore
defaults
strength.x = 5;
strength.y = 4;

player.light = off;
player.lightrange = 0;
player.lightblue = 0;
}

Cette surpuissance double la vitesse. Le joueur se déplace avec la vitesse donnée par "strength" dans movement.wdl. Lorsque le joueur prend cette surpuissance, il diffuse une lumière bleue autour de lui et se déplace avec une vitesse plus grande pendant environ 20 secondes. Lorsque le temps ext expiré, la vitesse redevient normale et la lumière autour du joueur disparait.

J'ai cherché à vous donner toutes les chances d'essayer toutes les surpuissances dans mon niveau test inclus, aussi j'ai ajouté une arme et notre invité spécial:

action
thebutcher //le boucher
{
my.light =
on;
my.lightred = 100;
my.lightgreen = 100;
my.lightblue = 100;
my.frame = 31;
while (player == null) {wait (1);}
while (1)
{
if (vec_dist(my.x, player.x) < 60 &&
player._health > 0)
{
if (player._armor >
0)
{
player._armor
-= 3 * time;
}
else
{
player._armor
= 0;
player._health
-= 2 * time;
}
}
wait (1);
}
}

Ce gentil monsieur blessera le joueur s'il arrive trop près. Maintenant vous pouvez tester la surpuissance santé et munitions (tirer toutes vos balles et prenez la surpuissance munitions). Testez également la surpuissance vitesse et essayez de courrir le plus loin possible, car je vous chasse avec mon arme superpuissante. Ah; ah... :)


Untitled

Traduction Olivier Burton - correction AB

Missiles à tête chercheuse

Ce mois j' ai inventé un projet autonome qui vous montre comment ajouter des missiles à tête chercheuse à vos projets.

Commençons par la fonction principale (main) :

function
main()
{
on_d = null;
level_load (heatseek_wmb);
}

Il n' y a rien de spécial ici. J' ai mis hors service la touche de mise au point ("D" pour A5) parce que j' emploie ZSQD pour déplacer le joueur.

action
player_moves
{
player = me;
camera.pan = player.pan;
my.invisible = on;
while (1)
{
vec_set (camera.pos, my.pos);
camera.tilt += 20 * mouse_force.y * time;
camera.pan -= 20 * mouse_force.x * time;
my.pan = camera.pan;
my.tilt = camera.tilt;
player_speed.x = 15 * (key_w - key_s) * time;
player_speed.y = 10 * (key_a - key_d) * time;
vec_set (temp, my.x);
temp.z -= 1000;
trace_mode
= ignore_me + use_box;
player_speed.z = -trace (my.x, temp);
move_mode = ignore_you + ignore_passable;
ent_move(player_speed, nullvector);
if (mouse_left == 1)
{
fire_rocket();
}
wait (1);
}
}

Le joueur est rendu invisible parce que nous employons une caméra à la 1ère personne. Nous pouvons faire tourner la caméra en employant la souris et nous nous déplaçons en utilisant "Z", "S", "Q" et "D". Nous employons trace pour garder les pieds du joueur sur le terrain tout le temps. Si nous pressons le bouton gauche de la souris (LMB) la fonction fire_rocket est appelée :

function
fire_rocket()
{
proc_kill(4); // only one instance
of this function is running
while (mouse_left ==
1) {wait (1);}
ent_create (rocket_mdl, player.pos,
shoot_rocket);
snd_play (rocket_wav, 50, 0);
}

La fonction ne devant être exécutée qu'une seule fois; nous attendons que le LMB soit relaché et ensuite nous créons une fusée et nous jouons un son. La fusée exécute sa fonction shoot_rocket :

function
shoot_rocket()
{
wait (1);
my.enable_entity = on;
my.enable_block = on;
my.event = remove_rocket;
my.pan = camera.pan;
my.tilt = camera.tilt;
my.skill20 = 0;
my.skill1
= 10;
my.skill2 = 0;
my.skill3 = 0;
my.skill1 *= time;
my.skill10 = 0;

La fusée est sensible aux blocs de niveau et aux entités; sa fonction d' événement est remove_rocket. La fusée a le même pan et tilt que la caméra dans sa création. Skills 1... 3 sont employés comme variables simples qui donnent la vitesse de la fusée.

while (my.skill20 < 500)
{
temp.x = 40; // angle de scan horizontal
temp.y = 60; // angle de scan vertical
temp.z = 500; // gamme de scan
if ((my.skill10 == 0) && (vec_dist (my.x, player.x) > 100))
{
scan_entity (my.x, temp);
}
my.skill20 += 1
* time;
ent_move (my.skill1,
nullvector);
temp.x
= my.x - 6 * cos(my.pan);
temp.y = my.y - 6 * sin(my.pan);
temp.z = my.z;
effect
(particle_trail, 10, temp, normal);
wait (1);
}
remove_rocket();
}

Tant que la fusée continue à exister (skill20 < 500), nous mettons ses angles de balayage et étendue : 40 degrés horizontalement, 60 degrés verticalement et une étendue de 500 quants. Si la distance entre le joueur et la fusée est de plus de 100 quants la fusée commencera à parcourir l' environnement, cherchant des entités avec leur drapeau mis à enable_scan. La fusée continue à se déplacer tout ce temps et nous créons une traînée de fumée longue de 6 quants derrière l' origine du modèle de la fusée.

function
particle_trail()
{
temp.x = random(2) - 1;
temp.y = random(2) - 1;
temp.z = random(2) - 1;
vec_rotate (temp, my.pan);
vec_normalize (temp, 3);
vec_add (my.vel_x, temp);
my.alpha = 40 + random(30);
my.bmap = ptrail_map;
my.size = random(2) + 5;
my.flare = on;
my.bright = on;
// my.beam = on;
my.move = on;
my.lifespan = 20;
my.function = fade_trail;
}

function
fade_trail()
{
my.alpha -= 10 * time;
if (my.alpha < 0) {my.lifespan = 0;}
}

C' est une fonction typique de particules, avec des vitesses aléatoires dans toutes les directions, transparence alpha et taille variables et une fonction simple qui efface les particules plus vieilles. Essayez d' employer my.beam = on si vous possédez A5 pro ou A6 pro - il rendra la traînée de la fumée plus intéressante.

Voyons ce qui arrive quand la fusée frappe quelque chose :

function remove_rocket()
{
wait (1);
my.event = null;
ent_playsound (my, destroyed_wav, 1000); // joue le son d' explosion
my.invisible = on; // cahce la fusée, tien le son ent_playsound
sleep (1.5); // attends 1.5 seconds
ent_remove(me); // maintenant enlève la fusée

elle regarde si son événement est mis à nul donc la fusée ne peut pas réagir à d' autres événements et ensuite elle joue un son d' explosion. Nous cachons la fusée pendant 1.5 secondes parce que nous voulons que le son d' explosion continue à jouer et ensuite nous enlevons la fusée.

Il est temps de s' occuper de nos ennemis!

action
enemy
{
my.enable_scan = on;
my.event = enemy_event;
}

Tous ces types doivent s' assurer qu' ils ont leur flag enable_scan = on. Aussitôt que la fusée parcourt un d' entre eux, la fonction enemy_event commencera à courir :

function
enemy_event()
{
if (event_type == event_scan)
{
you.skill10 = 1; // stop le scan
while (you != null) // aussi longtemps que la fusée existe
{
vec_set (temp.x, my.x);
vec_sub (temp.x, you.x);
vec_to_angle (you.pan, temp.x);
wait (1);
}
}
}

You.skill10 = 1 stopperas le scan de la fusée pour préserver de précieuses ressources CPU; jetez un coup d' oeil dans la fonction shoot_rocket si vous ne me croyez pas. Tant que la fusée existe (you! = null) la fusée tournera vers cet ennemi. Mes ennemis sont statiques, mais même si les cibles se déplaçaient elles n' auraient aucune chance si la vitesse de la fusée est plus grande que leur propre vitesse de mouvement.


Untitled

Mouvement RPG

Il arrive un moment dans la vie d'un homme où il doit montrer qu'il est un vrai homme. Et alors il saisit sa souris et commence à massacrer hordes de monstres après hordes de monstres, désireux de montrer que lui n'est pas simplement un rêveur, mais un poète, un joueur de guitare et... oh, ouais ... un guerrier puissant.

J'ai voulu créer un projet autonome, de cette façon vous pourrez y ajouter un maximum de choses plus facilement. Qu'allons-nous obtenir? Un joueur qui se déplace utilisant la gravité, jouant avec ses séquences d'animation marche ("walk") et attente ("stand") et se déplaçant vers la cible indiqué en cliquant sur la carte avec le bouton gauche de la souris. Vous pouvez choisir n'importe quel angle et position pour la caméra car cela n'a aucune importance; en fait, j'ai décidé d'utiliser une caméra mobile et libre de ces mouvements.

function main()
{
level_load (rpg1_wmb);
mouse_map
= pointer_pcx;
mouse_mode = 2;
camera.x = player.x;
camera.y = player.y;
camera.z = 200;
camera.pan = 90; // initial
pan angle
camera.tilt = -60;
while (1)
{
mouse_pos.x = pointer.x;
mouse_pos.y
= pointer.y;
if ((mouse_pos.x < 2) &&
(camera.x > -2500)) {camera.x -= 10 * time;}
if
((mouse_pos.x > screen_size.x - 2) && (camera.x < 2500)) {camera.x
+= 10 * time;}
if ((mouse_pos.y > screen_size.y
- 2) && (camera.y < 2500)) {camera.y -= 10 * time;}
if ((mouse_pos.y < 2) && (camera.y > -2500)) {camera.y += 10 * time;}
wait (1);
}
}

La fonction principale (main) charge le niveau, met pointer_pcx comme pointeur pour la souris, montre le pointeur puis met les valeurs initiales x, y, z, pan et tilt pour la caméra. Vous pouvez jouer avec ces valeurs initiales jusqu'à ce que votre jeu soit superbe (truc: choisissez une grande valeur de camera.z pour masquer ce qui est horrible à voir sur les modèles)

Le code à l'intérieur de la boucle met à jour la position de la souris à chaque cycle et crée une caméra libre de ses mouvements. Cette caméra se déplacera de -2500 à 2500 quants en x et y. IJ'ai obtenu ces valeurs dans Wed comme frontières de niveau mais vous pouvez utilser n'importe quelles autres valeurs ici en fonction de la taille de votre niveau.

Voyons la première image : si nous déplaçons la souris sur le côté gauche de l'écran (mouse_pos.x < 2) et si camera.x > -2500 (frontière gauche en x), nous soustrayons 10 * time de camera.x, la déplaçant sur la gauche. Les choses fonctionnent de la même façon lorsque nous déplaçons la souris sur le côté droit de l'écran, sur le bord haut ou sur le bord bas.

Il est temps de jeter un oeil à l'action qui contrôle notre joueur:

entity* destination;

action warlock
{
player = me;
while (1)
{
my.skill1 = 5 * time;
my.skill2 = 0;
vec_set (temp, my.x);
temp.z -= 3000;
trace_mode =
ignore_you + ignore_passable + use_box;
my.skill3
= -trace (my.x, temp);

Notez que j'ai défini un pointeur nommé "destination"; chaque fois que le joueur cliquera un point sur la carte nous assignerons "destination" à l'entité cible (le plot rouge qui apparaît près du pointeur de la souris). Le joueur se déplace en utilisant ses skills1 (qui donne la vitesse) et skill3 (utilisé pour la gravité).

      if (destination != null)
{
vec_set (temp.x, destination.x);
vec_sub (temp.x, my.x);
vec_to_angle (my.pan, temp);
my.tilt = 0; // don't bow :)
if (vec_dist (my.x, destination.x)
> 40)
{
move_mode = ignore_passable;
ent_move (my.skill1, nullvector); // moves using skill1..3
ent_cycle("walk", my.skill46);
my.skill46 += 10 * time; // "walk" animation speed
my.skill46 %= 100; // loop animation
}
else
{
destination.invisible = on;
ent_cycle("idle", my.skill46); // play "stand" frames animation
my.skill46 += 2 * time;
// "stand" animation speed
my.skill46 %= 100; // loop animation
}
}
wait
(1);
}
}

Si le pointeur nommé "destination" n'est pas null le joueur se tourne vers "destination" (le plot rouge); cette rotation changeant également son angle tilt, la ligne suivante remet sa valeur à zéro. Si la distance entre le joueur et e plot rouge est à plus de 40 quants, le joueur se déplacera ignorant tous les objets passables et il jouera son animation de marche ("walk") dans une boucle. Si la distance entre le joueur et "destination" est inférieur ou égal à 40 quants ("else") la partie rouge devient invisible; le modèle joueur jouera son animation d'attente ("stand") dans une boucle jusqu'à ce que le joueur presse de nouveau le bouton gauche de la souris.

on_mouse_left = set_target;

Chaque fois que nous pressons le bouton gauche de la souris,la fonction set_target est exécutée. Imaginons un court instant que quelqu'un fasse une image de vous pendant que vous jouez avec cette démo depuis l'intérieur du niveau.

Je ne savais pas que vous étiez si jeune!

La fine ligne bleue est la surface de votre écran. Vous avez cliqué le bouton gauche de la souris quelque part sur l'écran et maintenant vous espérez que votre warlock se déplace vers cette position dans un monde 3D! Comment cela peut-il fonctionner? Revenons sur notre fonction set_target.

function
set_target()
{
var pos1;
var pos2;
pos1.x = mouse_pos.x;
pos1.y = mouse_pos.y;
pos1.z = 0;
vec_for_screen (pos1, camera);
pos2.x = mouse_pos.x;
pos2.y = mouse_pos.y;
pos2.z = 20000;
// use a big value here
vec_for_screen (pos2, camera);
trace
(pos1, pos2); // now "target" holds the coordinates of the hit point
destination = ent_create (destination_mdl, target, show_target);
}

J'ai défini 2 variables locales qui sont mises de cette façon:

- pos1 stockera la position de la souris en tant que coordonnées du monde 3D qui est placé tout près de votre écran;

- pos2 stockera la position de la souris en tant que coordonnées du monde 3D qui est placé 20,000 quants devant les yeux du joueur.

Que fait vec_for_screen? Vous lui donnez la position x, y d'un monde 2 D, une profondeur et il retourne une position d'un monde 3D. Notre fonction trace entre pos1 et pos2 afin d'obtenir les coordonnées "cible". ("target"). Target est un vecteur prédéfini qui est mis automatiquementaux coordonnées monde 3D ou trace (pos1, pos2) touche le sol, comme indiqué par la flèche verte sur l'image ci-dessus.

La dernière ligne de la fonction crée l'entité "destination" à la position donnéepar "target" et lui attache la fonction show_target():

function
show_target()
{
my.passable = on;
while (mouse_left
== 1) {wait (1);}
while (mouse_left == 0) {wait (1);}
ent_remove (me);
}

La petite fonction fait l'entité cible (le petit plot rouge) passable puis elle attend que le joueur relache le bouton gauche de la souris. Dés que le joueur presse de nouveau le bouton gauche (une nouvelle cible est définie) l'ancienne entité cible est enlevée.

Vous pouvez (et vous devez) combiner le code de cet article avec le code du combat à l'épée de AUM12 pour obtenir un bon point de départ pour votre jeu.


Untitled

Mur cassable

Savez-vousque vous pouvez utiliser des entités wmb pour de nombreuses choses comprenant des géométries de niveaux destructibles? Cet article vous montre comment faire un trou dans un mur en utilisant votre lance roquettes.

J'ai pris le lance roquettes que j'aiconstruit dans Aum24 ("Roquettet camera") comme arme, mais vous pouvez utilser n'importe quelle autre arme comme entité pour tirer incluant le template rocket launcher (si vous y faite une légère modification). Lisez l'article d'AUM24 pour en savoir plus sur mon lance roquette; j'ai fait quelques petits changements à son code, mais la première ligne de la fonction ci-dessous (en rouge) est vraiment la seule chose importante:

function move_rocket()
{
my.skill1 = 1357;
wait (1);
my.enable_entity = on;
my.enable_block = on;
my.passable = on;
my.event = remove_rocket;
my.pan = camera.pan;
my.tilt = camera.tilt;
rocket_speed.y = 0;
rocket_speed.z = 0;
while (my != null)
{
rocket_speed.x
= 100;
rocket_speed *=
time;
ent_move (rocket_speed,
nullvector);
if (vec_dist (camera.x,
my.x) > 30)
{
my.passable = off;
}
wait (1);
}
}

Chaque entité (roquette) tirée par le lance roquette doit avoir son skill1 de mis à 1357 (bien entendu vous pouvez utiliser une toute autre valeur ici). La raison de ceci est fort simple : l'entité wmb qu i va se comporter comme morceau de mur va vérifier si elle est touchée par une roquette ou no en contrôlant son skill1. Vous ne devez pas pouvoir traverser un mur épais en le cognant (collision avec le modèle joueur) ou en tirant dedans avec une arme de petit calibre ou tout simplementen cognant le mur avec votre épée; seule une roquette doit pouvoir le faire.

Regardons comment fonctionne ce mur destructible. Tout d'abord vous créez un mur avec un énorme trou dedans. J'ai cré un trou simple en soustrayant un cube tourné dans le mur mais je vous encourage à faire un trou plus réaliste, avec des formes irrégulières.

Maintenant allez-y, créez plusieurs entités wmb qui peuvent être utilisées comme morceau cassés du mur. Pour cet exemple j'ai utilisé une simple "brique" et j'ai répété l'opération 3 fois, mais des entités différentes seraient beaucoup mieux!

Remplissez le trou dans le mur avec plusieurs entités; alignez-les avec précaution (leur textures doivent être parfaitement alignées) mais aidez le joueur a trouver le point faible de ce mur en montrant quelques fissures ou un petit trou par exemple etc.

Attachez action wall_bricks à chaque entité remplissant le trou:

action wall_bricks
{
my.enable_impact = on;
my.enable_entity = on;
my.event = destroy_me;
}

Vous pouvez voir que l'entité est sensible à l'impact d'autres entités, aussi dés qu'elle est touchée, son évènement destroy_me est exécuté:

function
destroy_me()
{
if (you != null) // hit by an entity?
{
if (you.skill1
== 1357) // hit by a rocket?
{
ent_playsound (my, bricks_wav, 500); // play a sound
sleep (0.5);
ent_remove (me);
}
}
}

Si la brique est touchée par une autre entité (you != null) et que cette entité est une roquette (son skill1 = 1357) nous jouons un son au point d'impact, nous attendons une 1/2 seconde puis nous enlevons l'entité. Tirez cette zone du mur plusieurs fois et vous créerez un trou assez grand pour laisser passer le joueur (si c'est ce que vous avez prévu).

Comme je l'ai dit plus haut, vous pouvez utiliser d'autres armes tirant des entités à la condition de mettre une des variables d'entités du projectile à une certaine valeur. La valeur de cette variable doit être vérifiée dans la fonction d'évènement de l'entité brique.


Untitled

Music

Les bonnes nouvelles au sujet de la musique "régulière" sont que plusieurs de ses composantes se répètent plusieurs fois pendant la chanson, gaspillent l'espace et augmentent la taille du jeu. Plus que cela, vous pourriez vouloir faire quelques boucles de musique de qualité et vouloir créer une grande chanson avec elles. Si vous recherchiez le code pour réaliser au moins un de ces buts, cessez de cherhcer : vous avez découvert music.wdl.

Mon exemple utilise 4 boucles de son wav (pour un total de 1Mb) et il est employé pour créer une chanson qui aurait eu besoin normalement de 3Mb. imaginez ce que vous pouvez créer avec 8 boucles (2Mb) et de combien d'espace vous pouvez sauver ! L'idée est (de nouveau) simple : nous créons un petit fichier dans n'importe quel éditeur de texte et nous y écrivons quelques nombres; voici le contenu de mon fichier de music.dat (vous pouvez employer n'importe quel autre nom) :

1 1 4 4 3 3 1 2 2 3 3

Je vous ai dit que j'emploie seulement 4 boucles ; les données à l'intérieur du fichier indiquent au moteur de jouer "1" deux fois (1 1), "4" deux fois (4 4), "3" deux fois (3 3), "1" une fois, "2" deux fois, "3" deux fois. Dès qu'une des boucles s'arrêtera, celle qui la suit dans music.dat commencera à jouer.

Voyons le merveilleux morceau de code qui change le moteur d'Acknex en compteur séquentiel :

var number_of_pieces = 11; // jouera 11 morceaux de musique (le nombre de chiffres dans music.dat)
var file_handle;
var music_handle = 0;
var piece_number;

function start_music()
{
file_handle = file_open_read("music.dat"); // ouvre le fichier
while (number_of_pieces >
0)
{
piece_number =
file_var_read (file_handle);
if (piece_number
== 1)
{
music_handle = snd_play (luke1_snd, 40, 0);
}
if (piece_number
== 2)
{
music_handle = snd_play (luke2_snd, 40, 0);
}
if (piece_number
== 3)
{
music_handle = snd_play (luke3_snd, 40, 0);
}
if (piece_number
== 4)
{
music_handle = snd_play (luke4_snd, 40, 0);
}
number_of_pieces
-= 1;
while (snd_playing
(music_handle) != 0) {wait (1);}
wait (1);
}
file_close (file_handle);
}

Est-ce simple? D'abord nous mettons le nombre de morceaux de musique à 11 (c'est la quantité de chiffres dans le fichier music.dat), puis nous ouvrons le fichier et nous lisons le premier chiffre. Si piece_number = 1 nous jouons la première boucle, si piece_number = 2 nous jouons la deuxième boucle etc. Cette ligne:

while (snd_playing (music_handle) != 0) {wait (1);}

s'assure qu'une seule boucle soit jouée en même temps. Les boucles sont jouées dans l'ordre indiqué par les chiffres dans music.dat et lorsque number_of_pieces = 0 la chanson s'arrête et le fichier est fermé.

Mais que dois-je faire si je veux utiliser disons ...... 8 boucles?
Définissez les fichiers wav, ajouez if (piece_number == 5..8) etc dans la fonction start_music et utilisez des chiffres de 1 à 8 dans le fichier music.dat. Si vous voulez, vous pouvez générer un piece_number aléatoire et torturer vos voisins avec la musique générée de cette façon.


Untitled

L'ombre des modèles en douceur

Vous connaissez le problème: lorsque votre modèle se déplace de la lumière vers l'obscurité, son ambiance change subitement. La même chose se produit lorsque le modèle se déplace d'un endroit sombre vers un endroit clair du niveau. Ne serait-ce pas sympa d'avoir un mdèle qui change sa lumière ambiante en fonction de la quantité de lumière dans le niveau ? L'idée de ce code est très simple : nous mettons unlit = on pour notre modèle afin de le rendre insensible à toute source de lumière, nous analysons tex_light sur le sol, sous ses pieds et nous mettons l'ambiance du modèle en fonction de la valeur de tex_light.

J'ai cherché à vous montrer la différence entre un ombrage normal et un ombrage en douceur, aussi j'ai placé 2 gardes dans le niveau de test. Un des gardes utilise l'action standard patrol_path action et l'autre garde utilise la nouvelle action patrol_path_ok, qui est une version modifiée de patrol_path:

action patrol_path_ok
{
actor_init();
smooth_shadow(); // le seul
changement est ici

// attach next path
temp.pan = 360;
temp.tilt = 180;
temp.z = 1000;
result = scan_path(my.x,temp);
if (result == 0) { my._MOVEMODE = 0; } // no path found

// find first waypoint
ent_waypoint(my._TARGET_X,1);

     while (my._MOVEMODE > 0)
{
// find direction
temp.x = MY._TARGET_X
- MY.X;
temp.y = MY._TARGET_Y
- MY.Y;
temp.z = 0;
result = vec_to_angle(my_angle,temp);

force = MY._FORCE;

// near target? Find next waypoint
// compare radius must exceed the turning cycle!
if (result < 25) { ent_nextpoint(my._TARGET_X); }

// turn and walk towards target
actor_turnto(my_angle.PAN);
actor_move();

          // Wait one tick,
then repeat
wait(1);
}
}

Vous pouvez voir que la ligne supplémentaire appelle notre fonction appelée smooth_shadow. Regardons ce qu'elle fait:

function
smooth_shadow()
{
my.unlit = on;
while (1)
{
vec_set(temp, my.x);
temp.z -= 1000;
trace_mode = ignore_me
+ ignore_passable + ignore_models + ignore_sprites + scan_texture;
trace (my.x, temp);
// get tex_light
my.ambient = tex_light
/ 2.5;
waitt(4); // trace
4 times a second
}
}

Cette fonction trace 1000 quants sous l'origine de l'acteur pour obtenir la valeur de tex_light - qui est la valeur ambiante de la brillance de la crte d'ombre. Tex_light peut avoir une valeur comprise entre 0 et 255, c'est pourquoi nous divisons cette valeur par 2.5 pour que la valeur obtenue, l'ambiance pour le modèle soit comprise entre 0 et 100. Si vous pensez que votre modèle est trop sombre, vous pouvez utilisez n'importe quelle valeur inférieure à 2.5

Pour voir mon niveau de test, copiez le répertoire \smooth dans votre répertoire Gamestudio, ouvrez smooth.wmp, compilez-le et exécutez-le. Vous devriez voir 2 gardes qui marchent sur le même chemin, je vous laisse deviner celui qui utilise l'ombrage en douceur.


Untitled

Pacmen

Je me souviens d'avoir joué mon premier Pacman sur un ordinateur Sinclair Spectrum. C'était une autre époque, celle ou lorsque vous programmiez vous deviez vous assurer que votre programme faisait moins de 48 Kb (on parle bien de kilo, pas de méga) sinon le programme n etenait pas dans la mémoire. Pacmen utilise un 'petit' peu plus de mémoire - c'est un Pacmen (il y a plusieurs hommes) non un Pacman.

La fonction Main ne présente rien de spécial regardons donc l'action qui gère notre héros:

action
pacman
{
player = me;
my.enable_entity = on;
my.enable_push = on;
my.enable_impact = on;
my.event = kill_me; // ici
il se passe une collision avec des ennemis
camera.diameter = 0; // la caméra peut passer à travers les murs
camera.tilt = -90; // regarde en bas
camera.z = my.z + 1000; //
1000 quants au dessus du joueur
pacman_speed.y = 0;
pacman_speed.z = 0;

Pacman est sensible aux autres entités, s'il cogne ou s'il est cogné par un ennemi, son évènement kill_me est déclenché. La caméra peut passer à travers les murs, regarder vers le bas, se déplacer avec le joueur et et placée à 1000 quants au dessus du joueur.

Pacman et les ennemis utilisent des antennes invisibles (capteurs) pour détecter leur environnement comme des créatures intelligentes. J'ai déjà utilisé cette technique dans ma première démonstration AI dans AUM. Dans cette image vous pouvez voir les antennes roses (exécutez le jeu avec 'pacmen -d markers' si vous voulez les voir aussi) :

Nous utilisons 4 capteurs pour pacman et 4 capteurs pour l'ennemi, comme ci-dessous :

Voyons ce que nous pouvons faire acec ces capteurs; Les explications qui suivent concernent le capteur gauche (le code pour les autres capteurs est le même):

while (player != null)
{
motion_type = 0;

Aussi longtemps que le joueur est vivant (player != null) nous remettons à zéro motion_type (que nous verrons en détail par la suite). Nous utilisons des coordonnées circulaires pour tous les capteurs car le joueur et l'ennemi sont sont des modèles circulaires aussi.

          player_left.x =
my.x;
player_left.y =
my.y + width;
player_left.z =
my.z;
result = content(player_left);
if (result == content_solid)
{motion_type += 1;}
...............................................................................................

Nous vérifions le contenu du capteur player_left et s'il est dans un solide, nous ajoutons 1 à motion_type. Si player_right == content_solid -> motion_type += 2, player_down == content_solid -> motion_type += 4, player_up == content_solid -> motion_type += 8. J'ai choisi ces valeurs binaires (1, 2, 4, 8) pour être sur qu'ils représentent une combinaison unique de nombres. Si les capteurs droit et haut sont dans du solide, motion_type vaudra 2 + 8 = 10 et c'est la seule combinaison qui donne 10 comme résultat. Regardez la table suivante pour avoir toutes les combinaisons :

Il est clair que dans l'absolu motion_type = 15 ne peut jamais arriver, mais j'ai souhaité vous montrer toutes les possibilités.

Bien, nous avons nos 4 capteurs, nous contrôlons leur contenu et en fonction du résultat, nous permettons au pacman ou à l'ennemi de se déplacer dans la direction de leur choix seulemento si la route est libre.

   if
(motion_type == 0)
{
if (key_cuu == 1) {my.pan = 0;}
if (key_cud ==
1) {my.pan = 180;}
if (key_cur ==
1) {my.pan = 270;}
if (key_cul ==
1) {my.pan = 90;}
}

if (motion_type == 1)
{
if (key_cuu == 1) {my.pan = 0;}
if (key_cud ==
1) {my.pan = 180;}
if (key_cur ==
1) {my.pan = 270;}
}

if (motion_type == 2)
{
if (key_cuu == 1) {my.pan = 0;}
if (key_cud ==
1) {my.pan = 180;}
if (key_cul ==
1) {my.pan = 90;}
}

if (motion_type == 3)
{
if (key_cuu == 1) {my.pan = 0;}
if (key_cud ==
1) {my.pan = 180;}
}

if (motion_type == 4)
{
if (key_cuu == 1) {my.pan = 0;}
if (key_cur ==
1) {my.pan = 270;}
if (key_cul ==
1) {my.pan = 90;}
}

if (motion_type == 5)
{
if (key_cuu == 1) {my.pan = 0;}
if (key_cur ==
1) {my.pan = 270;}
}

if (motion_type == 6)
{
if (key_cuu == 1) {my.pan = 0;}
if (key_cul ==
1) {my.pan = 90;}
}

if (motion_type == 7)
{
if (key_cuu == 1) {my.pan = 0;}
}

if (motion_type == 8)
{
if (key_cud == 1) {my.pan = 180;}
if (key_cur ==
1) {my.pan = 270;}
if (key_cul ==
1) {my.pan = 90;}
}

if (motion_type == 9)
{
if (key_cud == 1) {my.pan = 180;}
if (key_cur ==
1) {my.pan = 270;}
}

if (motion_type == 10)
{
if (key_cud == 1) {my.pan = 180;}
if (key_cul ==
1) {my.pan = 90;}
}

if (motion_type == 11)
{
if (key_cud == 1) {my.pan = 180;}
}

if (motion_type == 12)
{
if (key_cur == 1) {my.pan = 270;}
if (key_cul ==
1) {my.pan = 90;}
}

if (motion_type == 13)
{
if (key_cur == 1) {my.pan = 270;}
}

if (motion_type == 14)
{
if (key_cul == 1) {my.pan = 90;}
}

J'espère que la ligne suivante ne vous donnera pas mal à la tête mais pas moyen de faire autrement:

if ((my.pan == 0 && motion_type < 8) || ((my.pan == 90) && (motion_type % 2 != 1)) ||
(my.pan == 180 && motion_type != 4 && motion_type != 5 && motion_type != 6 && motion_type != 7 &&
motion_type != 12 && motion_type != 13 && motion_type != 14) || (my.pan == 270 && motion_type != 2 &&
motion_type != 3 && motion_type != 6 && motion_type != 7 && motion_type != 10 && motion_type != 11 &&
motion_type != 14))
{
ent_move (pacman_speed, nullvector);
}

Bien, cette ligne est en fait très simple : elle arrête le mouvement lorsque les capteurs sont dans du solide car nous ne voulons pas que Pacman se colle le nez dans le mur, n'est-ce pas? Si Pacman pointe son nez dans la bonne direction (la route est libre) ent_move est exécuté et Pacman se déplace. J'ai utilisé différentes techniques barbares dans cette instruction : motion_type % 2 != 1 remplace motion_type != 1 && motion_type != 3 .... && motion_type != 13 (tous les nombres impairs entre 1 et 13).

L'action attachée à l'ennemi est similaire; la seule grosse différence est que nous générons un nombre aléatoire qui determinera la direction que prendra l'ennemi.

Si vous regardez le premier exemple, l'ennemi vient de la droite vers la gauche,les directions possibles (libres) sont haut et droite. L'ennemi venant de la droite ce ne serait pas très intelligent de le faire rebondir dans le mur et repartir à droite, auusi cette posibilité a été neutralisée dans le code de l'ennemi. Dans ce cas l'ennemi montera. Dans le second exemple, l'ennemi vient de monter et il ne redescendra donc pas mais il ira à gauche parce que j'ai fixé cette limite dans son code pour cette situation.

L'ennemi doit pouvoir passer sur les points, aussi j'ai utilisé cette ligne dans son action (les points sont passables):

move_mode = ignore_passents;

Et ici l'action attachée à chaque point:

action
dot
{
my.push = -1;
my.z = player.z;
my.passable = on;
while (my != null &&
player != null)
{
if (vec_dist (my.x, player.x)
< 30)
{
play_sound (eatdot_snd, 100);
ent_remove (my);
score += 10;
}
wait (1);
}
}

Les points sont alignés sur le z du joueur. Ainsi je n'ai pas besoin de m'assurer, lorsque je les place dans WED, s'ils sont à la bonne hauter. Si le jour arrive à moins de 30 quants, le point sera mangé et le score augmenté de 10.

La fonction qui tue le joueur est simple. La taille du joueur est réduite progressivement puis le joueur est enlevé.

function
kill_me()
{
exclusive_global;
play_sound gotme_snd, 100;
while (my.scale_x > 0.1)
{
my.scale_x -= 0.1 * time;
my.scale_y
= my.scale_x;
my.pan += 1 / my.scale_x;
wait
(1);
}
waitt (2);
ent_remove (me);
}

Si vous voulez passer au niveau suivant, contrôlez le score dans une boucle; score = 600 vous amène au prochain niveau si vous avez 60 points (dots) dans le niveau.

Que diriez-vous d'une vraie intelligence artificielle ? (IA)? Cet ennemi ne nous fait rien mais il se déplace correctement dans le niveau! Cette partie est facile et elle seprésente comme un exercice pour vous. Les Pacmen ont 4 ennemis différents - J'en ai codé un bête. Ne vous inquiétez pas, je vous explique comment coder l'ennemi mangeur de pacman correctement ici:

L'ennemi doit juste s'assurer que ses coordonnées x et y sont égales aux coordonnées x et y du joueur (ce qui signifiera que l'ennemi a attrapé le joueur). Lorsque l'ennemi atteint le point 1 il ne peut pas s'approcher du joueur - il doit se déplacer vers la droite en direction du point 2. Lorsqu'il y arrive, son motion_type a changé; l'ennemi saitqu'en se déplaçant vers le haut la distance entre lui et le joueur va diminuer (sur l'y), aussi il avance vers le haut en direction du point 3.(nous n'avons pas utilisé de valeur aléatoire ici) En décidant d'avancer vers le point 4 (là encore, pas de valeur aléatoire), l'ennemi sait que la distance entre lui et le joueur (sa valeur x) sera réduite. Le 5 ème point puet être une décision aléatoire si vous le souhaitez dépendant de la posiition du joueur.

Et vous pensiez que créer un clone de Pacman était facile!


Untitled

Panneau défilant

Quelques fois vous aimeriez faire défiler un texte imprimé sur un panneau mais vous ne pouvez pas le faire simplement enchangeant ses paramètres pos_x et pos_y parce que vous ne voulez pas d'un grand panneau prenant une grande partie de l'écran. Vous avez besoin de quelques chose de petit, petit comme une fenêtre dans le panneau OK, en voci un:

Définissons tout d'abord notre panneau:

panel
spanel_pan
{
bmap = spanel_pcx;
pos_x = 0;
pos_y = 0;
layer = 30;
button 217, 25, arrow1_pcx, arrow1_pcx, arrow1_pcx, scroll_up, null, null;
button 217, 45, arrow2_pcx, arrow2_pcx,
arrow2_pcx, scroll_down, null, null;
window = 10,
20, 200, 50, text_pcx, text_pos.x, text_pos.y;
flags = overlay, d3d, refresh;
}

C'est une définition de panneau classique; nous avons 2 flêches en forme de bouton que nous utiliserons pour faire monter ou descendre le texte et la définition d'une fenêtre. Voyons cela de plus près :

window = 10, 20, 200, 50, text_pcx, text_pos.x, text_pos.y;

Cette instruction crée une fenêtre (un trou) dans le panneau. La fenêtre est placée à (10, 20) et fait 200 pixels de long par 50 pixels de haut. Le texte qui apparaîtra dans cette fenêtre se trouve sur text_pcx; ses coordonnées sont données par text_pos.x et text_pos.y

function scrolling_text()
{
mouse_toggle(); // montre le curseur
text_pos.x = 10;
text_pos.y = 20;
while (1)
{
if (key_h == 1) //
pressez H pour montrer le panneau
{
while (key_h == 1) {wait
(1);}
spanel_pan.visible
= (spanel_pan.visible == off);
}
wait (1);
}
}

La fonction scrolling_text est appelée depuis la fonction main; elle montre le curseur en utilisant une fonction prédéfinie (mouse_toggle) et met la position de départ du texte qui apparaîtra dans la fenêtre. La boucle while montre ou cache le panneau lors de l'appui sur la touche H en utilisant une simple ligne de code. Essayez de comprendre comment fonctionne cette ligne:

spanel_pan.visible = (spanel_pan.visible == off);

note du traducteur, vous auriez pu écrire plus simplement ceci :

spanel_pan.visible ^= 1;

ce qui produit exactement la même chose

Les dernières fonctions sont ::

function
scroll_up()
{
while (mouse_left
== 1)
{
if (text_pos.y <
350) // hauteur du texte moins la hauteur de la fenêtre
{
text_pos.y += 3 * time;
}
wait (1);
}
}

function
scroll_down()
{
while (mouse_left
== 1)
{
if (text_pos.y >
0)
{
text_pos.y
-= 3 * time;
}
wait (1);
}
}

Ces fonctions s'exécutent lorsqu'on appuie sur une des flêches faisant défiler le texte. Si vous pressez une des flêches le texte défilera dans la direction correspondante aussi longtemps que le bouton gauche de la souris reste pressé. Si le texte est arrivé au bout de ses limites, le défilement cessera. C'est sympa mais n'oubliez pas que les panneaux utilisent la mémoire vidéo; essayez de trouver le bon compromis largeur / hauteur pour chaque panneau.


Untitled

Pong

Tout à commencé avec Pong! Ok, il y avait bien quelques jeux d'aventure en texte avant pong mais c'est un des tous premiers (comme j'aime l'appeler) jeu "réel". J'ai cherché à le recréer car je suis un type sentimental (sniff) mais aussi pour vous montrer comment utiliser les graphiques 2D, la détection des collisions en 2D et plein d'autres choses!

Nous commençons avec différentes définitions de panneaux et cette figure les montre tous en action:

panel main_pan
// the main panel
{
bmap = main_pcx;
layer = 10;
pos_x = 0;
pos_y = 0;
digits = 330, 40, 2, digital_font,
1, player_score;
digits = 395, 40, 2, digital_font, 1, computer_score;
flags = refresh, d3d, visible;
}

panel
player_pan
{
bmap = player_pcx;
layer = 11;
pos_x = 60;
pos_y = 250;
flags = overlay, refresh,
d3d, visible;
}

panel
computer_pan
{
bmap = computer_pcx;
layer = 11;
pos_x = 715;
pos_y = 250;
flags = overlay, refresh,
d3d, visible;
}

panel
ball_pan
{
bmap = ball_pcx;
layer = 11;
pos_x = 504;
pos_y = 344;
flags = overlay, refresh,
d3d, visible;
}

Cette fois j'ai utilisé une grande fonction principale (main); la bonne nouvelle est que cette grande fonction est la seule fonction dans ce jeu et nous la décrypterons ligne par ligne:

function
main()
{
fps_max = 80; // limit the frame rate to keep the game playable on faster computers
level_load (dummy_wmb);
wait (2);
speed_x = 3 - 6 * (sys_seconds
% 2);
speed_y = 3 - random(6);

Nous limitons le taux d'affichage à 80 fps parce que le jeu doit pouvoir être joué à la même vitesse sur un P5 à 5GHz. Nous chargeons un niveau bidon (dummy), nous attendons que le niveau soit chargé puis nous générons 2 nombres aléatoires:
1) La direction de la balle au démarrage du jeu (vers le joueur ou vers l'ordinateur ) = -3 or 3, dépendant de sys_seconds (pair ou impair).
2) la vitesse verticale de la balle = -3...3.

     while ((computer_score != 15) && (player_score
!= 15))
{
if (key_cuu ==
1) {player_pan.pos_y -= 8;}
if (key_cud ==
1) {player_pan.pos_y += 8;}
player_pan.pos_y
= min(470, player_pan.pos_y);
player_pan.pos_y
= max(35, player_pan.pos_y);

Le jeu continue de s'exécuter aussi longtemps que le score (du joueur ou de l'ordinateur) est inférieur à 15. Si nous pressons la touche de direction "haut" (cuu), player_pan est déplacé vers le haut car son Y devient de plus en plus petit (jusqu'à atteindre 35). Si la touche de direction "bas" est pressée, player_pan est descendu (son Y est incrémenté jusqu'à 470).

ball_pan.pos_x += speed_x;
ball_pan.pos_y += speed_y;

La balle changera sa position à cause de speed_x et speed_y. Ici l'image qui montre le mouvement de la balle:

Vous savez que la balle change ses positions x et y en même temps, aussi son mouvement ressemble à quelque chose comme cela:

          if (ball_pan.pos_y
> 555)
{
speed_y = -3 - random(3);
snd_play (beep1_wav, 50, 0);
}
if (ball_pan.pos_y
< 32)
{
speed_y = 3 + random(3);
snd_play (beep1_wav, 50, 0);
}

Si la balle passe en dessous de 32 pixels ou au dessus de 555 pixels en y, elle rebondit en arrière en changeant sa vitesse et joue le son beep1_wav.

          if (ball_pan.pos_x
> 740) // player scores!
{
snd_play (goal_wav, 70, 0);
speed_x = -3 - random(3);
player_score += 1;
ball_pan.pos_x = 740;
}

Si ball_pan.pos_x > 740, le joueur a marqué un point. Nous jouons le son goal_wav, nous changeons la vitesse de la balle pour la faire rebondir vers le joueur, nous augmentons le score du joueur puis nous déplaçons la balle de 740 pixels en x. Voyons maintenant pourquoi ceci?

Bien, la balle continuerait vers 741, 742, 743, ... pixels avant de rebondir et enregistrerait plusieurs buts pour un simple tir. La solution est de déplacer la balle en dehors de cette zone dangereuse et nous changeons la vitesse de la balle de positifve en négative. De cette façon le code enregistrera un simple but pour un simple tir.

          if (ball_pan.pos_x
< 40) // computer scores!
{
snd_play (goal_wav, 70, 0);
speed_x = 3 + random(3);
computer_score += 1;
ball_pan.pos_x = 40;
}

Si c'est l'ordinateur qui marque nous appliquons le même le scénario.

          if ((ball_pan.pos_y
> player_pan.pos_y - 12) && (ball_pan.pos_y < player_pan.pos_y +
96) && (ball_pan.pos_x > 60) && (ball_pan.pos_x < 72))
{
// the player has blocked the ball
snd_play (beep2_wav, 70, 0);
speed_x = 3 + random(3);
speed_y = 3 - random(3);
}

Si le joueur bloque la balle, nous jouons le son beep2_wav et nous faisons rebondir la balle dans l'autre sens.

Qui veut d'un adversaire ordinataire parfait? Je sais je n'en veux pas.

          if ((ball_pan.pos_y
> 100) && (ball_pan.pos_y < 490))
{
computer_pan.pos_y = ball_pan.pos_y - 42;
}

Nous voulons que l'ordinateur fasse des erreurs aussi nous limitonsses mouvement ssur l'axe des Y, au lieu de lui permettre d'aller de 35 à 470, comme le fait le joueur. De cette façon vous avez une chance de battre l'ordinateur. Notez que l'ordinateur utilise un déplacement de 42 pixels en y pour suivre la balle; de cette façon il peut toucher le centre de la balle avec le centre de sa raquette (s'il peut atteindre la balle bien entendu.

          if ((ball_pan.pos_y
> computer_pan.pos_y - 12) && (ball_pan.pos_y < computer_pan.pos_y
+ 96) && (ball_pan.pos_x > 703) && (ball_pan.pos_x < 715))
{
// the computer has blocked the ball
snd_play (beep2_wav, 70, 0);
speed_x = -3 - random(3);
speed_y = 3 - random(3);
}

Si l'ordinateur bloque la balle nous jouons un son et nous la faisons rebondir dans l'autre sens.

Ok, ce n'est pas le coin des débutants mais comment le système de détection des collisions fonctionne-t-il? Comment pouvez vous savoir si la balle a touché une raquette?
J'ai expliqué comment le système travaille pour l'ordinateur, j'utilise exactement la même méthode pour le joueur aussi vous ne devriez pas avoir de problème avec lui. La triste vérité est qu'il n'y a aucun vrai système de collision dans une application 2D, c'est pourquoi il doit être truqué. Employons cet exemple:

Pouvez-vous dire qu ela balle va toucher la raquette? Il semblerait que oui, n'est-ce pas? Tout d'abord, la hauteur de la balle est identique à la hauteur de la raquette. En fait je suis pratiquement sur que la balle touchera la raquette à l'une des positions de l'image ci-dessous:

Ok, nous devons nous assurer que le bas de la balle est placé au dessus du haut de la raquette ( (balle #1). Ne pas oublié que n'importe quel panneau à son origine dans le coin supérieur gauche, aussi le premier test doit être le suivant:

if (ball_pan.pos_y > computer_pan.pos_y - 12)

La balle a 12 x 12 pixels. Comment devons-nous écrire le test pour la balle #7? Si le haut de la balle est placé en dessous du bas de la raquette, n'est-ce pas?

if (ball_pan.pos_y < computer_pan.pos_y + 96)

C'est le bon code car la raquette a 96 pixels en y.

Cela solutionne la détection de colision en y; Regardons à présent l'axe des X.

Comment pouvons-nous être sur que la balle ne passe pas à travers de la raquette? Bien, nous devons nous assurer que le côté droit de la balle à touché le coté gauche de la raquette ou en d'autres mots:

if (ball_pan.pos_x > 703)

parce que la raquette de l'ordinateur est placée à 715 pixels en x et que la balle a 12 pixels. Puis nous nous assurons que la balle est bien dans l'axe de la raquette et non dans l'espace vide derrière:

if (ball_pan.pos_x < 715).

Si je ne voulais pas utiliser la dernière instruction, il faudrait que l'ordinateur ait une raquette très large comme sur l'image ci-dessous:

Si vous combinez les 4 instructions ci-dessus, vous avez votre système de détection pour le jeu.


Untitled

Démo de physique

A6 commercial et A6 pro inclusent un bon moteur de physique et cet article vous montre combien il est facile de s'en servir.

Démarrons avec la fonction principale (main):

function
main()
{
level_load (phdemo1_wmb);
camera.x = 0;
camera.y = -600;
camera.z = 40;
camera.pan = 90;
ball_pos.x = -95; // initial
value, ranges from -95 to 105
ball_pos.y = 32;
ball_pos.z = 220;
ent_create (arrow_wmb, ball_pos, move_arrow);
}

Nous chargeons le niveau et paramétrons la position et l'angle pan de la caméra afin qu'elle regarde le tableau:

Avant que j'oublie, j'ai utilisé une simplle bitmap orientée pour le score.

Nous paramétrons ball_pos, puis nous créonsla flèche verte (arrow_wmb). La flèche se déplace tout le temps à cause de sa fonction associée:

function move_arrow()
{
my.passable = on;
while (balls > 0)
{
while (my.x <
105)
{
my.x += 5 * time;
ball_pos.x = my.x;
wait (1);
}
while (my.x >
-95)
{
my.x -= 5 * time;
ball_pos.x = my.x;
wait (1);
}
wait (1);
}
}

La flèche est passable et elle continue de bouger tant que nous n'avons pas épuisé les 5 balles (arrivant par la droite); la flèche change sa coordonnée x de -95 à 105 en utilsant 2 simples boucles while. Notez au passage que ball_pos.x = arrow_wmb.x à chaque cycle.

Ok, nous avons une flèche verte qui se déplace de la gauche versla droite et retour. Voyons ce qui se passe lorsque nous pressons la barre espace:

entity* ball_ptr;

on_space = create_ball;

function
create_ball()
{
if ((balls < 1) || (ball_ptr != null)) {return;}
ball_ptr = ent_create(ball_mdl, ball_pos, init_sphere);
balls -= 1;
}

Nous avons défini un pointeur nommé ball_ptr. Nous allons créer 5 balles et ce pointeur sera associé à chaque balle au moment de sa création. Si nousanquons de balles ou si le pointeur de balles n'est pas nul (ce qui signifie que la balle rebondit encore dans notre niveau) la fonction retourne. Si le nombre de balles est plus grand que zéro, nous créons une balle à la position donnée par ball_pos, exécutant la fonction init_sphere. Cette fonction est la seule partie de la démo qui utilse de la physique; ne vous inquietez pas, je commenterai ligne par ligne:

function init_sphere()
{
phent_settype(my, ph_rigid, ph_sphere);
Autorise la physque pour cette balle; elle se comportera comme une sphère lors d'une collision

phent_setmass(my, 1, ph_sphere);
La boule fait un 1Kg (~2 lbs) elle se comportera comme une sphère lorsqu'elle sera en rotation

phent_setfriction(my, 30);
Friction = 30 (peut être 0...100)

phent_setelasticity(my, 30, 0);
Rebondit un peu (value = 30, peut être 0...100)

temp.x = 0;
temp.y = 0;
temp.z = -380;
ph_SetGravity(temp);
Met gravité pour la sphère (temp.z = -380 correspond au facteur de gravité de la terre, g = 9.81 m/s2)

while (my.z > -150) {wait (1);}
Attend jusqu'à ce que la balle approche du sol du niveau

sleep (3);
On lui laisse 3 secondes pour rebondir

phent_setelasticity(my, 0, 0);
Maintenant arrête le rebond

phent_setdamping(my, 0, 100);
Applique un grand facteur d'atténuation pour empêcher la boule de tourner trop

sleep (2);
Roule sur le sol pendant 2 secondes

phent_settype(my, 0, 0);
Maintenant arrête la balle; A6 commercial nous autorise de n'utiliser qu'un seul objet (ball) à la fois

ball_ptr = null;
Libère le pointeur car nous en avons besoin pour l'assigner à une autre balle
}

C'est le seul code qui fait que la balle saute et rebondit et trouve finalement son chemin dans l'un des paniers. Si vous n'avez ni la version commerciale, ni la version professionnelle jetez un oeil sur le film et voyez ce que l'on peut faire avec très peu de lignes de code. Il est pratiquement impossible ce comportement réaliste sans un moteur de physique.

Physics movie (divx 5.01)


Untitled

Planet Survivors

Vos posts, e-mails et messages privés sont une fois encore à l'origine de ce travail. J'ai décidé de vous montrer comment créer un jeu complet à partir de zéro. J'ai choisi de faire un jeu qui soit une combinaison d'espace et de logique parce que cela nous permettra d'apprendre plein de nouvelles choses (du moins je l'espère) et que nous ne voulons pas d'une année pour finir ce jeu. La plupart des graphiques sont créés par les artistes de mon équipe mais vous êtes les bienvenus pour participer à ce projet et si vous avez des idées intéressantes, des modèles ou autres. e jeu terminé sera disponible en freeware et vous serez aux crédits pour votre travail. Je donnerai plus de détails à ce sujet sur le forum de Conitec.

Ce mois-ci j'ai créé une grosse partie du code utilisé pour le menu, à l'exception du sysème de sauvegarde / chargement. Vous pouvez démarrer le jeu en cliquant sur le vaisseau 3D dans la partie gauche de l'écran, vous pouvez déplacer la souris au dessus du vaisseau pour mettre en route ses moteurs, vous pouvez accéder au panneau d'aide, vous pouvez quitter le jeu etc. Il est temps à présent de commenter le code:

panel main_pan
{
bmap = main_tga;
layer = 20;
pos_x = 0;
pos_y = 0;
button = 397, 475, exit2_tga, exit1_tga, exit2_tga, exit_game, null, mouse_over;
button = 559, 431, help2_tga, help1_tga, help2_tga, show_help, null, mouse_over;
button = 647, 329, load2_tga, load1_tga, load2_tga, load_game, null, mouse_over;
button = 681, 226, save2_tga, save1_tga, save2_tga, save_game, null, mouse_over;
flags = overlay, refresh, visible;
}

La définition utilisée pour le panneau principal est classique, ellecontient 4 boutons pour quitter le jeu, montrer le panneaud'aide, charger et sauvegarder le jeu.

panel
help_pan
{
bmap = help_tga;
layer = 20;
pos_x = 0;
pos_y = 601;
on_click = show_main;
flags = overlay, refresh, visible;
}

Le panneau d'aide est, comme vous pouvez le voir, en traveau ("under construction") parce que nous ne savons pas encore quoi y mettre. Il y a seulement 2 choses dont nous devons parler ici : nous avons une position initiale de pos_y = 601, qui maintient le panneau en dehors de l'écran pour l'instant et une fonction nommée show_main qui est exécutée à chaque fois que nous pressons le panneau d'aide.

function
main()
{
fps_max = 50;
level_load (intro_wmb);
wait (3);
time_smooth = 0.9;
mouse_spot.x = 31;
mouse_spot.y = 17;
camera.x
= 0;
camera.y = 0;
camera.z = 350;
camera.tilt = -90;
mouse_mode = 2;

La fonction main limite le taux d'affichage à 50 fps puis charge le niveau intro_wmb. C'est un simple cube texturé avec une texture noire; j'ai inclu le fichier planets.wad aussi assurez-vous de le copier dans le bon répertoire ( \wads). Nous attendons que le niveau soit chargé puis nous mettons time_smooth à une valeur qui est lamoyenne des 10 derniers cycles. J'ai utilisé un gros curseur aussi il est important de donner une autre valeur pour le point chaud (hot spot) de la souris (mouse_spot.x et mouse_spot.y). Le point chaud d'un pointeur de souris correspond au point qui sert à cliquer (la pointe de la flèche par exemple, pour notre croix, le point chaud est au centre des croix).

Les dernières lignes de code donne une position adéquate pour la caméra, la positionnant au dessus (tilt = -90), puis montre le pointeur de la souris.

while (game_started == 0) // as long as we haven't started the game
{
mouse_pos.x = pointer.x;
mouse_pos.y = pointer.y;
if (random (1) > 0.95) // use the second bitmap for the pointer every 20 frames (or so)
{
mouse_map = pointer2_tga;
}
else
{
mouse_map = pointer1_tga;
}
wait (1);
}
}

J'ai défini une variable nommée game_started; tant que la valeur de cette variable sera 0,nous utiliserons pointer.x et pointer.y pour mettre à jour la position du curseur de la souris. Si random(1) > 0.95, ce qui se produit à peu près tous les 20 frames nous remplaçons l'image pointer1_tga sa soeur légèrement modifiée nommée pointer2_tga; sinon nous remettons le pointeur original pointer1_tga. Ce petit truc permet de montrer qu'il y a une activité même morsqu'il ne se passe rien.

Regardons le code utilisé par le joueur lorsque le menu principal est actif:

action
player_intro
{
if (version >= 6.00) // using A6?
{
my.ambient = -50;
my.roll = 6;
}
else // using A5
{
my.ambient = 80;
my.roll = 20;
}
my.enable_touch = on;
my.enable_release = on;
my.enable_click = on;
my.event = toggle_engines;
}

J'ai noté que les modèles paraîssaient différents selon que l'on soit dans A5 ou A6 aussi j'ai choisi différentes valeur d'ambiance et d'angles roll pour le vaisseau en fonction de la version du moteur. Au passage, vous pouvez voir le vaisseau à travers main_pan car la zone sombre du panneau principal est RVB = 000. Le vaisseau est sensible au toucher (déplacer le curseur au dessus déclenchera ses évènements), au relacher (déplacer la souris en dehors du vaisseau déclenche ses évènements) et au clic (cliquer le vaisseau déclenchera ses évènements). J'ai utilisé cette fonction d'évènements pour tous les évènements:

function toggle_engines()
{
if (event_type == event_touch)
{
my.engine = on;
if (snd_playing (engine_handle) == 0)
{
engine_handle
= snd_loop (engine1_wav, 10, 0);
}
}
if (event_type == event_release)
{
snd_stop (engine_handle);
my.engine = off;
}
if (event_type == event_click)
{
start_level1();
}
plasma_jet();
}

Si nous touchons le vaisseau, nous mettons ses moteurs (engine qui est un autre nom pour flag1) à 1; ce drapeau est utilisé pour démarrer ou arrêter le jet de flamme dans la fonction de particules. Si le son engine1_wav n'est pas déjà joué nous le jouons dans une boucle en utilisant l'instruction snd_loop.

Si nous déplaçons le curseur en dehors du vaisseau, nous arrêtons le son engine1_wav et nous mettons le drapeau "engine" à 0. Pour terminer si nous cliquons le vaissea, la fonction start_level1() est exécutée. La dernière ligne à l'intérieur de notre fonction d'évènement met à jour le statut du moteur. Sera mis à 0 ou à 1 en fonction de "my.engine".

regardons quelques fonctions à présent :

function
mouse_over() // is executed when we are over the menu buttons
{
snd_play (mouse_wav, 70, 0);
}

function exit_game()
{
exit;
}

function load_game()
{
beep;
}

function save_game()
{
beep;
}

La fonction mouse_over() est exécutée lorsque le curseur est placé au dessus d'un des boutons du menu principal et la fonction exit_game() arrête simplement le moteur du jeu. Load et save ne sont pas implantés pour l'instant aussi ces fonctions génèrent un bip lorsque nous pressons les boutons correspondants.

function
show_help()
{
while (main_pan.pos_y > -600)
{
main_pan.pos_y -= 10;
help_pan.pos_y = main_pan.pos_y + 600;
wait
(1);
}
snd_play (menu_wav, 20, 0);
}

La fonction show_help affiche le menu d'aidelorsque nous cliquons le bouton d'aide (help); il décrémente pos_y pour main_pan et pour help_pan dans une boucle while, en conservant 600 pixels de différence sur l'axe des y tout le temps. J'ai choisi "10" pour la vitesse de défilement ce qui signifie que je soustrait 10 pixels de pos_y à chaque frame, mais vous pouvez utilisez n'importe quelle autre valeur à condition qu'elle soit un diviseur de 600 comme : 5, 6, 8, 10, 12, 15, 20, 25, 30, ...

Le défilement s'arrête lorsque main_pan a disparu et que help_pan couvre entièrement l'écran. Maintenant comment faisons-nous revenir main_pan lorsque nous en avons besoin?

function
show_main()
{
while (main_pan.pos_y < 0)
{
main_pan.pos_y += 10;
help_pan.pos_y = main_pan.pos_y + 600;
wait (1);
}
snd_play (menu_wav, 20, 0);
}

La fonction ci-dessus s'exécute lorsque nous cliquons help_pan, et elle fait la même chose qu'avec show_help(), mais le panneau se déroule dans l'autre sens. Le mêm son menu_wav est joué lorsque les panneaux ont cessé de bouger.

J'aimerai que nous étudions à présent le code utilisé pour les flammes du moteur aussi laissez moi vous montrer les effets sous un meilleur angle.

function
plasma_jet()
{
proc_late();
var engine_left;
var engine_right;
var jet_speed;
while
(my.engine == on)
{
vec_for_vertex
(engine_left, my, 52);
vec_for_vertex (engine_right,
my, 54); // right engine vertex
temp.pan =
my.pan;
temp.tilt = my.tilt;
temp.roll = my.roll;
jet_speed.x = -5;
jet_speed.y = 0;
jet_speed.z
= 0;
vec_rotate (jet_speed, temp);
effect(particle_jet, 4, engine_left, jet_speed);
effect(particle_jet, 4, engine_right, jet_speed);
wait(1);
}
}

La fonction ci-dessus est appelée par la fonction event associée au joueur. La première ligne de code s'assure que la fonction attende que engine_left et engine_right soient mis correctement parce qu'ils dépendent de la valeur réelle du joueur. Les particules seront générées aussi longtemps que my.engine (my.flag1) est mis à un pour le joueur. Les points de départ pour les particules sont données par les sommets 52 et 54; ce ne sont pas les sommets exacts créés pour le moteur mais paraîssent meilleirs lorsque voyez le modèle dans une vue de dessus et comme cela que nousle voyons dans notre menu.

La taille de la flamme est donnée par jet_speed.x; ce vecteur est tourné par les angles stockés dans temp, le résultat donnant un jet de particule qui suit le vaisseau correctement, dans sa position et ses angles. Nous générons 4 particules en même temps pour chaque moteur et nous lui disons de générer leur comportement avec la fonction particle_jet()

function
particle_jet()
{
my.alpha = 10 + random(90);
my.bmap = jet_tga;
my.flare = on;
my.bright
= on;
my.beam = on;
my.move = on;
my.size = 10;
my.function = particle_fade;
}

Notez que le facteur de transparence peut avoir une valeur comprise entre 10 et 100; de cette façon nous avons une flamme qui change de longueur en permanence. Ne vous inquiétez pas si votre vaisseau n'utilise pas les effets de laser, les particules rendent aussi bien car elles ont une durée de vie courte. La taille de la particule est donnée par 10 et la fonction qui gère chaque particule est particle_fade():

function
particle_fade()
{
my.alpha -= 20 * time;
if (my.alpha <= 0)
{
my.lifespan
= 0;
}
}

Ves particules ne vivent pas longtemps, dès queleur alpha est inférieur ou égal à 0 elles disparaissent.

Que se passe-t-il lorsque nous cliquons sur le vaisseau? La fonction suivante est exécutée:

function
start_level1()
{
mouse_mode = 0;
snd_stop
(engine_handle);
main_pan.visible = off;
level_load
(level1_wmb);
}

Cette fonction cache la souris, arrête le son du moteur, cache le panneau principal puis chargele premier niveaujouable, c'est un cube blanc avec un vaisseau tournant pour le moment mais je vous prmes de faire quelque chose de plus excitant pour le mois prochain. Level1_wmb a son propre fichier script qui lui est associé (leve1.wdl) et c'est là où nous trouvons le code qui fait tourner le vaisseau.

action
enemy1
{
while (1)
{
my.pan += 3 * time;
my.roll += 2 * time;
wait (1);
}
}

Souvenez-vous e ce truc : chaque nouveuniveau doit avoir son propre fichier script mêmsi vous prévoyez d'utiliser les actions etles fonctions inclusent dans les anciens scripts.

N'oubliez pas que vous êtes tous invités à m'aider à créer et à finir ce jeu, aussi jetez un oeil sur le forum. J'espère vous voir d'ici le prochain mois!


Untitled

Planet Survivors - partie 2

Ce mois-ci nous allons jouer à notre premier niveau P.S (Planet Survivor). Jetez d'abord un coup d'oeil à ces écrans:

Ce premier niveau n'inclut pas les ennemismais il y a une mutitude d'astéroïds qui vont vous faire une poussée d'adrénaline dans les veines.Le script main.wdl a subi quelques petits changement, le gros du code se trouvant dans level1.wdl. Nous commençons par quelques panneaux et un texte:

panel
shield_pan
{
bmap = shield_tga;
layer = 10;
pos_x = 732;
pos_y = 555;
digits = 7, 15, 3, figures_font,
1, player.shield;
flags = overlay, refresh, visible, transparent;
}

panel ammo_pan
{
bmap = ammo_tga;
layer = 10;
pos_x = 0;
pos_y = 555;
digits = 12, 15, 3, figures_font, 1, player.ammo;
flags
= overlay, refresh, visible, transparent;
}

panel crosshair_pan
{
bmap = pointer2_tga;
layer = 20;
pos_x = 368;
pos_y = 282;
flags = transparent, overlay, refresh;
}

panel score_pan
{
bmap = score_tga;
layer = 10;
pos_x = 300;
pos_y = 5;
flags = overlay, refresh, visible;
}

text score_txt
{
pos_x = 380;
pos_y = 5;
layer = 10;
font = score_font;
string = score_str;
flags = visible;
}

es panneaux affichent le bouclier (en bas à droite), les munitions (en bas à gauche), le viseur (centré sur l'écran de 800x600) et l'image en haut de l'écran qui a score d'écrit dessus. Le texte affiche le score (oui le score est converi en chaîne avant d'être affiché).

Le joueur vole avec une grande vitesse au dessus de la surface de la planète, à priori! Et bien non, le joueur ne bouge pas (excépté pour les scènes coupées) et c'est la planète en dessous (un mdl) qui change son paramètre "v" dans une boucle while:

action
planet_level1 // attached to the "planet" (a flat mdl, really)
{
my.passable = on;
fog_color = 1;
camera.clip_far
= 100000; // works only with A6, delete this line if you own A5
// clip_range = 100000; // remove the comment if you own A5
camera.z
= -100;
camera.fog_start = 50;
camera.fog_end
= 20000;
while (ready_to_play == 0)
{
my.v += 3 * time;
wait (1);
}
my.z -= 5000;
my.ambient
-= 30;
while (player_is_dead == 0)
{
my.v += 2 * time;
wait (1);
}
}

Comme je le dis, la planète est un modèle plan et il décale sa texture tant que le vaisseau du joueur n'est pas détruit. La texture utilisée pour le modèle est "bord à bord" aussi le niveau donne l'impression d'être sans fin; en fait il ne peut pas se terminer car le vaisseau du joueur se tient immobile.

J'ai sorti mon arme secrète, un brouillard orange avec RGB = 200, 121, 50 et j'ai mis des valeurs adéquates pour cela en utilisant camera.fog_start et camera.fog_end. Au passage, j'ai fait un maximum d'effort pour que le projet s'exécute aussi bien avec une version A6 qu'une version A5 mais vous pouvez avoir de mauvaises valeurs (remplacez camera.clip_far par clip_range, choisissez différentes valeurs pour le brouillard etc) si vous possédez A5. Jetez un oeil averti sur les photos d'écran pour voir à quoi votre niveau est sensé ressembler. Ready_to_play est une variable qui est mise à 1 lorsque le joueur est en mesure de contrôler le vaisseau (pile poil après a scène coupée au dpart). Vous pouvez voir que la planète est déplacé de 5000 quants en dessous lorsque le jeu démarre et il est rendu un peu plus sombre. Le décalage "v" de la texture se fait à petite vitesse aussi longtemps que le joueur n'est pas mort.

J'ai utilisé un modèle similaire pour le ciel et un décalage "u" pour sa texture.

action
sky_level1
{
my.passable = on;
my.flare =
on;
my.ambient = -20;
while (ready_to_play ==
0)
{
my.u -= 0.3 *
time;
wait (1);
}
ent_remove (my);
}

Le ciel peut être enlevé dès que la scène coupée se termine. La plus horrible action que vous n'avez jamais vue est ci-dessous:

action
player_level1
{
player = my;
my.ammo = 200;
my.shield = 100;
my.metal = on;
my.ambient
= 50;
my.transparent = on;
my.alpha = 100;
my.enable_impact = on;
my.enable_entity = on;
my.event = player_was_hit;
vec_set(temp, player.x);
vec_sub(temp, camera.x);
vec_to_angle(camera.pan, temp);
track_handle = media_play ("track1.wav", null, 50);
fade_factor = 1000;
my.engine = on;
plasma_jet(65, 63);

C'est l'action attachée au joueur; il met la quantité de munitions (ammo = simplement un autre nom pour skill21) et le boucleir (shield = skill22). Le vaisseau est transparent (nous verrons plus tard pourquoi) mais son facteur alpha est 100 (opaque) pour le moment. Le vaisseau est sensible à l'impact des autres entités (les astéroids); son évènement player_was_hit sera exécuté chaque fois qu'il se cognera avec quelque chose. Nous utilisons une instruction vec_to_angle pour tourner la caméra vers le vaisseau du joueur puis nous jouons le son nommé track1.wav. J'ai utilisé un fichier wav car toutes les éditions ne peuvent pas lire un fichier mp3 ou ogg mais vous pouvez utiliser un fichier audio compressé.

Vous souvebz-vous de la fonction particule utilisée pour le moteur du joueur dans le menu? Bien, j'ai simplement ajouté un simple fade_factor à cette fonction et à présent je peux contrôler la durée vie des particules en mettant fade_factor àune bonne valeur. J'ai choisi fade_factor = 1000 parce que les particules doivent être dtruites rapidement; la vitesse du vaisseau est trop grande pour des particules lentes. Pour terminer je mets le drapeau "engine" (flag1) et j'exécute la fonction qui crée le jet de plasma au sommet passé comme paramètre (la fonction plasma_jet() de main.wdl a été légèrement modifiée également).

  while (my.x < 3000)
{
my.roll += 1.5 * time;
ent_playsound (my, engine1_wav,
300);
my.x += 52 * time;
vec_set(temp, my.x);
vec_sub(temp, camera.x);
vec_to_angle(camera.pan, temp);
wait (1);
}
ready_to_play = 1;
my.x = -10000;
my.y = 0;
my.z = -100;
my.roll = 0;

   crosshair_pan.visible = on;
camera.x = -10230;
camera.y = 0;
camera.z = -50;
camera.pan = 0;
camera.tilt = 0;
camera.fog_start = 15000;
camera.fog_end
= 50000;

fade_factor = 100;
snd_play (engage_wav, 70, 0);

Le code ci-dessus joue la scène coupée; le vaisseau se déplace de sa position initiale (x = -10,000) à x = 3,000 en changeant son angle roll et jouant le son du moteur. La caméra suit le vaisseau tout le temps (c'est un autre vec_to_angle qui fait cela). Dés que le joueur approche la limite de x = 3000 quants, ready_to_play est mis à 1 (le ciel disparaît) et le vaisseau est déplacé à -10,000 quants car nous n'avons pas une grande planète pour jouer avec et l'utilisateur n'y verra que du feu. Nous montrons le viseur, nous mettons la bonne position pour la caméra et nous choisissons d'autres valeurs pour le jeu de brouillard. Pour terminer nous mettons fade_factor à 100 car le joueur se tient immobile à présent (bien qu'il donne l'impression de voler grâce au décalage "v" de la planète) et nous jouons le jouons engage_wav, pour que le joueur sache qu'il en a le contrôle à présent.

   while (my.shield > 0)
{
camera.tilt += 10 * mouse_force.y * time;
camera.pan
-= 10 * mouse_force.x * time;
if ((camera.pan
> 30) && (camera.pan < 330)) // don't allow the player to blow our
cover (if it looks back)
{
if (camera.pan < 180)
{
camera.pan = 30;
}
else
{
camera.pan = 330;
}
}
if (camera.tilt < -35)
{
camera.tilt = -35;
}
if (camera.tilt > 25)
{
camera.tilt = 25;
}
my.pan = camera.pan;
my.tilt = camera.tilt;

Tant que le vaisseau du joueur n'est pas complètement détruit (shield > 0), la caméra change ses angles lorsque nous déplaçons la souris. L'angle pan de la caméra peut varier dans un intervalle de -30 à 30 degrés, et l'angle tilt de -35 à 25 degrés. Nous faisons cela car nous ne voulons pas que le joueur regarde derrière lui car il verrait les limites du niveau. Le vaisseau hérite des mêmes angles pan et tilt que la caméra.

vec_set (temp_var, my.pos); // copy player's position
to temp_var
vec_to_screen(temp_var, camera); // now
temp_var contains the 2D position (x, y) of the ship
if ((abs(temp_var.x - 400) < 50) || (abs(temp_var.y - 300) < 40)) // the
crosshair and the ship tend to overlap?
{
my.alpha = (abs(temp_var.x - 400) + abs(temp_var.y - 300));
}
else
{
if (my.alpha < 99)
{
my.alpha += 0.5 * time;
}
}

     if ((mouse_left == 1) && (player.ammo
> 0))
{
fire_rocket();
}

Les lignes ci-dessus conertissent la position 3D du vaisseau en une position 2D d'écran, et ajuste la transparence du vaisseau si le curseur le chevauche. Jetez un oeil sur les images ci-dessous pour voir comment ça fonctionne.

Cette méthode permet au joueur de voir n'importe quel attaquant / astéroïde même s'il se trouve devant le vaisseau. La dernière ligne du code appelle la fonction fire_rocket() si le joueur presse le bouton gauche de la souris (LMB) et si les munitions sont supérieures à zéro.

      if ((key_z == on) && (my.y <
100))
{
my.y += 10 * time;
if (my.roll
< 30)
{
my.roll += 3 * time;
}
}
if ((key_x == on) && (my.y > -100))
{
my.y -= 10 * time;
if (my.roll > -30)
{
my.roll -= 3 * time;
}
}
if (key_z + key_x == 0)
{
if (my.roll > 1)
{
my.roll -= 1 * time;
}
if (my.roll < -1)
{
my.roll += 1 * time;
}
}
wait (1);
}

Savez-vous pourquoi le joueur peut se déplacer vers la gauche ou vers la droite en utilisant les touches "Q" et "X" ? C'est la seule possibilité qu'il ait pour échapper à ses ennemis; notez que ces déplacements sont limités dans l'intervalle: -100, 100 quants. Le vaisseau change son angle roll lorsqu'il se déplace et il se repositionne en douceur lorsque key_z + key_x = 0 (c'est à dire aucune des touches de pressées).

   crosshair_pan.visible = off;
camera.pan =
0;
camera.tilt = -10;
while (my.z > -1990)
{
if (camera.tilt > -30)
{camera.tilt -= 0.2 * time;}
my.x += 30 * time;
my.z -= 20 * time;
my.roll += 10 * time;
wait (1);
}
player_is_dead = 1;
my.skill10 = 10;
ent_create (explo13_pcx, my.pos, explode_me);
my.tilt = -50; // choose a weird position for the ship
ent_playsound
(my, hit_wav, 5000);
while (1)
{
temp.x = 3 - random(6);
temp.y = 3 - random(6);
temp.z = 4 + random(4);
effect(particle_smoke,
10, my.pos, temp);
wait (1);
}
}

La dernière partie de l'action s'exécute lorsque le joueur est mort (shield <= 0). Nous mettons un bon angle pour la caméra puis nous la montrons perdant de la hauteur et tournant pendant que l'angle tilt de la caméra est réduit lentement afin de suivre le vaisseau. Nous mettons player_is_dead à 1, et nous arrêtons le décalage "v" de la texture et nous créons un énorme sprite d'explosion en lui attachant la fonction explode_me. My.tilt = -50 met une mauvaise position pour le vaisseau, nous entendons une explosion et nous émettons de la fumée dans une boucle while. Repose en paix, courageux joueur ... sniff....

Bon ce niveau ne contient pas d'ennemis conventionnels mais des centaines d'asteroids. Voyons ce qui se passe:

action
asteroid_maker
{
var temp_var;
my.passable
= on;
my.invisible = on;
while (1)
{
temp.x = my.x;
temp.y = 5000 - random(10000); // -5,000 to 5,000
temp.z = 1000 - random(2000);
if (ready_to_play
== 0) // game not started yet?
{
if ((temp.y > -500) && (temp.y
< 500)) // the asteroid is too close to the player during the cut scene?
{
temp.y = 500; // then avoid the player until it is able to control the ship
}
}
temp_var = random(1);
if (temp_var < 0.33)
{
ent_create (asteroid1_mdl, temp, move_asteroid);
}
if ((temp_var >= 0.33) && (temp_var
< 0.66))
{
ent_create (asteroid2_mdl, temp, move_asteroid);
}
if (temp_var > 0.66)
{
ent_create (asteroid3_mdl,
temp, move_asteroid);
}
wait (2);
}
}

Le modèle qui a cette action qui lui est attachée est rendu passable et invisible. Son x = 10,000 dans Wed, aussi les asteroids seront générés à cet x et a une valeur de y aléatoire (-5000 à 5000) et de z (-1000 to 1000). Si ready_to_play = 0 (ce qui se produit pendant la scène coupée) les asteroids qui ne doivent pas cogner le joueur sont déplacés sur y = 500 d'où ils ne peuvent pas endommager le vaisseau. Nous générons un nombre aléatoire et nous le stockons dans temp_var; choisissant l'un des 3 modèles d'astéroïd. La fonction qui déplace les asteroids est nommée move_asteroid:

function
move_asteroid()
{
var meteor_angle;
var meteor_speed;
my.enable_impact = on;
my.enable_entity = on;
my.event = destroy_asteroid;
my.scale_x = 5 + random(10);
my.scale_y = my.scale_x;
my.scale_z = my.scale_x;
my.skill10 = my.scale_x; // store the scale in skill10 - we will use it
to adjust the size of the explosion later
meteor_angle.x = 3
- random(6); // random pan speed
meteor_angle.y = 3 - random(6);
// random tilt speed
meteor_angle.z = 3 - random(6); // random
roll speed
meteor_speed.x = (-100 - random(80)) * time;
meteor_speed.y = 0;
meteor_speed.z = 0;

Les asteroids sont sensibles à l'impact avec les autres entités (les autres asteroids, le vaisseau du joueur ou les roquettes du joueur ) et exécutent leur fonction d'évènement "destroy_asteroid" lorsqu'ils cognent quelque chose. Nous mettons une échelle aléatoire pour l'asteroids, et nous la stockons dans leur propre skill10; plus tard nous utiliserons cette valeur pour mettre l'explosion à l'échelle parce que un petit astéroïd doit avoir une petite explosion alors qu'un grand astéroïde doit avoir une grande explosion. Les atéroïds tournent avec des vitesses aléatoires de pan, tilt et roll , et se déplace en utilisant une vitesse négative de -100...-180 quants / frame.

  while (my.x > -21000)
{
my.pan += meteor_angle.x * time;
my.tilt +=
meteor_angle.y * time;
my.roll += meteor_angle.z
* time;
move_mode = ignore_you + ignore_passable;
ent_move (nullvector, meteor_speed);
wait (1);
}
ent_remove (my);
}

Les asteroids se déplacent vers le vaisseau du joueur jusqu'à ce que leur x soit sous -21,000 quants, tournant tout le temps. Dès qu'un astéroïd passe sous la limite des -21,000 quants il est enlevé. Examinons leur fonction évènement à présent:

function
destroy_asteroid()
{
if (you.skill40 == 1234)
{
score_value += int(1000 / (my.skill10 + 1));
}
my.x = -30000;
wait (1);
my.event = null;
ent_create (explo13_pcx, my.pos, explode_me);
my.passable = on;
my.invisible = on;
ent_playsound (my, hit_wav, 1000);
sleep (2);
ent_remove(my);
}

Si l'asteroid est touché par une balle du joueur (son skill40 = 1234) nous augmentons le score, ajoutant un score plus important pour un astéroïd plus petit, parce qu'ils sont plus difficiles à atteindre; l'échelle est stockée dans skill10, vous vous souvenez? L'asteroïd qui a été touché est déplacé avec un x = -30,000 ou il attendra qu'un autre astéroïd vienne le cogner,c'est comme cela que nous obtenons de superbes explosions au début de la scène coupée. La fonction évènement est mise à null, le sprite explosion est créé et un son d'explosion est joué. Nous gardons l'astéroïd pour encore 2 secondes (il est invisible et passable) pour que le son ait le temps de se finir. Que se passe-t-il ensuite?

function
explode_me()
{
my.oriented = on;
my.flare
= on;
my.bright = on;
my.ambient = random(100);
my.passable = on;
my.roll = random(360);
my.skill30 = you.skill10;
while (my.frame < 14)
{
if (my.scale_x < my.skill30)
{
my.scale_x += 5 * time;
my.scale_y
= my.scale_x;
}
my.frame += 1 * time;

vec_set (temp.x, camera.x);
vec_sub (temp.x, my.x);
vec_to_angle (my.pan, temp); // turn towards the player to make sure that the explo looks great

wait (1);
}
ent_remove (me);
}

C'est la fonction qui gère le sprite de l'explosion! Le sprite est orienté, passable et a une valeur ambient aléatoire. Mieux que cela, il a un angle roll aléatoire puisque nous voulons simuler différentes exlosions avec le même sprite afin de n epas trop utiliser de mémoire vidéo. Nous stockons you.skill10, qui est l'échelle de l'asteroid utilsant skill30 de notre sprite explosion puis nous jouons les animations augmentant la taille jusqu'à sa taille d'origine. Une autre instruction vec_to_angle tourne le sprite vers la caméra afin que le joueur puisse voir l'explosion dans toute sa splendeur. Le sprite est enlevé à la fin de sa dernière frame d'animation.

Voyons à présent ce qui se passe lorsque le joueur presse le bouton gauche de la souris (LMB):

function
fire_rocket()
{
proc_kill(4);
while (mouse_left
== 1) {wait (1);}
player.ammo -= 1;
snd_play
(rocket_wav, 50, 0);
ent_create (rocket_mdl, player.pos, move_rocket);
}

La première ligne de code s'assure qu'il n'y ait bien qu'une seule instane de la fonction qui s'exécute et nous attendons que le bouton gauche soit relâché. Les munitions du joueurs sont diminuées, un son est joué et une roquette (fichier rocket_mdl) est créée à la position du joueur, exécutant la fonction "move_rocket" :

function
move_rocket()
{
my.enable_entity = on;
my.enable_block
= on;
my.event = remove_rocket;
my.pan = camera.pan;
my.tilt = camera.tilt;
my.skill40 = 1234;
my.skill10 = 3; // default explosion scale for the rocket
my.skill20
= 0;
while (my.skill20 < 500)
{
my.skill1 = 500 * time;
my.skill2 = 0;
my.skill3 = 0;
my.skill20 += 1 * time;
ent_move (my.skill1,
nullvector);

       temp.x
= my.x;
temp.y = my.y;
temp.z = my.z;
effect (particle_trail,
1, temp, normal);
wait (1);
}
remove_rocket();
}

La roquette est sensible aux autres entités et au blocs du niveau, sa fonction d'évènement est nommée remove_rocket. La roquette prend le même angle pan et tilt que la caméra et sera identifiée en tant que roquette car son skill40 à une valeur atypique de 1234. Nous mettons skill10 (échelle du possible sprite d'explosion) à 33, puis nous utilisons skill20 pour limiter la distance maximale pouvant être couverte par la roquette. La vitesse de la roquette est donnée par skill1,et l'origine de la trainée de particules est donnée par les coordonnées de la roquette.

Lavitesse de la roquette est trop grande aussi nous avons besoin d'utiliser la fonction particle_trail si nous voulons voir quelque chose : jetez un oeil sur le rayon laser rouge de l'image ci-dessous.

function particle_trail()
{
temp.x = 100;
temp.y = 0;
temp.z = 0;
vec_rotate (temp,
you.pan);
vec_add (my.vel_x, temp);
my.alpha
= 70 + random(30);
my.bmap = ptrail_tga;
my.size
= 7;
my.flare = on;
my.bright = on;
my.beam = on; // comment this line if your engine can't use "beam"
my.move = on;
my.lifespan = 30;
my.function
= fade_particles;
}

C'est notre fonction de particules habituelle. Elle a une grande vitesse car nous voulons pouvoir utiliser "beam" (commentez cette ligne si vous n'avez pas l'édition le permettant).Les particules ont la même orientation que leur créateur et sont exécutées par leur fonction nommée "fade_particles":

function
fade_particles()
{
my.alpha -= 100 * time;
if (my.alpha < 0) {my.lifespan = 0;}
}

Vous pouvez voir que ces particules sont enlevées rapidement, ce serait une bonne idée de diminuer la valeur "100" si vous pouvez utiliser "beam";ce changement peut améliorer l'effet. Nous approchons de la fin à présent:

function
remove_rocket()
{
my.event = null;
ent_playsound
(my, destroyed_wav, 1000);
ent_create (explo13_pcx, my.pos,
explode_me);
my.invisible = on;
sleep (2);
ent_remove(me);
}

La fonction ci-dessus est l'évènement pour notre roquette, il rend la roquette insensible aux autres évènements, joue le son destroyed_wav crée le sprite explosion et cache la roquette pour au moins 2 seccondes, permettant au son destroyed_wav d'être joué correctement. La dernière ligne de code enlève la roquette

Regardons ce qui se passe lorsque le joueur se cogne avec un astéroïd:

function
player_was_hit()
{
my.event = null; // stop reacting to events
for now
my.shield -= you.skill10 * 1.5; // bigger asteroids cause
greater damage
my.skill33 = 0;
snd_play (hit_wav,
100, 0);
if (player.shield > 0) // the game isn't over yet?
{
while (my.skill33 < 10)
{
camera.roll = 2 - random(4); // shake the player a bit
my.skill33 += 1 * time;
wait
(1);
}
player.roll
= 0; // then restore player's roll
}
my.event
= player_was_hit;
}

Le joueur cesse de réagir aux évènements pendant un temps, son bouclier est diminué dépendant de la taille (skill10) de l'astéroïds. Nous remettons skill33 à 0 puis nous l'utilisons comme compteur, secouons un peu le joueur en utilisant un angle roll aléatoire (-2...2) à chaque frame, puis nous remettons l'angle roll du joueur à zéro. Ceci se produit uniquement si le bouclier ("shield") est plus grand que zéro, le vaisseau n'est pas secoué si le joueur est déjà mort. La dernier ligne rend le joueur sensible aux impacts de nouveau.

Si player.shield <= 0, le vaisseau tombe sur le sol et explose; regardons la fonction qui crée les particules de fumée:

function
particle_smoke()
{
my.alpha = 40 + random(50);
my.bmap = smoke_tga;
my.bright = on;
my.flare
= on;
my.move = on;
my.beam = on; // if your
engine supports it
my.size = 30;
my.function
= particle_fade;
}

Il n'y a pas grand chose à dire sur cette fonction; n'oubliez pas de commenter la ligne "beam" si votre édition ne la gère pas. Oh et nous utilisons la même fonction particle_fade qui est utilisée pour les jets de plasma du moteur. Il est temps de voir la dernière fonction:

starter
compute_score()
{
while (1)
{
str_for_num(temp_str, score_value); // convert the value
to string
if (str_len(temp_str) == 1)
{
str_cpy(score_str, "0000000");
str_cat(score_str, temp_str);
}
if (str_len(temp_str) == 2)
{
str_cpy(score_str, "000000");
str_cat(score_str, temp_str);
}
if (str_len(temp_str) == 3)
{
str_cpy(score_str, "00000");
str_cat(score_str, temp_str);
}
if (str_len(temp_str) == 4)
{
str_cpy(score_str, "0000");
str_cat(score_str, temp_str);
}
if (str_len(temp_str) == 5)
{
str_cpy(score_str, "000");
str_cat(score_str, temp_str);
}
if (str_len(temp_str) == 6)
{
str_cpy(score_str, "00");
str_cat(score_str, temp_str);
}
if (str_len(temp_str) == 7)
{
str_cpy(score_str, "0");
str_cat(score_str, temp_str);
}
if (str_len(temp_str) == 8)
{
str_cat(score_str, temp_str);
}
wait (1);
}
}

Cette fonction starter est assez longue mais j'ai voulu afficher le score en utilisant une vieille technique apprise à l'école, c'est à dire si le score fait 1500 j'affiche "00001500" plutôt que "1500". Maintenant vous comprenez pourquoi j'utilise une chaîne plutôt qu'un affichage numérique pour le score. Nous convertissons les valeurs numériques (score_value) en une chaîne, nous prenons sa longueur et nous complétons à gauche avec le bon nombre de zéro. C'est tout.

Je pense que Planet Survivors à du potentiel devant lui. Pour l'instant je le vois comme une combinaison d'action / logique / rpg mais qui sait ce qu'il peut devenir dans le futur? Je voudrais remercier Nemisis pour ses grandes idées et son support; la juabilité ne serait pas aussi bonne sans son aide. Je vous revoie le mois prochain!


Untitled

Planet Survivors - part 3

Ce mois-ci nous allons voir le deuxième niveau de notre jeu;le vaisseau atterrit sur une plateforme et nous jouerons à un jeu de plateau afin d'essayer d'obtenir plusde munitions et de bouclier.

J'ai été obligé de faire quelques modifications à l'action player_level1 mais pas d'inquiétude nous en reparlerons. La modifif principale est une variable nommée landing, qui contrôle l'atterrissage (landing) du vaisseau, de quelle manière? Lorsque cette variable est à 0, le vaisseau vole comme nous l'avons vu dans le planet survivor précédent mais lorsque landing = 1 le vaisseau prépare son atterrissage.

while
(players_shield > 0)
{
if (landing == 0)
{
// old stuff from Aum37 here
}
else // landing
= 1
{
player.passable = on;
player.transparent = off;
camera.x = 500;
camera.y = -200;
camera.z = -2000;
lbase.visible = on;
crosshair_pan.visible =
off;
my.pan = 0;
my.tilt = 0;
my.roll = 0;
my.y = 0;

La branche "else" s'exécute lorsque landing = 1 et fait les choses suivantes : rend le joueur passable, afin de prévenir une éventuelle collision avec un astéroïde et met le drapeau "transparent" à off. Nous choisissons une position adéquate (expérimentale) pour la caméra puis nous rendons la base d'atterrissage (lbase) visible. Pour terminer nous cachons le curseur et nous mettons à 0 les angles et le y du vaisseau car le joueur doit avoir une bonne position pour l'atterrissage.

      while (player.x < 0)
{
if (player.z >
-2270) {player.z -= 14 * time;}
player.x += (vec_dist(player.x, lbase.x) / 60 + 2) * time;
vec_set(temp, player.x);
vec_sub(temp, camera.x);
vec_to_angle(camera.pan, temp);
engine_handle = ent_playsound (my, engine1_wav, 400);
my.skill15 = vec_dist(player.x, lbase.x);
if (my.skill15 < 1000)
{
snd_tune(engine_handle, 0, (my.skill15 / 10), 0);
}
wait (1);
}

Le vaisseau aura atterri lorsque x = 0, y = 0, z = -2270. Nous utilisons une boucle while pour augmenter son x et diminuer son z. La vitesse de l'axe des x dépend de la distance entre le joueur et la base d'atterrissage (lbase); elle deviendra plus lente à l'approche de la zone d'atterrissage, la vitesse minimale étant 2* time. La camera est dirigée en permanence vers le vaisseau et le bruit du moteur varie en fonction de la distance entre le vaisseau et la base. Ce qui signifie que le bruit du moteur diminue lorsque le vaisseau s'approche de la base.

sleep (2);
my.engine = off;
temp.x = -35;
temp.y = 0;
temp.z = -2285;
ent_create (human_mdl, temp, move_human);
return;
}
wait (1);
}

A présent le vaisseau a atterri. Nous attendons 2 secondes puis nous arrêtons le système de particules (le moteur). Nous mettons x = -35, y = 0, z = -2285 (valeur expérimentale) et nous créons une entité human_mdl à cette position, avec l'action move_human qui lui est attachée.Comme tout cela se passe dans une boucle while, nous sortons de la fonction en utilisant un return.

Ce sont les principaux changements du code de l'Aum37.Regardons à présent le nouveau code.

function
compute_time()
{
sleep (130);
landing =
1;
}

Cette petite fonction est appelée par player_level1; il attend que la piste son soit presque terminée et il met landing à 1. je n'ai pas inclu track1.wav dans l'archive de ce mois-ci car je sais que certains possèdent encore des liaisons modem 56Kb modems; n'oubliez donc pas de remplacer le fichier track1.wav avec l'un de ceux d'Aum37.

action
landing_base
{
lbase = my;
my.invisible =
on;
my.passable = on;
while (landing == 0) {wait
(1);}
my.invisible = off;
}

L'action ci-dessus est attachée à la base d'atterrissage, elle la rend invisible et passable (nous de voulons pas de collisions indésirables) puis nous attendons que landing soit mis à 1, ce qui rendra la base visible. which makes the base visible.J'ai caché la base jusque là car notre niveau est très petit mais il fallait donner l'impression qu'il est infini au joueur innocent.

Voici la fonction qui gère le petit humain:

function
move_human()
{
my.passable = on;
human =
my;
my.pan = 270;
my.enable_touch = on;
my.event = logical_help;
while (my.y > -170)
{
my.y -= 1.4 * time;
ent_cycle("walk", my.skill1);
my.skill1
+= 5 * time;
my.skill1 %= 100;
wait (1);
}
ent_cycle ("walk", 25);

L'humain est passable est a un angle pan de 270]. Il est sensible au toucher et exécute la fonction logical_help lorsque vous placez la souris sur lui. Le petit gars se retrouve sous le vaisseau et se déplace à la vitesse donnée par 1.4 * time jusqu'à ce qu'elle soit inférieure à -170 quants; la vitesse de son animation de marche est donnée par 5 * time. La dernière ligne choisit une position d'attente adéquate (25 étant une valeur experimentale).

  camera.x = -40;
camera.y = -140;
camera.z
= -2100;
camera.pan = 270;
camera.tilt = -65;
camera.roll = 0;
mouse_map = logical_pcx;
mouse_mode = 2;
mouse_spot.x = 0;
mouse_spot.y
= 0;
temp_bonus = 100;
sound_vol = 20;
while (logical_ended == 0)
{
temp_bonus -= 0.1 * time;
if (temp_bonus <
0) {temp_bonus = 0;}
mouse_pos.x = pointer.x;
mouse_pos.y = pointer.y;
wait (1);
}

L'humain s'est approché du tableau logique, nous choisissons un nouveau jeu de coordonnées et d'angles pour la caméra nous mettons un nouveau pointeur de souris, nous rétablissons la valeur par défaut de mouse_spot values (qui avait été changé pour le menu principal) puis nous mettons temp_bonus à 100, qui est le maximum de bonus que peut obtenir un joueur qui résout très rapidement le niveau. je n'ai pas arrêté les astéroïds ausii ils continuent à se cogner et à produire des sons dans le fond, ce qui peut déconcentrer un peu le joueur aussi je réduit tous les volumes sonores de 20%. Pour terminer nous diminuons temp_bonus dans une boucle et nous autorisons le joueur à bouger la souris jusqu'à ce qu'il ait résolu le niveau logique (jusqu'à ce que logical_ended = 1).

  sleep (1);
sound_vol = 100;
while (my.pan
> 90)
{
my.pan -= 25 *
time;
wait (1);
}
while (my.y < -100)
{
my.y += 1.4 * time;
ent_cycle("walk", my.skill1);
my.skill1 += 5 * time;
my.skill1 %= 100;
wait (1);
}
ent_remove(my);

Ici le niveau lofique est résolu, nous attendons une seconde, nous remettons le volume des explosions des astéroïds à leur pleine valeur et nous tournons l'humain dans la direction du vaisseau avec une vitesse de 25 * time. Nous déplaçons l'humain dans une boucle en jouant son animation de marche. L'humain est enlevé avant d'atteindre sa cible (le vaisseau) mais le joueur ne le voit pas.

  camera.x = 500;
camera.y = -200;
camera.z
= -2000;
vec_set(temp, player.x);
vec_sub(temp,
camera.x);
vec_to_angle(camera.pan, temp);
while (minimum_bonus > 0) {wait (1);}
sleep (1);
fade_factor = 1000;
player.engine = on;
my = player;
plasma_jet(65, 63);

Le vaisseau se prépare pour le décollage, nous remettons les positions initiales pour la caméra et nous la tournons vers le vaisseau. Nous attendons que le joueur récupère son bonus (munition et bouclier), nous attendons une seconde puis nous mettons fade_factor à 1000. Ceci afin que les particules aient une durée de vie courte, cela est nécessaire car la vitesse du vaisseau est vraiment grande.

Nous mettons le drapeau "engine", nous disons au moteur de particules que "my", l'entité qui a besoin des particules, est le joueur lui-même. La dernière ligne démarre le jet de plasma.

  while (vec_dist(camera.x, player.x) < 10000)
{
player.skill15 = min(100, vec_dist(player.x, lbase.x)
/ 10);
player.x += (player.skill15 + 1) * time;
player.z += 5 * time;
engine_handle = ent_playsound (player, engine1_wav, 1000);
if (player.skill15 < 10000)
{
snd_tune(engine_handle, 0, (player.skill15),
0);
}
vec_set(temp,
player.x);
vec_sub(temp, camera.x);
vec_to_angle(camera.pan, temp);
wait (1);
}
beep;
exit;
}

La fin de la fonction augmente les coordonnées x du vaisseau de 1 * time à 101 * time, dépendant de la distance entre le joueur et la base. Nous augmentons le z également car le vaisseau a décollé et nous jouons le son du moteur en utilisant une fréquence sonore calée sur la vitesse de x. La caméra suit le vaisseau jusqu'à environ 10,000; lorsque cela se produit nous émettons un puis nous arrêtons les moteurs, c'est ici que nous mettrons une instruction level_load le mois prochain.

Regardons à présent les actions et les fonctions utilisées par notre jeu de plateau:

starter init_logical()
{
mouse_range = 5000;
while ((tile1 == null) || (tile2 == null) || (tile3 == null) || (tile4 == null) || (tile5 == null) || (tile6 == null) || (tile7 == null) || (tile8 == null) || (tile9 == null)) {wait (1);}

C'est une fonction starter qui rend les entités sensibles aux clics de la souris du moment qu'elles soient à moins de 5000 quants de la caméra. Nous attendons ensuite que tous les carrés colorés soient créés puis nous calculons les valeurs pour les variables nommées status1... status9. Ici une configuration classique du plateau:

J'ai créé 3 entités wmb différentes (rouge, verte et bleue) et je les ai placées comme sur l'image ci-dessus. Chaque carré à une simple action qui lui ait attaché:

action
tile_1
{
tile1 = my;
}

Chaque carré rouge a son drapeau 1 (flag1) de mis, chaque carré vert a son drpeau 2 (flag2) de mis et chaque carré bleu a son drapeau 3 (flag3) de mis. La bonne nouvelle est que ce paramétrage vous permet de placer les carrés dans n'importe quel ordre à la condition d'utiliser 3 carrés pour chaque couleur. J'ai choisi une configuration initiale qui peut être résolue en quelques mouvements, c'est le premier niveau logique après tout!

Revenons
à notre fonction logique:

while (1)
{
status1 = 1 * (tile1.flag1 == on) + 2 * (tile1.flag2 == on)
+ 3 * (tile1.flag3 == on);
status2 = 1 * (tile2.flag1
== on) + 2 * (tile2.flag2 == on) + 3 * (tile2.flag3 == on);
status3 = 1 * (tile3.flag1 == on) + 2 * (tile3.flag2 == on) + 3 * (tile3.flag3
== on);
status4 = 1 * (tile4.flag1 == on) + 2 * (tile4.flag2
== on) + 3 * (tile4.flag3 == on);
status5 = 1 * (tile5.flag1
== on) + 2 * (tile5.flag2 == on) + 3 * (tile5.flag3 == on);
status6 = 1 * (tile6.flag1 == on) + 2 * (tile6.flag2 == on) + 3 * (tile6.flag3
== on);
status7 = 1 * (tile7.flag1 == on) + 2 * (tile7.flag2
== on) + 3 * (tile7.flag3 == on);
status8 = 1 * (tile8.flag1
== on) + 2 * (tile8.flag2 == on) + 3 * (tile8.flag3 == on);
status9 = 1 * (tile9.flag1 == on) + 2 * (tile9.flag2 == on) + 3 * (tile9.flag3
== on);
if ((status1 == status2) && (status2
== status3))
{
if ((status4 == status5) && (status5 == status6))
{
give_bonus();
return;
}
}
if ((status1 == status4) && (status4 ==
status7))
{
if ((status2 == status5) && (status5 == status8))
{
give_bonus();
return;
}
}
wait (1);
}
}

Ce sont 9 variables qui peuvent prendre la valeur 1 (carré rouge, flag1 = on), ou 2 carré vert, flag2 = on), ou 3 (carré bleu, flag3 = on). Si la variable qui contrôle le carré1, la variable qui contrôle le carré2 et la variable qui contrôle le carré3 ont la même valeur alors le premier rang (1-2-3) contient des couleurs identiques. L'intérieur de la branche if teste si le 2ème rang contient des couleurs identiques (4-5-6). Si c'est également vrai alors le joueur a résolu le niveau car obligatoirement le 3eme rang (7-8-9) ne peut contenir que des couleurs identiques. Nous appelons la fonction give_bonus() et nous sortons de la fonction

Si la variable qui contrôle le carré1, la variable qui contrôle le carré4 et la variable qui contrôle le carré7 ont la même valeur alors la première colonne (1-4-7) contient des couleurs identiques. L'intérieur de la branche if teste si la 2ème colonne contient des couleurs identiques (2-5-8). Si c'est également vrai alors le joueur a résolu le niveau car obligatoirement la 3eme colonne (3-6-9) ne peut contenir que des couleurs identiques.

Mais comment changeons-nous les couleurs? En appuyant sur un des 6 boutons (entités): v1... v3 et h1... h3.

J'ai cliqué sur l'entité v3 dans l'exemple ci-dessus, les carrés sont poussés d'un espace vers le haut, celui initialement en haut se retrouvant en bas. La même chose se produit avec les boutons h1... h3 mais les carrés se déplacent horizontalement (h comme horizontal et v comme vertical bien sur), Nous avons 6 entités boutons pour déplacer les carrés mais le code est sur le mêm principe pour tous aussi nous n'en étudierons qu'un seul.

action
h1
{
my.ambient = -100;
my.enable_click =
on;
my.event = h1_clicked;
}

Ce bouton brillant est sensible au clic de la souris et exécute sa fonction d'évènement h1_clicked lorsqu'il est cliqué.

function
h1_clicked()
{
my.ambient = 100;
sleep (0.2);
my.ambient = -100;
if (status2 == 1) // tile2 is a red
tile?
{
ent_morph(tile1, tilered_wmb);
// then change into a red tile
tile1.flag1
= off; // reset all the flags
tile1.flag2 =
off;
tile1.flag3 = off;
tile1.flag1 = on; // tile1 will be a red tile from now on
}
if (status2 == 2) // tile2 is a green tile?
{
ent_morph(tile1, tilegreen_wmb); // then
change into a green tile
tile1.flag1 = off;
// // reset all the flags
tile1.flag2 = off;
tile1.flag3 = off;
tile1.flag2 = on; // tile1 will be a green tile from now on
}
if (status2 == 3) // tile2 is a blue tile?
{
ent_morph(tile1, tileblue_wmb); // then change
into a blue tile
tile1.flag1 = off; // // reset
all the flags
tile1.flag2 = off;
tile1.flag3 = off;
tile1.flag3 = on; // tile1 will be a blue tile from now on
}

Les boutons jaunes augmentent leur brillance pendant 0.2 seconde lorsqu'ils sont cliqués. Si status2 (qui pour le cas où vous l'auriez déjà oublié est la variable qui contrôle le drapeau du deuxième carré) est à 1 alors tile2 est un carré rouge. Ce qui signifie que nous devons transformer le premier carré en un carré rouge (1 <-- 2 <-- 3). Nous remettons tous les drapeaux à 0 et nous mettons le drapeau approprié pour un carré rouge, qui est flag1. Si status2 est à 2 alors le carré est vert et donc nous transformons le premier carré en un carré vert (une fois encore, 1 <-- 2 <-- 3). Nous remettons tous les drapeaux à 0 et nous mettons le drapeau approprié pour un carré vert, qui est flag2. Pour terminer si status2 = 3 alors tile2 est un carré bleu, la fonction transforme donc le premier carré en un carré bleu.

  if (status3 == 1) // tile3 is a red tile?
{
ent_morph(tile2, tilered_wmb); // then change into a
red tile
tile2.flag1 = off; // reset all the
flags
tile2.flag2 = off;
tile2.flag3 = off;
tile2.flag1 = on; // tile2
will be a red tile from now on
}
if (status3
== 2) // tile3 is a green tile?
{
ent_morph(tile2, tilegreen_wmb); // then change into a green tile
tile2.flag1 = off; // // reset all the flags
tile2.flag2 = off;
tile2.flag3 = off;
tile2.flag2 = on; // tile2 will be a green tile from
now on
}
if (status3 == 3) // tile3 is a blue
tile?
{
ent_morph(tile2, tileblue_wmb);
// then change into a blue tile
tile2.flag1
= off; // // reset all the flags
tile2.flag2
= off;
tile2.flag3 = off;
tile2.flag3 = on; // tile2 will be a blue tile from now on
}

Les choses se répètent ici avec le contrôle du 3ème carré (status3). Si status3 = 1, tile3 est un carré rouge, si status3 = 2, tile3 est un carré vert et si status 3 = 3, tile3 est un carré bleu. Le code transforme l'ancien carré en nouveau carré, remettant les drapeaux à 0 et remettant le drapeau approprié à la nouvelle couleur pour chaque carré.

if (status1 == 1) // tile1 is a red tile?
{
ent_morph(tile3, tilered_wmb); // then change into a red tile
tile3.flag1 = off; // reset all the flags
tile3.flag2 = off;
tile3.flag3 = off;
tile3.flag1 = on; // tile3 will be a red tile from now on
}
if (status1 == 2) // tile1 is a green tile?
{
ent_morph(tile3, tilegreen_wmb); // then change into a green tile
tile3.flag1 = off; // // reset all the flags
tile3.flag2 = off;
tile3.flag3 = off;
tile3.flag2 = on; // tile3 will be a green tile from now on
}
if (status1 == 3) // tile1 is a blue tile?
{
ent_morph(tile3, tileblue_wmb); // then change into a blue tile
tile3.flag1 = off; // // reset all the flags
tile3.flag2 = off;
tile3.flag3 = off;
tile3.flag3 = on; // tile3 will be a blue tile from now on
}
}

La dernière partie de la fonction contrôle la couleur du premier carré (status1) et change sa couleur en fonction de la couleur de son voisin (tile3 car tile3 doit passer de la droite de l'écran à la gauche de l'écran) Examinons encore 2 petites fonctions et nous en aurons terminé!

function
give_bonus()
{
logical_ended = 1; // the logical level has
ended
mouse_mode = 0; // // so hide the mouse
snd_play (victory_wav, 50, 0);
sleep (2);
minimum_bonus
+= temp_bonus; // add what's left of temp_bonus to minimum_bonus
while (minimum_bonus > 0)
{
snd_play (bonus_wav, 100, 0);
players_shield
+= 2;
players_ammo += 2;
minimum_bonus -= 2;
sleep (0.1);
}
sleep (1);
}

La fonction ci-dessus est appelée dès que le niveau est résolu. Elle cache la souris, joue une musique de victoire, attend 2 seconde et ajoute à la variable minimum_bonus (10) ce qui reste dans temp_bonus, qui contenait 100 au départ du niveau mais dont la valeur a diminué dans une boucle while au fur et à mesure du temps qui passait. Nous jouons un petit son et nous augmentons la valeur du bouclier et des munitions du joueur d'une valeur comprise entre 10 et 110.

function
logical_help()
{
logical_txt.visible = on;
sleep (5);
logical_txt.visible = off;
}

Cette dernière fonction s'exécute lorsque vous déplacez le pointeur de la souris au desus de l'humain et affiche un texte d'aide pendant 5 secondes. J'ai utilisé une fonte à espacement fixe pour ce texte, ce qui la fait paraître un peu berk mais c'est pour assurer une compatibilité avec la version A5 du moteur.


Untitled

Planet Survivors - partie 4

Ce mois-ci nous allons rencontrer nos premiers ennemis: 2 vaisseaux de l'espace et (ils sont incontournables dans un jeu de l'espace) quelques astéroïdes. Avant que je commence, gardez bien à l'esprit que ce projet est un mélange de jeux.

J'ai pitié de vous aussi j'ai décidé de faire un script autonome, de cette façon vous ne serez pas obligé de jouer aux niveaux précédents pour jouer à celui-ci. Vou trouverez le code de ce projet autonome au début du fichier level2.wdl; aussi assurez-vous de bien exécuter ce fichier script si vous voulez jouer au niveau.

Nous commençons par regarder le code qui génère ce magnifique effet de champ d'étoiles:

starter
particle_generator
{
var particle_start[3];
wait (3); // wait until the level is loaded
camera.pan = 0; //
now set the new camera position and angles
camera.tilt = -90;
camera.roll = 0;
camera.x = 330;
camera.y
= 0;
camera.z = 1000;
while(1)
{
particle_start.x = 1000;
particle_start.y = 1000 - random(2000);
particle_start.z
= -500 + random(200);
effect (particle_init,
1, particle_start, normal);
wait (1);
}
}

Nous avons défini une variable appelée particle_start; ce sera l'origine de nos particules. Vous pouvez voir que la caméra utilise une position fixe, regardant le vaisseau du joueur vers le bas (tilt = -90); nous donnons l'impression au joueur que le vaisseau se déplace dans le champ d'étoiles. Les étoiles sont générées par la fonction particle_init:

function
particle_init()
{
temp.x = -(2 + random (15)) * starfield_speed;
temp.y = 0;
temp.z = 0;
vec_add
(my.vel_x, temp);
my.bmap = star_tga;
my.alpha
= 40 + random(60); // play with this value
my.size = 5 + random(5);
my.bright = on;
my.move = on;
my.lifespan
= 500;
my.function = size_particles;
}

function
size_particles()
{
my.size += 0.01 * time;
}

Ces particules ont une valeur aléatoire négative sur l'axe des x; jouez avec starfield_speed si vous voulez changer la vitesse globale. J'ai utilisé l'image bitmap star_tga pour les étoiles et je leur ai mis un facteur de transparence et une taille aléatoires. Sa fonction associée size_particles() augmente simplement la taille des particules durant leur vie.

Il est temps de jeter un oeil à l'action attachée au joueur:

action
player_level2
{
player = my;
my.ambient =
-100;
my.enable_impact = on;
my.enable_entity
= on;
my.event = damage_player;
var speed_factor_x;
var speed_factor_y;

Le joueur est sensible à l'impact avec les autres entités; sa fonction "damage_player" est déclenchée dés qu'une telle collision se produit. Pour terminer nous avons défini 2 variables locales qui stockent la vitesse de déplacement dujoeur sur l'axe des x et des y.

  while (players_shield > 0)
{
if (key_cul == on)
{
if (speed_factor_y < 10) {speed_factor_y += 5 * time;}
}
else
{
if (speed_factor_y > 0) {speed_factor_y
-= 2 * time;}
}
if (key_cur == on)
{
if (speed_factor_y > -10) {speed_factor_y -= 5 * time;}
}
else
{
if (speed_factor_y < 0) {speed_factor_y
+= 2 * time;}
}
if (abs(speed_factor_y) > 1)
{
if (((speed_factor_y > 0) &&
(my.y < 500)) || ((speed_factor_y < 0) && (my.y > -500)))
{
my.y += speed_factor_y;
}
}

Tant que le joueur est en vie (players_shield > 0) le joueur peut augmenter doucement speed_factor_y en maintenant appuyé la touche de direction gauche; la vitesse décroit en douceur dés que le joueur relache la touche. La même chose se produit avec la touche de direction droite. Pourquoi utilisons-nous les variables speed_factor_x et speed_factor_y ? Principalement parce que de cette façon le vaisseau se déplace plus en douceur en augmentant ou diminuant sa vitesse en douceur. Si speed_factor_y est plus grand que 1 ou plus petit que -1, nous déplaçons le vaisseau sur le côté en utilisant les valeurs stockées actuellement dans speed_factor_y. Notez que le vaisseau peut se déplacer sur l'axe des y entre -500 et 500 quants.

     if (key_cuu == on)
{
if (speed_factor_x <
10) {speed_factor_x += 5 * time;}
}
else
{
if (speed_factor_x > 0) {speed_factor_x -= 2 * time;}
}
if (key_cud == on)
{
if (speed_factor_x >
-10) {speed_factor_x -= 5 * time;}
}
else
{
if (speed_factor_x < 0) {speed_factor_x += 2 * time;}
}
if (abs(speed_factor_x) > 1)
{
if (((speed_factor_x > 0) && (my.x < 650)) || ((speed_factor_x <
0) && (my.x > 0)))
{
my.x
+= speed_factor_x;
}
}

La même chose se passe sur l'axe des x; le joueur augmente et diminue speed_factor_x lorsque nous pressons les touches haut et bas. Si le joueur a son x plus grand que 0 et inférieur à 650 quants, ilpeut se déplacer à la vitesse stockée dans speed_factor_x.

if ((key_space == on) || (mouse_left == on))
{
player_fires();
}
wait (1);
}
}

Si le joueur presse la barre espace ou le bouton gauche de la souris, la fonction player_fires() est appelée:

function
player_fires()
{
proc_kill(4);
while ((key_space
== on) || (mouse_left == on)) {wait (1);} // wait until the player releases the
keys
snd_play (fire2_wav, 40, 0);
vec_set (temp.x,
my.x);
temp.x += 70; // the laser is generated in front of the
ship
ent_create (laser2_mdl, temp, move_bullet1);
players_ammo -= 1;
}

Lapremière ligne s'assure simplement qu'il n'y ait qu'une seule copie de cette fonction qui s'exécute. Nous attendons que le joueur relache la barre espace ou le bouton gauche de la souris, nous jouons le son fire2_wav, nous calculons une position 70 quants devant le joueur et nous créons une entité laser2_mdl en lui assignant la fonction appelée move_bullet1(). La dernière ligne de code diminue players_ammo parce que le joueur a tiré une balle.

function
damage_player()
{
wait (1);
snd_play (playerhit_wav,
70, 0);
players_shield -= 10;
if (players_shield
< 0)
{
players_shield =
0;
my.invisible = on;
my.passable = on;
effect(particles_explo2,
30, my.x,nullvector);
my.event = null;
}
}

La fonction ci-dessus est l'évènement associé au joueur. Lorsque le joueur est touché par une balle, par un des ennemis ou par un astéroïde, nous jouons un son playerhit_wav, nous diminuons players_shield, puis si players_shield est inférieur à 0 nous le mettons à 0, nous rendons le vaisseau invisible et passable, nous mettons player.event à null et nous créons un effet d'explosion en utilisant les particules:

function
particles_explo2()
{
my.bmap = explo2_tga;
my.bright = on;
my.transparent = on;
my.alpha
= 100;
my.move = on;
temp.x = 1 - random(2);
temp.y = 1 - random(2);
temp.z = 0;
vec_normalize
(temp.x, (1 + random(1)));
vec_add (my.vel_x, temp.x);
my.size = 2;
my.lifespan = 50;
my.function
= explode_2;
}

function
explode_2()
{
my.alpha -= 4 * time;
my.size
+= 3 * time;
if (my.alpha <= 0)
{
my.lifespan = 0;
}
}

L'image utilisée pour l'explosion est explo2_tga; C'est une image brillante, transparente avec son alpha de mis à 100. L'image se déplace dans une direction aléatoire sur l'axe des x et des y; elle est associée à la fonction explode_2 qui diminue l'alpha et augmente la taille de l'image, enlevant la particule dès que son alpha atteint 0decreases the alpha and increases the size of the bitmap, removing the particle if its alpha goes below zero.

Examinons à présent la fonction utilisée par les vaisseaux ennemeis:

// uses follow_player
action shuttle
{
my.ambient
= -100;
while (player == null) {wait (1);}
my.z
= player.z; // set the same height for the enemy ships and the player
var shuttle_speed;
my.skill10 = 1;
my.skill40
= 1; // I'm a ship
my.enable_impact = on;
my.event
= destroy_them;
shuttle_speed.x = -2 * time;
shuttle_speed.y = 0;
shuttle_speed.z = 0;
my.skill1
= 1 + int(random(70));

Le vaisseau attend que le joueur soit créé puis il met son z à la valeur z du joueur. Nous mettons skill10 à 1 (cette valeur est utilisée dans la fonction évènement), nous mettons skill40 à 1 (cette variable stocke l'identifiant de l'entité), nous rendons le vaisseau sensible à l'impact et nous lui associons la fonction d'évènement destroy_them(). Les vaisseaux se déplacent uniquement sur l'axe des x, ici une image de la vue de dessus dans Wed qui vous donnent les informations:

Il n'est pas question de vous montrer tout le niveau ici; libre à vous de le faire le plus grand possible en augmentant sa taille sur l'axe des x, placez plus de vaisseaux, d'astéroïdes etc. Revenons à notre code:

    while ((my.x > -300) &&
(my.skill10 == 1))
{
if (my.follow_player
== on)
{
vec_set(temp, player.x);
vec_sub(temp, my.x);
vec_to_angle(my.pan,
temp);
}
move_mode = ignore_you + ignore_passable;
ent_move
(nullvector, shuttle_speed);
if ((vec_dist
(my.x, player.x) < 800) && (total_frames % 90 == my.skill1)) // random
firing intervals
{
if (my.x > player.x) // still got the chance to hit the player?
{
snd_play (fire1_wav, 40, 0);
ent_create (laser_mdl, my.x, move_bullet1);
}
}
wait
(1);
}
ent_remove (my);
}

Tant que les vaisseaux ne se sont pas déplacés jusqu'à l'arrière du joueur et qu'ils n'ont pas été détruits (leur skill10 = 1), si nous avons le drapeau follow_player de mis à on, les vaisseaux se tourneront en direction du vaisseau du joueur. C'est ce qui différencie les 2 types de vaisseaux ennemis: l'un d'eux tire toujours le long de son axe x, touchant le joueur uniquement s'il se trouve sur le même x tandis que l'autre se tourne vers le joueur, essayer de le toucher devient plus difficile :). Les vaisseaux avancent ignorant les objets passables et tirent dès qu'ils sont à moins de 800 quants du joueur et si total_frames % 90 est égal à une valeur aléatoire stockée dans skill1. En clair celasignifie que le vaisseau tire toute les 1.5 secondes (nous avons limité le taux d'affichage à 60 fps) mais le moment du premier tir est différent pour chacun des vaisseaux. Si la coordonnée x du vaisseau est supérieure au x du joueur, nous jouons un son fire1_wav, et nous créons une entité laser_mdl, lui associant la fonction move_bullet1():

function
move_bullet1()
{
var bullet_speed;
my.passable
= on;
my.enable_impact = on;
my.enable_entity
= on;
my.event = destroy_bullet;
my.pan = you.pan;
bullet_speed.x = 50 * time;
bullet_speed.y = 0;
bullet_speed.z = 0;
my.ambient = 100;
my.bright = on;
my.light = on;
my.red = 50;
my.green = 255;
my.blue = 50;

Cette fonction est utilisée par les balles du joueur et par les vaisseaux ennemis. La balle est passable au départ et elle est rendue sensible à l'impact avec les autres entités, sa fonction d'évènement étant destroy_bullet. La balle prend le même angle pan que son créateur (le joueur ou le vaisseau ennemi) et sa vitess est 50 * time. Nous utilisons quelques lumières colorées pour le modèle qui seront utilisées pour la balle.

  while (my != null) // as long as the bullet exists
{
if (you != null)
{
if(vec_dist (my.x, you.x) > 50)
{
my.passable = off;
}
}
move_mode = ignore_you
+ ignore_passable;
ent_move (bullet_speed,
nullvector);
wait (1);
}
}

Tant que la balle existe et si son créateur continue d'exister et que la distance entre la balle et son créateur est supérieure à 50 quants, la balle devient impassable. La balle continue de se déplacer ignorant son créateur et toutes les entités passables. La fonction d'évènement associée à la balle joue un son et enlève la balle:

function
destroy_bullet()
{
wait (1);
snd_play (explo1_wav,
30, 0);
ent_remove (my);
}

Regardons à présent le code des astéroïdes:

action
asteroid
{
var asteroid_speed;
my.metal =
on;
my.ambient = -random(100);
my.scale_x =
2 + random(5);
my.scale_y = 2 + random(5);
my.scale_z
= 2 + random(5);
my.skill40 = 2; // I'm an asteroid
while (player == null) {wait (1);}
my.z = player.z; //
set the same height for the enemy ships and the player
my.enable_impact
= on;
my.event = destroy_them;
asteroid_speed.x
= -2 * time;
asteroid_speed.y = 0;
asteroid_speed.z
= 0;
my.skill10 = 1;
my.skill1 = 4 - random(8);
my.skill2 = 4 - random(8);
my.skill3 = 4 - random(8);
while ((my.x > -300) && (my.skill10 == 1))
{
move_mode = ignore_you + ignore_passable;
ent_move (nullvector, asteroid_speed);
my.pan += my.skill1 * time;
my.tilt += my.skill2
* time;
my.roll += my.skill3 * time;
wait (1);
}
ent_remove
(my);
}

Ces astéroïdes ont une taille et un ambient aléatoires, leur skill40 est mis à 2, ce qui nous permet de savoir si telle entité est un astéroïde ou non. Les asteroïdes attendent que le joueur soit créé, puis mettent leur hauteur à la hauter du joueur ce qui nous assure qu'ils entreront bien en ciollision avec le joueur même s'il change de hauteur :). Les asteroîdes utilisent la même mystérieus fonction destroy_them, et se déplacent en direction du joueur à la vitesse donnée par -2 * time. Nous générons 3 vitesses aléatoires de rotation pour pan, tilt et roll et nous les stockons dans skill1... skill3. Les asteroïdes se déplacent et tournent dans une boucle while qui continue de s'exécuter si leur x est plus grand que -300 quants et si leur skill10 continue à être mis à 1 (les asteroïdes n'ont pas été détruits).

Bien, examinons la fonction qui peut détruire le vaisseau du joueur; les vaisseaux ennemis et les astéroïdes:

function
destroy_them()
{
my.skill10 = 0;
snd_play
(explo1_wav, 50, 0);
if (my.skill40 == 1) // a ship was hit?
{
score_value += 100; // get 100 points
for a ship
effect(particles_explo2, 30, my.x,nullvector);
}
if (my.skill40 == 2) // hit one of the asteriods?
{
score_value += 50; // get 50 points
for an asteroid
effect(particles_explo1, 20,
my.x, nullvector);
}

Si le joueur ou un vaisseau ennemi ou un astéroïde est touché, il met son skill10 à 0 (ce qui signifie qu'il est détruit). Nous jouons le son explo1_wav sound et nous testons l'identifiant (skill40) de 'entité: si skill40 = 1,nous avons touché un vaisseau ennemi aussi nous augmentons score_value de 100 et nous générons quelques particules en utilisant la fonction dont nous avons déjà parlé, particles_explo2. Si skill40 = 2, nous avons touché un asteroïde aussi nous augmentons score_value de 50 et nous générons des particules en utilisant la fonction particles_explo1(). Je ne vous parlerais pas du code de cette fonction car elle est identique à la fonction particles_explo2(), utilisant simplement une autre image et un autre jeu de valeurs numériques. Ici l'action des 2 fonctions particule:

Je vous revoie tous le mois prochain.


Untitled

Planet Survivors - partie 5

Ce mois-ci l'action va consister à apprendre à créer 3 nouveaux types d'ennemis, incluant un boss à la fin du niveau. Avant que je commence, faisons une petite liste de ce qui change:

-Les balles du joueur sont détruites si leur x est supérieur à 700 quants; de cette façon il ne peut pas tuer les ennemis qui sont très loin, il devra attendre de les voir. Regardez la fonction move_bullet1() dans level2.wdl pour voir ce qu'elle fait.

- Ce niveau est énorme à présent (40,000 quants) parce que nous avons ajouté de nouveaux ennemis.

Voici l'action qui dirigent les nouveaux premiers ennemis:

action
shuttle2
{
my.ambient = 30;
while (player
== null) {wait (1);}
my.z = player.z; // set the same height
for the enemy ships and the player
var shuttle_speed;
my.skill10 = 1;
my.skill40 = 4; // I'm a ship
my.enable_impact = on;
my.event = destroy_them;
shuttle_speed.x = -2 * time;
shuttle_speed.y = 0;
shuttle_speed.z = 0;
while ((my.x > -300) &&
(my.skill10 >= 0))
{
if
((vec_dist (my.x, player.x) < 800) && (total_frames % 90 == 1))
{
if (my.x > player.x) // still got the chance to hit the player?
{
snd_play
(fire1_wav, 40, 0);
vec_set (temp.x, my.x);
temp.x -= 30;
temp.y -= 83;
ent_create (bolt_mdl, temp.x, move_bullet2);
vec_set (temp.x, my.x);
temp.x -= 30;
temp.y += 83;
ent_create (bolt_mdl, temp.x, move_bullet2);
}
}
move_mode
= ignore_you + ignore_passable;
ent_move (nullvector,
shuttle_speed);
wait (1);
}
ent_remove (my);
}

Cette navette attend que le joueur apparaîsse dans le niveau puis il ajuste sa hauteur s'assurant qu'il puisse (lui ou ses balles) entrer en collision avec le joueur s'il tente sa chance :). Nous mettons skill40 à 4 pour cette navette car nous voulons lui donner un identifiant qui sera utilisé dans la fonction évènement destroy_them.

Le vaisseau se déplace jusqu'à ce que son x soit à moins de -300 quants (sinon le joueur ne peut pas le voir) si son skill10 est supérieur ou égal à 0. Nous devons le toucher 2 fois si nous voulons détruire une de ces navettes. Si le vaisseau est à moins de 800 quants du joueur il peut commencer à tirer toutes les 1.5 secondes car le taux d'affichage est bloqué à 60 fps et nous avons vérifié total_frames % 90, pour que cela se passe chaque 90 / 60 = 1.5 secondes. J'ai dit que le vaisseau "pouvait" commencer à tirer car il tire uniquement si son x est plus grand que le x du joueur (sa position est au dessus de celle du joueur). Si ceci est correct nous jouons le son fire1_wav, nous mettons une position temporaire pour le premier projectile, nous le créons et nous faison sla même chose pour le deuxième projectile. Ces projectiles sont gérés par la fonction move_bullet2(). Oh, avant de voir cette fonction sachez que la navette ignore les entités passables et est enlevée si son x devient inférieur à -300 ou s'ils sont touchés 2 fois.

function
move_bullet2()
{
var bullet_speed;
my.passable
= on;
my.enable_impact = on;
my.enable_entity
= on;
my.event = destroy_bullet;
my.pan = you.pan;
bullet_speed.x = 50 * time;
bullet_speed.y = 0;
bullet_speed.z = 0;
my.ambient = 100;
my.bright = on;
my.light = on;
my.red = 50;
my.green = 255;
my.blue = 50;
while (my
!= null) // as long as the bullet exists
{
if (you != null)
{
if(vec_dist (my.x, you.x) > 50)
{
my.passable
= off;
}
}
my.roll += 200 * time;
move_mode = ignore_you + ignore_passable;
ent_move (bullet_speed, nullvector);
wait (1);
}
}

Notez que la balle est passable au départ puis devient impassable dès que la distance entre elle et son créateur (la navette ici) est supérioeure à 50 quants. Les projectiles tournent à la vitesse donnée par 200 * time; jouez avec cette valeur si vous voulez des vitesses différentes pour vos projectiles.

Le second vaisseau est un peu plus intéressant; il crée une sphère de particules qui rougeoie encore et encore puis disparait détruisant toutes les entités (joueur, vaisseau, astéroïdes) dans sa zone. Examinons son code:

action
shuttle3
{
my.ambient = 30;
while (player
== null) {wait (1);}
ent_create (sphere_mdl, my.x, move_sphere);
my.z = player.z; // set the same height for the enemy ships and the player
var shuttle_speed;
my.skill10 = 1;
my.skill40
= 1; // I'm a ship
my.enable_impact = on;
my.event
= destroy_them;
shuttle_speed.x = -2 * time;
shuttle_speed.y = 0;
shuttle_speed.z = 0;
while
((my.x > -300) && (my.skill10 == 1))
{
move_mode = ignore_you + ignore_passable;
ent_move
(nullvector, shuttle_speed);
wait (1);
}
ent_remove (my);
}

La deuxième navette crée une entité sphere_mdl à sa position et lui assigne la fonction move_sphere. Le vaisseau se déplace jusqu'à ce que son x atteigne -300, si iol reste en vie pendant tout ce temps (si my.skill10 continue d'être à 1).

function
move_sphere()
{
proc_late();
var index;
var vertices;
my.passable = on;
my.invisible
= on;
vertices = ent_vertices(my);
while (you
!= null)
{
vec_set (my.x,
you.x);
if (you.x < 800)
{
if (my.scale_x == 1)
{
snd_play (scanning_wav, 60, 0); // this sound will be played only once
}
my.scale_x += 1 * time;
my.scale_x
= min(30, my.scale_x); // limit the scale to 30
my.scale_y = my.scale_x;
my.scale_z = my.scale_x;
temp.pan = 360;
temp.tilt
= 360;
temp.z = my.scale_x
* 37; // 37 = experimental value, must fit the size of the particle sphere
scan_entity (camera.x, temp);
}

La sphère est invisible et passable; nous obtenons son nombre de sommets, et nous utilisons une boucle qui durera tant que la navette existera. La boucle met les mêmes coordonnées au vaisseau et à la sphère invisible. Si le vaisseau a son x inférieur à 800 quants, si le facteur d'échelle de la spère est 1 (ce qui ne se produit qu'une fois) le son qui donne la chair de poule scanning_wav est joué. La sphère augmente sa taille jusqu'à atteindre 30, et utilise une instruction scan_entity à chaque frame, utilisant un rayon d'action qui augmente à chaque frame. Vous devrez jouer avec la valeur 37, c'est une valeur expérimentale.

    index = 0;
while (index < vertices)
// while without wait!
{
vec_for_vertex (temp, my, index);
effect (particle_effect3, 10, temp, normal);
index += 1;
}
my.pan
+= 3 * time;
my.tilt += 3 * time;
my.roll += 3 * time;
wait (1);
}
ent_remove (my);
}

L'effet visuel est créé par les lignes ci-dessus .nous utilisons une boucle rapide (sans wait) pour passer à travers tous les sommets de la spère et nous générons les particules en utilisant la fonctipn particle_effect. Pendant tout ce temps la spère tourne en changeant ses angles pan, tilt et roll.

function
particle_effect3()
{
temp.x = 5 - random (10);
temp.y = 0;
temp.z = 0;
vec_add (my.vel_x,
temp);
my.bmap = glow_tga;
my.alpha = 80;
my.size = 5;
my.bright = on;
my.move
= on;
my.lifespan = 1;
my.function = destroy_particles;
}

function
destroy_particles()
{
my.alpha -= 10 * time;
if (my.alpha < 0)
{
my.lifespan
= 0;
}
}

Ici rien de spécial. Les particules sont détruites dès que leur alpha atteint 0

Prêt pour dévorer le boss à la fin du niveau?

action
boss_level2
{
my.ambient = -30;
while (player
== null) {wait (1);}
my.z = player.z; // set the same height
for the enemy ships and the player
var boss_speed;
my.skill10 = 1;
my.skill20 = 100;
my.skill40
= 3; // I'm the boss
boss_speed.x = -2 * time;
boss_speed.y = 0;
boss_speed.z = 0;
while (my.x
> 650)
{
move_mode = ignore_you
+ ignore_passable;
ent_move (nullvector, boss_speed);
wait (1);
}
my.enable_impact
= on;
my.event = destroy_them;
boss_speed.x
= 0;
boss_speed.z = 0;
while (my.skill20 >
0)
{
if (player.y > my.y)
{
my.y += 5 * time;
}
else
{
my.y -= 5 * time;
}

Le boss commence son déplacement en direction du joueur jusqu'à ce que son x soit inférieur à 650 quants, puis il s'arrête devant sensible à l'impact des balles du joeur. Le boss essaye de se caler sur le y du joueur en se déplaçant vers la droite ou la gauche en fonction des déplacements du joueur.

     if (((my.y - player.y) < 200) && (total_frames
% 40 == 1))
{
if (player.invisible == on) {break;}
vec_set (temp.x, my.x);
temp.x
-= 120;
snd_play (fire1_wav,
40, 0);
ent_create (laserboss_mdl,
temp.x, move_bullet3);
wait
(1);
vec_set (temp.x, my.x);
temp.x -= 40;
temp.y += 145;
snd_play (fire1_wav,
40, 0);
ent_create (laserboss_mdl,
temp.x, move_bullet3);
wait
(1);
vec_set (temp.x, my.x);
temp.x -= 40;
temp.y -= 145;
snd_play (fire1_wav,
40, 0);
ent_create (laserboss_mdl,
temp.x, move_bullet3);
}
wait (1);
}
ent_remove (my);

Si le boss est très proche du joueur sur l'axe des y il commence à tirer toutes les 40 frames,ce qui correspond à 40 / 60 = 0.6 seconds. Si le joueur est invisible (il a été détruit) le boss sort de la boucle s'enlevant lui même du niveau. Si le joueur n'est pas encore mort le boss crée 3 spots de feu (correspondant à ses 3 armes) et crée 3 entités laserboss_mdl, leur attachant la fonction move_bullet3.

sleep (3);
my = null;
level_load (level3_wmb);
wait (3);
camera.pan = 0; // set the correct camera position and angles
camera.tilt = -90;
camera.roll = 0;
camera.x = 330;
camera.y = 0;
camera.z = 1000;
}

Le boss est dédruit ici aussi c'est la fin du niveau. Nous attendons 3 secondes puis nous chargeons le niveau suivant (que nous verrons le mois prochain) nous attendons qu'il soit chargé et nous donnons la nouvelle positions et les nouveaux angles à la caméra. Revenons à la fonction qui gère les balles tirées par le boss:

function
move_bullet3() // used by the boss at the end of the level
{
var bullet_speed;
my.passable = on;
my.enable_impact
= on;
my.enable_entity = on;
my.event = destroy_bullet;
my.pan = you.pan;
bullet_speed.x = 35 * time;
bullet_speed.y = 0;
bullet_speed.z = 0;
my.ambient = 100;
my.bright = on;
my.light =
on;
my.red = 50;
my.green = 255;
my.blue = 50;
while (my != null) // as long as the bullet exists
{
if (you != null)
{
if(vec_dist (my.x,
you.x) > 50)
{
my.passable = off;
}
}
if (vec_dist(player.x, my.x) > 400) //
not too close to the player?
{
vec_set(temp, player.x); // rotate the bullet towards player's ship
vec_sub(temp, my.x);
vec_to_angle(my.pan, temp);
}
move_mode = ignore_you + ignore_passable;
ent_move
(bullet_speed, nullvector);
wait (1);
}
}

La seule partie du code qui nécessite des explications est la boucle while qui s'exécute aussi longtemps que la balle existe. Si le créateur de la balle (le boss) est vivant et la distance entre le boss et les balles inférieures à 50 quants, la balle est passable; sinon elle est impassable. Si les balles sont éloignées du joueur (plus de 400 quants) elles sont tournées vers le vaisseau du joueur ce qui les transforme en missiles à tête chercheuse rendant la tâche plus difficile pour le joueur.

function
destroy_them()
{
snd_play (explo1_wav, 50, 0);
if (my.skill40 == 1) // a ship was hit?
{
my.skill10 = 0;
if (you.skill40 == 99) // if
it was player's bullet
{
score_value += 100; // get 100 points for a ship
}
effect(particles_explo2, 30, my.x, nullvector);
}
if (my.skill40 == 2) // hit one of the asteriods?
{
my.skill10 = 0;
if (you.skill40 == 99) // if it was player's bullet
{
score_value
+= 50; // get 50 points for an asteroid
}
effect(particles_explo1, 20, my.x, nullvector);
}

Cette fonction qui détruit tout a quelque peu changée aussi j'ai décidé de vous la montrer de nouveau. Si skill40 est mis à 1, le joueur a touché un vaisseau aussi skill10 est mis à 0, détruisant le vaisseau. Nous vérifions si le vaisseau a été touché par une balle du joueur (you.skill40 = 99) et seulement si c'est le cas nous ajoutons 100 points au score. Pour terminer nous générons un effet de particules pour l'explosion. La même chose se produit si nous touchons un astéroïde; le joueur n'a pas de points si les astéroïdes sont touchés par les vaisseaux ennemis.

  if (my.skill40 == 3) // hit the boss at the end of the level?
{
my.skill10 = 0;
if (you.skill40 == 99) // if it was player's bullet
{
score_value
+= 100; // get 100 points for a shot
}
my.skill20 -= 10; // decrease the shield for the boss
if (my.skill20 <= 0)
{
snd_play (bossexplo_wav,
100, 0);
effect(particles_explo3,
100, my.x, nullvector); // similar to the other particle explosions
}
}
if (my.skill40
== 4) // shuttle2 was hit?
{
my.skill10 -= 1;
if (my.skill10 < 0)
{
if (you.skill40 == 99) // if it was player's bullet
{
score_value += 200; // get 200 points for shuttle2
}
effect(particles_explo2,
50, my.x, nullvector);
}
}
}

Si le joueur a touché le boss à la fin du niveau (skill40 = 3) il obtient 100 points à chaque tir car le boss ne sera dédruit qu'après avoir été touché 10 fois. Pour terminer si le joueur détruit une des 2 navettes il obtient 200 points car la navette a besoin d'être touchée 2 fois.

action
player_level3
{
while (1)
{
my.roll += 4 * time;
wait (1);
}
}

L'action ci-dessus démontre que le 3eme niveau est chargé lorsque nous tuons le boss à la fin du niveau. Ne soyez pas inquiet nous aurons une action utile pour le joueur le mois prochain, dans un environnement et (je l'espère) excitant.


Untitled

Planet Survivors - partie 6

Ce mois-ci le joueur crashe son vaisseau et il est obligé de se poser sur une nouvelle planète, sauter hors de son vaisseau, nous lui permettrons d'exécuter quelques mouvements afin de pouvoir explorer le niveau et nous lui donnerons une arme avec des munitions illimitées (pour le moment). Le niveau a été créé par Nemisis, mon partenaire pour Planet Survivor :) et vous trouverez toutes les cartes et les wads pour cela à l'intérieur de l'archive. Examinons les nouvelles actions et fonctions qui sont dans le fichier level2.wdl.

function
ship_explosion(coordinates)
{
var number_of_gibbits;
effect (explosion_core, 10 + random(10), coordinates, nullvector);
sleep (0.3);
ent_create (explosion_pcx, coordinates, explosion_sprite2);
sleep (0.1);
number_of_gibbits = random(15) + 5;
while (number_of_gibbits > 0) // while without wait!
{
ent_create (gibbit_mdl, coordinates, move_gibbits);
number_of_gibbits -= 1;
}
}

Le joueur entre dans un champ d'astéroïdes et cette fois les astéroïdes qui sont en métal sont indestructibles donc bien entendu votre vaisseau est sévèrement endommagé et vous verrez un joli effet d'explosion. Je n'ai pas l'intention de vous explmiquer ici comment il fonctionne, il utilise un des effets proposé dans un autre article de ce magazine..

action
metal_asteroid // unbreakable asteroids
{
while (first_boss
!= null) {wait (1);}
sleep (3);
my.x -= 33000;
my.scale_x = 2 + random(5); // set a random scale on x, y, z
my.scale_y = 2 + random(5);
my.scale_z = 2 + random(5);
asteroid_speed.x = -15 * time; // the metal asteroids have a big speed
asteroid_speed.y = 0;
asteroid_speed.z = 0;
my.skill1 = 4 - random(8); // set random rotation (pan, tilt, roll) speeds
my.skill2 = 4 - random(8);
my .skill3 = 4 - random(8);
while (my.x > -300)
{
move_mode = ignore_you + ignore_passable;
ent_move (nullvector, asteroid_speed);
my.pan += my.skill1 * time;
my.tilt +=
my.skill2 * time;
my.roll += my.skill3
* time;
wait (1);

Ces asteroides attendent que le premier boss (le gros vaisseau du niveau précédent) soit détruit; vous pouvez mettre cette ligne en commentaire si vous voulez allez directement à la fin du niveau. Lorsque le boss est détruit, les astéroïdes commencent à s'approcher du vaisseau du joueur de 33,000 quants car ils étaient immobiles et ont besoin de couvrir un grand espace rapidement. Nous mettons une échelle aléatoire (2...7) sur les axes x, y et z pour chaque asteroïde, puis nous mettons leur vitesse sur l'axe des x. Nous choissons une vitesse de rotation aléatoire (-4...4) pour les angles pan, tilt et roll puis nous démarrons le mouvement des astéroïdes dans une boucle "while".

if ((vec_dist (player.x, my.x) < 100) && (ship_down == 0))
{
ship_down = 1;
players_shield = 0;
ship_explosion (vector(player.x, player.y, player.z + 20));
snd_play (explostart_wav, 100, 0);
while (player.z > -10000)
{
player.z -= 100 * time;
player.roll += 25 * time;
wait (1);
}
level_load (level3_wmb);
}
if (ship_down
== 1)
{
my.passable = on;
}
}
ent_remove (my); // remove the asteroids
as soon as they can't be seen
}

Si le joueur s'approche à moins de 100 quants de l'un des astéroïdes et que cela se produise pour la première fois, le vaisseau sera réduit et players_shield sera mis à zéro. Ceci aura pour effet que nous perdrons le contrôle du vaisseau et le joueur ne pourra plus se déplacer. Nous créons une explosion 20 quants au dessu sdu vaisseau, nous jouons un son puis nous démarrons la descente du vaisseau changeant son angle roll en même temps. Le niveau suivant (level3_wmb) dès que la hauteur du vaisseau est inférieure à -10,000 quants. Tous les astéroïdes sont rendus passables dès la descente du vaisseau de cette façon il ne se passe rien s'ils se cognent. La dernière ligne de code enlève les asteroïdes s'ils ne sont plus visibles (leur x est inférieur à -300 quants).

action
halo_flare
{
my.ambient = 100;
my.transparent
= on;
my.bright = on;
my.light = on;
my.lightred = 255;
my.lightgreen = 255;
my.lightblue = 255;
my.lightrange = 0;
my.facing
= on;
my.passable = on;
while(1)
{
my.alpha = min(100, vec_dist(my.x,
camera.x) / 20);
my.scale_x = min(3,
my.alpha / 50);
my.scale_y = my.scale_x;
wait(1);
}
}

action
crystal_glow
{
my.transparent = on;
my.bright
= on;
my.ambient = 100;
my.light = on;
my.lightred = 255;
my.lightgreen = 255;
my.lightblue = 100;
my.lightrange = 0;
}

Ces 2 actions contrôlent les sprites lumineux et les ampoules qui peuvent être observés dans la première image de cette page. J'ai utilisé des sprites passables illuminés par une lumière complètement blanche (RGB = 255 255 255) et faisant face tout le temps à la caméra pour les éclats. Nous obtenons leur distance à la caméra dans une boucle et nous modulons leur transparence en fonction d'elle. La taille de ces éclats peut être augmentée jusqu'à 3 fois. L'ampoule a un éclairage jaune.

starter
level3_camera()
{
while (level_number != 3) {wait (1);}
while (player == null) {wait (1);}
camera.x = 20000;
camera.y = 9600;
camera.z = -500;
camera.pan
= 110;
camera.tilt = 0;
camera.roll = 0;
}

Les lignes ci-dessus mettent la caméra en position pour le 3ème niveau. J'ai décidé d'utiliser une fonction "starter" aussi elle démarre dès que nous exécutons le jeu c'est pourquoi j'utilise 2 boucles while l'une attendant que nous soyons bien dans le 3ème niveau et l'autre que le joueur soit bien chargé. Le reste des lignes met la bonne position et les bons angles pour la caméra.

action
player_level3
{
player = my;
my.ambient
= -50;
level_number = 3;
my.scale_x = 5;
my.scale_y = my.scale_x;
my.scale_z = my.scale_x;
my.x = 10000;
my.y = 28000;
my.z = 8000;
my.pan = 295; // and angles
my.tilt = -50;
my.skill1 = 15000;
while (my.skill1 > 100)
{
vec_set (temp, my.x);
temp.z -= 15000;
trace_mode = ignore_me +
ignore_you + ignore_passable;
my.skill1 =
trace (my.x, temp);
my.roll += 20 * time;
my.x += 70 * time;
my.y -= 125 * time;
my.z -= 120 * time;
effect (particle_smoke, random(10) + 5, my.x, nullvector);
wait (1);
}

Cette action est attachée au vaisseau tombant dans le 3ème niveau. Nous mettons la bonne position et les bons angles pour le vaisseau, nous mettons sa variable skill1 à une très grande valeur (15,000) puis nous exécutons le mouvement de descente tant que la distance entre le vaisseau et le terrain est supérieure à 100 quants. C'est une méthode simple et effective qui permet au vaisseau d'atterrir dans une certaine zone sans utiliser de chemins prédéfinis ou d'autres méthodes qui fonctionneent très mal de par la grande vitesse. Nous tournons le vaisseau en augmentant son angle roll et nous générons de la fumée en utilisant la fonction particle_smoke du script explosions.wdl; lisez l'article de ce magazine à propos des explosions avancées pour plus d'information.

   snd_play (explomid_wav, 20, 0);
my.skill1
= 0;
while (my.skill1 < 100)
{
my.skill1 += 10 * time;
camera.roll =
2 - random(4);
wait (1);
}
sleep (3);
ent_create (myhero_mdl, my.x, move_player);
while (player_1st == null) {wait (1);}
while
(vec_dist (player_1st.x, my.x) < 2000)
{
effect (particle_smoke, random(3) + 2, vector(my.x, my.y + 80, my.z + 200), nullvector);
wait (1);
}
}

Le vaisseau s'est posé aussi nous jouons le son explomid_wav et nous mettons skill1 à 0, car nous voulons l'utiliser comme compteur à l'intérieur d'une boucle qui secouera la caméra légèrement en changeant son angle roll à chaque frame. Nous attendons 3 secondes de plus, nous créons le joueur 1ère personne et nous lui attachons la fonction "move_player". Nous attendons que le joueur apparaîsse dans le niveau et nous créons une légère fumée jusqu'à ce que le joueur se soit éloigné de 2000 quants du vaisseau.

Examinons le code qui déplace le joueur:

function
move_player()
{
var players_speed;
var track2_playing
= 0;
player_1st = my;
player = null;
my.invisible = on;
players_shield = 25;
players_ammo = 30;
my.x = 14830;
my.y = 19420;
my.z = -80;
my.pan = 300;
my.tilt = -50;
sleep (1);
camera.x = my.x;
camera.y
= my.y;
camera.z = my.z;
camera.pan = my.pan;
camera.tilt = my.tilt;
snd_loop (wind3_wav, 15, 0);
snd_play (smoked_wav, 100, 0);
sleep (3);

Notre joueur est passé d'un vaisseau à un humain, n'est-ce pas? Nous utiliserons le nouveau pointeur player_1st pour le joueur à partir de maintenant. Le joueur est invisible (nous n'avons pas besoin de le voir en 1ère personne) et il a 25 points de bouclier et 30 balles; il a aété blessé pendant son atterrissage mais je suis prêt à parier qu'il trouvera des packs de santé le mois prochain :). Nous choisissons une bonne position et de bons angles pour le joueur, proches du haut du vaisseau car nous voulons donner l'illusion qu'il sort du vaisseau. Cette position et ces angles (copiés de la caméra) nous montrent également que nous sommes dans un cockpit enfumé.

Nous commençons à jouer le son wind3_wav sound dans une boucle à l'aide de l'instruction snd_loop qui boucle un son à la perfection et nous jouons le son "cough cough" pendant quelques secondes pour terminer.

   while (my.x < 15000)
{
my.x += 20 * time;
my.y -= 40 * time;
my.z -= 25 * time;
my.tilt += 2 * time;
camera.x = my.x;
camera.y = my.y;
camera.z = my.z;
camera.tilt = my.tilt;
wait (1);
}
snd_play (jump_wav, 80, 0);
while (my.tilt < 0)
{
my.tilt += 3 * time;
camera.tilt = my.tilt;
wait (1);
}
while (players_shield > 0)
{
players_speed.x = 15 * (key_w - key_s)
* time;
players_speed.y = 10 * (key_a
- key_d) * time;
vec_set (temp, my.x);
temp.z -= 5000;
trace_mode = ignore_me + ignore_passable + use_box;
players_speed.z = -trace (my.x, temp);
my.pan -= 5 * mouse_force.x;
my.tilt += 5
* mouse_force.y;

Le joueur saute sur le sol en diminuant son z; nous jouons le son jump_wav et nous augmentons l'angle tilt jusqu'à ce que la caméra soit correcte. Tant que le joueur est en vie nous pouvons le déplacer à l'aide des touches "Z", "S", "Q", "D" et nous nous maintenons sur le sol en traçant 5,000 quants sous ses pieds et en ajustant la hauteur. Pour terminer nous pouvons observer autour de nous à l'aide de la souris.

       if (mouse_left == 1)
{
players_bullet();
// then fire a bullet!
}
move_mode = ignore_passable + ignore_passents + glide;
ent_move(players_speed, nullvector);
camera.x
= my.x;
camera.y = my.y;
camera.z = my.z + 30;
camera.pan = my.pan;
camera.tilt = my.tilt;
if (((my.z <
-750) || (mouse_left == 1)) && (track2_playing == 0))
{
track2_playing
= 1;
media_loop("track2.wav",
null, 100);
}
wait (1);
}
my.skill46 = 0;
while (my.skill46 < 80)
{
ent_cycle("death", my.skill46);
my.skill46
+= 2 * time;
wait (1);
}
}

Si le joueur presse le bouton gauche de la souris (LMB) nous exécutons la fonction "players_bullet". La caméra est placée 30 quants au dessus de l'origine du joueur et elle utilise les mêmes angles pan et tilt que le joueur. Si la hauteur du joueur approche de -750 quants (le joueur s'est approché de la première base) ou si le joueur a tiré une balle nous jouons le son "track2.wav" dans une boucle. Les lignes à la fin de l'action s'exécuterons uniquement si le joueur est mort, ce qui n'est pas le cas pour le moment; elles jouent tout simplment l'animation de mort dans une boucle.

function
players_bullet()
{
proc_kill(4);
while (mouse_left
== 1) {wait (1);}
snd_play (bullet_wav, 50, 0);
ent_create (pbullet_mdl, player_1st.x, move_players_bullet); // create player's
bullet
}

La fonction ci-dessus s'exécute à chaque fois que nous pressons le bouton gauche de la souris. Nous nous assurons que cette fonction ne s'exécute qu'une à la fois puis nous attendons que le joueur relâche le bouton. Nous jouons un son bullet_wav, et nous créons le modèle de la balle en lui attachant la fonction "move_players_bullet".

function
move_players_bullet()
{
my.pan = you.pan; // the bullet and
the player have the same pan angle
my.tilt = you.tilt; // and
tilt angle
my.passable = on; // at first
my.enable_entity
= on; // the bullet is sensitive to other entities
my.enable_impact
= on;
my.enable_block = on; // and to level blocks
my.event = remove_bullet;
my.ambient = 100;
my.light
= on;
my.lightred = 150;
my.lightgreen = 150;
my.lightblue = 255;
while (my != null)
{
if (vec_dist (player_1st.x, my.x) > 30) {my.passable = off;}
my.skill1 = 100 * time; // bullet speed
my.skill2
= 0;
my.skill3 = 0;
move_mode = ignore_you + ignore_passable;
ent_move
(my.skill1, nullvector);
my.roll += 100 * time;
wait (1);
}
}

La balle a les mêmes angles pan et tilt que leur créateur (le joueur). Nous la rendons passable au début, sensible à l'impact avec les autres entités ou les blocs de niveau. Sa fonction d'évènement est appelée "remove_bullet()". La balle se déplace en utilisant une boucle while, devient impassable dès que sa distance au joueur est supérieure à 30 quants. Oh, et la balle tourne sur elle-même en changeant son angle "roll".

function
remove_bullet()
{
my.event = null;
ent_playsound
(my, phit_wav, 1000); // player's bullet hits something
sleep
(0.4);
ent_remove (me);
}

C'est la fonction évènement pour la balle: elle met son event à 0, joue un son à la position de l'impact, attend un peu et enlève la balle pour de bon.

function
toggle_passable()
{
player.passable = (player.passable ==
off);
if (player.passable == on)
{
player.enable_scan = off;
player.transparent = on;
player.alpha
= 30;
}
else
{
player.enable_scan = on;
player.transparent
= off;
player.alpha = 100;
}
}

on_t = toggle_passable;

Cette fonction vous permet de rendre le vaisseau du joueur invincible durant le deuxième niveau vous permettant d'atteindre le 3ème niveau sans encombres. Pressez la touche"T" dès que le niveau est chargé et le joueur deviendra passable aussi les ennemis, les balles ou les astéroïdes passeront à travers lui. N'oublions pas que certains ennemis utilisent "scan_entity" pour blesser le joueur aussi nous avons inhiber également le enable_scan du joueur. Vous noterez que le joueur est également transparent si vous l'aviez fait transparent; Pressez de nouveau "T" pour rendre de nouveau le joueur vulnérable et opaque.


Untitled

Planet Survivors - part7

Ce mois-ci le joueur va acquérir une autre façon de se déplacer en utilisant une action légèrement modifiée qui inclut l'inertie et le frottement, prendre du plaisir avec des packs de santé et de munitions et rencontrer le premier ennemi sur la planète, une fourmi géante,qui tire des projectiles de feux mortels. Je ne discuterais pas des modifications concernant le code du mouvement car ils restent très simple et nous en avons déjà discuté dans des magazines précédents.

action
health_pack
{
my.passable = on;
my.metal
= on;
while (player_1st == null) {wait (1);}
while (vec_dist(player_1st.x, my.x) > 70)
{
my.pan += 3 * time;
wait (1);
}
snd_play (pickedup_wav, 50, 0);
ent_remove
(my);
players_shield += 20;
players_shield
= min(200, players_shield);
}

action
ammo_pack
{
my.passable = on;
my.metal = on;
while (player_1st == null) {wait (1);}
while (vec_dist(player_1st.x,
my.x) > 70)
{
my.pan +=
3 * time;
wait (1);
}
snd_play (pickedup_wav, 50, 0);
ent_remove (my);
players_ammo += 50;
players_ammo = min(500, players_ammo);
}

Les actions pour les packs santé et les packs munitions sont identiques, aussi nous ne parleront que de la dernière.La munition est passable et son drapeau "metal" est mis à 1. La boite de munitions attend que player_1st (le joueur à la premièr epersonne) soit créé puis elle attend que le joueur soit à moins de 70 quants d'elle, changeant son angle pan (rotation) dans une boucle en attendant que le joueur se rapproche. Lorsque le joueur est proche un son pickedup_wav est joué, la boîte disparaît et players_ammo augmente de 50 balles. La dernière ligne de code limite les munitions à 500 balles. A noter également que players_shield est limité à 200 unités.

Examinons le code qui gère la fourmi géante:

action
ant1
{
var ant_speed;
var ant_gun1;
var ant_gun2;
my.skill20 = random(100); // choose a random
"stand" frame for each ant
my.lightred = 0; // my.red for A6,
etc
my.lightgreen = 255;
my.lightblue = 0;
my.light = on;
my.lightrange = 0;
my.skill48
= 100; // the ant has 100 health points
my.enable_impact = on;
my.enable_entity = on;
my.event = damage_ant;
while (player_1st == null) {wait (1);}

La fourmi choisit une valeur aléatoire pour my.skill20 au tout début, skill20 controle le cycle d'animation, de cette façon si nous plaçons plusieurs fourmis dans notre niveau leurs animations "stand" ne sernt pas synchronisées, ce qui est plutôt une bonne chose pour nous. Chaque fourmi verdoie et a 100 points de santé, qui sont stockées dans leur propre skill48. Les fourmis sont sensibles à l'impact avec les autres entités et démarent leur fonction évènement damage_ant dès qu'elles se cognent avec une autre entité. La dernière ligne de code demande aux fourmis d'attendre temps que le joueur n'est pas créé..

  while (my.skill48 > 0) // as long as I'm alive
{
if ((vec_dist (my.x, player_1st.x) < 5000) &&
(players_shield > 0))
{
vec_set(temp, player_1st.x);
vec_sub(temp, my.x);
vec_to_angle(my.pan,
temp); // the ant rotates toward the player
temp.tilt = 0;
temp.roll
= 0;
temp.pan = -my.pan;
vec_rotate(normal, temp);
temp.tilt = -asin(normal.x);
temp.roll = -asin(normal.y);
my.tilt += 0.1 * ang(temp.tilt - my.tilt);
// play with 0.1
my.roll
+= 0.1 * ang(temp.roll - my.roll); // play with 0.1

Aussi longtemps que la fourmi est en vie, elle attend que le joueur soit à moins de 5000 quants d'elle et qu'il soit en vie. Lorsque ces 2 conditions sont réunies, la fourmi se tourne dans la direction du joueur. et nous mettons le vecteur normal dans la direction opposée à l'angle pan de la fourmi. Ca nous permet de calculer les pentes pour les angles tilt et roll; nous utilisons ces valeurs pour conserver une orientation correcte de la fourmi sur le terrain. Vous noterez que l'augmentation ou la diminution des angles tilt et roll se fait lentement; Vous pouvez jouer avec la valeur, en la diminuant, "0.1"pour obtenir un effet de déplacement "ralenti" si vous le désirez.

        ant_speed.x = 25 * time;
ant_speed.y = 0;
vec_set (temp, my.x);
temp.z
-= 1000;
trace_mode = ignore_me
+ use_box;
ant_speed.z =
-trace (my.x, temp);
ent_move(ant_speed,
nullvector);
ent_cycle("walk",
my.skill19); // play "walk"
my.skill19 += 8 * time; // "walk" animation speed
my.skill19 %= 100; // loop the animation

La fourmi se tourne vers le joueur à la vitesse de 25 * time et ajuste sa hauteur en fonction de la hauteur du terrain sous ses pieds. La vitesse de l'animation "walk" estdonnée par 8 * time.

        if (vec_dist (my.x, player_1st.x) <
2000) // if the player is closer than 2000 quants attack him
{
my.skill20 = 0; // reset the "attack" frames
while (my.skill20 < 100)
{
ent_cycle("attack", my.skill20); // play "attack"
my.skill20 += 10 * time; // "attack" animation speed
if
((my.skill20 > 40) && ((total_frames % 10) == 1))
{
vec_for_vertex (ant_gun1, my, 273);
ent_create (antsbullet_mdl, ant_gun1, ant_hits_player);
}
if ((my.skill20 > 40) && ((total_frames % 10) == 5))
{
vec_for_vertex (ant_gun2, my, 282);
ent_create (antsbullet_mdl, ant_gun2, ant_hits_player);
}
wait (1);
}
}
}

Lorsque le joueur est à moins de 200 quants de la fourmi, nous remettons le cyccle d'attaque à 0 (skill20) puis nous jouons l'animation attack en augmentant skill20. Lorsque la fourmi a joué au moins 40% de son cycle d'attaque elle tire 6 balles à la seconde avec chacune de ses armes en alternance (ant_gun1 tire après les frames 1, 11, 21, 31, ...et ant_gun2 tire après les frames 5, 15, 25, 35, .... frames).

  else // the player is farther than 5000 quants away
{
ent_cycle("stand", my.skill20); // play "stand"
my.skill20 += 2 * time; // "stand" animation speed
my.skill21 %= 100;
}
wait (1);
}
my.skill20 = 0;
while (my.skill20
< 90) // the enemy is dead
{
ent_cycle("death",
my.skill20); // play "death"
my.skill20 += 7 * time; //
"death" animation speed
wait (1);
}
my.alpha = 100;
my.transparent = on;
while (my.alpha >
0)
{
my.alpha -= 2 * time;
wait
(1);
}
ent_remove (my);
}

Si le joueur s'éloigne à au moins 5000 quants, la fourmi bascule sur l'animation "stand", jouant cette animation dans une boucle. si nous arrivons de nouveau à la ligne qui met skill20 à 0, la fourmi est morte, ausi nous jouons l'animation "death" puis la fourmi disparaît en "fondu". La dernière ligne enlève le modèle de fourmi de notre niveau.

function damage_ant()
{
if (you.skill47 == 1) // if this is a bullet fired by player's gun
{
my.skill48 -= 10; // lose 10 health points
ent_playsound (my, antangry_wav, 3000);
}
}

La fonction ci-dessus est exécutée si une balle du joueur atteint la fourmi. Les premières lignes si la fourmi entre en collision avec une balle ou pas (elle peut se cogner à une autre fourmi, au joueur etc.) et si c'est le cas, elle enlève 10 points de santé et joue le son antangry_wav.

function
ant_hits_player()
{
my.skill47 = 5; // that's an bullet fired
by the ant
my.pan = you.pan; // the bullet and the ant have the
same pan
my.tilt = you.tilt; // and tilt
my.enable_entity
= on; // the bullet is sensitive to other entities
my.enable_impact
= on;
my.enable_block = on; // and to level blocks
my.event = remove_bullet;
my.skill30 = 0;
while (my != null)
{
my.skill1
= 100 * time; // bullet speed
my.skill2 = 0;
if (my.skill30 < 1500)
{
my.skill3 = 0; // keep
the bullet running in a straight line at the beginning
}
else
{
my.skill3 -= 1 * time; // make it descend
after a while
}
move_mode = ignore_you + ignore_passable + ignore_push; // ignores the ant (its
creator = you)
my.skill30 += ent_move(my.skill1,
nullvector); // store the distance covered by ant's bullet in skill30
wait (1);
}
}

Cette fonction déplace la balle de la fourmi en direction du joueur. J'ai fait en sorte que les fourmis ratent leur cible le plus souvent car elles ont vraiment une cadence de tir impressionnante, c'est pourquyoi les balles ont le même angle tilt que celui de la fourmi. Les balles sont sensibles à l'impact avec les autres entités, les blocs de terrain / niveau et leur fonction évènement associée s'appelle remove_bullet. Nous mettons skill30 à 0 puis nous entrons dans une boucle qui s'exécutera tant que la balle existera. La balle se déplace à la vitesse donnée par 100 * time; tant qu'elle n'a pas couvert 1500 quants, elle se déplace en ligne droite, autrement on diminue régulièrement sa vitesse sur l'axe des z afin qu'elle termine sa course dans le sol. La balle ignore son créateur (la fourmi) et stocke le résultat de ses mouvements dans son propre skill30, utilisé en tant que traqueur de distance dans la même boucle.

function
remove_antsbullet()
{
my.event = null;
ent_playsound
(my, anthit_wav, 1000);
sleep (0.4);
my.passable
= on;
my.alpha = 100;
my.transparent = on;
while (my.alpha > 0)
{
my.alpha -= 2 * time;
wait (1);
}
ent_remove (my);
}

La fonction ci-dessus est celle qui gère l'évènement pour les balles de la fourmi. Elle fait en sorte que la balle ne réagisse plus aux autres évènement, joue le son anthit_wav, attend un peu puis rend la balle passable et transparente, diminuantson alpha dans une boucle puis la fait disparaître.

function
hurt_player1st()
{
if (you.skill47 == 5)
{
my.event = null;
snd_play(playerhurt_wav, 50, 0);
players_shield
-= 5;
sleep (0.5);
my.event = hurt_player1st;
}
players_shield
= max(0, players_shield); // don't allow players_shield to go below 0
while (players_shield <= 0)
{
if (camera.roll < 70)
{
camera.roll += 5 * time;
}
wait (1);
}
}

La dernière fonction blesse le joueur lorsqu'il est atteint par une des balles tirées par la fourmi. Nous commençons par vérifier si ce qui a touché le joueur est bien une balle ennemie (skill47 = 5). Si c'est le cas nous jouons le son playerhurt_wav, nous diminuons players_shield, nous rendons le joueur insensible à une autre balle pendant 0.5 seconde puis nous remettons la fonction évènement. Nous n'autorisons pas que players_shield puisse avoir une valeur négative (health = -15 ça n'est pas génial lorsque c'est affiché sur un panneau, non?) puis nous changeons l'angle roll de la caméra dans une boucle, afin de mettre le joueur dans une position inconfortable et de faire savoir qu'il est mort à présent..


Untitled

Planet Survivors - part8

Ce mois-ci j'ai ajouté un nouvel ennemi (l'alien canonnier), une nouvelle arme, le fusil à lunette, j'ai amélioré le code de mouvement et des petites choses.

action
alien_gunner
{
var gun_pos;
while (player_1st
== null) {wait (1);}
my.skill20 = 1; // the alien that controls
this turret isn't dead
ent_create (blastshield_mdl, my.x, stick_with_parent);
ent_create (alien_mdl, my.x, stick_with_gun);
while (my.skill20 == 1)
{
vec_set(temp, player_1st.x); // rotate the gun towards the player
vec_sub(temp, my.x);
vec_to_angle(my.pan,
temp);
if (my.tilt < -35) {my.tilt = -35;}
// the alien gunner has got a firing range of 15,000
quants
// fire every 1.5 seconds if the player
is in range and visible
if ((vec_dist (player_1st.x,
my.x) < 17000) && (total_frames % 90 == 1))
{
vec_set (temp, my.x);
temp.z += 150; // the alien uses its
head position to "see" (trace) the player
trace_mode = ignore_me + ignore_passable + use_box;
trace_distance = trace (temp.x, player_1st.x);
if ((trace_distance > 100) && (trace_distance < 15000)) // fire
only if the player is visible
{
vec_for_vertex
(gun_pos, my, 1); // fire from the first vertex of the gun
ent_create (projectile_mdl, gun_pos.x, move_projectile);
}
}
wait
(1);
}
while (my.tilt < 35)
{
my.tilt += 3 * time;
wait (1);
}
}

Le code ci-dessus est attaché au modèle de canon; il crée un bouclier et les modèles alien_mdl. Tant que skill20 = 1 le canon se tourne vers le joueur avec sa limite basse pour tilt de mise à -35 degrées. Si le joueur est à moins de 17,000 quants, toutes les 1.5 secondes (total_frames % 90 = 1 à 60 fps) nous traçons depuis une position placée à 150 quants devant le canon au joueur. Si le joueur est visiblenous obtenons le sommet pour la bonne position de tir puis nous créons le projectile. Lorsque skill20 != 1 l'alien canonnier est mort aussi le canon prend une position vers le haut.

function
stick_with_parent()
{
my.scale_x = 0.7;
my.scale_y
= my.scale_x;
my.scale_z = my.scale_x;
my.light
= on;
my.lightred = 100;
my.lightgreen = 100;
my.lightblue = 200;
my.lightrange = 0;
my.bright = on;
my.metal = on;
my.transparent
= on;
my.alpha = 30;
while (1)
{
my.x = you.x;
my.y = you.y;
my.z = you.z;
my.pan = you.pan;
my.tilt = you.tilt;
my.roll = you.roll;
my.lightblue = 100 + random(150);
wait (1);
}
}

La fonction ci-dessus est attachée au bouclier, en synchro avec le canon. Le bouclier émet une couleur bleuatre aléatoire.

function
stick_with_gun()
{
my.frame = 2; // needed by the alien model
my.skill1 = 100;
my.enable_impact = on;
my.enable_entity = on;
my.event = gunner_is_hit;
while (my.skill1 > 0) // not dead yet?
{
my.x = you.x;
my.y = you.y;
my.z = you.z;
my.pan = you.pan;
my.tilt = you.tilt;
my.roll = you.roll;
wait (1);
}
// the
alien was hit here (it is dead)
snd_play (aliendies_wav, 50,
0);
you.skill20 = 0; // stop the turret
while
(my.frame < 7)
{
my.frame
+= 0.3 * time;
wait (1);
}
}

function
gunner_is_hit()
{
wait (1);
my.skill1 -= 100;
}

Les fonctions ci-dessus gère l'alien: la première, le synchronise avec le modèle de canon si l'alien n'est pas mort et arrête la tourelle lorsque l'alien est touché. La seconde est l'évènement qui tue l'alien si le joueur arrive à le toucher.

function
move_projectile()
{
my.skill47 = 123; // to identify a bullet
fired by the alien gunner
my.pan = you.pan; // the bullet and
the weapon have the same pan
my.tilt = you.tilt; // and tilt
angle
my.enable_entity = on; // the bullet is sensitive to other
entities
my.enable_impact = on;
my.enable_block
= on; // and to level blocks
my.event = projectile_explodes;
my.ambient = 100;
my.light = on;
my.lightred
= 250;
my.lightgreen = 150;
my.lightblue = 55;
while (my.passable == off)
{
my.skill1 = 1000 * time; // bullet speed
my.skill2
= 0;
my.skill3 = 0;
move_mode = ignore_you + ignore_passable;
ent_playsound
(my, rocketflying_wav, 3000);
ent_move (my.skill1,
nullvector);
wait (1);
}
}

Le projectile tiré par l'alien canonnier se déplace à 1000 quants * time / frame et joue un son 3D pendant son déplacement.

function
projectile_explodes()
{
my.event = null;
sleep (0.1);
my.passable = on;
my.invisible
= on;
if (vec_dist (player_1st.x, my.x) < 300)
{
players_shield -= 1000 / (vec_dist(player_1st.x,
my.x) + 10);
}
ent_playsound (my, pexplo_wav,
2000); // the projectile hits something
effect (explosion_core,
10 + random(10), my.x, nullvector);
sleep (0.1);
ent_create (explosion_pcx, my.x, explosion_sprite2);
sleep (0.1);
ent_create (explosion_pcx, my.x, explosion_sprite2);
sleep
(0.1);
ent_create (explosion_pcx, my.x, explosion_sprite2);
sleep (0.1);
ent_create (explosion_pcx, my.x, explosion_sprite2);
sleep (0.1);
ent_create (explosion_pcx, my.x, explosion_sprite2);
sleep (0.1);
ent_create (explosion_pcx, my.x, explosion_sprite2);
}

Si le projectile touche quellque chose, il vérifie sui le joueur est à moins de 300 quants de lui et si oui, il dommage le joueur. Nous jouons un son d'explosion et plusieurs effets d'explosion avec mon fichier explosions.wdl.

action
sniper_weapon
{
my.passable = on;
while (player_1st
== null) {wait (1);}
while (vec_dist (my.x, player_1st.x) >
100) {wait (1);}
snd_play (gotsniper_wav, 50, 0);
my.invisible = on;
got_sniper_rifle = 1;
}

L'action ci-dessus est attachée au fusil à lunette, elle attend que le joueur soit tout proche de lui et met got_sniper_rifle à 1 et disparaît. Ici le fusil est placé à l'entrée de la première base

function
toggle_sniper()
{
if (level_number != 3) {return;} // don't
show the crosshair if the player hasn't reached the 3rd level
if (got_sniper_rifle == 0) {return;}
sniper_pan.visible = (sniper_pan.visible
== off);
camera.arc = 70 * (sniper_pan.visible == off) + 10 *
(sniper_pan.visible == on);
}

on_mouse_right = toggle_sniper;

Le fusil peut-être utilisé avec ou sans sa lunette; pressez le bouton droit de la souris pour mettre ou non le mode lunette. Le fusil ne fonctionnera que si vous avez atteint au moins le 3eme niveau (celui-ci) et seulement si le joueur l'a pris. Le facteur de grossissement de la lunette va de 1 (camera.arc = 70) à 7 (camera.arc = 10).

L'alien canonnier étant très dur à abattre j'ai ajouté un code de "triche" que vous obtenez en pressant la touche F5:

function
cheater()
{
got_sniper_rifle = 1;
players_ammo
= 999;
players_shield = 999;
}

on_f5 = cheater;

Si vous n'arrivez pas à terminer le niveau avec 999 boucliers et 999 balles vous pouvez metrte des valeurs encore plus grande bien entendu (ou arrêter de jouer NDT). Notez que nous avons 3 aliens et 2 fourmis géantes dans notre niveau Vous devriez ajouter plus d'ennemis mai intéressons-nous au vaisseau de fuite:

action
ship_takes_off
{
while (player_1st == null) {wait (1);}
while (vec_dist (player_1st.x, my.x) > 350) {wait (1);}
end_level3 = 1; // free the camera
var ship_dist;
ship_dist.y = 0;
snd_play (shipout_wav, 50, 0);
while (my.y < 25000)
{
ship_dist.x = 800 * time;
ship_dist.z = 130
* time;
ent_move(ship_dist, nullvector);
vec_set(temp, my.x); // rotate the camera towards player's
ship
vec_sub(temp, camera.x);
vec_to_angle(camera.pan, temp);
wait (1);
}
exit;
}

Ce vaisseau attend que le joueur soit proche de lui et il monte jusqu'à ce que son y soit supérieur à 25,000 quants. Le jeu se termine en appelant l'instruction "exit".

J'ai également changé le code du mouvement, à présent vous pouvez tomber en douceur et (plus important) vous pouvez voler en pointant votre nez vers le haut. De cette façon vous pouvez sauter par dessus de nombreux obstacles et arriver ainsi plus vite à la fin de cet énorme niveau.


Untitled

Planet Survivors - récap

Ce mois-ci j'ai regroupé tous les niveaux afin que vous puissiez jouer en séquence, j'ai ajouté un peu de code pour que tout cela fonctionne ensemble, corrigé quelques bugs etc. Il y a beaucoup de nouveaux codes sous le capot mais repassons d'abord en revue tous les éléments du jeu.

L'action commence avec un jeu de tir dans l'espace en 3 D où vous devez tirer sur des astéroïdes

Quelques minutes plus tard le vaisseau atterrit sur une plateforme:

Le joueur doit résoudre un petit problème de logique (un puzzle) afin de pouvoir se déplacer su rla suite du niveau il doit avoir 3 rangs et 3 lignes de couleurs identiques:

Si le joueur solutionne le puzzle il reçoit des points de munitions et de boulcier et le vaisseau décolle pour poursuivre sa mission:

L'action se déplace vers un jeu d'arcade de l'espace 2D en vue de dessus avec 3 ennemis différents et un boss à la fin du niveau:

Le joueur se déplace dans un champs d'asteroides qui ne peuvent pas être détruites aussi son vaisseau estconsidérablement endommagé:

Le vaisseau s'écrase sur le sol d'une planète:

Le joueur parvient à s'extraire du vaisseau, prend un fusil à lunette et doit se préparer à affronter deux types d'ennemis, des forumis géantes vertes et des aliens tireurs de roquettes.

Le joueur réussit à obtenir un nouveau vaisseau et peut s'échapper de cette planète, terminant ainsi le jeu.. Vous pouvez faire voler le joueur dans le niveau de tir en lui faisant lever le nez, de cette façon vous pouvez vous déplacer au dessus des collines et arriver à la fin du niveau en quelques minutes (à la condition de tuer les aliens bien entendu).

Ok, je vais à présent répondre aux nombreuses questions que vous m'avez posées : comment est-ce que je résous le niveau du puzzle en utilisant une méthode scientifique (pas par tatonnement)?

Je vais utiliser l"exemple de la démo mais l'explication vaut pour toutes les autres combinaisons.

1) Créer un carré de 2x2 avec l'alignement correct des couleurs:

La bonne nouvelle c'est que nous aloons à présent jouer avec la rangée rouge et la colonne qui n'ont pas de rapport avec notre carré aligné:

Observons la séquence des 2 premières cellules dans la rangée du haut. C'est bleu suivi de vert. Ce qui signifie que la rangée du bas doit aussi commencer par bleu et être suivi par vert, nous poussons donc le carré bleu 'en bas à droite) d'un pas vers la gauche:

Maintenant nous faisons descendre le vert qui se trouve en haut à droite

Nous avons à présent en bas un bleu suivi d'un vert, pousson sces 2 carrés vers la gauche.

Et voiulà, le niveau est résolu et le joueur peut continuer le jeu, nous avons aligné 2 colonnes / 2 rangs et la troisème colonne / rangée s'est faite automatiquement. N'oubliez pas que vous obtenez plus de bouclier et de munitions si vous résolver ce problème rapidement. Le mois prochain nous regarderons le chargement et la sauvegarde du niveau.


Untitled

Planet Survivors - la fin

Croyez-le ou pas,cette série est terminée! J'ai eu besoin de quelques mois pour la créer mais nous avons au final un projet qui inclut 4 jeux complètement différents dans une mêm ossature. Ce mois-ci j'ai fixé quelques bugs mineurs et j'ai créé un système de chargement / sauvegarde, aussi nous allons concentrer nos efforts dans cette direction.

string name1_str = " "; // up to 39 characters, feel free to increase the limit
string name2_str = " "; // up to 39 characters, feel free to increase the limit
string name3_str = " "; // up to 39 characters, feel free to increase the limit
string name4_str = " "; // up to 39 characters, feel free to increase the limit

text
slot1_txt
{
pos_x = 205;
pos_y = 180;
layer
= 150;
font = system_font;
string = name1_str;
}

.............................

text
slot4_txt
{
pos_x = 205;
pos_y = 480;
layer
= 150;
font = system_font;
string = name4_str;
}

J'ai décidé de créer 4 possibilités de sauvegarde; name1_str contiendra le nom donné à la première sauvegarde, name2_str contiendra le nom de la deuxième sauvegarde etc. J'ai également défini 4 textes qui afficheront les noms sur les panneaux "Load game" et"Save game".

panel blackpanel_pan
{
bmap = blackpanel_pcx;
layer = 2;
pos_x = 0;
pos_y = 0;
flags = overlay,
refresh;
}

panel savegame_pan
{
bmap = savegame_pcx;
layer
= 100;
pos_x = 0;
pos_y = 0;
button = 198, 158,
slot2_pcx, slot1_pcx, slot2_pcx, ps_save1, null, mouse_over;
button
= 198, 258, slot2_pcx, slot1_pcx, slot2_pcx, ps_save2, null, mouse_over;
button = 198, 358, slot2_pcx, slot1_pcx, slot2_pcx, ps_save3, null, mouse_over;
button = 198, 458, slot2_pcx, slot1_pcx, slot2_pcx, ps_save4, null, mouse_over;
flags = overlay, refresh;
on_click = game_was_saved;
}

panel loadgame_pan
{
bmap = loadgame_pcx;
layer
= 100;
pos_x = 0;
pos_y = 0;
button = 198, 158,
slot2_pcx, slot1_pcx, slot2_pcx, ps_load1, null, mouse_over;
button
= 198, 258, slot2_pcx, slot1_pcx, slot2_pcx, ps_load2, null, mouse_over;
button = 198, 358, slot2_pcx, slot1_pcx, slot2_pcx, ps_load3, null, mouse_over;
button = 198, 458, slot2_pcx, slot1_pcx, slot2_pcx, ps_load4, null, mouse_over;
flags = overlay, refresh;
on_click = cancel_loading;
}

J'ai défini un panneau avec une image noire; je l'utilise pour cacher la fenètre de jeu lorsque le joueur presse la touche échap pour venir sur le menu principal. Les panneaux suivants sont utilisés pour la sauvegarde et le chargement: chacun d'eux a 4 boutons correspondant aux 4 possibilités de sauvegarde / chargement. Lorsque nous cliquons le panneau savegame_pan, sa fonction associée game_was_saved() est exécutée; la même chose se produit avec loadgame_pan et cancel_loading().

Que se passe-t-il lorsque vous cliquez sur échap pendant le jeu? Cette touche prévue à l'origine pour arrêter le jeu immédiatement a à présent un autre rôle - elle exécute la foncton suivante :

function
return_to_main()
{
if (main_pan.visible == on) {return;}
freeze_mode = 1;
blackpanel_pan.visible = on;
main_pan.visible
= on;
mouse_mode = 2;
while (freeze_mode == 1)
{
mouse_map = pointer1_tga;
mouse_pos.x = pointer.x;
mouse_pos.y = pointer.y;
wait (1);
}
}

Si le panneau principal est déjà visible, la fonction retourne; autrement toutes les actions dans le niveau sont stoppées et le panneau noir qui cache ce quise passe dans le niveau est affiché. Nous montrons également le panneau principal; ce panneau a une couche supérieure à celle du panneau noir afin d'apparaître par dessus. Nous montrons le pointeur de la souris puis nous autorisons la souris à se déplacer aussi longtemsp que le jeu est gelé.

function
save_game()
{
loadgame_pan.visible = off;
savegame_pan.visible
= on;
save_handle = file_open_read("save1.dat"); // open the file
if (save_handle != 0) // if the file exists
{
file_str_read(save_handle,
name1_str);
slot1_txt.string = name1_str;
slot1_txt.pos_x
= 205 + (39 - str_len(name1_str)) * 5; // center the text, allow up to 39 characters,
5 = 10 / 2 (character width / 2)
slot1_txt.visible = on;
file_close(save_handle);
}
.....................................................
save_handle = file_open_read("save4.dat"); // open the file
if (save_handle
!= 0) // if the file exists
{
file_str_read(save_handle,
name4_str);
slot4_txt.string = name4_str;
slot4_txt.pos_x
= 205 + (39 - str_len(name4_str)) * 5; // center the text, allow up to 39 characters,
5 = 10 / 2 (character width / 2)
slot4_txt.visible = on;
file_close(save_handle);
}
}

La fonction ci-dessus est jouée lorsque le joueur presse le bouton "Save" sur le panneau du menu principal. Cette fonction cache le panneau "Load game" s'il est visible et affiche le panneau "Save game". Nous ouvrons le fichier nommé save1.dat (si il existe) et nous lisons le nom de la sauvegarde pour ce fichier et lui assignons le texte slot1_txt. Nous centrons le texte dans son emplacement et nous le rendons visible. Pour terminer nous fermons le fichier et le processus se répèete pour les 4 emplacements.

Si vous vous demandez pourquoi utilser ces save1.dat... save4.dat vous n'êtes pas le seul; je me suis moi-même posé la question lorsque je les ai utilisé... Mais si maintenant je me souviens I Nous avons besoin de sauver les noms des emplacements (le nom du jeu sauvegardé) dans un fichier séparé car nous voulons l'afficher dans l'emplacement "Load game" ou"Save game" avant le chargement / sauvegarde d'un nouveau jeu. Ma démo est livrée avec des jeux sauvegardés nommés level1... level4 mais vous pouvez utiliser n'importe quel nom pour vos sauvegardes, à la seule condition que son nom soit inférieur ou égal à 39 caractères. Pourquoi cette limte? Un caractère dans ma police fait 10 pixels de large et le bouton 400 pixels, ce qui signifie que je peux sans risque utiliser 39 * 10 = 390 pixels sur les 400 que font le bouton.

function
load_game()
{
media_stop(media_handle); // stop the soundtrack
savegame_pan.visible = off;
loadgame_pan.visible = on;
save_handle
= file_open_read("save1.dat"); // open the file
if (save_handle != 0)
// if the file exists
{
file_str_read(save_handle,
name1_str);
slot1_txt.string = name1_str;
slot1_txt.pos_x
= 205 + (39 - str_len(name1_str)) * 5; // center the text, allow up to 39 characters,
5 = 10 / 2 (character width / 2)
slot1_txt.visible = on;
file_close(save_handle);
save1_exists = 1;
}
else
{
save1_exists = 0;
}

  ...................................................
save_handle = file_open_read("save4.dat"); // open the file
if (save_handle
!= 0) // if the file exists
{
file_str_read(save_handle,
name4_str);
slot4_txt.string = name4_str;
slot4_txt.pos_x
= 205 + (39 - str_len(name4_str)) * 5; // center the text, allow up to 39 characters,
5 = 10 / 2 (character width / 2)
slot4_txt.visible = on;
file_close(save_handle);
save4_exists = 1;
}
else
{
save4_exists = 0;
}
}

La fonction load_game() chaque fois que vous pressez le bouton "Load" sur le menu principal. Il arrête tous les pistes sons qui pourraient être jouée pour le moment, cache le panneau "Save game", affiche le panneau "Load game" et lit les données du fichier save1.dat (si le fichier existe), stockant le nom du jeu sauvegardé dans name1_str. La chaîne est passée au texte slot1_txt, qui sera centré au milieu du bouton puis rendu visible. Si le fichier existe, la variable save1_exists est mise à 1; autrement, il est mis à zéro. Nous utiliserons cette variable un peu plus tard; nous en avons besoin parce que nous ne voudrions pas que notre utilisateur clique et charge un jeu sauvegardé à partir d'un emplacement où rien n'a été sauvé, OK? La même chose se produit avec le reste des emplacements.

function
ps_save1()
{
if (inkey_active == on) {return;}
if (players_shield
<= 0) {return;}
slot1_txt.visible = on;
str_cpy(name1_str,
"");
while (str_cmpi(name1_str, "") == 1)
{
inkey(name1_str);
wait (1);
}
slot1_txt.string
= name1_str;
slot1_txt.pos_x = 205 + (39 - str_len(name1_str)) * 5;
save_handle = file_open_write("save1.dat");
file_str_write (save_handle,
name1_str);
file_close(save_handle); // close the file
game_save("save",
1, sv_all - sv_info);
game_was_saved();
}

C'est la fonction qui s'exécute lorsque le joueur presse le premier bouton sur le panneau"Save game"; Me croyez-vous si je vous dis que les autres fonctions sont similaires. La première ligne nous fait sortir si une touche est déjà active (si le joueur a pressé le bouton 2c fois). La même chose se produit si le joueur veut sauvegarder un jeu alors que le joueur est mort. Nous affichons le texte qui contient le nom du jeu sauvegardé, nous remettons à zro sa chaine associée name1_str, puis nou sattendons dans une boucle while jusqu'à ce que nous soyons sur que le joueur ait tapé quelque chose à l'intérieur, stockant l'entrée dans name1_str.

On sort de la boucle while à présent, nous centrons le texte à l'écran et nous créons et ouvrons le fichier nommé save1.dat, écrivons le nom de la sauvegarde à l'intérieur de ce fichier. Pour terminer nous sauvegardons le jeu en utilisant l'instruction de Conitec "game_save" dans le fichier save1.sav. La dernière ligne exécute la fonction game_was_saved() qui cache quelques panneaux, cire les chaussures, sert le café etc.

function
ps_load1()
{
if (save1_exists == 0) // the slot is empty?
{
return; // get out of here!
}
game_load("save",
1);
game_was_loaded();
if (level_number == 1)
{
media_play ("track1.wav", null, 50); // play the first soundtrack
(3D space + logical level)
}
if (level_number == 3)
{
media_loop("track2.wav", null, 100); // play the second soundtrack
(shooter leve)
}
}

La fonction ci-dessus (aussi bien que ses 3 soeurs) s'exécute lorsque vous pressez un bouton sur le panneau "Load game". Il vérifie si le jeu est sauvegardé existe ou pas; si il existe il charge les données qui se trouvent dans le fichier save1.sav, puis exécute la fonction game_was_loaded() qui fait un peu de nettoyage. Si la sauvegarde provient du premier niveau, nous jouons la premire piste musicale, autrement si la sauvegarde provien du 3eme niveau 'jeu de tir) nous jouons la seconde piste dans une boucle. Pourquoi faisons-nous cela? Bien nous avons arrêté la musique et il n'estpas facile de la reprendre là où nous l'avons arêtée (sauf à utiliser plein de petis morceaux de musique ou à utiliser un code très complexe) donc nous rejouons tout simplement les pistes lorsque nous chargeons le 1er ou 3eme niveau.

function
game_was_saved()
{
if (players_shield <= 0)
{
savegame_pan.visible = off;
return;
}
else
{
sleep (2);
slot1_txt.visible
= off;
slot2_txt.visible = off;
slot3_txt.visible
= off;
slot4_txt.visible = off;
savegame_pan.visible
= off;
blackpanel_pan.visible = off;
main_pan.visible
= off;
if (level_number == 1)
{
if (human == null)
{
mouse_mode = 0;
mouse_map = pointer1_tga;
}
else
{
mouse_mode = 2;
mouse_map
= logical_pcx;
}
}
if (level_number == 2)
{
mouse_mode
= 0;
}
if (level_number == 3)
{
mouse_mode = 0;
}
sleep
(1);
freeze_mode = 0;
}
}

Cette fonction s'exécute à la fin du processus de sauvegarde, faisant différentes choses en fonction en fonction du numéro du niveau. Si le joueur est mort, la fonction cache le panneau"Save game" et sort du jeu, autrement il attend 2 secondes puis cache les textes qui contiennent les noms des jeux sauvegardés aussi bien que le panneau principal. Si nous sommes au premier niveau et que le pointeur "human" est null, cela signifie que nous jouions au jeu de l'espace 3D, faisant partie du premier niveau, alors nous cachons la souris (le code utilisé pour le vaisseau la montrait) puis nous mettons pointer1_tga comme pointeur de souris. Si "human" n'est pas null, nous sommes en train de joeur la partie du jeu d elogique du premeir niveau puisque "human" et le petit bonhomme qui sort du vaisseau et qui est créé dès l'atterrisage du vaisseau. Nous montrons le pointeur de la souris et lui donnons l'image logical_pcx.

Si nous jouions au 2eme niveau, nous cachons le pointeur de la souris car il ne sert à rien. Lamême chose se produitsi nous jouions dans le 3ème niveau. Nous laissons une seconde au joueur pour qu'il se remémore ou il était lorsqu'il a sauvegardé le jeu et nous dégelons le jeu.

function
cancel_loading()
{
loadgame_pan.visible = off;
slot1_txt.visible
= off;
slot2_txt.visible = off;
slot3_txt.visible = off;
slot4_txt.visible = off;
}

Cette fonction est exécutée si le joueur clique sur le panneau "Load game".J'avais écrit "cliquez sur le haut de l'écran si vous êtes arrivé ici par erreur" sur le panneau mais en réalité vous pouvez cliquer n'importe ou sur le panneau à l'exception des boutons, si vous avez cliqué sur "load" du menu principal par erreur. Si cela se produit le panneau de chargement et ses textes associés sont rendus invisibles.

function
game_was_loaded()
{
loadgame_pan.visible = off;
savegame_pan.visible
= off;
slot1_txt.visible = off;
slot2_txt.visible = off;
slot3_txt.visible = off;
slot4_txt.visible = off;
main_pan.visible
= off;
sleep (1);
freeze_mode = 0;
}

La dernière fonction fait du ménage après le chargement d'un jeu sauvegardé; elle cache quelques panneaux et textes, attend une seconde puis reprend le jeu.

S'il vous plait, permettez moi de clore cette série avec quelques principes applicables si vous créer un jeu complet:

- Eviter dans la mesure du possible les fonctions "starter"; elles vous rendent la vie difficile en fin de jeu;

- Essayez d'utiliser un maximum de variables locales. Eviter autant que faire se peut les variables globales;

- Faites un maximum d'initialisation à l'intérieur de votre fonction principale (main). Vous serez appelé à relancer votre jeu en exécutant de nouveau la fonction main() (sauf si vous utilsez je ne sais quelle autre méthode compliquée).


Untitled

Radar

Une bonne chose au sujet des vieux moteurs de raycasting était que vous pouviez avoir facilement un radar. Ce morceau de code vous permettra de voir la carte entière sur un panneau et plus que cela, ce qui se déplace dessus.

D'abord j'ai ces valeurs (dans Wed) pour les frontières de niveau dans le niveau office. wmp:

var level_minx = -1800;
var level_maxx = 1800;
var level_miny = -1500;
var level_maxy = 1500;

Vous êtes libre de mettre des valeurs approximatives car si vous utilisez un petit panneau radar cela ne changera rien que la valeur de level_minx soit -1800 ou -1824.

L'idée derrière ce code de radar est assez simple: nous employons un panneau pour la carte (un screenshot de la vue supérieure dans Wed, sans géométrie inutile dessus) et plusieurs petits panneaux avec des couches plus élevées pour les entités qui se déplacent sur le panneau.

panel radar_pan
{
bmap = radar_map;
layer = 20;
pos_x = 0;
pos_y = 0;
flags = overlay, refresh, d3d, visible;
}

panel entity1_pan
{
bmap = pentity_map; // the player
layer = 21;
pos_x = 0;
pos_y = 0;
flags = transparent, overlay, refresh, d3d;
}
...............................................................................

panel entity5_pan
{
bmap = entity_map;
layer = 21;
pos_x = 0;
pos_y = 0;
flags = transparent, overlay, refresh, d3d;
}

Vous pouvez voir que radar_pan (le niveau) a la couche = 20 et les autres panneaux (les entités) ont la couche = 21 ainsi ils seront montrés par dessus radar_pan. J'ai seulement employé 5 panneaux d'entité dans cet exemple, mais vous pouvez ajouter autant de panneaux/entités que vous voulez.

Chaque entité qui veut être montrée sur le radar doit inclure cette ligne dans son action/fonction (le mieux c’est la première ligne):

radarx_ptr = me; X = 1. .5 dans mon exemple

le joueur a une ligne radar1_ptr = me dans son action de player_prog, la garde de patrouille a la ligne radar2_ptr = me dans sa action de patrol_prog et ainsi de suite. Par ailleurs, le panneau pour le joueur (entity1_pan) emploie une carte binaire différente (le joueur est bleu et toutes les autres entités sont rouges sur le radar).

Nous devons démarrer le radar en exécutant

function init_radar()
{
while (1)
{
if (radar1_ptr != null)
{
entity1_pan.visible = on;
entity1_pan.pos_x = scalex * (abs(radar1_ptr.x - level_maxx));
entity1_pan.pos_y = scaley * (abs(radar1_ptr.y - level_miny));
}
...........................................................................................................................

if (radar5_ptr != null)
{
entity5_pan.visible = on;
entity5_pan.pos_x = scalex * (abs(radar5_ptr.x - level_maxx));
entity5_pan.pos_y = scaley * (abs(radar5_ptr.y - level_miny));
}
wait (1);
}
}

Si l'entité doit apparaître sur le radar (son indicateur de radarx_ptr n'est pas nul) nous rendons son panneau visible et nous calculons sa position sur le radar. Jjetons un coup d'oeil à cette ligne :

entity1_pan.pos_x = scalex * (abs(radar1_ptr.x - level_maxx));

Vous savez que vous pouvez déplacer un panneau sur l’écran en changeant ses paramètres pos_x et pos_y;C’est ce que nous faisons ici. Pour radar1_ptr.x = -1800 and level_maxx = 1800:

scalex * (abs(radar1_ptr.x - level_maxx)) = length_of_the_radar_bitmap (128 pixels) -> scalex = 128 / 3600 = 0.0355

Je pourrais avoir mesuré la longueur de la carte binaire (en utilisant bmap_width et bmap_height) et me débarrasser de scalex et de scaley mais ces paramètres vous permettent de faire "un accord fin" au radar. J'emploie une formule semblable pour l'axe de y ainsi je ne l'expliquerai pas ici.

Ok, j'ai obtenu votre point mais je dois faire la même action pour 5 monstres? Je ne peux pas assigner un simple pointeur à tous!

Je vois votre point mais j'ai la réponse. Contrôler un drapeau différent (placez une valeur différente de skill) pour chaque monstre et placez les indicateurs de cette façon:

if (my.flag1 == 1) {radar3_ptr = me;}
if (my.flag2 == 1) {radar4_ptr = me;}

C’est ce que j'ai fait avec les deux ascenseurs qui apparaissent au niveau d’office (et sur le radar, naturellement). J’ai zippé doors.wdl et office.wdl qui incluent tous ces changements, ainsi vous aurez un exemple qui fonctionne.


Redéfinir les touches

La possibilité de redéfinir les touches peut être implémenté même si vous utilisez A4 mais c'estplus facile avec A5. J'ai créé un exemple autonome qui déplace une boite wmb dans un petit niveau; vous pouvez redéfinir les touches de mouvement aussi souvent que vous voulez en cliquant sur un petit panneau.

Nous devons tout d'abord définir quelques chaînes de caractères, variables et panneau (sur lequel on cliquera pour redéfinir les touches) et un texte:

string
empty_str, " ";
string up_str, "monter";
string down_str,
"descendre";
string left_str, "aller à gauche";
string right_str, "aller à droite";
var left_key;
var
right_key;
var up_key;
var down_key;
entity* player;

panel
redefine_pan
{
bmap redefine_map;
pos_x = 510;
pos_y = 10;
flags = refresh, overlay, visible;
on_click = change_keys();
}

text
redefine_txt
{
pos_x = 10;
pos_y = 10;
font = comic_font;
string = empty_str;
}

La fonction main est simple:

function
main()
{
fps_max = 50;
d3d_panels = on;
load_level (<redeftst.wmb>);
wait (2);
camera.x = 0;
camera.y = 0;
camera.z = 490;
camera.tilt = -90;
mouse_map = pointer_map;
mouse_mode = 2;
while(1)
{
mouse_pos.x = pointer.x;
mouse_pos.y = pointer.y;
wait(1);
}

}

Nous donnons un angle et une position à la caméra, nous selectionnons le pointeur de la souris et le rendons visible (mouse_mode = 2); la fonction main se termine avec le code qui gère les déplacements de la souris. L'action associée au joueur contient une simple ligne de code:

action set_player
{
player = me;
}

A chaque fois que nous cliquons le panneau redefine_pan, change_keys est exécutée:

function
change_keys()
{
redefine_txt.visible = on;

redefine_txt.string = up_str;
while (key_any == 0) {wait(1);}
while (key_any == 1) {wait(1);}
up_key = key_lastpressed;
key_set(up_key, move_up);

redefine_txt.string = down_str;
while (key_any == 0) {wait(1);}
while
(key_any == 1) {wait(1);}
down_key = key_lastpressed;
key_set(down_key,
move_down);

redefine_txt.string = left_str;
while (key_any == 0)
{wait(1);}
while (key_any == 1) {wait(1);}
left_key = key_lastpressed;
key_set(left_key, move_left);

redefine_txt.string = right_str;
while (key_any == 0) {wait(1);}
while (key_any == 1) {wait(1);}
right_key = key_lastpressed;
key_set(right_key, move_right);

redefine_txt.visible = off;
}

Regardons en détail le code pour redéfinir la touche qui permet de monter, le code pour les autres touches fonctionne sur le même principe:

redefine_txt.visible = on;
Rend le texte visible

redefine_txt.string = up_str;
Affiche la chaîne"monter"

while (key_any == 0) {wait(1);}
Attend que nous ayons pressé une touche

while (key_any == 1) {wait(1);}
Attend qu'on ait relâché la touche

up_key = key_lastpressed;
Stocke la dernière touche pressée dans la variable up_key

key_set(up_key, move_up);
Assigne la fonction move_up à la touche

J'ai utilisé un esimple fonction pour faire monter l'objet, sans détection de collision. Bien en tendu vous ne devez pas utiliser une telle fonction dans votre niveau.

function
move_up()
{
while (key_pressed(up_key) == 1)
{
player.x +=
10 * time;
wait (1);
}
}

Aussi longtemps que la touche définie dans up_key est pressée, le joueur (la boîte) est déplacée vers le haut. Les fonctions qui déplacent la boîte vers le bas, la gauche ou la droite sont identiques. Assurez-vous que les fonctions associées à votre joueur n'utilise qu'une seule touche à la fois (vous avez besoin d'une fonction distincte pour chacune des directions).


Untitled

Robby le robot

Qui est Robby? Et bien c'est votre robot journalier. C'est un npc mais il peut ajouter de l'atmosphère à votre jeu.Que fait-il? Pas grand chose mais il le fait bien. Il attend quelques secondes puis il marche quelques secondes...Est-ce que j'ai mentionné qu'il pouvaitdétecter et éviter n'importe quel obstacle et mieux encore, qu'il ne pouvait pas se coincer?

Chargez et exécutez le niveau dès à présent. Je dois dire que le code est assez impressionnant compte tenu du fait qu'il n'utilise qu'une action et deux petites fonctions, mais qui voudra me croire? Jetons un oeil au code attaché à Robby:

action
robby_the_robot
{
my.enable_entity = on;
my.enable_impact = on;
my.enable_block = on;
my.event = robby_event;
ent_create(antenna_pcx, nullvector, check_front);

Le robot est sensible aux autres entités, à l'impact et aux blocs de niveaux; si un de ces évènements arrivait, robby_event sera exécuté. Nous créons une "antenne" virtuelle - j'ai utilisé 2 antennes similaires dans la première démo AI dans Aum4.

while(1)
{
standing_time = 3 + random(7);
while (standing_time > 0)
{
standing_time -= time / 16;
ent_cycle("stand", my.skill20);
my.skill20 += 2 * time;
my.skill20 %= 100; // loop
wait (1);
}

Robby utilise une boucle while (1) qui quoiqu'il fasse se répètera toute la journée. Nous générons standing_time = 3...10 seconds - c'est le temps utilisé par le robot pendant son animationd'attente ("stand") dans une boucle while. Avez-vious noté que nous avons soustrait time / 16 de standing_time à chaqque cycle? 9a nous permet de soustraire une seconde de standing_time chaque... seconde. C'est hyper pratique car nous travaillons avec des valeurs "réelles", nous n'avons pas à nous demander combien de tick sont nécessaires etc.

walking_time = 10 + random(10);
walk_speed.x = 1;
walk_speed.y = 0;
walk_speed.z = 0;
walk_speed *= time;
while (walking_time > 0)
{
if (content(front_coords) == content_solid)
{
my.skill40 = my.pan;
my.skill41 = 30 + random(90);
while (my.pan < my.skill40 + my.skill41) // rotate 30..120 degrees
{
my.pan += 3 * time;
wait (1);
}
}

Le robot est sorti de sa première boucle while utilisé pour l'attente; maintenant il est temps de marcher pour 10...20 secondes - c'est la valeur qui est mise pour walking_time et la vitesse est donnée par walk_speed. Nous ne mettons que la coordonnée x aussi le robot ira dans la direction donnée par son angle pan actuel. Vous vous souvenez des antennes? C'est le sprite rouge en démo mais vous pouvez enlever le commentaire dans le fichier wdl et il disparaîtra. Ces antennes sont placées 30 quants au devant du robot (faites moi confiance pour l'instant mais nous verrons cette fonction plus loin). Si l'antenne détecte content_solid (murs, entités wmb, etc) le robot stocke son angle pan actuel dans skill40 et chang sa valeur pan en ajoutant une valeur aléatoire (30...120) pour cela.

else
{
walking_time -= time / 16;
ent_cycle("walk", my.skill20);
my.skill20 += 4 * time;
my.skill20 %= 100; // loop
move_mode = ignore_passable;
result = ent_move (walk_speed, nullvector);
if (result == 0) // got stuck!
{
walk_speed.x *= -1; // reversed movement
my.skill40 = my.pan;
my.skill41 = 30 + random(90);
while (my.pan < my.skill40 + my.skill41) // rotate 30..120 degrees
{
my.pan += 3 * time;
ent_cycle("walk", my.skill20); // play reversed "walk" animation
my.skill20 -= 4 * time;
my.skill20 %= 100; // loop
move_mode = ignore_passable; // ignore passable entities
ent_move (walk_speed, nullvector);
wait (1);
}
walk_speed.x *= -1; // restore walk_speed
}
}
wait (1);
}
wait (1);
}
}

Si l'antenne n'a pas
trouvé  content_solid, le robot est capable d'aller
de l'avant aussi il joue son animation de marche ("walk") dans une boucle et se
déplace. Regardons de plus près cette ligne:

result = ent_move (walk_speed, nullvector);

Elle parait "normale", n'est-ce pas? La bonne nouvelle est que le résultat sera la distance couverte par Robby sauf si result est à zéro, ce qui signifie que Robby est coincé et que nous devrions faire quelque chose à son sujet. Aussi que faisons-nous si Robby se coince? Nous inversons walk_speed en le multipliant par -1, nous changeons son angle pan en ajoutant une valeur aléatoire (30...120 degrés) pour cela, nous jouons uneanimation de marche ("walk") inversée (notez la ligne avec -= 4 * time) et nous déplaçons le robot en arrière. Si result n'est pas à zéro (Robby n'est plus coincé) nous multiplions walk_speed par -1 de nouveau afin de remettre la valeur originale pour walk_speed.

Il
est temps de regarder les 2 petites fonctions dont je vous ai parlé :

function check_front()
{
my.passable = on;
my.transparent = on;
while (1)
{
my.x = you.x +
30 * cos(you.pan); // the antenna is placed
my.y = you.y +
30 * sin(you.pan); // 30 quants in front of the player
my.z = you.z;
vec_set (front_coords,
my.pos);
wait (1);
}
}

La fonction check_front est la fonction associée à l'antenne; il rend le sprite rouge passable, transparent et le place 30 quants devant le robot, en permanence. Mais cette fonction fait également une choseimportante; mettre la variable nommée front_coords aux coordonnées du sprite rouge. Si vous étiez ici quand nous avons parlé de l'action associée au robot, vous savez que nous vérifions le contenu de front_coords pour voir si le chemin est dégagée ou pas.

Peut-être que vous vous demandez : si nous avons une antenne si puissante, pourquoi nous embêter avec result = 0 etc? Bien... la vie est une dure lutte. Robby pourrait se trouver dans une situation comme celle montrée dans l'image ci-dessous:

Vous pouvez voir que l'antenne pense que tout est ok et pourtant Robby est coincé par le coin de la boite.

function
robby_event()
{
if (event_type == event_entity)
{
snd_play (sorry_wav,
50, 0);
}
}

La dernière fonction dit si Robby est entré en collision avec une entité ou pas. J'ai pensé que ce serait sympa si Robby pouvait dire "Désolé!" lorsqu'il cogne le joueur. Ce simple évènement joue un son, le code qui fait que Robby évite les autres entités (y compris le joueur) est basé sur result = 0 et nous avons déjà discuté de cela.

J'espère que vous aurez passé du bon temps avec ce projet


Untitled

Roquette caméra

Si vous voulez être capable de voir à travers les "yeux" de votre roquette, ce bout de code sera bien utile. Si vous voulez apprendre comment créerune arme à partir de zéro, ce morceau de code vous sera extrèmement utile.

Je ne veux pas utiliser le templates guns parce que je n'aime pas l'idée de changer les templates, mêm si nous l'avons fait quelques fois :)
Avant tout nous définissons une entité qui sera utilisée pour l'arme qui apparaîtra à l'écran; mettez ses coordonnées x, y et z jusqu'à ce que ça vous plaise:

entity
rocket_ent
{
type = <rlauncher.mdl>;
layer = 10;
view = camera;
x = 20; // x y z set he position
of the entity
y = -10;
z = -10;
}

La caméra qui suit la roquette utilise une nouvelle vue:

view
rocket_view
{
layer = 15;
pos_x = 0; // appears in
the upper left corner
pos_y = 0;
size_x = 640;
size_y = 100;
arc = 80;
flags = visible;
}

La nouvelle vue a 640 x 100 pixels; si votre jeu utilise une résolution différente, changez ces valeurs. Rocket_view a un arc différent, un peu plus grand que celui utilisé pour la caméra (60 par défaut); de cette façon vous pouvez voir un peu plus de détails dans rocket_view. Regardons l'action attachée au lance roquette:

action
rocket_launcher
{
my.passable = on;
while (player == null) {wait (1);}
while (vec_dist (player.x, my.x) > 100)
{
my.pan += 3 * time;
wait (1);
}
ent_remove (me);
rocket_ent.visible = on;

L'arme est passable et tourne jusqu'à ce que le joueur soit à moins de 100 quants. Lorsque cela se produit, le lanceur est enlevé et l'entité lance roquette estrendu visible.

     while (player._health > 0) // fire only if
the player is alive
{
if (key_ctrl ==
1)
{
while (key_ctrl == 1) {wait (1);} // disable auto fire
vec_set (temp, rocket_ent.pos);
vec_for_screen (temp, camera);
ent_create (rocket_mdl, temp, move_rocket);
}
wait (1);
}
}

Nous pouvons tirer avec l'arme uniquement si le joueur est vivant. Si nous pressons sur la touche feu (ctrl) nous attendrons que cette touche soit relâchée puis nous créerons la roquette dans l'entité rocket_ent. Si vous lisez le manuel, vous savez que cette entité particulière n'existe pas dans le monde 3D, ça fonctionne juste comme un panneau. Alors pourquoi créer la roquette dans rocket_ent? Nous mettons temp à la position de rocket_ent puis nous utilisons la grande instruction vec_for_screen pour convertir la position 2D en coordonnées du monde 3D.

Jetons un oeil à la fonction qui déplace la roquette:

function
move_rocket()
{
my.enable_entity = on;
my.enable_block = on;
my.passable = on;
my.event = remove_rocket;
my.pan = camera.pan;
my.tilt = camera.tilt;

La roquette est sensible aux entités et aux blocs de niveau; Nous la faisons passable pour l'instant car elle pourrait entre en collision avec le joueur. Si l'entité entre en contact avec une entité ou un bloc, son évènement remove_rocket sera déclenché. La roquette ayantles mêmes angles tilt et pan que la caméra, elle volera dans la direction pointée par la caméra.

     while (my != null)
{
rocket_view.x =
my.x - 100 * cos(my.pan);
rocket_view.y =
my.y - 100 * sin(my.pan);
rocket_view.z =
my.z + 10;
rocket_view.pan
= my.pan;
rocket_view.tilt
= -5;
rocket_speed.x
= 30;
rocket_speed *=
time;
ent_move (rocket_speed,
nullvector);
if (vec_dist (camera.x,
my.x) > 30)
{
my.passable = off;
}
wait (1);
}
}

Tant que la roquette existe, nous plaçons la nouvellevue (rocket_view) 100 quants derrière la roquette et 10 quants au dessus. La nouvelle vue a son angle tilt de mis à -5, ce qui la fera regarder vers le bas. La roquette se déplace à la vitesse donnée par rocket_speed.x; changez 30 pour modifier la vitesse. Si la distance entre la camera et la roquette est supérieure à 30 quants,la roquette devient impassable; maintenant nous savons qu'elle ne risque plus d'entrer en collision avec le modèle utilisé pour le joueur.

Si la roquette utilise quelque chose nous verrons cetet fonction s'exécuter:

function remove_rocket()
{
if (you != null)
{
you._health -=
150;
}
vec_set (temp, my.pos);
ent_create(explosion_pcx,
temp, sprite_explosion);
ent_remove (me);
}

Si la roquette touche une entité, elle décrémente sa santé de 150 points (health); points from its health; si elle touche une voiture, elle ne lui fera rien du tout mais si elle touche un acteur elle le tuera. Nous créons un sprite d(explosion au point d'impact et nous exécutons la fonction suivante:

function sprite_explosion()
{
my.passable = on;
my.flare = on;
my.bright = on;
my.ambient = 100;
ent_playsound (my, explosion_wav,
400);
while (my.frame < 7)
{
my.frame += 1 *
time;
wait (1);
}
ent_remove (me);
}

Il n'y a rien de spécial ici; l'explosion est passable et lumineuse. Nous jouons un son d'explosion, nous parcourons toutes les frames de l'animation puis nous enlevons le sprite.


SCRAP - SCReen cAPture code (code pour capture d'écran)

Si vous me ressemblez, vous avez regardé le film MÉTAL dans "la rubrique des particularités" et vous vous demandez : comment a-t-il fait ce film ? Il doit avoir employé deux PC et une grande carte de capture... C'est une bonne méthode et je l'ai employé avec succès pour mes premiers films de prise de contrôle, mais alors j'ai pensé que je devais employer un simple PC et j'ai inventé ce code de capture tout simple.

Tout d'abord, je voudrais dire que j'ai entendu quelqu'un sur le forum parlant de cela et il m'a inspiré (je ne peux plus trouver le poste désormais, peut-être a t'il été supprimé par erreur). L'idée était d'employer la COPIE D'ÉCRAN chaque fois le joueur se déplaçait et de sauvegarder ces instantanés sur le disque dur, et d'assembler ensuite les copies d'écran pour en faire un film. SCRAP travaille d'une façon différente, mais emploie la même idée.

Vous devrez INCLURE (INCLUDE) dans votre fichier de jeu; n'oubliez pas d'insérer scrap_movie (); dans main appeler la fonction scrap,

var Count = 11000; // juste un nombre plus grand que 10000
var Count1 = 0;
var Count2 = 0;
var Count3 = 0;
var Count4 = 0;
var Count5 = 0;
var Count6 = 0;
var Count7 = 0;
var Count8 = 0;
var Count9 = 0;

function
scrap_movie ()
{
WHILE (1)
{
IF (Count == 11000 && KEY_M == 1) {WAIT (2); Count = 0;}
WHILE (KEY_M == 1) {WAIT (1);}
WHILE (count < 10000)
{
Count1 = Count - 1000;
Count2 = Count - 2000;
Count3 = Count - 3000;
Count4 = Count - 4000;
Count5 = Count - 5000;
Count6 = Count - 6000;
Count7 = Count - 7000;
Count8 = Count - 8000;
Count9 = Count - 9000;

IF (Count < 10) {screenshot "Mv000",Count;}
IF (Count >= 10 && Count < 100) {screenshot "Mv00",Count;}
IF (Count >= 100 && Count < 1000) {screenshot "Mv0", Count;}
IF (Count >= 1000 && Count < 1010) {screenshot "Mv100",
Count1;}
IF (Count >= 1010 && Count < 1100) {screenshot "Mv10",
Count1;}
IF (Count >= 1100 && Count < 2000) {screenshot "Mv1",
Count1;}

IF (Count >= 2000 && Count < 2010) {screenshot "Mv200", Count2;}
IF (Count >= 2010 && Count < 2100) {screenshot "Mv20", Count2;}
IF (Count >= 2100 && Count < 3000) {screenshot "Mv2", Count2;}

..........................................................................
// repète pour Count = 3000...8000

IF (Count >= 9000 && Count < 9010) {screenshot "Mv900", Count9;}
IF (Count >= 9010 && Count < 9100) {screenshot "Mv90", Count9;}
IF (Count >= 9100 && Count < 10000) {screenshot "Mv9", Count9;}

Count += 1;
IF (KEY_M == 1) {WAIT (2); Count = 11000;}
WHILE (KEY_M == 1) {WAIT (1);}
WAIT (1);
}
WAIT (1);
}
}

Nous employons Count pour commencer ou arrêter le film quand la touche "M" est appuyée; si Count est plus petit que 10000, les instantanés sont sauvegardés sur votre disque dur; pour arrêter ce processus, nous mettons Count à 11000 (chacun nombre plus grand que 10000 est un nombre de Counter valable).

SCREENSHOT peut sauvegarder jusqu'à 999 instantanés aussi si votre film est à 25 fps, vous êtes limités à 999 / 25 = 40 secondes. J'ai ajouté quelques variables (Count1...Count9) qui peuvent être utilisée pour aller jusqu'à 9999 instantanés c'est à dire 7 minutes à 25 fps.

Lorsque vous appuyez sur M, vous notez un ralentissement- c'est normal parce que les instantanés sont sauvegardés sur votre disque dur. Lorsque vous voulez terminer la capture, pressez M de nouveau. Sortez du jeu et déplacez vous dans le répertoire des instantanés - que vous avez pu créer comme ceci :

SAVEDIR "C:\\SHOTS";

Si vous n'utilisez pas une ligne comme celle-ci dans votre fichier principal, les instantanés seront sauvegardés dans votre répertoire de travail, se mélangeant avec les autres fichiers.

Jetez un oeil sur ces instantanés; leur nom commence par mv0000.pcx et peuvent aller jusqu'à mv3201.pcx par exemple. Utilisez n'importe quel programme qui transforme une suite d'image en un film vidéo (Animation Shop qui est fourni Paint Shop Pro est un bon exemple), puis sauvez le film comme avi, mpg, etc.

Utilisez des petites résolutions pour votre film;ils apparaîtront grands de toute façon parcequ'il tourne dans des petites fenêtres, pas en plein écran. J'utilise 320x200 à chaque fois et c'est de cette façon que j'ai créé le film METAL. Changez juste la résolution d'écran à 320x200 dans votre jeu, puis pressez M pour démarrer la capture.Si vous avez déjà vu des films en présentation de jeux, vous avez noté qu'ils étaient dans des fenêtres de petites tailles.

J'ai fait des essais sur 2 de mes ordinateurs: Athlon 900 MHz, 60 GB Quantum Fireball Plus AS 7200 rpm et Athlon 750 MHz, 13 GB Quantum Fireball 4400 rpm. Scrap sauvegarde environ 25 sur le système avec le disque le plus rapide et 15 fps pour le deuxième PC. Si vous arrivez à avoir plus de 8 fps, alors vous pouvez créer un film - le film sur mon site a été fait en 15 fps et avec une taille convenable.

Si vous avez un bon processeur avec un disque dur rapide, vous pouvez créer des films en 400x300 pixels, mais leur taille est trop grande à moins que vous ne fassiezun film de 2 secondes. Intel PIII 1000 MHz avec 60GB QFB Plus AS 7200 rpm donnera 15 fps à 400x300.


Code pour scénes coupées de caméra
Comment fonctionne-t-il ? vous pressez la touche R (record = enregistrement) et vous déplacez la caméra autour du joueur ou où vous voulez, pendant tout ce temps les coordonnées de la caméra (x y z pan tilt roll) sont stockées dans un immense tableau. Appuyez de nouveau sur R pour arrêter l'enregistrement - un petit fichier (cutdata.txt) est sauvegardé sur votre disque dur. A présent vous pouvez le charger et le jouer autant que vous voulez. Voyons voir un peu le code:

view cutscene_camera{}
var start_pos[6];

action
cutscene_on
{
start_pos[0] = my.x;
start_pos[1] = my.y;
start_pos[2]
= my.z;
start_pos[3] = my.pan;
start_pos[4] = my.tilt;
start_pos[5]
= my.roll;

waitt (64);

camera.visible = off;
cutscene_camera.size_x = screen_size.x;
cutscene_camera.size_y
= screen_size.y;
cutscene_camera.pos_x = 0;
cutscene_camera.pos_y =
0;
cutscene_camera.x = my.x;
cutscene_camera.y = my.y;
cutscene_camera.z
= my.z;
cutscene_camera.visible = on;
on_mouse_right = null;
while
(1)
{
cutscene_camera.x += 30 * cos(cutscene_camera.pan) * (mouse_right
- mouse_left) * time;
cutscene_camera.y += 30 * sin(cutscene_camera.pan)
* (mouse_right - mouse_left) * time;
cutscene_camera.z += 100 * sin(cutscene_camera.tilt)
* (mouse_right - mouse_left) * time;
cutscene_camera.pan -= 20 * (mouse_force.x)
* time;
cutscene_camera.tilt += 25 * (mouse_force.y) * time;
if (key_r
== 1 && index == 0) // if we press r and we aren't recording already
{
record_data();
}
if (key_p == 1) // if we press p
{
play_data();
}
wait (1);
}
}

Tout d'abord vous devez placer une entité dans votre niveau et lui attacher l'action cutscene_on; Sa position doit être proche de la position de la scéne finale. Nous stockons la position initiale (x y z pan tilt roll) dans start_pos[6] et nous attendons 4 secondes pour être sur que l'écran de départ ait bien disparu. J'ai créé une vue de caméra nommée cutscene_camera qui remplace la vue par défaut (camera). La nouvelle vue cutscene_camera se déplace en utilisant la souris et ses boutons (incluant le bouton droit - RMB) par conséquent j'ai inhibé les autres actions du click droit. Le code qui déplace la cutscene_camera est simple (jetez un oeil sur le code caméra d'Aum4 - j'ai un code identique ici). Vous aimeriez savoir ce que l'on fait avec les boutons de la souris (mouse_right - mouse_left). Lorsque vous cliquez le bouton droit (RMB), mouse_right = 1 et la caméra se déplace en avançant; lorsque vous pressez le bouton gauche (LMB), mouse_right = 0 et mouse_left = 1 donc la caméra se déplace en arrière. Si les 2 boutons sont pressés la caméra ne bouge pas car (mouse_right - mouse_left) = 0 (cela se produit également si aucun bouton n'est pressé).

if (key_r == 1 && index == 0)
{
record_data();
}
if (key_p == 1)
{
play_data();
}

Si nous pressons R et que nous ne sommes pas en train d'enregistrer, record_data est appellée; nous devons presser P pour charger et jouer les données déjà enregistrées.

La fonction record_data stocke les coordonnées de la caméra dans le tableau et sauve le fichier qui les contient sur le disque dur.

var cutscene_position[60000];

function
record_data()
{
index = 0;
while (index < 60000)
{
cutscene_position[index] = 0;
index += 1;
}
index = 0;
while
(key_r == 0 && index < 59994)
{
cutscene_position[index+0]
= cutscene_camera.x;
cutscene_position[index+1] = cutscene_camera.y;
cutscene_position[index+2] = cutscene_camera.z;
cutscene_position[index+3]
= cutscene_camera.pan;
cutscene_position[index+4] = cutscene_camera.tilt;
cutscene_position[index+5] = cutscene_camera.roll;
index += 6;
wait (1);
}
filehandle = file_open_write("cutdata.txt");
temp = 0;
while (temp <= index)
{
file_var_write(filehandle,
cutscene_position[temp]);
temp += 1;
}
file_close(filehandle);
beep;
}

Nous commençons par remettre le tableau à 0 pour être sur de ne pas récupérer des données enregistrées précédemment. J'utilise un tableau de 60 000 éléments; nous en utilisons 6 à chaque frame (x y z pan tilt roll) ce qui permet de stocker 10 000 frames; donc si votre jeu s'exécute à 100 fps vous pouvez enregistrer pour 100 secondes (augmentez le nombre d'éléments si ce n'est pas assez pour vous).

L'enregistrement stoppe lorsque nous pressons R de nouveau ou lorsque le tableau est plein. La première instruction après la boucle while ouvre (crée etouvre s'il n'existe pas encore) le fichier nommé cutdata.txt. Les données dans cutscene_position isont écrites dans le fichier; à la fin un simple beep vous informe que l'enregistrement est vraiment terminé.

La dernière fonction ouvre le fichier cutdata.txt, lit son contenu et remet les positions de cutscene_camera à chaque frame:

function
play_data()
{
filehandle = file_open_read("cutdata.txt");
index = 0;
temp = 0;
while (key_l == 0 || temp < 59994)
{
cutscene_camera.x = file_var_read(filehandle);
cutscene_camera.y = file_var_read(filehandle);
cutscene_camera.z = file_var_read(filehandle);
cutscene_camera.pan =
file_var_read(filehandle);
cutscene_camera.tilt = file_var_read(filehandle);
cutscene_camera.roll = file_var_read(filehandle);
if (cutscene_camera.x
+ cutscene_camera.y + cutscene_camera.z + cutscene_camera.pan + cutscene_camera.tilt
+ cutscene_camera.roll == 0)
{
index = 0;
cutscene_camera.x = start_pos[0];
cutscene_camera.y = start_pos[1];
cutscene_camera.z = start_pos[2];
cutscene_camera.pan = start_pos[3];
cutscene_camera.tilt = start_pos[4];
cutscene_camera.roll = start_pos[5];
return;
}
temp += 1;
wait (1);
}
}

La camera s'arrête lorsque nous avons atteint la fin du fichier (temp > 59994) ou lorsque nous avons trouvé le premier élément à zéro dans le tableau; la camera sera déplacée à sa position initiale. Il y a 2 petits panneaux pour l'enregistrement (rouge) et rejouer (vert) mais ils sont très simple.


Untitled

Sélection du joueur

Cet article vous aide à un créer un menu de sélection du joueur qui inclu 3 modèles différents, chacun d'eux ayant 3 peaux différentes. Vous choisissez le modèle désiré à l'aide des touches de direction gauche et droite puis vous choisissez sa peau à l'aide des touches de direction haut et bas. Pressez la touche Entrée pour démarrer le niveau qui utiliserale joueur selectionné lorsque vous serez content de votre choix.

C'est un projet autonome mais la fonction principale (main) est vraiment simple:

function main()
{
on_d = null;
fps_max = 60;
level_load (dummy_wmb);
}

Nous chargeons un niveau appelé dummy_wmb; ce niveau est un simple cube creux avec 3 modèles à l'intérieur.

J'utilise une petite lumière placée près du sol, aussi le niveau apparaît complètement noir, bien que la texture utilisée pour le cube ne soit pas noire.

Chaque modèle a cette action qui lui est attachée:

action
rotate_model
{
my.skin = 1;
my.light = on;
my.lightred = 155;
my.lightgreen = 155;
my.lightblue = 155;
my.lightrange = 0;

Chaque modèle affiche sa première peau au démarrage du jeu; les modèles ont leur drapeau "light" de mis aussi ils paraissent rougeoyant à cause de leurs valeurs lightred, lightgreen et lightblue, malgré que leur lightrange soit mis à zéro.

     while (game_started == 0)
{
my.pan += 3 * time;
if (key_cuu ==
1)
{
while (key_cuu == 1) {wait (1);}
my.skin += 1;
if (my.skin == 4) {my.skin = 1;}
}
if (key_cud ==
1)
{
while (key_cud == 1) {wait (1);}
my.skin -= 1;
if (my.skin == 0) {my.skin = 3;}
}
if (key_cur ==
1)
{
while (key_cur == 1) {wait (1);}
camera.pan -= 40;
}
if (key_cul ==
1)
{
while (key_cul == 1) {wait (1);}
camera.pan += 40;
}

La boucle while ci-dessus s'exécute tant que game_started est à zéro. Les modèles tournent autour de leur angle pan; si nous pressons la touche de direction "haut", nous attendons que la touche soit relâchée puis nous mettons la peau suivante pourle modèle. La même chose se passe lorsque nous pressons la touche de direction "bas"; notez que nous avons limité my.skin to 1..3 car chaque modèle a 3 peaux.

Si vous pressez la touche de direction "droite", nous attendons qu'elle soit relâchée puis nous soustrayons 40 degrés de camera.pan, afin d'afficher le modèle suivant. Jetoez un oeil sur l'image ci-dessous:

Notre mission est simple, nous devons tourner la caméra de 120 degrés si nous voulons voir (et choisir) un nouveau modèle. Alors pourquoi ces 40 degrés? Et bien, nous avons 3 modèles et chacun d'eux a cette action qui lui est attachée aussi lorsque je presse la touche de direction "droite", la camera va tourner de 40 degrés pour chaque modèle, elle va donc tourner de 40x3 = 120 degrés, CQFD! La mêm chose se passe si nous pressons la touche de direction "gauche" (dans la direction opposée, bien entendu).

          camera.pan %= 360;
if (key_enter ==
1)
{
game_started = 1;
player_model = camera.pan / 120 + 1;
player_skin = my.skin;
start_game();
}
wait (1);
}
}

Nous avons besoin de limiter camera.pan dans l'intervalle 0...359 degré puis nous vérifions si la touche Entrée est appuyée. Si oui, nous mettons game_started à 1 afin de sortir de cette boucle puis nous mettons player_model à 1, 2 ou 3, en fonction de l'angle de la caméra (0, 120 ou 240). Pour terminer, nous mettons une variable nommée player_skin au numéro de la peau qui a été sélectionnée par le joueur puis nous appelons la fonction start_game():

function start_game()
{
level_load (level_wmb);
wait (3);
if (player_model == 1)
{
player = ent_create
(cbabe_mdl, start_pos, player_moves);
}
if (player_model == 2)
{
player = ent_create
(guard_mdl, start_pos, player_moves);
}
if (player_model == 3)
{
player = ent_create
(hero_mdl, start_pos, player_moves);
}
}

Cette fois nous chargeons le vrai niveau puis nous créons le modèle en fonction des valeurs stockées dans player_model; la fonction qui le gère est appelée player_moves():

function
player_moves()
{
my.skin = player_skin;
while (1)
{
my.skill1 = 5 *
(key_w - key_s) * time;
my.skill2 = 0;
my.skill3 = 0;
my.pan += 3 * (key_a
- key_d) * time;
move_mode = ignore_passable
+ ignore_passents + glide;
ent_move(my.skill1,
nullvector);
if (key_w + key_s
> 0)
{
ent_cycle("walk", my.skill46);
my.skill46 += 5 * time;
my.skill46 %= 100;
}
camera.x = my.x;
camera.y = my.y;
camera.z = my.z
+ 200;
camera.tilt = -90;
wait (1);
}
}

La première ligne met la bonne peau en fonction de la valeur stockée dans player_skin; le joueur peut se déplacer en utilisant les touches ZSQD. Si le joueur marche (Z ou S est pressée), il joue son animation "walk" dans une boucle. La caméra est placée 200 quants au dessus de son origine et hérite de ses x et y. Regardez l'image ci-dessous, elle provient du niveau jouable.


Untitled

Skeletons Inc.

Je ne sais pas pour vous mais moi je ressens de la pitié pour le vieux squelette essayant de trouver la sortie.... Quoi vous n'avez pas encore joué à Sk. Inc.?

Le but de ce jeu est d'obtenir suffisemment de points pour pouvoir passer au niveau suivant (très original n'est-ce pas?). Le nombre de vies, de points et les textes correspondants sont montrés avec le ci-dessous:

panel skinc_pan
// affiche les nombres
{
pos_x = 0;
pos_y = 0;
digits = 120, 575, 4, skinc_font,
1, lives;
digits = 650, 575, 4, skinc_font, 1, points;
flags = refresh, visible;
}

text
skinc_txt // affiche "Lives:" et"Points:"
{
pos_x = 0;
pos_y = 575;
font = skinc_font;
string = skinc_str;
flags = visible;
}

La fonction main est aussi simple que possible:

function
main()
{
level_load (skinc_wmb);
wait (2); // wait for the level to be loaded
clip_size = 0; // show all the triangles for
all the models
fps_max = 30; // lock the
frame rate to 30 fps
}

La camera est attachée à arrow.mdl placé dans le niveau en utilsant le code ci-dessous:

action
init_camera
{
my.invisible = on;
while (player == null) {wait (1);}
vec_set (camera.pos, my.pos);
while (1)
{
camera.arc = max
(10, (camera.arc - 2 * time * key_equals)); // press "+" and "-"
camera.arc = min
(70, (camera.arc + 2 * time * key_minusc)); // to zoom in  / out
vec_set (temp.x,
player.x);
vec_sub (temp.x,
my.x);
vec_to_angle (camera.pan,
temp); // rotate towards the player
wait (1);
}
}

La caméra tourne en permanence autour du joueur; la distance apparente entre la caméra et le joueur peut être changée en pressant "+" et "-". Pourquoi distance apparente? Parce que je ne change pas réellement la distance mais camera.arc qui donne un effet similaire. Jetons un oeil à l'action attachée au joueur:

action
skeleton // the player
{
while (key_any == 1) {wait
(1);}
my.z = 30;
player = me;
falling = 0;

La première ligne attend que toutes les touches soient relachées; c'est pratique lorsque l'on redémarre le jeu. Le squelette est placée avec ses pieds sur le sol, jouez avec le 30 si vous utilisez un autre modèle et / ou niveau. La variable nommée falling est mise à 1 si le joueur tombe et à 0 s'il se tient ou qu'il marche sur un sol solide.

     while (lives > 0)
{
if (key_cuu ==
1)
{
my.pan = 0;
my.skill10 = my.x;
while ((my.x < my.skill10 + step_width) && (falling == 0))
{
ent_cycle("walk", my.skill20);
my.skill20 += animation_speed * time;
my.skill20 %= 100;
my.x += speed * time;
check_content();
wait (1);
}
}

Aussi longtemps que le joueur est vivant, si nous presons la flèche de direction haute, le joueur tournera dans cette direction (pan = 0) et sauvegardera sa position actuelle x dans son skill10. Tant que le joueur n'aura pas marché sur une distance égale ou supérieure à step_width et s'il se tient sur un sol solide, tout ce qui se trouve dans la boucle sera exécuté. Ceci inclut l'animation et le mouvement qui est donné par un simple changement de la coordonnée x du joueur parce que nous n'avons pas à nous occuper des collisions avec les autres entités. La fonction check_content détectera si le joueur se tient sur un sol solide ou pas.

Le code pour les autres touches de direction (bas, gauche et droite) est identique aussi je vous laisse le regarder; regardons par contre la fin de cette fonction :.

          if (key_cud ==
1)
{
my.pan = 180;
my.skill10 = my.x;
while ((my.x > my.skill10 - step_width) && (falling == 0))
{
ent_cycle("walk", my.skill20); // play walk frames animation
my.skill20 -= animation_speed * time; // reversed "walk" animation
my.skill20 %= 100; // loop the animation
my.x -= speed * time;
check_content();
wait (1);
}
}
if (key_cul ==
1)
{
my.pan = 90;
my.skill10 = my.y;
while ((my.y < my.skill10 + step_width) && (falling == 0))
{
ent_cycle("walk", my.skill20); // play walk frames animation
my.skill20 += animation_speed * time; // "walk" animation speed
my.skill20 %= 100; // loop the animation
my.y += speed * time;
check_content();
wait (1);
}
}
if (key_cur ==
1)
{
my.pan = 270;
my.skill10 = my.y;
while ((my.y > my.skill10 - step_width) && (falling == 0))
{
ent_cycle("walk", my.skill20); // play walk frames animation
my.skill20 += animation_speed * time; // "walk" animation speed
my.skill20 %= 100; // loop the animation
my.y -= speed * time;
check_content();
wait (1);
}
}
wait (1);
}

Vu? Je vous aveais dit que le code était identique. Si ce code s'exécute c'est que le joueur a perdu toutes ses vies:

my.skill20 = 0;
while (my.skill20 < 100) // the player is dead
{
ent_cycle("death", my.skill20); // play death frames animation
my.skill23 += 5 * time; // "death" animation speed
wait (1);
}
}

Nous remettons skill20 à 0, car il est utilisé pour l'animation "walk" et "death" doit démarrer de sa première frame.

La
fonction check_content() teste le sol et met falling à 0 si le joueur marche
ou attend sur un sol solide, sinon il le met à 1.

function
check_content()
{
vec_set (temp, my.pos);
temp.z -= 64;
if (content (temp) != content_solid)
// if it is a hole
{
exclusive_global;
lives -= 1;
falling = 1;
snd_play (falling_sound,
70, 0);
while (player.z
> -1000)
{
player.z -= 35 * time;
wait (1);
}
sleep (2);
if (lives >
0)
{
points = 0;
main();
}
}
else
{
falling = 0; //
walking on solid ground
}
}

La première ligne stocke les coordonnées du joueur dans temp, puis nous soustrayons 64 quants de temp.z; ce point doit être placé sous les pieds du joueur (et à l'intérieur du bloc sol) . Si content (temp) voit un trou, le joueur tombera, aussi nous arrêtons toutes les autres phases de l'action, nous décrémentons le nombre de vies, nous mettons falling à 1 (il est utilisé dans l'action du squelette), nous jouons un son de chute, nous le déplaçons vers le bas jusqu'à ce que sa hauteur soit à plus de -1000 quants, nous attendons 2 secondes, nous remettons le score à 0, nous redémarrons le niveau, nous...oh j'arrête à présent.

Si content(temp) est solide, falling est mis à afin que le joueur puisse se déplacer.

Chaque sphère qui ajoute des points au score à cette action qui lui est attachée:

action
bonus1
{
my.passable = on;
my.transparent = on;
my.light = on;
my.bright = on;
my.lightgreen = 250;
my.lightrange = 0;
while (vec_dist (player.x,
my.x) > 30)
{
my.pan += 3 * time;
// rotate if the player is far
wait (1);
}
snd_play (bonus_sound, 30, 0);
while (my.scale_x < 3)
{
my.scale_x += 0.1
* time;
my.scale_y = my.scale_x;
my.scale_z = my.scale_x;
my.z += 0.5 * time;
my.alpha -= 2 *
time;
wait (1);
}
points += 10;
ent_remove (me);
}

La sphère est passable, transparente et est illuminée par une lumière verte. Si le joueur est éloigné de plus de 30 quants, la sphère tournera. Lorsque le joueur est proche, nous jouons un son et nous réduisons la sphère jusqu'à atteindre 3, nous la faisons monter et décrémentons son alpha. Nous ajoutons 10 points au score et nous enlevons la sphère.

Le jeu serait ennuyeux si nous n'utilisions pas quelques trucs, en voici un:

action block_breakable
{
while (vec_dist (player.x, my.x) > 70) {wait (1);}
sleep (1.5); // wait 1.5 seconds
ent_morph (my, "block2.wmb");
// morph the initial block into this one
snd_play (break_sound, 50, 0);
sleep (0.1);
ent_morph (my, "block3.wmb");
// then this block
sleep (0.1);
ent_morph (my, "block4.wmb");
// and finally this block
sleep (0.1);
while (my.z > -500) //
now move the final block downwards
{
my.z -= 15 * time;
wait (1);
}
ent_remove (me); // and then remove it
}

Ce bloc se cassera en petits morceaux si vous marchez dessus, aussi vous aurez un jeu d'options limités pour terminer le niveau. Il se repose tranquillement dans l'obscurité jusqu'à ce que le joueur soit à moins de 70 quants, puis il attend 1.5 secondes, puis se transforme en un autre bloc, joue un son, attend 0.1 seconde, se transforme en un autre bloc etc. Le dernier bloc est descendu jusqu'à ce que son z soit à 500 quants et il est enlevé.then it will be removed. Vous pouvez être sur que la diabolique fonction check_content() que nous avons vu plus haut mettra falling à 1 si vous passez trop de temps sur un de ces blocs.

Le deuxième (et désolé le dernier) bloc est :

action block_end
{
my.invisible = on;
my.passable = on;
while (points < 100) {wait (1);} // will be invisible until the player has
100 points
my.passable = off;
my.invisible = off;
snd_play (finished_sound,
30, 0);
sleep (0.5);
my.invisible = on;
snd_play (finished_sound,
30, 0);
sleep (0.5);
my.invisible = off;
snd_play (finished_sound,
30, 0);
}

Ce bloc est comme un pont qui se baisserait dès que vous avez obtenu 100 points, mais attendez! Ce n'est pas un pont et il n'est pas abaissé...Quoi qu'il en soit, c'est le bloc rouge qui vous permet d'atteindre la fin du niveau. Ce bloc sera invisible et passable jusqu'à ce que le joueur ait 100 points; dès que ceci se produira, le bloc clignotera, un bruit sera joué plusieurs fois et le joueur pourra se déplacer vers le drapeau :

action
flag
{
my.passable = on;
while (player == null) {wait (1);}
while (vec_dist (my.x, player.x) > 50) {wait (1);}
player.light = on;
player.lightred = 250;
player.lightrange = 100;
sleep (3); // wait for 3
seconds
exit; // shut down the engine
}

Si le joueur arrive assez près du drapeau, il rougeoira (il faut bien marquer l'évènement d'une façon ou d'une autre) et après 3 secondes le moteur s'arrêtera. Je sais que c'est une mauvaise idée pour passer au prochain niveau mais si vous connaissez des personnes qui s'en plaignent rappelez leur que ce jeu est simplement une démo et qu'ils veulent plus ils n'ont qu'à payer, non mais sans blagues !


Stratego: RTS and TBS template

Avant toute chose, RTS signifie Real Time Strategy et TBS signifie Turn Based Strategy, mais peut-être le saviez-vous déjà. J'ai créé un jeu de démonstration qui vous permettra de commencer si vous projetez d'écrire un jeu de stratégie. La démonstration vous permet :
- déplacer la caméra;
- créer deux types de batiments; un produit des soldats, l'autre ne produit rien (peut servir pour une mise à jour de technologie par exemple))
- Déplacer les unités en employant un système de découverte de chemin primitif.

J'ai mis la résolution d'écran à 800x600 pixels et j'ai mis mouse_range à 3000 juste pour m'assurer que je peux toucher et cliquer sur tout sur l'écran (par défaut = 1000). J'emploie aussi

var walk_speed = 2; // vitesse du soldat
var animation_speed = 6; // vitesse d'animation du soldat.

Ma fonction principale est simple (comme toujours):

function
main()
{
D3D_PANELS = ON;
LOAD_LEVEL (<strat1.wmb>);
move_camera(); // nouveau code ici
}

jetons un coup d'oeil à move_camera:

function
move_camera()
{
level_marginx = 200; // déplace la caméra de -200 à 200
level_marginy = 200; // en X et Y
CAMERA.ARC = 70;
CAMERA.X = 0; // démarre votre niveau avec le point de départ à l'origine
CAMERA.Y = 0; // parce qu'au début du jeu la caméra doit pointer sur l'origine
CAMERA.Z = camera_height; // change la hauteur de caméra pour être plus près ou
plus loin du sol
CAMERA.PAN = 90; // met la caméra pour regarder en bas.
CAMERA.TILT = -90;
CAMERA.ROLL = 0;

MOUSE_MAP = cursor_map; // choisit le curseur et le fait apparaître au centre
de l'écran
MOUSE_POS.X = SCREEN_SIZE.X / 2;
MOUSE_POS.Y = SCREEN_SIZE.Y / 2;
MOUSE_MODE = 2;

WHILE (1)
{
MOUSE_POS.X = POINTER.X; // mouvement de la souris;
MOUSE_POS.Y = POINTER.Y;

IF (MOUSE_POS.X < 1 && CAMERA.X > level_marginx * (-1)) {CAMERA.X
-= 10 * TIME;}
IF (MOUSE_POS.X > SCREEN_SIZE.X - 2 && CAMERA.X < level_marginx)
{CAMERA.X += 10 * TIME;}
IF (MOUSE_POS.Y > SCREEN_SIZE.Y - 2 && CAMERA.Y > level_marginy
* (-1)) {CAMERA.Y -= 10 * TIME;}
IF (MOUSE_POS.Y < 1 && CAMERA.Y < level_marginy) {CAMERA.Y += 10
* TIME;}
WAIT (1);
}
}

TLe
code pour le mouvement de la caméra se trouve ici:

IF
(MOUSE_POS.X < 1 && CAMERA.X > level_marginx * (-1))
{
CAMERA.X -= 10 * TIME;
}

Si la souris déplace à gauche et atteint le bord gauche (MOUSE_POS.X < 1) et nous pouvons déplacer la caméra (CAMERA.X >-200) alors nous déplaçons la caméra à gauche (défilement gauche). Les 3 autres déclarations IF travaille de la même manière; Nous employons SCREEN_SIZE.X et SCREEN_SIZE.Y pour obtenir la taille de l'écran en pixels pour le bord du bas et à droite. Nous aurions pu employer des nombres parce que nous savons déjà que nous fonctionnons en 800x600 pixels, mais nous souhaitons que notre jeu soit préparé pour des jeux de stratégie sérieux; le code peut donc lire les résolutions et adapter le mouvement de caméra automatiquement. N'oubliez pas que l'Axe des ordonnées est inversé, ainsi si nous voulons faire défiler vers le haut nous devons réduire CAMERA.Y pour le faire travailler.

Nous ajoutons un panneau qui crée des bâtiments s'il est cliqué et joue un son agréable quand nous sommes sur les boutons employant la fonction de mouse_over :

PANEL
main800_pan
{
BMAP main800_map;
LAYER 20;
POS_X 0;
POS_Y 0;
BUTTON 4, 4, building1_map, building1_map, building11_map, create_building1, NULL,
mouse_over;
BUTTON 4, 65, building2_map, building2_map, building22_map, create_building2,
NULL, mouse_over;
FLAGS OVERLAY, REFRESH, TRANSPARENT, VISIBLE;
}

Si nous cliquons sur le bouton building1, il déclenchera la fonction de create_building1 :

function
create_building1()
{
WHILE (MOUSE_LEFT == 1 || MOUSE_RIGHT == 1) {WAIT (1);} // attend qu'on relache
le bouton de la souris
WHILE (1)
{
IF (MOUSE_LEFT == 1)
{
build1_pos.X = MOUSE_POS.X;
build1_pos.Y = MOUSE_POS.Y;
build1_pos.Z = camera_height; // place les bâtiments sur la position du sol
vec_for_screen (build1_pos,CAMERA);
CREATE (<build1.wmb>, build1_pos, build1_action);
END;
}
IF (MOUSE_RIGHT == 1) {END;} // annule si le bouton droit de la souris est pressé.
WAIT (1);
}
}

Si vous appuez sur le bouton building1, vous déclenchez plusieurs évènements c'est le pourquoi de cette ligne

WHILE (MOUSE_LEFT == 1 || MOUSE_RIGHT == 1) {WAIT (1);}

attend que le bouton gauche soit relaché, alors le code continue. Si le bouton gauche est cliqué de nouveau,nous donnons les coordonnées de la souris à build_pos, qui les convertit en coordonnées de notre monde avec vec_for_screen. Nous créons le batiment 1 (build1.wmb) à la position du pointeur et lançons build1_action:

function
build1_action()
{
MY.PUSH = 20; // les unités de soldats ont une poussée PUSH = 10, les constructions
ont PUSH = 20
WHILE (MOUSE_LEFT == 1) {WAIT (2);} // ne crée pas d'unités par erreur
MY.ENABLE_SCAN = ON; // est rendu sensible au balayage
MY.ENABLE_CLICK = ON;
MY.EVENT = build_stuff1; // crée des unités lorsque cliqué
scan_area.PAN = 360;
scan_area.TILT = 60;
scan_area.Z = 900;
vec_set (MY_POS, build1_pos);
SCAN MY_POS, MY_ANGLE, scan_area;
IF (RESULT > 0 && RESULT < 200) // les bâtiments doivent être séparés
par 200 quants ou plus
{
PLAY_SOUND cantplacehere_snd, 100;
REMOVE ME;
}
ELSE
{
PLAY_SOUND buildingplaced_snd, 100;
}
}

Building1 est sensible au balayage (SCAN) et au clic (CLICK). Lorsqu'il est posé sur le sol, il commence à balayer autour de lui. Si d'autres bâtiments se trouvent près (RESULT > 0 signifie que SCAN a trouver un autre bâtiment) et qu'il se trouve à moins de 200 quants, la présente création de bâtiment est instentanément interrompue. S'il n'y a aucune construction alentour, la construction se réalise et le son buildingplaced_snd est joué.

Qu'arrive-til lorsqu'un bâtiment est cliqué? C'est l'évènement build_stuff1 qui est déclenché :

function
build_stuff1()
{
IF (EVENT_TYPE == EVENT_CLICK) // on s'assure que c'estbien un clic
{
IF (MY.SKILL10 > 2)
{
END; // 8 positions remplies, on arrête de créer des unités
}
IF (MY.SKILL9 == 1 && MY.SKILL10 == 1) // exclut le centre du bâtimentof
the building
{
MY.SKILL9 += 1;
}
temp.X = unitstartxpositions[MY.SKILL9] + MY.X;
temp.Y = unitstartxpositions[MY.SKILL10] + MY.Y;
temp.Z = MY.Z;
CREATE (<guard.mdl>, temp, unit1_action);
PLAY_SOUND unitcreated_snd, 50;
MY.SKILL9 += 1;
IF (MY.SKILL9 > 2)
{
MY.SKILL9 = 0; // prochain rang
MY.SKILL10 += 1;
}
}
}

Chaque bâtiment peut créer jusqu'à 8 unités (soldats). Nous avons quelques tableaux pour nous aider à le faire:

var unitstartxpositions[3] = -65, 0, 65; // stocke les décalages d'unités par rapport au centre du wmb
var unitstartypositions[3] = -65, 0, 65; // nous n'utilisons pas 0,0 (centre du bâtiment)

L'idée est de placer le building1 wmb sur le terrain; ces tableaux nous aideront à placer les unités - regardez cette image

Nous pouvons placer 8 unités autour du batiment (3 rangs et 3 colonnes, mais nous avons exclu le centre). Les chiffres dans l'image sont les décalages des unités liées au bâtiment. J'emploie SKILL9 et 10 pour ces décalage (les 8 premières variables sont accessibles dans Wed et j'ai pensé que je devais les laisser libres pour votre utilisation). Quand le bâtiment est cliquée pour la première fois, SKILL9 et SKILL10 sont 0, donc le premier soldat aura un décalage donné par unitstartxpositions[0] = -65 et unitstartypositions[0] = -65. Le bâtiment a SKILL9 qui augmente de 1 au deuxième clic (deuxième soldat), aussi l'unité aura le décalage (0, -65). Cela sans arrêt (excluant (0,0)) jusqu'à ce que les (8 soldats) soient créés. Finallement, la fonction la plus complexe - move_unit - va vous être expliquée :

function
move_unit ()
{
MY.PUSH = 10; // poussée basse pour les unités
IF (EVENT_TYPE == EVENT_ENTITY)  // collision avec les bâtiments ou une autre
unité
{
// caveman's path finding code
EXCLUSIVE_ENTITY;
MY.PAN = RANDOM (360);
WAITT (10); // attend pour remettre la cible de nouveau
temp.X = MY.SKILL12; // stocke les coordonnées du pointeur de souris
temp.Y = MY.SKILL13;
temp.Z = 0;
vec_sub (temp, MY.X);
vec_to_angle (MY.PAN, temp); // tourne l'unité en direction de la cible de nouveau
//enlever les lignes ci dessus si vous voulez écrire votre propre code pour trouver
un chemin
}
IF (EVENT_TYPE == EVENT_CLICK)
{
MY.SKILL11 = 0;
WHILE (MOUSE_LEFT == 1) {WAIT (2);} // attend que le bouton gauche soit relâché
WHILE (MY.SKILL11 == 0) //attend que la cible soit mise (clic gauche)
{
IF (MOUSE_LEFT == 1)
{
EXCLUSIVE_ENTITY; // arrête l'exécution d'actions parallèles pour cette
unité
MY.SKILL11 = 1; // la cible est mise
temp.X = MOUSE_POS.X;
temp.Y = MOUSE_POS.Y;
temp.Z = camera_height;
vec_for_screen (temp, CAMERA); // temp contient les coordonnées du pointeur à
présent
MY.SKILL12 = temp.X; // stocke les coordonnées du pointeur avant qu'elle ne soient
perdues
MY.SKILL13 = temp.Y;
vec_sub (temp, MY.X);
vec_to_angle (MY.PAN, temp); // fait tourner l'unité vers la souris
MY.SKILL15 = 500; // longueur maximale du chemin = carburant :)
WHILE (abs(MY.X - MY.SKILL12) + abs(MY.Y - MY.SKILL13) > 3 && MY.SKILL15
> 0) // s'arrête près du pointeur de souris
{
move_mode = ignore_you + ignore_passable;
ent_move (walk_speed, nullvector);
ent_cycle("walk", MY.SKILL14);
MY.SKILL14 += animation_speed * TIME;
IF (MY.SKILL14 > 100) {MY.SKILL14 = 0;}
MY.SKILL15 -= TIME; // brule du carburant
WAIT (1);
}
ent_cycle("stand", 0); // met la frame d'attente
}
WAIT (1);
}
}
}

Nous ignorons IF (EVENT_TYPE == EVENT_ENTITY) pour l'instant; cela arrive seulement si le soldat entre en collision avec une des constructions ou d'autres soldats. Pour déplacer les soldats, nous cliquons sur eux (la production d'un EVENT_CLICK), ensuite nous cliquons quelque part sur la carte et nous nous demandons à l'unité de se déplacer jusque là. J'emploie les variables 11 à 15 du soldat dans cette fonction; j'aurais pu employer SKILL9 et 10 parce qu'ils ne sont pas de liens avec les SKILL9 et 10 employés dans les fonctions de construction, mais j'ai voulu faire les choses aussi claires que possible.

Quand nous cliquons sur l'unité, son SKILL11 est mis à zéro et il restera jusqu'à ce que nous ne cliquions sur la carte pour mettre la cible. Si nous avons mis la cible, temp est employé pour obtenir les coordonnées du pointeur de souris; nous les convertissons en positions du monde avec vec_for_screen et nous les stockons sans risque dans SKILL12 et 13 parce que temp peut soudainement changer sa valeur; nous connaissons déjà la coordonnée de Z de l'unité, donc nous ne devons pas la stocker.

L'unité doit tourner vers la souris et commencer à se déplacer, essayant d'atteindre sa cible. Mon code pour trouver le chemin est vraiment simple, donc l'unité peut réfléchir trop avant la découverte du bon chemin; c'est pourquoi j'ai ajouté "le carburant". Si une unité ne peut pas trouver son chemin correct, elle se déplacera vers la cible avant qu'elle ne brûle tout son carburant.

WHILE (abs(MY.X - MY.SKILL12) + abs(MY.Y - MY.SKILL13) > 3 && MY.SKILL15 > 0)

Fera l'arrêt d'unité quand elle atteint la cible ou quand SKILL15 = 0 (est tombeé en panne de carburant). J'emploie une méthode simple pour vérifier si l'unité a atteint la cible; rappelez-vous s'il vous plaît que SKILL12 et SKILL13 tiennent les coordonnées de la cible.

L'unité avance vers la cible et les différences "bleues" deviennent de plus en plus petits. Quand leur somme est plus petite que 3, les unités s'arrêtent. J'ai employé le nouveau fichier guard.mdl; il marche avec walk_speed de mis au commencement et est animé employant ces lignes :

ent_cycle("walk", MY.SKILL14);
met la frame d'animation a la première frame de marche (MY.SKILL14 = 0 pour l'instant, ainsi le garde est mis à la première frame de la marche)

MY.SKILL14 += animation_speed * TIME;
va à travers les frames de l'animation; SKILL14 contient la frame courante de la marche

IF (MY.SKILL14 > 100) {MY.SKILL14 = 0;}
boucle l'animation

Si l'unité est arrêtée (soit qu'elle a atteint son but, soit qu'elle n'a plus de carburant), elle bascule en mode debout

ent_cycle("stand", 0); // met la frame d'attente= première frame debout

Vous ai-je dit que c'était la dernière fonction? Nous en avons parlé dans le code de recherche de chemin, et il est vraiment simple:

IF
(EVENT_TYPE == EVENT_ENTITY)  // collision avec un bâtiment ou une unité
{
// caveman's path finding code
EXCLUSIVE_ENTITY;
MY.PAN = RANDOM (360);
WAITT (10); // attend pour remettre la cible de nouveau
temp.X = MY.SKILL12; // stocke les coordonnées du pointeur de souris
temp.Y = MY.SKILL13;
temp.Z = 0;
vec_sub (temp, MY.X);
vec_to_angle (MY.PAN, temp); // tourne l'unité vers la cible de nouveau
// enlevez ces lignes si vous voulez mettre votre propre code
}
.................................................................................

Si l'unité entre en collision avec quelque chose, elle choisit une direction aléatoire et se déplace pendanr environ une 1/2 seconde puis elle lit de nouveau la bonne direction à partir de SKILL12 et 13 et essaye de nouveau d'atteindre sa cible.

Il y a différentes choses que nous pouvons ajouter à ce code pour le rende parfait ;)
- Meilleur code de recherche de chemin;
- Les constructions ne peuvent pas être placées les unes sur les autres, mais vous pouvez placer les constructions sur des unités isolées et c'est mauvais. Je n'emploierais pas de balayage pour chaque unité parce qu'il ralentirait le jeu dans la théorie (je l'ai fait avec les constructions, mais il n'y en a pas trop dans un niveau). Je stockerais toutes les positions d'unité dans un tableau et je comparerais les coordonnées de la construction avec toutes les coordonnées d'unité stockées dans le tableau. Le tableau pourrait être aussi employé pour améliorer l'algorithme de recherche de chemin.
- Etc.

Notez s'il vous plaît que j'ai employé un modèle régulier avec 644 polygones pour le soldat. Je ne le ferais pas à votre place - avec 100 modèles visibles sur l'écran en même temps, vous ralentirez le moteur et vous ne le voulez pas.


Untitled

Stratego 2

Si vous avez aimé Stratego dans Aum2, vous aimerez Stratego2 pour toutes ses fonctionalités:
- Vrais niveaux 3D
- Unité sélectionnable (clic)
- Sélection multiple d'unités (en traçant un rectangle autour des unités)
- Des sons differents pour chaque unité

Je suppose que vous êtes familier avec Stratego, sinon reportez-vous au magazine n°2, aussi je n'explique plus comment on construit les batiments les unités etc.je préfère que nous nous concentrions sur le nouveau code.

Stratego 2 est un projet à lui tout seul c'est pourquoi nous avons notre propre fonction main:

function
main()
{
level_load (stratego2_wmb);
wait (2); // attendons que le niveau soit chargé
clip_size = 0; // montre tous les triangles pour
tous les modèles
fps_max = 40; // bloque le
taux d'affichage
camera.x = 0;
camera.y = 0;
camera.z = camera_height;
camera.tilt = -90;
camera.pan += 90;
mouse_mode = 2;
mouse_map = pointer_map;
mouse_pos.x = screen_size.x
/ 2;
mouse_pos.y = screen_size.y
/ 2;
move_camera();
multiple_selection();
while (index < 9)
{
temp.x = start_pos[index];
temp.y = start_pos[index
+ 1];
temp.z = start_pos[index
+ 2];
ent_create (guard_mdl,
temp, init_unit);
index += 3;
}
}

La caméra regarde en bas parce que son tilt = -90; son pan est tourné de 90 degrés parce qu'elle doit pointer dans la bonne direction. Le curseur de la souris est rendu visible, placé au centre de l'écran pour l'instant et un certain nombre de fonctions sont initialisées (nous les verrons un peu plus tard). Trois unités (guard.mdl) sont créées; leur position de départ sont lue de:

var start_pos[9] = 200, 200, 500, -200, 200, 500, 0, -200, 500;

Le premier garde est placé à = 200, 200, 500, le second à -200, 200, 500 et ainsi de suite. Chaque garde exécute la fonction init_unit():

function init_unit()
{
wait (1);
my.enable_click = on;
my.enable_entity = on;
my.enable_block = on;
my.event = move_unit;
vec_set (temp, my.pos);
temp.z -= 3000;
trace_mode = ignore_me +
ignore_sprites + ignore_models + use_box;
my.z -= trace (my.pos, temp); // place l'unité sur le sol pour le cas ou
elle aurait été créée en l'air
my.target_x = 100000;
while (1)
{
while (my.target_x
< 50000) {wait (1);}
if ((my.x >
upleft_coords.x) && (my.x < upright_coords.x) && (my.y <
upleft_coords.y) && (my.y > downleft_coords.y) && (my.unit_selected
== 0))
{
my.unit_selected = 1;
move_unit();
}
else
{
if (my.unit_selected == 1)
{
vec_set (upleft_coords, nullvector); // reset selection coords
vec_set (upright_coords, nullvector);
vec_set (downleft_coords, nullvector);
vec_set (downright_coords, nullvector);
}
}
wait (1);
}
}

Les unités réagissent
aux clics de la souris, les collisions avec la géométrie du niveau
et les collisions avec d'autres entités. Les gardes sont générés
en l'air (leur coordonnée Z est à 500 dans le tableau, souvenez-vous?)
aussi nous avons besoin de les faire descendre sur le sol. Nous traçons
3000 quants sous l'unité et nous soustrayons le résultat de son
z initial; my.target_x = 100000 signifie simplement que l'unité n'a pas
de destination pour l'instant (les coordonnées sont données en quants
aussi 100000 quants est une façon de dire hors intervalle).

Vous allez voir beaucoup de my.target_x ou my.unit_selected, etc dans Stratego 2 mais n'ayez crainte: ce sont des variables que j'ai défini au début de stratego2.wdl: par exemple, my.target_x est la même chose que my.skill12 mais est beaucoup plus parlant, n'est-il pas?

Chaque unité a une boucle while(1) autorisant ainsi une autonomie de fonctionnement même si l'unité accomplit d'autres tâches; si le target_x de l'unité est plus grand que 50000 (nous avons posé comme postulat que target_x = 100000 n'était pas une destination, OK?) et l'unité a été selectionnée (seule ou avec d'autres) par le traçage d'un rectangle autour d'ell, l'unité sera sélectionnée (my.unit_selected = 1) et elle commencera son déplacement vers sa destination. Dés que l'unité est selectionnée les coordonnées de la sélection sont remises à zéro. Parlons un peu de ces coordonnées :

function multiple_selection()
{
while (1)
{
if (mouse_left
== 1)
{
upper_left.visible = on;
upper_right.visible = on;
lower_left.visible = on;
lower_right.visible = on;
if (first_click == 0) // pour êtrer sur qu'il n'est exécuté
qu'une foismake sure that this "if"
{
first_click = 1;
upper_left.pos_x = pointer.x; // stocke la position du panneau
upper_left.pos_y = pointer.y;
}
lower_right.pos_x = pointer.x; // stocke la position du panneau
lower_right.pos_y = pointer.y;

lower_left.pos_x = upper_left.pos_x; // stocke la position du panneau
lower_left.pos_y = lower_right.pos_y;

upper_right.pos_x = lower_right.pos_x; // stocke la position du panneau
upper_right.pos_y = upper_left.pos_y;

          }
else // la sélection
multiple  est terminée
{
upper_left.visible = off;
upper_right.visible = off;
lower_left.visible = off;
lower_right.visible = off;
first_click = 0;

upleft_coords.x = upper_left.pos_x; // project upper_left's panel coords on the
map
upleft_coords.y = upper_left.pos_y;
upleft_coords.z = camera_height;
vec_for_screen (upleft_coords, camera);

upright_coords.x = upper_right.pos_x; // project upper_left's panel coords on
the map
upright_coords.y = upper_right.pos_y;
upright_coords.z = camera_height;
vec_for_screen (upright_coords, camera);

downleft_coords.x = lower_left.pos_x; // project upper_left's panel coords on
the map
downleft_coords.y = lower_left.pos_y;
downleft_coords.z = camera_height;
vec_for_screen (downleft_coords, camera);

downright_coords.x = lower_right.pos_x; // project upper_left's panel coords on
the map
downright_coords.y = lower_right.pos_y;
downright_coords.z = camera_height;
vec_for_screen (downright_coords, camera);
}
wait (1);
}
}

La fonction multiple_selection() met les coordonnées qui sont nécessaires de chaque unité pour voir si elle entre dans la sélection ou pas par le rectangle dessiné à la souris. Explication avec ce schéma:

Lorsque l'on clique sur la carte (mouse_left = 1) 4 panneaux simples (upper_left, upper_right, lower_left, lower_right) sont affichés à l'écran. Ces panneaux sont les petits coins verts que vous apercevez sur l'image ci-dessus.Le premier panneau (upper_left) apparaît à l'endroit où vous avez cliqué et reste là, ; les autres panneaux changent leur position en fonction du déplacement de la souris sur l'écran pour dessiner un rectangle plus ou moins large / haut. Bien entendu nous aurions pu utiliser un simple carré et jouer sur ses valeurs x et y.

Lorsque la souris est relachée (le rectangle a été dessiné autour des unités) nous allons à l'embranchement else: Les panneaux sont rendus invisibles et les coordonnées des 4 coins sont traduits en coordonnées géométriques de niveau en utilisant l'instruction vec_for_screen, comme sur cette image:

A ce point nous avons créé un paralépipède virtuel qui a la même taille que le rectangle que nous avions dessiné à l'écran; le code dans init_unit vérifie si les unités correspondantes sont placées dans ce paralépipède virtuel:

if ((my.x > upleft_coords.x) && (my.x < upright_coords.x) && (my.y < upleft_coords.y) && (my.y > downleft_coords.y) && (my.unit_selected == 0))
{
..............
}

Dans mon exemple toutes les unités sont placées à l'intérieur de mon paralépipède virtuel donc elles seront toutes sélectionnées.

Voici la fonction move_unit():

function
move_unit()
{
if (event_type == event_block || event_type == event_entity)  // collision
avec d'autres entités ou la géométrie du niveau
{
if (abs(my.x -
my.target_x) + abs(my.y - my.target_y) < 100) // près de la destination
mais il y a déjà d'autres entités qui sont là
{
my.target_reached = 1; // l'unité a atteint son but
my.unit_selected = 0;
my.target_x += 100000; // donne une valeur de destination éloignée
pour sortir de la branche if
}
else
{
// un code de recherche de cheminarchaïque - voir Aum2
my.pan += 90 - random(180);
waitt (10); // wait a little
temp.x = my.target_x; // stored mouse pointer coordinates
temp.y = my.target_y;
temp.z = 0;
vec_sub (temp, my.x);
vec_to_angle (my.pan, temp); // rotate unit towards the target again
my.tilt = 0; // stand tall :)
}
}
if (event_type == event_click || my.unit_selected
== 1)
{
my.destination
= 0;
my.target_reached
= 0; // target not available yet
ent_create (selected_pcx,
my.pos, selected_unit);
my.selected_handle
= snd_play (selected_snd, 70, 0);
snd_tune (my.selected_handle,
70, 80 + random(20), 0); // different "huh?" voices
while (mouse_left
== 1) {wait(1);} // wait for the mouse release
while (my.destination
== 0) // as long as the target hasn't been set
{
if (mouse_left == 1)
{
mouse_map = pointerhigh_map;
my.ok_handle = snd_play (ok_snd, 70, 0); // store the sound handle in a skill,
no need to use a separate var
snd_tune (my.ok_handle, 70, 80 + random(50), 0); // different "ok" voices
waitt (4); // show the cross for 0.25 seconds
mouse_map = pointer_map;
my.destination = 1;
temp.x = mouse_pos.x;
temp.y = mouse_pos.y;
temp.z = camera_height;
vec_for_screen (temp, camera); // temp holds pointer's coords now
my.target_x = temp.x; // store pointer's coords before they get lost
my.target_y = temp.y;
vec_sub (temp, my.x);
vec_to_angle (my.pan, temp); // rotate the unit towards the target
my.tilt = 0; // we only need the correct "pan" angle, not tilt
my.fuel = 300; // maximum path length (fuel)
while (abs(my.x - my.target_x) + abs(my.y - my.target_y) > 3 && my.fuel
> 0 && my.target_reached != 1) // stop near the target
{
my.unit_selected = 0;
vec_set (temp, my.x);
temp.z -= 3000;
trace_mode = ignore_me + ignore_sprites + ignore_models + use_box;
unit_speed.z = -trace (my.x, temp);

move_mode = ignore_you + ignore_passable;
ent_move (unit_speed, nullvector);
ent_cycle("walk", my.animation_frame); // play walk frames animation
my.animation_frame += 5 * time; // "walk" animation speed
if (my.animation_frame > 100) {my.animation_frame = 0;} // loop animation
my.fuel -= time; // burn fuel
wait (1);
}
my.target_reached = 1; // the unit has reached the target
my.target_x += 100000;
ent_cycle("stand", 0); // the unit stands still now
}
wait (1);
}
}
}

Cette fonction est exécutée lorsque l'unité est cliquée (son event_click est déclenché) ou si my.unit_selected = 1 (l'unité a été selectionnée par le dessin de notre rectangle). Si un de ces évènement arrive nous créons un sprite vert qui entoure l'unité, jouons un son "huh?" avec une fréquence aléatoire (c'est pourquoi nous utilisons snd_tune et random) et nous attendons que le bouton gauche de la souris soit relâché. A présent nous n'attendons plus que la destination, si nous cliquons quelque part sur la carte, le pointeur se change en croix (pointerhigh_map), le son "ok" - là encore avec une fréquence différente pour chaque unité - est joué et le pointeur de la souris redevient normal. Une autre instruction vec_for_screen convertit les coordonnées de la souris en une position dans le niveau - qui sera la destination de nos unités - Les gardes se tournent vers cette destination et se déplacent aussi longtemps que la distance entre eux et la destination est supérieure à 3.

D'un autre côté une destination pourrait être inatteignable pour certaines unités, dans ce cas elles doivent s'arrêter au bout d'un certain temps, c'est pourquoi j'utilise my.fuel: les unités démarrent avec fuel = 300 et elles brûlent ce carburant tant qu'elles se déplacent dans le niveau. Lorsque fuel < 0, il n'y a plus de carbura nt, les unités s'arrêtent même si elles sont loin de leur destination.

J'utilise encore trace car je dois garder les unités sur le sol en permanence; les unités 'tracent' uniquement pendant les déplacements. Lorsqu'une unité a atteint son but, ou n'a plus de carburant, son target_x est mis à une valeur d'environ 100000 (qui signifie que ce n'est plus une destination) et son drapeau target_reached est mis à 1 (cible atteinte).

     if (event_type == event_block || event_type ==
event_entity)  // collision with other entities or level geometry
{
if (abs(my.x -
my.target_x) + abs(my.y - my.target_y) < 100) // close to the target but other
entities are already there
{
my.target_reached = 1; // the unit has reached the target
my.unit_selected = 0;
my.target_x += 100000; // move the target far away to get out of this "if" branch
}
else
{
// caveman's path finding code - read more about it in Aum2
my.pan += 90 - random(180);
waitt (10); // wait a little
temp.x = my.target_x; // stored mouse pointer coordinates
temp.y = my.target_y;
temp.z = 0;
vec_sub (temp, my.x);
vec_to_angle (my.pan, temp); // rotate unit towards the target again
my.tilt = 0; // stand tall :)
}
}

Je ne sais pas si vous avez vu ces lignes au dessus dans move_unit. Elles sont rès utiles lorsque les unités se cognent entre elles ou avec un bloc du niveau. Si la destination lorsque la collision intervient c'est pratiquement sur que l'autre entité est déjà à destination. Toutes les unités ont la même destination (si plus d'une est selectionnée) aussi lorsqu'une unité atteint sa destination en 2ème, 3ème etc position, elle s'arrêtera dès qu'elle touchera une autre entité dans le même secteur. Si une unité touche un bloc ou une autre entité mais loin de sa destination, on change son angle pan, l'unité avance un peu dans cette nouvelle direction, puis elle essaye de nouveau d'atteindre sa destination.

function
selected_unit()
{
my.unlit = on; // shouldn't be affected by the lights in the level
my.ambient = 100; // make
it bright
my.oriented = on;
my.passable = on;
my.tilt = 90;
while (you.target_reached
== 0) // as long as the target hasn't been reached
{
vec_set (my.pos,
you.pos); // move with the unit
wait (1);
}
ent_remove (me); // the target has been reached
so the square has to disappear
}

La fonction selected_unit déplace le sprite vert qui entoure l'unité jusqu'à ce qu'elle arrive à destination et enlève ensuite le sprite vert.

La dernière fonction estreprise d'Aum2:

function
move_camera()
{
waitt (4); // wait for the level to load
level_marginx = 1000; // the map has 2000 quants on x (-1000...+1000)
level_marginy = 1000; //
and on y (-1000...+1000)
while (1)
{
mouse_pos.x = pointer.x;
mouse_pos.y = pointer.y;
if (mouse_pos.x
< 1 && camera.x > level_marginx * (-1)) {camera.x -= 10 * time;}
if (mouse_pos.x
> screen_size.x - 2 && camera.x < level_marginx) {camera.x += 10
* time;}
if (mouse_pos.y
> screen_size.y - 2 && camera.y > level_marginy * (-1)) {camera.y
-= 10 * time;}
if (mouse_pos.y
< 1 && camera.y < level_marginy) {camera.y += 10 * time;}
wait (1);
}
}

Vous pouvez mettre les frontières du niveau (aussi lontemps que la caméra est concernée) avec level_marginx et level_marginy et ajuster sa vitesse de scrolling en remplaçant 10 avec une valeur plus petite ou plus grande.

Je veux croire que Stratego1 & 2 peuvent être utilisés pour créer un bon jeu de stratégie; la seule chose qui nous manque c'est une bonne IA (pour trouver son chemin) et le combat rapproché. Démarrez le développement de votre jeu aujourd'hui et je peux vous assurez que vous aurez ces éléments dans de futur AUM dans quelques mois :)


Untitled

Jeu de tir à la 3ème personne

Non, ce n'est pas le 3ème jeu de tir dans ce magazine, mais le code pour un jeu de tir à la 3ème personne. La bonne nouvelle c'est que vous pouvez utiliser ce code pour n'importe quel jeu d'arcade. Ce projet à sa propre fonction main:

function
main()
{
level_load
(shoot3rd_wmb);
wait (2);
clip_size = 0;
fps_max = 50;
on_d = null;
}

Rien de spécial sur ce main; J'ai bloqué la fréquence d'affichage à 50 fps et interdit à la touche "D" de montrer le panneau débug car nous allons l'utiliser pour nos mouvements (WSAD).

L'action attachée au joueur est la plus importante aussi je commence par elle avant que vous soyez fatigué:

action
player_init
{
player = me;
player.skill20 = 100;
my.enable_impact = on;
my.event = player_damage;
camera.z = my.z + 700;
camera.tilt = -90;
ent_create(soldiergun_mdl, nullvector,
attach_soldiergun); // on lui donne une arme

Nous utilisons la variable d'entité skill20 pour stocker sa santé. Le joueur est sensible à l'impact; s'il est touché par quelque chose cela déclenchera son évènement player_damage. La caméra est placée 700 quants au dessus du joueur et regarde dans sa direction. Nous donnons une arme au joueur, mais nous parlerons de la fonction attach_soldiergun un peu plus tard.

while (my.skill20 > 0)
{
my.pan += 4 * (key_a
- key_d) * time - 20 * mouse_force.x * time;
player_speed.x = 10 * (key_w - key_s) *
(1 - mouse_left) * time;
player_speed.y = 0;

Tant que le joueur est vivant (skill20 > 0), i peut tourner en changeant son angle pan avec les touches "Q" et"D". Le joueur se déplace vers l'avant / arrière avec les touches "Z" et"S", mais uniquement si le bouton gauche de la souris(LMB) n'est pas pressé. J'ai fait cela parce que lorsque le joueur tire, son arme pointe vers l'ennemi et lorsque ce modèle de joueur marche son arme pointe vers la gauche.

      if (mouse_left == 1)
{
ent_cycle
("attack", 100);
ent_create (bullet_mdl,
my.skill12, init_shot);
snd_play (shoot1_snd,
70, 0);
while
(mouse_left == 1) {wait (1);}
}
else
{
if (player_speed.x !=
0)
{
ent_cycle
("walk", my.skill10);
my.skill10
+= 10 * time;
if
(my.skill10 > 100) {my.skill10 = 0;}
}
}

Si nous pressons le bouton gauche de la souris (LMB), le joueur change sa frame avec la dernière frame "attack" et une balle est tirée. Nous ne souhaitons pas un tir continu aussi nous attendons que le bouton soit relaché. Si le bouton gauche n'est pas pressé, le joueur peut jouer son animation marche dans une boucle..

     vec_set (temp, my.x);
temp.z -= 3000;
trace_mode = ignore_me + ignore_sprites + ignore_models
+ use_box;
player_speed.z = -trace (my.x, temp);
ent_move(player_speed, nullvector);

     camera.pan = my.pan;
camera.x = my.x + 250 * cos (my.pan);
camera.y = my.y + 250 * sin
(my.pan);

wait (1);
}
}

Le joueur se déplace en utilsant la gravité, vous avez un code similaire avec stratego2. Finalement la caméra est centrée sur un point qui est placé 250 quants devant le joueur.

function
attach_soldiergun()
{
proc_late();
my.passable = on;
my.metal =
on;
while(you != null)
{
vec_set(my.x,you.x);
vec_set(my.pan,you.pan);
my.frame = you.frame;
my.next_frame
= you.next_frame;
vec_for_vertex(you.skill12,
my, 6); // la position de la balle du soldat est stockée dans la variable
d'entité skill12 du soldat
wait(1);
}
ent_remove(my);
}

La fonction qui donne une arme au joueur existe aussi lontemps que le joeur n'est pas enlevé du jeu. L'arme est animée en même temps que le joueur et comme elle a été sauvée en tant que modèle indépendant nous avons juste besoin de mettre son x, pan et frame aux mêmes valeurs que celle du joueur. Nous sauvegardons les coordonnéesdu sommet de la balle dans la variable d'entité skill12 du joeur (you.skill12 = player.skill12 parce que c'est le joueur qui a créé l'arme).

Si le joueur ou l'ennemi tire la fonction init_shot est exécutée:

function init_shot()
{
my.pan = you.pan;
my.tilt = you.tilt;
my.skill10 = 1;
my.enable_entity = on;
my.enable_block = on;
my.event = remove_bullet;
my.ambient = 100;
my.skill1 = 0;
bullet1_speed.x = 10;
bullet1_speed.y
= 0;
bullet1_speed.z
= 0;
bullet1_speed
*= time;
while ((my
!= null) && (my.skill1 < 500)) // 500 = max distance
{
my.skill1 += 1 * time;
move_mode = ignore_you
+ ignore_passents;
ent_move (bullet1_speed, nullvector);
wait (1);
}
ent_remove
(me);
}

function
remove_bullet()
{
wait (1);
ent_remove (me);
}

La balle a les mêmes angles pan et tilt que son créateur; elle peut entrer en collision avec les entités et les blocs du niveau. La balle existera (pour peu qu'elle n'est entrée en collision avec rien) pour une période limitée - en fait jusqu'à ce que son skill1 atteigne 500. La balle ignore son créateur ou n'importe quelle entité passable aussi il n'y a pas de collision avec l'arme qui la tire, avec l'eau etc. Si la balle touche quelque chose, elle est enlevée la frame suivante.

Si le joueur est touché par une balle ennemi, sa fonction player_damage est déclenchée:

function player_damage()
{
if (you.skill10
!= 1)
{
return;
}
else
{
my.skill20 -= 10;
if (my.skill20 <=
0)
{
my.enable_impact
= off;
my.event = null;
my.skill10
= 0;
my.skill30
= 1; // I'm dead
while (my.skill10 <
67)
{
ent_cycle("death",
my.skill10);
my.skill10
+= 6 * time;

if (camera.z > player.z + 300)
{
camera.z -= 10 * time;
}
wait (1);
}
}
}
}

Le joueur peut être touché par un mur, un ennemi ou une balle ennemi. C'est pourquoi nous testons skill10 au début de la fonction pour être sur qu'il a bien été touché par une balle (nous l'avons mise à 1 dans la fonction init_shot, souvene-vousr?) Chaque balle lui enlève 10 points de santé et si la santé du joueur arrive à 0, nous jouons son animation de mort "death". La caméra zoome légèrement sur lui pour que nous puissions bien le voir mourir.

Il est temps de s'occuper de l'ennemi! En premier voci l'action qui crée les ennemis:

action
generate_enemy
{
my.scale_x
*= 0.7;
my.scale_y = my.scale_x;
my.passable = on;
my.oriented = on;
my.bright = on;
my.flare = on;
while (player == null) {wait (1);}
while (vec_dist
(my.x, player.x) < 500) {wait (1);}
waitt (48 + random (64));
vec_set (temp,
my.pos);
temp.z += 50;
ent_create (enemy_mdl, my.pos, move_enemy);
}

Ce jeu à la 3ème personne permet la création d'ennemis à la volée; j'ai utilisé quelques sprite orienté comme générateur d'ennemis. Lorsque le joueur s'approche de l'un d'eux, il démarre la création d'ennemis. De cette façon vous pouvez disperser la création d'ennemis tout au long du chemin du joueur.

Le générateur attend que le joueur soit créé; S'il est à moins de 500 quants, l'ennemi est créé après 3..7 secondes, 50 quants au dessus du sprite. L'ennemi se déplace avec la fonction suivante :

function move_enemy
{
my.enable_impact = on;
my.event = enemy_damage;
ent_create(enemygun_mdl, nullvector, attach_enemygun); // give him a gun

L'ennemi est également sensible à l'impact; il utilise une arme avec une fonction similaire.

while (vec_dist(my.x, player.x) > (300 + random(100)))
{
enemy_speed.x = 10 * time;
enemy_speed.y = 0;
enemy_speed.z = 0;

vec_set (temp.x, player.x);
vec_sub (temp.x, my.x);
vec_to_angle (my.pan, temp);

      move_mode = ignore_you + ignore_passents;
ent_move (enemy_speed, nullvector);
ent_cycle ("walk",
my.skill10);
my.skill10 += 10 *
time;
if (my.skill10 >
100) {my.skill10 = 0;}
my.tilt = 0;
wait (1);
}

Tant
que le joueur se trouve à plus de  300..400 quants,
l'ennemi se déplace vers lui en utilisant son animation marche dans une
boucle.

ent_cycle("attack", 100);
waitt (2);
while (my.skill30 == 0 && player.skill20 > 0)
{
vec_set (temp.x, player.x);
vec_sub (temp.x, my.skill12);
vec_to_angle (my.pan,
temp);
ent_create (bullet_mdl,
my.skill12, init_shot);
my.tilt = 0;
snd_play (shoot1_snd, 70, 0);
waitt (32);
}
}

Lorsque l'ennemi est suffisamment proche, il passe en animation "attack" et commence à tirer sur le joueur si le joueur et l'ennemi sont vivants. L'ennemi se tourne vers le joueur et tire une balle toutes les 2 secondes environ.

Si l'ennemi est touché par une balle il exécute sa fonction enemy_damage. La bonne nouvelle c'est que les ennemis peuvent se tirer dessus :)

function enemy_damage()
{
if (you.skill10
!= 1) {return;}
wait (1);
my.enable_impact = off;
my.event = null;
my.skill10 = 0; // start with the first
death frame
my.skill30 = 1; // I'm dead
while (my.skill10 < 67)
{
ent_cycle("death", my.skill10); // play
death animation
my.skill10 += 6 * time;
wait (1);
}
my.passable
= on; // can pass through the corpse
}

Avant tout nous vérifions que l'ennemi est bien touché par une balle. Si c'est le cas nous jouons son animation de mort et rendons son corps passable pour que le joueur puisse passer dessus.

Ce sera tout pour aujourd'hui. On se reverra dans quelques semaines avec un projet autonome!


Untitled

Tir sur des interrupteurs / statues

Le morceau de code vous montre comment vous pouvez passer au niveau suivant uniquement si vous avez détruit un certain nombre d'idole / statues / interrupteurs ou tout ce que vous voulez.

action
statue
{
my.enable_shoot
= on;
my.event = destroy_me;
statues_left += 1;
while (1)
{
if (vec_dist(my.x, player.x)
< 50 || my.skill10 == 1)
{
play_sound
(destroyed_snd, 70);
statues_left
-= 1;
_gib
(10);
ent_remove
(me);
return;
}
wait (1);
}
}

Chaque statue est rendue sensible au tir; son nombre est compté au départ. Si le joueur vient à toucher une statue ou à lui tirer dessus, un son est joué, le nombre de statue est décrémentée, la statue se casse en morceau et disparaît.

Que se passe-t-il lorsqu'on tire dessus? Voici l'évènement qui est déclenché lorsque la statue est tirée:

function
destroy_me()
{
if (event_type == event_shoot)
{
my.skill10 = 1;
}
else
{
my.skill10 = 0;
}
}

Il ne semble pas se passer grand chose avec les statues, n'est-ce pas? Si vous regardez l'action statue, plus au dessus vous pouvez voir que la statue est détruite si le joueur la touche OU si skill10 = 1 - ce que fait l'évènement.

Nous devons avoir une autre entité qui nous amène au prochain niveau sur un impact:

action next_level
{
my.enable_impact
= on;
my.event = change_level;
}

Lorsqu'il y a un impact avec cette entité, c'est l'évènement change_level qui est déclenché:

function
change_level()
{
if (statues_left
!= 0) {return;}
me = null;
beep; beep;
// load_level <next.wmb>;
wait (2);
}

Vous pouvez voir que vous ne pouvez pas aller au prochain niveau tant que vous n'avez pas détruit toutes les statues. Vous devez enlever les 2 / devant la ligne load_level et changer le nom avec votre_prochain_niveau..wmb


Untitled

Tourelles

Ces moyennes tourelles peuvent vous tuer en 1 seconde ou 2 parcequ'elles ont été entraînée à cela il y a 5 ans déjà. Jetez un oeil à l'action qui leur estattachée:

action
turret
{
if (my.skill1 == 0) {my.skill1 = 1000;} // set the range with skill1, default
= 1000 quants
if (my.skill2 == 0) {my.skill2
= 180;} // default viewing angle in front of the turret = 180
my.enable_impact = on;
my.event = destroy_turret;
while (player == null) {wait
(1);}
while (my != null &&
player != null)
{
my.skill10 = abs(ang(player.pan)
- ang(my.pan));
if (vec_dist (my.x,
player.x) < my.skill1)
{
if ((my.skill10 > 180 - my.skill2 / 2) && (my.skill10 < 180 + my.skill2
/ 2))
{
vec_set (temp.x, player.x);
vec_sub (temp.x, my.x);
vec_to_angle (my.pan, temp);
ent_create (bullet_mdl, my.pos, move_bullet);
}
}
waitt (8); // fires
2 bullets a second
}
}

Les tourelles ont des portées et des angles de tir variables; si vous omettez de donner une valeur à skill1 et skill2 dans WED nous prendrons des valeurs par défaut. Si l'unité esttouchée par une balle elle est détruite.in Wed they'll use the default values. If the unit is hit by a bullet, it will be destroyed; we'll talk about th Nous verrons cela un peuplus loin. Tant que la tourelle et le joueur sont vivants, la tourelle calcule la distance entre au joueur, se tourne vers lui et tire 2 balles par seconde.

my.skill10 = abs(ang(player.pan) - ang(my.pan));
................................
if ((my.skill10 > 180 - my.skill2 / 2) && (my.skill10 < 180 + my.skill2 / 2))

Ces 2 lignes sont faites pour être sur que le joueur et la tourelle se voient mutuellement, j'aurais pu tout aussi bien utiliser une instruction scan_entity. La tourelle peut 'voir' le joeur si l'angle entre elle et le joueur est compris entre 180 - skill2 / 2 .... 180 + skill2 / 2. Si nous utilisons les valeurs par défaut pour skill2, la tourelle peut voir le joueur dans un angle compris entre 90 et 270 degrés.

function
destroy_turret()
{
wait (1);
if (you == player) {return;}
// ne peut pas être détruite par le joueur lui-même
snd_play (explode_snd, 70, 0);
ent_remove (me);
}

Si la tourelle est touchée par quelque chose, elle sera détruite. Nous ne voulons pas que le joueur puisse la détruire simplement en la touchant aussi si (you == player) rien ne se passe. La fonction qui déplace les balles tirées par la tourelle est classique et a déjà été expliquée plusieurs fois dans AUM:

function
move_bullet()
{
wait (1);
my.enable_entity = on;
my.enable_block = on;
my.event = remove_bullet;
my.passable = on;
my.pan = you.pan;
bullet_speed.x = 100;
bullet_speed.y = 0;
bullet_speed.z = 0;
bullet_speed *= time;
while (my != null)
{
if (you == null)
{return;}
if (vec_dist (my.x,
you.x) < 100) // don't collide with the turret
{
my.passable = on;
}
else
{
my.passable = off;
}
ent_move (bullet_speed,
nullvector);
wait (1);
}
}

La fonction finale:

function
remove_bullet()
{
wait (1);
if (you == player) {player._health
-= 10;}
ent_remove (me);
}

Si la balle atteint le joueur, elle soustrait 10 points de santé de la santé du joueur puis elle disparaît. Si elle atteint un mur elle disparaît sans faire de dégâts.


Untitled

Tremblement de terre

Si vous devez ajouter un tremblement de terre à votre jeu à la première personne, il y a une solution simple : stockez la hauteur de l'oeil du joueur (eye_height_up) des calibres, puis jouez avec elle et avec l'angle roll du joueur. Reconstituez les valeurs originales quand le tremblement de terre est fini et tout fonctionnera comme avant.

La bonne chose avec ce code est que vous ne pouvez pas sauter du niveau parce que vous ne vous déplacez pas du tout (naturellement vous pouvez ajouter le code pour cela) ; malheureusement, votre client ne sait pas qu' il tombera certainement dans de la lave en fusion lorsqu'il se déplacera pour rester dans un secteur sûr :)

Le tremblement de terre commence quand vous courez dans une entité (attachez lui l'action quakegen, en la rendant invisible si vous voulez). J'utiliserais un escalier visible (construit comme entité wmb) qui mène à un pont de corde placé au-dessus de la lave :

action quakegen
{
my.enable_impact = on;
my.event = earthquake;
}

function earthquake
{
my.skill9 = eye_height_up;
my.skill10 = random(100)
+ 20;
my.skill11 = random(100)
+ 100;
waitt(my.skill10);
while (my.skill11 > 0)
{
if (random(1) > 0.9)
{
play_sound eq_snd, 30;
}
if (eye_height_up >
0.2)
{
eye_height_up += 0.1 - random(2)/10;
}
player.roll += 10 - random(20);
my.skill11 -= 0.1 * time;
waitt(2);
}
eye_height_up = my.skill9;
player.roll = 0;
}

Skill10 donne le délai initial (de cette façon le joueur ne sait pas quand se produira le tremblement de terre) et skill11 donne la durée. Vous pouvez jouer avec ces valeurs.

Skill10 donne l'initial retarde (ainsi le joueur ne peut pas dire quand/s'il a déclenché le tremblement de terre) et skill11 donne la durée ; jeu avec ces valeurs.


Untitled

Téléportation

Déplacer le joueur d'un endroit vers un autre est vraiment facile - tout ce qu'il y a à faire c'est de changer ses coordonnées et quelques fois ses angles. Le petit bout de code ci-dessous vous permet de mettre les coordonnées de destination ainsi que les angles directement dans WED,ce qui fait que vous n'avez pas à modifier ces valeurs dans le fichier C-script à chaque nouvelle destination.

L'action attachée à l'entité téléportée est :

action teleport
{
while (player == null) {wait(1);}

vec_set (my.skill15, strength);
vec_set (my.skill18, astrength);

person_3rd = 0.5;
camera_dist.x = 200;
camera_dist.z = -300;
head_angle.tilt = -45;

Nous attendons que le joueur soit créé puis nous stockons ces valeurs prédéfinies strength et astrength (vitesse du joueur en x y z et vitesse angulaire du joueur). J'utilise les variables d'entité skills15..17 pour stocker strength et skills18..20 pour astrength. Si vous êtes un ancien lecteur de AUM vous savez déjà pourquoi nous les stockons dans 3 variables consécutives.

Nous forçons le mode caméra à la 3ème personne et nous donnons une bonne distance de la caméra en distance x et z et un bon angle en changeant les valeurs prédéfinies.

     while (1)
{
while (vec_dist
(my.x, player.x) > 40) {wait (1);}
vec_set (strength,
nullvector); // the player can't move
vec_set (astrength,
nullvector); // or rotate from now on
temp.z = my.z;
my.skill10 = particle_emmiters;
my.skill11 = min_radius;
while (my.skill10
> 0)
{
temp.x = player.x + min_radius * cos(my.skill12);
temp.y = player.y + min_radius * sin(my.skill12);
temp.z += up_step;
my.skill12 += angle_step;
min_radius += radius_step;
effect (create_fx, 1, temp.x, normal);
my.skill10 -= 1;
}

Lorsque le joueur est à moins de 40 quants de l'origine de l'entité de téléportation, nous mettons à 0 ses variables strength et astrength ce qui fait qu'il ne peut plus ni se déplacer, ni tourner à partir de maintenant. Nous créons des particules en cercle autour du joueur - le rayon et la hauteur de ce cercle devenant de plus en plus grand comme sur l'image ci-dessous:

player.shadow = off;
player.transparent = on;
player.alpha = 100;
while (player.alpha > 0)
{
player.alpha -= 7 * time;
wait (1);
}
player.invisible = on;
waitt (16);
min_radius = my.skill11;
player.x = my.skill1; // set skill1..4 in Wed
player.y = my.skill2;
player.z = my.skill3;
player.pan = my.skill4;
player.transparent = off;
player.invisible = off;
vec_set (strength, my.skill15); // restore player's speed on x, y, z
vec_set (astrength, my.skill18); // and player's angle speeds

wait (1);
}
}

Si le joueur a son drapeau d'ombre de mis, son ombre disparaîtra; le joueur disparaîtra en fondu en modifiant sa valeur alpha. Après une seconde, le joueur sera déplacé vers sa nouvelle position donnée par les variables d'entité skills1..4 pour x, y, z et pan parce que quelques fois on souhaite que le joueur fasse face à une certaine direction après sa téléportation.

Souvenez-vous de skill15..17 et skill18..20? Nous y avions stockés les valeurs initiales de strength et astrength - à présent il est temps de récupérer ces valeurs parce que nous en avons besoin pour déplacer le joueur.

Il ne reste plus qu'à expliquer l'effet de particules:

function
create_fx()
{
temp.x = random(2) - 1;
temp.y = random(2) - 1;
temp.z = random(2) + 1;
vec_add (my.vel_x, temp);
my.flare = on;
my.bright = on;
my.alpha = 50;
my.bmap = effect_pcx;
my.size = 30;
my.move = on;
my.lifespan = 100;
my.function = fade_fx;
}

function
fade_fx()
{
my.alpha -= 2 * time;
if (my.alpha < 0) {my.lifespan = 0;}
}

Vous pouvez voir que les particules ont une vitesse aléatoire en x, y et z mais que la valeur z est toujours plus grande que 0 ce qui permet aux particules de monter, en décrémentant leur valeur alpha jusqu'à complète disparition.


Untitled

Texte éditable

Oui je comprends que ce titre ne vous dise pas grand chose mais j'étais très excité lorsque j'ai eu l'idée de réaliser cet article. N'aimeriez-vous pas vouloir placer un texte dans votre monde 3D, pourquoi pas pour afficher quelques bannières de pub pour votre jeu de sport ou le nom du gagnant dans une course de voiture ou un code d'accès qui change de temps en temps? Nous savons comment utiliser un texte classique et le faire apparaître à l'écran mais ce qui serait sympa c'est de le faire apparaître en tant qu'objet dans notre monde 3D. C'est exactement ce que nous allons faire aujourd'hui. Ca c'est pour la bonne nouvelle. La mauvaise nouvelle c'estque ce code ne fonctionne pas du tout avec la version A5.

L'idée utilisée dans ce code est toute simple, nous définissons une image de notre police de caractère (pas en tant que police mais biene en tant qu'image) et nous lisosns tous ses pixels. A chaque fois que nous rencontrons un pixel qui n'est pas du noir (RVB = 0000) nous dessinons une particule dans le niveau. Et si nous commencions à regarder le code??

bmap system_font = <system.pcx>;
string temp_str; // juste une chaîne temporaire qui contiendra votre texte

Vous pouvez voir que system_font a été défini en utilisant une définition classique pour une bmap; temp_str est la chaîne qui contiendra notre texte ("Acknex rules!" dans ma démo).

function
create_text(my_string)
{
var number_of_chars;
var format;
var char_minx;
var char_miny;
var char_maxx;
var char_maxy;
var pixel_value;
var char_width;
var char_height;
var particle_width = 1.3;
var particle_height = 1.5;
var particle_space = 12;

Voyons en détail chaque définition de variable :

- number_of_chars stocke le nombre de caractère de notre chaîne (13 dans ma démo);

- format est retourné par l'instruction bmap_lock et donne le type de la texture;

- char_minx est le x minimum (en pixels) pour le caractère actuel sur l'image de notre fonte;

- char_miny est le y minimum (en pixels) pour le caractère actuel sur l'image de notre fonte;

- char_maxx est le x maximum (en pixels) pour le caractère actuel sur l'image de notre fonte;

- char_maxy est le y maximum (en pixels) pour le caractère actuel sur l'image de notre fonte;

- pixel_value stocke la valeur RVB du pixel actuel sur notre image de fonte;

- char_width et char_height donne la largeur et la hauteur de chaque caractère dans notre image de fonte;

- particle_width met la largeur des lettres (la distance entre 2 particules dans l'axe des x);

- particle_height met la hauteur des lettres (la distance entre 2 particules dans l'axe des y);

- particle_space met l'espace entre les lettres.

Il faut jouer avec les 3 dernières valeurs pour obtenir des textes plus ou moins large à partir de la même image bitmap.

  char_width = bmap_width(system_font) / 32;
char_height
= bmap_height(system_font) / 4;
number_of_chars = str_len(my_string);
format = bmap_lock(system_font, 0);

Les lignes ci-dessus calculent char_width (chaque rang de system_font a 32 caractères) et char_height (system_font a 4 rangées de caractères). Nous prenons le nombre de caractère contenus dans notre chaîne (Acknex rules!) puis nous bloquons (lock) l'image bitmap car nous avons besoin de travailler un peu tranquillement avec elle :)

    while (number_of_chars > 0) // create the
particles char by char
{
temp = str_to_asc(my_string);
str_clip (my_string, 1);
char_minx = (temp % 32) * char_width;
char_miny = int(temp / 32) * char_height;
char_maxx = char_minx + char_width;
char_maxy = char_miny + char_height;

Nos caractères basés sur la particule sont créés un par un aussi nous utilisons une boucle pour faire cela. Nous convertissons le premier caractère de notre texte en son équivalent ASCII puis nous enlevons ce caractère de la chaîne car la prochaine fois nous voulons lire le deuxième caractère puis le troisième etc. Nous obtenons char_minx et char_miny, l'x et le y minimum de notre caractère dans l'image de la fonte puis nous calculons char_maxx et char_maxy.

L'image ci-dessus montre ces différentes valeurs pour le caractère "C" pour notre image de fonte dans notre exemple.

         while(char_minx <
char_maxx)
{
char_miny = char_maxy - char_height;
while(char_miny < char_maxy)
{
pixel_to_vec(pixel_value, temp, format, (pixel_for_bmap(system_font, char_minx,
char_miny)));
if (pixel_value > 0)
{
vec_set (temp, vector (100, 80, -5));
temp.y += (char_maxx - char_minx) * particle_width + number_of_chars * particle_space;
temp.z += (char_maxy - char_miny) * particle_height;
effect(particle_text, 1, temp, nullvector);
}
char_miny
+= 1;
}
char_minx += 1;
}
number_of_chars -= 1;
}
bmap_unlock(system_font);
}

Nous utilisons 2 boucles pour dessiner la particule "pixels" de notre caractère; la boucle intérieure dessine les colonnes et l'autre boucle augmente les coordonnées x. Nous utilisons pixel_to_vec pour lire la valeur RVB du pixel (pixel_value) et s'il n'est pas complètement noir (RVB > 0) nous créons une particule.Nous mettons la position approximative de notre texte dans le monde 3D en utilisant une simple instruction vec_set;faites attention que la taille du texte dépend de particle_width, particle_height et particle_space, aussi vous devrez jouer avec les valeurs (x = 100, y = 80, z = -5) de ma démo jusqu'à ce que le texte appara^sse dans votre niveau. Si vous voulez changer l'orientation du texte le long de l'axe des x, remplacez simplement "temp.y = ...;" par "temp.x = ...;". la ligne "effect" crée les particules; vous pouvez utiliser ent_create pour faire des petits sprites si vraiment vous détestez les particules mais vous perdrez la capacité de pouvoir changer la couleur ou de faire du fondu ou toutes autres possibilités offertes avec les particules. Number_of_chars est décrémenté en dehors des 2 boucles, afin de passer au prochain caractère de notre texte. Pour termnier nous débloquons l'image bitmap car elle doit être bien fatiguée d'avaoir autant servi àprésent...

C'était la partie la plus difficile, regardons à présent la fonction qui gère les particules:

function
particle_text()
{
my.size = 1;
my.red =
255;
my.green = 100;
my.blue = 100;
my.function = keep_particles;
}

function
keep_particles()
{
my.lifespan = 100;
}

Si vous créez de grands textes changez les valeurs de particle_width, particle_height et particle_space, mais n'oubliez pas de choisir une taille adéquate pour les particules (1 dans ma démo). Les lignes suivantes mettent la combinaison des couleurs RVB pour les particules et la dernière ligne de la fonction particle_text met la fonction keep_particles() comme nurse à particules. Tout ce que fait cette fonction c'est de donner une durée de vie( lifespan) de 100 à chaque particuler each particle, ce qui fait que chaque particule vivra tout le temps. Vous devez diminuer cette valeur si vous souhaitez utiliser différents textes dans le même spot.

Regardons la fonction qui teste notre merveilleux script:

function
test_particles()
{
str_cpy (temp_str, "Acknex
rules!");
create_text(temp_str);
}

on_t = test_particles;

Ok, aussi nous devons presser la touche "T" pour voir notre texte. La fonction associée à la touche "T" copie le texte désiré dans temp_str et appelle la merveilleuse fonction dont nous avons déjà parlé plus haut en utilsant notre chaîne comme paramètre.


Untitled

Torche

Ce mois-ci nous allons apprendre à créer une torche qui pourra être prise et transportée avec nous éclairant au passage ce qui l'entoure.

action
torch
{
my.passable = on;
while (player ==
null) {wait (1);}
while(1)
{
effect(flame_init, 5, my.x, nullvector);
my.lightred
= 180 + random(30);
my.lightgreen = 150 + random(20);
my.lightblue = 60;
my.lightrange = 200 + random(5);
my.ambient
= 50;
if (vec_dist (player.x, my.x) < 70)
// the player has come really close to the torch?
{
my.x = player.x + 50 *
cos(player.pan) + 20 * sin(player.pan); // 50 quants in front of the player
my.y = player.y + 50 * sin(player.pan)
- 20 * cos(player.pan); // and 20 quants sideways (to the right)
my.z = player.z + 25; // the height of the torch is set 25 quants above player's
origin
my.pan = player.pan;
my.tilt = player.tilt;
my.roll = player.roll;
}
wait (1);
}
}

Le model torch est passable et attend que le joueur soit créé, dès que cela se produit, la torche commence à créer des particules à son origine en utilisant l'instruction "effect". Chaque particule se comportera en fonction de ce que la fonction "flame_init" lui demande de faire. La torche émettra une lumière jaunâtre dont l'étendue varie entre 200 et 205 quants donnant l'effet d'un léger clignotement. Si le joueur est vraiment proche de la torche (moins de 70 quants) la torche est prise, puis placée 50 quants au devant du joueur et 20 quants sur le côté (sur le côté droit de l'écran). Le modèle torche est placé 25 quants au dessus de l'origine du joueur sur l'axe des z et il hérite de ses angles pan, tilt et roll.

function
flame_init()
{
temp.x = (random(2) - 1) / 2;
temp.y = (random(2) - 1) / 2;
temp.z = random(1) + 1;
vec_set(my.vel_x, temp);
my.move = on;
my.bmap = flame_tga;
my.flare = on;
my.bright
= on;
my.alpha = 70;
my.lifespan = 100;
my.size = 17;
my.function = fade_flames;
}

function
fade_flames()
{
my.alpha -= 25 * time; // play with 25
if(my.alpha < 0) {my.lifespan = 0;}
}

La fonction ci-dessus prend soin du feu de particules; nous mettons une vitesse aléatoire sur l'axe des x et y (dans une plage de -0.5 à 0.5), tout comme sur l'axe des z (étendue de 1 à 2). les particules de feu commencent avec un facteur de transparence de 70; jouez avec cette valeur jusqu'à ce que vous soyez satisfait du résultat. La fonction qui contrôle le sparticules diminue leur alpha très rapidement. Nous ne pouvons pas nous permettre d'employer de plus petites valeurs car la torche se déplace en même temps que nous et cela créerait des trainées peu réalistes derrière le joueur. La dernière ligne de code enlève les particules qui ne sont plus vues. Vous pouvez utiliser une autre ligne de code "effect" à l'intérieur de l'action "torch" et des fonctions légèrement modifiées si vous voulez ajouter également de la fumée.

Petit truc de programmeur fainéant : j'ai utilisé un modèle de torche avec une origine un peu particulière: de cette façon pas besoin d'utiliser des forumules mathématiques compliquées (juste utiliser my.x) afin de m'assurer que les flammes démarrent bien à l'intérieur du modèle de la torche. Mêm si dans ce cas les mathématiques n'étaient pas nécessaires puisque la torche est liée au joueur mais j'ai pensé que c'était un truc à connaître pour vos futurs projets.


Untitled

Virtua Cup

Si vous avez déjà joué à Virtua Cop vous savez qu'il s'agit d'un jeu rapide et furieux. Si vous n'y avait jamais joué, voici comment il fonctionne : vous êtes un flic et vous essayez de tuer un maximum de criminels. Vous êtes en position d'attente et les méchants apparaîssent de temps en temps à des positions aléatoires. Vous leur tirez dessus, ils vous tirent dessus..très simple, huh? Virtua Cup est un clone de Virtua Cop.

Ce
jeu est autonome aussi il a sa propre fonction principale main:

function main()
{
level_load (virtuacup_wmb);
wait (2);
clip_size = 0;
fps_max = 50;
generate_enemies();
}

Après que nous ayons chargé le niveau, nous bloquons la vitesse d'affichage à 50 et nous démarrons la génération des ennemis:

function generate_enemies()
{
while (1)
{
waitt (48 + random
(48));
index = 3 * int(random(10));
temp.x = enemy_pos[index + 0];
temp.y = enemy_pos[index
+ 1];
temp.z = enemy_pos[index + 2];
ent_create(enemy_mdl,
temp, enemy_shoots);
if (index > 9) {index = 0;}
}
}

Les méchants apparaîssent toutes les 3...6 secondes à des positions aléatoires. J'ai défini un tableau qui contient les valeurs xyz pour chaque position:

var enemy_pos[30] = -450,315,0, 60,315,0, 575,315,0, -325,540,255, -65,540,255, 195,540,255, 450,540,255, -190,850,515, 65,850,515, 320,850,515;

Je n'ai pas inventé ces valeurs, elles proviennent tout simplement de Wed. Les 3 premiers nombres sont les coordonnées xyzpour la première position, les 3 suivants sont les coordonnées xyz pour la 2ème position et ainsi de suite. Mon tableau à 30 éléments ce qui signifie qu'il stocke 10 positions différentes. Revenons à notre fonction generate_enemies:

index = 3 * int(random(10));

génèrera un nombre aléatoire entre 0 et 27 par palier de 3 (0, 3, 6, 9, 12, ..., 27). Nous copions les coordonnées aléatoires dans temp et nous créons notre ennemi qui utilisera la fonction enemy_shoots:

function
enemy_shoots()
{
wait (1);
my.pan += 270;
my.enable_impact = on;
my.event =
kill_enemy;
ent_create(enemygun_mdl, nullvector,
attach_enemygun);
my.skill10
= 0;
while (my != null && my.skill10
< 100)
{
ent_cycle("attack", my.skill10); // play
"attack" frames animation
my.skill10 += 3 * time;
wait (1);
}
snd_play (enemyshot_snd,
70, 0);
vec_set (temp.x,
player.x); // se tourne vers le joueur
vec_sub (temp.x, enemy_gun.x);
vec_to_angle
(my.pan, temp);
ent_create
(bullet_mdl, enemy_gun, move_bullet);
waitt (32);
ent_remove (me);
}

L'ennemi est tourné vers le joueur. Si l'ennemi est touché par une balle, c'est son action kill_enemy qui est exécutée -nous verrons cela un peu plus loin. La ligne ci-dessous et la fonction qui la suit:

ent_create(enemygun_mdl, nullvector, attach_enemygun);

function attach_enemygun()
{
proc_late();
my.passable = on;
my.metal =
on;
while(you != null)
{
vec_set(my.x,you.x);
vec_set(my.pan,you.pan);
my.frame = you.frame;
my.next_frame = you.next_frame;
vec_for_vertex(enemy_gun,
my, 148);
wait(1);
}
ent_remove(my);
}

donne une arme à l'ennemi. La balle de l'ennemi est créée à la positiondonnée par le vertex 148 sur le modèle de l'arme.

L'ennemi joue ses frames d'animation "attack" s'il n'est pas tué par le joueur:

while (my != null && my.skill10 < 100)
{
ent_cycle("attack",
my.skill10);
my.skill10 += 3 * time;
wait (1);
}

Jusqu'à ce que l'ennemi termine son animation "attack" , il joue un son, tourne vers le joueur et tire une balle. L'ennemei restera encore visible pendant 2 secondes pour vous laisser une chance de l'abattre.

snd_play (enemyshot_snd, 70, 0);
vec_set (temp.x,
player.x);
vec_sub (temp.x,
enemy_gun.x);
vec_to_angle
(my.pan, temp);
ent_create
(bullet_mdl, enemy_gun, move_bullet);
waitt (32);
ent_remove (me);

Ici la fonction qui déplace la balle(c'est la même pour le joueur et pour l'ennemi):

function move_bullet()
{
wait (1);
if (my == null) {return;}
my.enable_entity = on;
my.enable_block = on;
my.event = remove_bullets;
my.passable = on;
my.pan = you.pan;
my.tilt = you.tilt;

my.skill1 = 0;
bullet_speed.x = 500;
bullet_speed.y = 0;
bullet_speed.z = 0;
bullet_speed *= time;
while (my.skill1 < 50)
{
if (my == null) {return;}
if (my.skill1 <
0.1)  // don't collide with the creator
{
my.passable = on;
}
else
{
my.passable = off;
}
my.skill1 += 0.1 *
time;
ent_move (bullet_speed,
nullvector);
wait (1);
}
remove me;
}

function
remove_bullets()
{
wait (1);
ent_remove (me);
}

La balle est sensible à l'impact avec les entités et les blocs du niveau; Si elle touche l'un ou l'autre elle est supprimée par une simple fonction remove_bullets. La balle prend l'angle pan et l'angle tilt de son créateur (ennemi ou joueur), ce qui fait que si vous visez dans la bonne direction la balle ira dans la bonne direction, n'est-il pas ? Vous avez certainement déjà vu des fonctions identiques dans d'autres éditions d'AUM.

Il est temps d'étudier la fonction kill_enemy:

function
kill_enemy()
{
wait (1);
exclusive_entity; // arrête toutes
les fonctions pour cet ennemei
my.enable_impact
= off; // ne peut pas être tiré pour le moment
my.event = null;
dead_enemies
+= 1;
my.skill10 = 0;
snd_play (enemydead_snd, 70, 0);
while (my.skill10 < 90)
{
ent_cycle("die", my.skill10); // joue l'animation
"die" (mourrant)
my.skill10 += 5 * time;
wait (1);
}
waitt (16);
ent_remove (me);
}

Si l'ennemi est touché par la balle du joueur,nous arrêtons toutes les autres fonctions associées à cet ennemei qui pourraient fonctionner en même temps. L'ennemi devient insensible aux autres balles maintenant et son évènement est mis à null. Nous ajoutons un à la variable dead_enemies qui comptent les ennemis tués, nous jouons un son funèbre et l'animation "mourant". Le corps reste visible encore 1 seconde puis il disparaît.

Nous en avons fini avec le code de l'ennemi, voyons celui qui est associé au joueur:

action virtua_player
// attaché au joueur
{
player = me;
player.invisible = on;
player.skill1
= 100;
my.enable_impact
= on;
my.event =
decrease_players_health;
camera.x =
player.x;
camera.y = player.y;
camera.z = player.z;
camera.pan = 90;
while (player.skill1 > 0) // tant
que le joueur est vivant
{
camera.tilt += 10 *
mouse_force.y * time;
if (camera.tilt < -25) {camera.tilt
+= 1;}
if (camera.tilt >
25) {camera.tilt -= 1;}
camera.pan -= 10 * mouse_force.x * time;
if (camera.pan <
60) {camera.pan += 1;} // 90 - 30
if (camera.pan > 120) {camera.pan -=
1;} // 90 + 30
if (mouse_left == 1
&& bullets > 0)
{
player_shoots();
}
player.pan = camera.pan; // tourne également
le joeur
player.tilt = camera.tilt;
// pas seulement la caméra
if (mouse_right == 1 && bullets
== 0) // clic droit pour recharger l'arme
{
bullets = 10;
while
(mouse_right == 1) {wait (1);}
}
wait (1);
}
}

Le joueur est invisible parce que nous ne voulons pas le voir se tenir devant nous, mais ne vous y trompez pas, l'ennemi lui connait parfaitement sa position. Le joueur a 100 points de santé (stockés dans skill1). Si le joueur est touché par une balle, son évènement (decrease_players_health) est exécuté.

La caméra est placée à l'origine du joueur aussi assurez-vous de choisir la bonne position du joueur. Tant que skill1 > 0 (le jouieur est vivant) nous pouvons déplacer la caméra et tirer des balles. J'ai limité les mouvements de la caméra à 60...120 degrés pour son angle pan et -25...25 degrés pour son angle tilt. Pourquoi regarder par dessus son épaulle lorsque tous mes ennemis sont devant ? Si nous sommes à court de munitions il suffit de cliquer sur le bouton droit de la souris pour recharger notre arme, si nous pressons le bouton gauche (LMB)et que nous ayons encore des balles (bullets > 0 ) alors nous exécutons la fonction player_shoots:

function player_shoots()
{
exclusive_global;
while (mouse_left == 1) {wait (1);}
snd_play (playershot_snd, 70, 0);
ent_create (bullet_mdl, player.pos, move_bullet);
bullets -= 1; // vous avez utilisé une balle

Lorsque nous appuyons sur le bouton gauche de la souris, la fonction player_shoots sera exécutée de nombreuses fois si nous ne l'arrêtions pas par exclusive_global. Nous attendons jusqu'à ce quele joueur relache le bouton gauche, nous jouons un son de tir et nous créons une balle que nous allons déplacer en utilisant la même fonction same move_bullet dont nous avons discuté un peu plus haut. Et pour finir nous décrémentons une balle.

La dernière fonction est exécutée lorsque le joueur est touché par une balle ennemie:

function
decrease_players_health()
{
wait (1);
if (player.skill1 > 0)
{
player.skill1 -= 10;
// enlève 10 points de vie à chaque balle reçue
}
else
{
player.skill1 = 0;
}
}

Si le joeur est encore vivant, nous diminuons sa santé de 10 points. Si le joueur est déjà mort, nous mettons sa santé à 0 (ce n'est pas profesionnel de montrer une valeur négative).


Untitled

Viseur précis

Un des problèmes avec le template sur les armes c'est que les balles ne sont pas tirées dans le centre du viseur (pressez la touche K pour montrer ou cacher le viseur si vous utilisez les templates). Le problème est évident si vous êtes près d'un mur et que vous utilisez une balle qui a hit_hole dans ses définitions; weapon_mg_animated est un bon exemple. La raison est simple: l'arme n'est pas placée au centre de l'écran (elle est habituellement sur le côté droit) aussi la balle ne peut pas toucher le centre de l'écran qui s'avère être justement le centre du viseur également. Pire que ça, si vous voulez tirer sur de petits objets placés à de grandes distances - comme une corde qui abaisserait un pont - vous êtes sur de les louper.

Maintenant que vous connaissez le problème, vous serez certainement heureux d'apprendre qu'on peut régler ce problème avec seulement 11 lignes de code. Nous devons modifier weapons.wdl but mais soyez sans crainte j'ai inclu le fichier weapons.wdl modifié.

L'idée est simple: nous créons 2 positions (vecteurs) au devant du joueur: un à 20 quants et l'autre à 10,000 quants et nous traçons entre les deux. Les 2 positions sont créées au centre de l'écran aussi nous savons que chaque fois que nous tirons une balle elle touchera le centre du viseur - c'est une ligne droite!

Si vous ne voulez pas utiliser ce truc vous devez placer toutes vos armes aucentre de l'écran comme sur l'image ci-dessous:

Voyons ce morceau de code impressionnant - il est placé à l'intérieur de la fonction gun_fire() dans weapons.wdl :

damage = MY._DAMAGE;
fire_mode = MY._FIREMODE;

 my.skill40 = (screen_size.x + bmap_width(cross_bmp)) / 2;
my.skill41 = (screen_size.y + bmap_height(cross_bmp))
/ 2;
my.skill42 = 20;
vec_set (my.skill43, my.skill40);
my.skill45 = 10000;
vec_for_screen (my.skill40, camera);
vec_for_screen (my.skill43, camera);
trace_mode = ignore_me + ignore_passable;
trace
(my.skill40, my.skill43);
vec_set (gun_target,
target);
vec_diff (shot_speed,
gun_target, gun_muzzle);

 //
if player is not already passable
if(player.passable
== OFF)
{
player.passable = ON;   // make
it so the gun doesn't hit the player
gun_shot();
// fire the shot
player.passable
= OFF;
}
else
{
gun_shot();
}

L'ancien code est rouge, lenouveau est jaune.

J'utilise skill40..skill42 comme une variable et skill43..skill45 comme une autre variable. Tout d'abord nous avons besoin des coordonnées du centre de l'écran. Si vous n'êtes pas endormi vous avez du remarquer que le centre de l'écran est donné par quelque chose du genre:

my.skill40 = (screen_size.x - bmap_width(cross_bmp)) / 2; // with "-" not "+"
my.skill41 = (screen_size.y - bmap_height(cross_bmp)) / 2; // with "-" not "+"

Vous avez raison: il y a un petit bug dans weapons.wdl; Le panneau du viseur n'est pas placé exactement au centre de l'écran - C'est soncoin supérieur gauche qui est au centre, il faudrait ajouter la moitié de la longueur de la bitmap représentant le curseur pour être centrée. Passons : skill42 est mis 20 quants devant l'écran car nous ne voulons pas que notre instruction trace touche le joueur ou son arme, donnant un mauvais résultat.

Maintenant nous copions skill40 à skill43; Nous utilisons ces variables comme vecteur aussi skill44 = skill41 et skill45 = skill42 automatiquement. skill45 a besoin d'être mis à 10,000 quants du joueur aussi nous modifions ses valeurs. L'instruction suivante vec_for_screen créera ces positions virtuelles à l'intérieur de notre monde 3D. Nous traçons entre ces 2 positions et nous mettons le résultat (target - lisez le manuel à son propos) à gun_target puis nous donnons la direction (shot_speed) de gun_muzzle à gun_target.

J'ai déplacé cette ligne au début de weapons.wdl:

BMAP cross_bmp, <cross.pcx>;

parce que j'ai besoin de cross_bmp dans mon code et qu'il doit être défini avant.


Untitled

Visionneuse de graphiques

Cet article vous offre une chance d'impressionner votre patron, de stupéfier vos parents, de montrer à tous que vous êtes finalement un garçon sérieux, obtenir une meilleure place dans la société etc.

Le code lit les données dans un fichier texte et les affiche en graphiques 3D. Avant de commencer regardons ce que j'ai mis dans le fichier data.txt fourni avec cet exemple:

data1, 100
data2, 50
data3, 35
data4, 75
data5, 58
end

Démarrons avec la fonction qui lit ces données:

function
read_data()
{
data_handle = file_open_read("data.txt"); // open the file
while (str_cmp (temp_string,
"end") == 0) // read until you have reached "end"

Nous ouvrons le fichier nommé data.txt pour le lire et nous continuons à lire son contenu jusqu'à ce que nous trouvions la fin des données ("end").

{
file_str_read(data_handle, temp_string);
str_cpy(data_name.string[i], temp_string);
if (str_cmp (temp_string, "end") == 0) // don't try to read inexistent data numbers after "end" -> this can create trouble
{
data_value[i] = file_var_read(data_handle);
if ((data_value[i] < 0) || (data_value[i] > 100)) // data_value must be in the 0..100 interval
{
error_text.visible = on; // make the error text visible
sleep (5); // for 5 seconds
exit; // and then shut down the engine
}
}
else
{
break; // get out of the loop -> don't increase "i" because it would be wrong and we need its correct value in main
}
i += 1;
}
ready = 1; // all the data was read
}

Vous pouvez voir que nous avons 2 colonnes dans notre fichier data.txt: une pour le texte et l'autre pour la valeur qui lui est associée. Le texte est lu dans la chaine temporaire temp_string et son contenu est copié dans data_name, un tableau de chaine. Les valeurs sont stockées directement dans un tableau nommé data_value. Si les valeurs lues sont négatives (inférieures à 0) ou supérieures à 100 nous montrons une erreur pendant 5 secondes puis nous arrêtons le moteur. La variable nommée i (pour index) doit être à 1 pour le premier enregistrement (donnée+valeur), 2 pour le deuxième enregistrement etc. Lorsque nous sortons de la boucle i à la valeur de 5 si le fichier contient 5 enregistrements, 2 si le fichier a 2 enregistrements etc.

Dés que nous sommes à la fin du fichier ("end") nous utilisons break pour sortir de la boucle while et nous mettons ready à 1; c'est une variable utilisée dans main. A ce point nous avons toutes les chaînes de stockées dans data_name et toutes les valeurs stockées dans data_value.

function main()
{
camera.arc = 75; // set the field of view
read_data(); // read the data stored in data.txt
level_load (dummy_wmb);
while (ready == 0) {wait
(1);} // wait until all the data is read
temp.x = 270;
temp.y = 100 - (5 - i) *
25;

Nous mettons notre champ de vision à 75 degrés, nous appelons la fonction qui lit data.txt, nous chargeons un niveau bidon (dummy) et nous attendons que les données soient lues (jusqu'à ce que ready = 1).

Laissez moi vous dire quelque chose à propose du niveau bidon - c'est un niveau spécial. Si vous l'ouvrez dans wed vous verrez qu'il contient un simple bloc utilisé comme origine pour mes barres 3D mais il n'a pas de bloc creux autour de lui. Si vous omettez de poser votre niveau à l'intérieur d'un bloc creux ou d'une spère, le moteur rendra des distances infinies (en fait la limite est clip_range mais on ne va pas chipoter la-dessus, ok?). Pourquoi je veux faire cela? Si quelque chose apparait dans le niveau pendant quelques cycles, il restera là pour toujours mêm si vous enlevez ces entités (panneaux, textes etc.) J'ai appliqué cette méthode parce que je veux utiliser un simple panneau pour toutes les colonnes. Si mon fichier coontenait 50 enregistrements j'aurais besoin de 50 panneaux, ok? Avec mon truc, j'utilise un simple panneau pour afficher toutes mes valeurs qui apparaîtront sous forme de barre 3D. Je prends la premère valeur et je l'affiche par dessus la première barre pendant quelques cycles puis je lis la valeur suivante et je l'affiche par dessus la 2eme barre pour quelques cycles etc.

Le seul inconvénient à ce truc serait si je devais déplacer la caméra, il y aurait des traces sales vers le haut. Mais pourquoi ferais-je cela? Alors revenons à nos moutons.

Les barres 3D sont en fait des modèles; leur scale_z est changé en fonction de la valeur qui leur est attribuée, faisant que les barres apparaîssent plus ou moins longue. Les modèles sont placés 270 quants devant le joueur, leur position en y changeant selon le nombre d'enregistrement (donnée+valeur) comme sur la figure ci-dessous:

Vous pouvez voir que quelque soit le nombre d'enregistrements lus (de 1 à 5)les barres sont toujours alignées correctement. La ligne qui permet cela est :

temp.y = 100 - (5 - i) * 25;

Vous vous souvenez du i de la fonction read_data? Si nous avons 5 enregistrements dans le fichier data.txt, i = 5 aussi temp.y = 100; pour i = 4, temp.y = 75 etc. Les barres 3D seront créées à la position donnée par temp; nous savons gérer l'alignement sur l'axe des y mais qu'en est-il pour l'axe des z? Notre modèle de barre a son origine en son centre aussi si nous redimenssionnons plusieurs barres sans précautions particulières nous obtiendrons quelque chose du genre :

Bien, il est clair que si nous redimensionnons un pbjet, son origine reste à la même hauteur; nous devons donc déplacer la barre vers le bas ou vers le haut en fonction de la valeur lue dans data.txt.

     j = 0;
while (j < i)
{
temp.z = -100 +
data_value[j];

Nous utilisons un autre compteur nommé j car i contient le nombre de données lues et nous ne voulons pas perdre cette valeur. Le modèle de barre a une hauteur de 2 quants. Si la valeur associée à une barre donée est 100, cette barre remplira tout l'espace en z (200 quants); pour des valeurs pluspetites la barre sera descendue d'une quantité égale à data_value; de cette façon toutes les barres sont alignées sur le "sol".

          ent_create (bar_mdl,
temp, size_bar);
temp.y -= 50;
j += 1;
wait (3);
}
}

Toutes les barres sont créées à la position donnéepar temp; la deuxième barre à son y de décrémenté de 50 quants etc. Nous avons besoin d'attendre 3 cycles car nous voulons afficher 5 valeurs différenetes à 5 places différentes en utilisant un seul panneau avec un digit. Si vous avez une vieille carte 3D qui utilise le double buffering vous pouvez utiliser wait (2) mais il est plus sur de supposer que la carte puisse utiliser un triple buffering aussi wait (3) est la meilleure solution.

Chaque barre créée exécute cette fonction:

function size_bar()
{
my.albedo = 100;
my.light = on;
my.lightrange = 0;
my.lightred = bar_color[k+0];
my.lightgreen = bar_color[k+1];
my.lightblue = bar_color[k+2];
k += 3;
my.scale_z = data_value[j];
vec_set(temp_value, my.pos);
vec_to_screen(temp_value,
camera);
value_pan.pos_x = temp_value.x;
value_pan.pos_y = 80 + (100
- data_value[j]) * 4;
temp_value = data_value[j];
value_pan.visible = on;
}

J'ai mis albedo à 100 parce que la barre doit être sensible à la lumière; leur couleur provienne d'un tableau - sa définition est donnée ci-dessous:

var bar_color[15] = 250, 0, 0, 0, 250, 0, 0, 0, 250, 250, 250, 0, 250, 250, 250; // red, green, blue, yellow, white

Les 3 premières valeurs donnent la couleur de la première barre, les 3 valeurs suivantes donnent la couleur de la 2ème barre etc. J'ai décidé d'utiliser rouge,vert, bleu, jaune et blanc; toutes les couleurs sont appliquées à la barre de modèle blanc etlui donne de l'éclat car j'ai mis my.light = on.

Les barres sont redimensionnées en z en fonction de la valeur stockée dans data_value; nous voulons afficher les figures sur les barres aussi nous utilisons vec_to_screen pour avoir les coordonnées écran de chaque barre, puis nous plaçons value_pan à sa bonne position, en fonction de depending on data_value. regardons comment ça marche:

value_pan.pos_y = 80 + (100 - data_value[j]) * 4;

Si la barre a une valeur de 100, value_pan.pos_y est mis à 80; sila valeur est inférieure à 100, value_pan.pos_y sera plus grand car la coordonnée y a son 0 en haut de l'écran. Pour chaque point en dessous de 100, nous ajoutons 4 à value_pan.pos_y parce que la barre a une hauteur de 2 quants et parce que chaque barre en dessous de 100 est descendue afin de maintenir l'alignement vers le bas.

Temp_value est utilisé pour afficher les valeurs par dessus les barres, en utilisant un simple tableau avec un digit pour toutes les valeurs:

panel value_pan
// displays the values over the bars
{
pos_x = 0;
pos_y = 0;
layer = 10;
digits = 0, 0, 3, standard_font,
1, temp_value;
flags = d3d;
}

Vous avez noté que j'utilise un tableau pour les chaînes de caractères - vous pouvez en savoir plus à ce sujet dans le manuel. Vous pouvez utiliser un texte avec plusieurs chaîne pour créer un tableau de chaînes:

text data_name
// créer un tableau de chaine en utilisant un texte
{
font = standard_font;
pos_x = 0;
pos_y = 0;
strings = 6;
string = "
"; // 20 characters each
string = "
";
string = "
";
string = "
";
string = "
";
string = "
";
}

Cette définition de texte nous permet de créer un tableau de chaînes avec 6 éléments mais nous pouvons employer n'importe quelle valeur. Ce projet peut travailler avec 1 à 5 enregistrements aussi un tableau avec 6 éléments est plus que suffisant.

Je suis un programmeur un peu fainéant aussi j'essaye de travailler le moins possible; c'est pourquoi j'ai créé un petit programme en c++ pour m'aider durant le développement. Tout ce que vous avez à faire c'est d'exécuter filegen.exe avec quelques paramètres et il créera le fichier data.txt file pour vous. Ici quelques exemples :

filegen 56 23

créera le fichier data.txt suivant:

data1, 56
data2, 23
end

filegen 99 88 77 66 55

créera le fichier data.txt suivant:

data1, 99
data2, 88
data2, 77
data2, 66
data2, 55
end

De cette façon vous n'avez pas à éditer data.txt pour voir vos graphes avec différentes valeurs. J'ai inclu le code source.

Le projet peut facilement être modifié pour accepter les fichiers .csv (Comma Separated Values), importés par de nombreux tableurs incuant Excel). Vous pouvez afficher les noms des données sous ou dessus les barres enutilisant un simple texte. Si vous voulez afficher plus de barre en même temps, changez leur scale_y en fonction du nombre d'enregistrements ou faites les plus fins pour commencer.


Untitled

traduction Olivier Burton

Visionneuse de modèles

Cet outil vous permet de voir les modèles dans toute leur splendeur. Copiez juste les modèles à l'intérieur du dossier de la visonneuse, tapez leur nom, choisissez le début et la fin des frames, ajuster la lumière ambiante et ajuster la vitesse d'animation et admirez!

Voyons quelques définitions de corde d'abord :

string modelname_str = " "; // 20 caractères, stocke le nom du modèle
string start_str = " "; // contient la frame de départ
string end_str = " "; // contient la dernière frame
string empty_str = ""; // un string vide

Le panneau principal inclut 2 curseurs qui contrôlent la vitesse d'animation et la lumière ambiante :

panel
main_pan
{
bmap = main_pcx;
pos_x = 824;
pos_y = 0;
layer = 10;

vslider = 35, 340, 200,
slider_pcx, 1, 50, frame_speed;
vslider = 133, 340,
200, slider_pcx, 0, 100, model_ambient;

digits = 95, 155, 3, arial_font, 1, frame_speed;
digits = 85, 210, 4, arial_font, 1, player.frame;

on_click input_data;

flags = refresh,
d3d, visible;
}

Si nous cliquons sur ce panneau, la fonction input_data () courra. Nous emploierons 3 textes pour stocker et montrer le nom modèle, la frame d'animation de départ et la frame de fin d'animation :

text
modelname_txt
{
layer = 30;
pos_x = 857;
pos_y = 628;
font = arial_font;
string = modelname_str;
}

text
start_txt
{
layer = 30;
pos_x = 900;
pos_y = 45;
font = arial_font;
string = start_str;
}

text
end_txt
{
layer = 30;
pos_x = 900;
pos_y = 100;
font = arial_font;
string = end_str;
}

Voyons le code à l'intérieur de la fonction main :

function
main()
{
level_load (mview_wmb);
wait (2);
camera.x = -150;
camera.y = 0;
camera.z = 30;
clip_size = 0; // montre tous les triangles pour tous les modèles
on_d = null; // désactive le panneau de débuggage (clé "D")
fps_max = 40; // limite le framerate à 40
fps
mouse_init();
while (1)
{
if ((mouse_pos.x < 824) || (mouse_mode == 0)) // si le pointeur n'est pas placé
sur le panneau
{
camera.x += 10 * cos(camera.pan) * (mouse_right - mouse_left) * time;
camera.y += 10 * sin(camera.pan) * (mouse_right - mouse_left) * time;
camera.z += 30 * sin(camera.tilt) * (mouse_right - mouse_left) * time;
camera.pan -= 5 * mouse_force.x * time;
camera.tilt += 3 * mouse_force.y * time;
}
wait (1);
}
}

Nous chargeons le niveau (un simple cube creux), nous attendons qu'il soit chargé et ensuite nous mettons la position de caméra initiale derrière le modèle. Nous nous assurons que nous verrons tous les triangles pour tous les modèles, nous mettons hors service le panneau de mise au point et ensuite nous limitons le framerate à 40 fps.

Si l'indicateur de souris n'est pas placé sur le panneau principal, il est employé pour déplacer la caméra, avec l'aide des 2 boutons de souris. Si vous avez pris soin de ragarder dans la fonction main vous avez pu remarquer que j'ai "oublié" de parler de mouse_init (). En fait, j'ai voulu vous montrer son code ici :

function
mouse_init()
{
mouse_map = cursor_pcx;
mouse_mode = 2;
while (1)
{
if (key_space == 1) {mouse_mode = 0;}
else {mouse_mode = 2;}
mouse_pos.x = pointer.x;
mouse_pos.y = pointer.y;
wait (1);
}
}

Cette fonction met le bitmap du pointeur de la souris, le rend visible et le déplace sur l'écran. Si nous tenons pressée la touche "espace", l'indicateur de souris disparaîtra et nous serons capables de nous déplacer à l'intérieur du cube, pour obtenir une meilleure vue de notre modèle.

Vous rappelez-vous le panneau principal ? Jetez s'il vous plaît un coup d'oeil à son bitmap : main.pcx. Si nous y cliquons cette fonction courra :

function
input_data()
{
if ((mouse_pos.x > 855) &&
(mouse_pos.x < 985) && (mouse_pos.y > 630) && (mouse_pos.y
< 650))
{
modelname_txt.visible = on;
str_cpy (modelname_str, empty_str);
while (str_cmpi (modelname_str, empty_str) == 1)
{
inkey modelname_str;
wait (1);
}
str_cat(modelname_str, ".mdl");
if (file_open_read(modelname_str) == 0)
{
beep; beep;
}
else // the model exists
{
if (player != null)
{
ent_remove (player);
}
ent_create(modelname_str, nullvector, model_driver);
}
}

Si nous avons cliqué quelque part près la boîte de texte"du nom Modèle" , modelname_txt est rendue visible et son string est vidé. La boucle while s'assure que nous avons tapé un nom pour le modèle; le nom du modèle est stocké dans modelname_str. Vous ne devez pas taper guard.mdl parce que le code ajoute .mdl à votre nom entré, utilisez "guard", "warlock", etc (sans extension) pour vos noms de modèles.

Si le modèle n'existe pas (vous avez tapé un faux nom ou vous avez oublié de le copier dans le dossier de la visionneuse), le moteur jouera des signaux sonores deux fois indiquant un message d'erreur. Si le modèle peut être trouvé dans le dossier actuel et son nom a été tapé correctement, nous enlevons le modèle précédemment chargé (s'il y en a un) et nous plaçons le nouveau modèle à l'origine, lui attachant la fonction model_driver () - plus de renseignements sur cette fonction un peu plus tard.

if ((mouse_pos.x > 895) && (mouse_pos.x < 955) && (mouse_pos.y
> 45) && (mouse_pos.y < 65))
{
start_txt.visible = on; // show the text
str_cpy (start_str, empty_str);
while (str_cmpi (start_str, empty_str) == 1) // s'assure que le joueur a tapé
quelque chose
{
inkey start_str; // show the cursor -> stocke le texte dans start_str
wait (1);
}
start_frame = str_to_num(start_str);
}
if ((mouse_pos.x > 895) && (mouse_pos.x < 955) && (mouse_pos.y
> 100) && (mouse_pos.y < 120))
{
end_txt.visible = on; //
show the text
str_cpy
(end_str, empty_str);
while (str_cmpi (end_str, empty_str) == 1) // s'assure que le joueur a tapé
quelque chose
{
inkey end_str; // show the cursor -> stocke le texte dans start_str
wait (1);
}
end_frame = str_to_num(end_str);
}
}

Si nous cliquons quelque part près des boîtes de "Start frame" ou "End frame" , nous montrons le texte correspondant et notre entrée sera stockée dans start_str et end_str. Revenons à model_driver () :

function
model_driver()
{
my.albedo = 0;
while (1)
{
camera.ambient = model_ambient;
my.frame += 0.03 * frame_speed * time;
if (my.frame > end_frame)
{
my.next_frame = start_frame;
}
else
{
my.next_frame = 0;
}
if (my.frame >=
end_frame + 1)
{
my.frame -= (end_frame - start_frame + 1);
}
wait (1);
}
}

C'est la fonction associée à notre modèle. J'ai mis albedo à zéro parce que je veux que le modèle semble aussi pure que possible; camera.ambient obtient l'ambiance du modèle. L'ambiance et la vitesse d'animation sont mis en employant 2 curseurs sur main_pan, vous vous souvenez ?

Les boucles entre start_frame et end_frame mis par nous, employentune interpolation lisse des frames. Voici un exemple qui emploie ce cas de figure; il anime un modèle employant start_frame = 8 et end_frame = 11.

if
(my.frame > 11)
{
my.next_frame = 8;
}
else
{
my.next_frame = 0;
}
if (my.frame >= 12)
{
my.frame -= 4;
}

J'ai inclus deux modèles avec ce projet : guard et warlock.


Untitled

Voiture avec IA partie 1

Je sais que vous êtes nombreux à vouloir créer une course de voitures, aussi j'ai décidé de démarrer cette nouvelle série de voiture avec IA (Intelligence Artificielle). Ce mois nous apprendrons comment créer une voiture qui utilisera un chemin, accélère, deccélère, s'arrête après un nombre de tours prédéfinis, incluant un son de moteur variant avec la vitesse de la voiture ... Oh et nous apprendrons à créer un chronomètre qui affiche les econdes et les millisecondes!

Commençons par définir un texte et un panneau:

text
info_txt
{
font = system_font;
pos_x = 5;
pos_y = 5;
string = "Lap:
Time:";
flags = visible;
}

panel
info_pan
{
pos_x = 0;
pos_y = 0;
layer = 10;
digits = 45, 5, 1, system_font,
1, laps_scr;
// digits
= 125, 5, 3.3, system_font, 1, total_time; // A6 version
digits = 125, 5, 3, system_font,
1, total_time; // // A5 version
flags = overlay, refresh, visible;
}

Le texte affiche "Lap:" et "Time:" (Tour: Temps) en haut de l'écran et le panneau affiche les valeurs. La première ligne "digits" affiche le nombre de tour et la seconde ligne "digits" affiche le temps en seconde (version A5) ou secondes + millisecondes (version A6). Enlevez le double slah devant la ligne concernant la version A6 pour les mettre devant la ligne concernant la version A5 si vous avez la version A6.

function
main()
{
fps_max = 30;
level_load (carai_wmb);
while (1)
{
if (key_c != 1)
{
camera.x = -3400;
camera.y = 1325;
camera.z = 840;
camera.pan = 326;
camera.tilt = -17;
}
else
{
camera.x = car1.x - 150 * cos(car1.pan);
camera.y = car1.y - 150 * sin(car1.pan) ;
camera.z = car1.z + 30;
camera.pan = car1.pan;
camera.tilt = 0;
}
if (car1.race_over
== 0)
{
total_time = total_frames / 30;
}
wait (1);
}
}

La fonction principale (main) limite le taux d'affichage à 30 fps puis charge le niveau. Ma démo utilise 2 caméras: pressez et maintenez la touche "C" pour passer à une caméra suivant la voiture ou relâchez la touche "C" pour passer à une caméra fixe en vue isométrique. Tant que la touche "C" est pressée, la caméra est placée 150 quants derrière la voiture et 30 quants au dessus d'elle.

Le temps est calculé et stocké dans total_time tant que la voiture se déplace (car1.skill20 qui est aussi car1.race_over n'est pas mis à zéro). Regardons comment se simple chrnomètre fonctionne:

total_time = total_frames / 30;

Nous avons limité fps_max à 30, n'est-ce pas? Ce qui signifie que sur un pc digne de ce nom le jeu sera rendu à 30 frames par seconde. La variable prédéfinie total_frames contient le nombre de frames qui sont passées depuis que le moteur a démarré. Regardons quelques valeurs possibles:

total_frames = 0 -> total_time = 0.000
total_frames = 1 -> total_time = 0.033
total_frames = 2 -> total_time = 0.066
total_frames = 3 -> total_time = 0.100
total_frames = 4 -> total_time = 0.133

............................................................

total_frames = 29 -> total_time = 0.966
total_frames = 30 -> total_time = 1.000
total_frames = 31 -> total_time = 1.033

...........................................................

De cette façon vous avez un chronomètre décent en utilisant une simple ligne de code. Regardons à présent l'action attachée à la voiture:

action car
{
car1 = me;
my.race_over = 0;
var waypoint_number = 0;
var car_speed;
var target_coords; // the
coordinates of the next waypoint on the path
var engine_handle;
var angle_diff;
my.counting_laps = 1;
my.laps = -1;
if (my.skill1 == 1)
{
ent_path("car01");
}
engine_handle = ent_playloop
(my, engine_wav, 50);
ent_waypoint(target_coords, 1);

J'ai décidé d'utiliser un pointeur pour la voiture car je dois être capable de la poursuivre avec la caméra. Chaque voiture doit avoir une variable définie comme race_over qui sera mise à 0 au départ du jeu et à 1 lorsque la voiture aura terminée la course. Nous avons quelques autres variables locales de définies mais nous les verrons plus tard. Si la voiture a sa variable skill1 de mis à 1, elle suivra le chemin nommé "car01" dans Wed. La voiture joue le son nommé engine_wav dans une boucle, et la dernière ligne de code met target_coords au premier point du chemin "car01".

     while ((waypoint_number < 300) &&
(my.race_over == 0))
{
temp.x = target_coords.x
- my.x;
temp.y = target_coords.y
- my.y;
temp.z = 0;
if (vec_to_angle
(angle_diff.pan, temp) < 150)
{
waypoint_number = ent_nextpoint(target_coords);
}
angle_diff.pan
= ang(angle_diff.pan - my.pan);
my.pan += car_speed
* angle_diff.pan * 0.005;

Mon chemin a moins de 300 points, aussi"while (waypoint_number < 300)" " équivaut à "while (1)", n'est-ce pas? La voiture continuera de se déplacer jusqu'à ce que my.race_over soit mis à 1, et que se passe-t-il à ce moment là? Ca signifie que c'est la fin de la course! Nous stockons le vecteur de la position courante de la voiture (my.x et my.y) à target_coords (premier point du chemin) dans temp, puis nous tournons la voiture vers le prochain point du chemin.Le prochain point de destination est mis lorsque la voiture est à moins de 150 quants de sa destination actuelle.

Nous augmentons l'angle pan de la voiture doucement en fonction de angle_diff.pan. Vous pouvez jouer avec la valeur 0.005, mais faites attentionque si la vitesse de la voiture est trop grande et cette valeur trop petite, il se pourrait que la voiture n'est pas le temps de se tourner vers son point de destination.

          if (((waypoint_number
> 65) || (waypoint_number < 8)) || ((waypoint_number > 34) &&
(waypoint_number < 49)))
{
if (car_speed.x < 60 * time)
{
car_speed.x += 1 * time;
}
}
else
{
if (car_speed.x > 40 * time)
{
car_speed.x -= 0.5 * time;
}
}

Les lignes de codes ci-dessus accélère ou ralentisse en fonction de la configuration de la piste. Vous avez du mal à vous imaginer qu'une voiture pilotée par l'ordinateur sache où et quand il faut réduire sa vitesse, n'est-ce pas? Jetez un oeil sur l'image ci-dessous:

L'image montre tous les points du chemin "car01". La voiture accélèrera jusqu'à ce que sa vitesse atteigne 60 * time si elle se trouve dans la région : 65...8 ou 34...49; si elle est dans la région (8...34 ou 49...65) la voiture ralentira jusqu'à atteindre 40 * time.

          snd_tune (engine_handle,
50, car_speed.x * 5, 0);
car_speed.y = 0;
car_speed.z = 0;
move_mode = ignore_you
+ ignore_passable + glide;
ent_move(car_speed,
nullskill);
vec_set (temp,
my.x);
temp.z -= 1000;
trace_mode = ignore_me
+ ignore_passable + scan_texture;
trace (my.x, temp);
if (str_cmpi ("metalplainl1",
tex_name))
{
add_laps();
}
if (my.laps ==
3)
{
my.race_over = 1; // get out of the while loop after 3 laps
}
wait(1);
}

La voiture change la fréquence de son bruit de moteur en fonction de la valeur de car_speed.x; nous traçons sous la voiture pour obtenir le nom de la texture utilisée pour la route. Si le nom est "metalplainl1", la voiture a fait un tour complet aussi nous exécutons la fonction nommée add_laps(). Si le nombre de tour de la voiture est 3, nous mettons race_over à 1, ce qui aura pour effet de nous sortir de la première boucle.

while (car_speed.x > 1)
{
car_speed.x -= 1 * time;
snd_tune (engine_handle, 50, car_speed.x * 5, 0);
car_speed.y = 0;
car_speed.z = 0;
move_mode = ignore_you + ignore_passable + glide;
ent_move(car_speed, nullskill);
wait (1);
}
snd_stop (engine_handle);
}

La boucle while diminue la vitesse de la voiture jusqu'à ce qu'elle soit inférieure à 1 * time en diminuant le son en conséquence. La voiture avancera lentement jusqu'à s'arrêter complètement parce que la boucle while se sera arrêtée de fonctionner. Le bruit du moteur s'arrêtera de la même façon. Jetons un oeil à la dernière fonction:

function add_laps()
{
if (my.counting_laps == 0) {return;}
my.counting_laps = 0;
my.laps += 1;
snd_play (lap_wav, 100, 0);
laps_scr = my.laps;
sleep (10);
my.counting_laps = 1;
}

La voiture pouvant détecter metalplainl1 sous ses roues pendant plus d'un cycle, surtout si sa vitesse est faible,ce qui aurait pour conséquence d'appeler plusieurs fois add_laps() générant de nombreux tours. La solution est facile: nous mettons counting_laps = 0 dés le premier appel de la fonction, et nous n'autoriserons cette fonction qu'après 10 secondes en remettant counting_laps = 1. De cette façon nous pouvons augmenter le nombre de tour pour la voiture (my.laps += 1), nous pouvons jouer un son (lap_wav) et nous pouvons afficher le nombre de tour à l'écran (laps_scr) seulement une fois par tour du fait que la voiture met moins de 10 secondes pour traverser le bloc de métal.

Peut-être avez-vous vu un bloc de niveau isolé dans la vue de dessus de Wed:

Ce bloc m'a servi à réaliser toutes les parties courbes de la piste. J'ai tourné des copies de ce bloc de 15, 30, 45, 60... degrés et j'ai pensé que ça pourrait vous servir de base si vous souhaitiez modifier ma piste. Le mois prochain nous apprendrons à créer plusieurs voitures contrôlées par l'ordinateur et nous verrons comment elles inter-agissentr entre elles, aussi tenez vous prêts!


Untitled

Voiture avec IA partie 2

Ce mois-ci nous allons apprendre à créer plusieurs voitures conduites par l'ordinateur. Ces voitures utiliseront des chemins différents et aurint des vitesses différentes à chaque fois, aussi chaque course paraîtra unique.Oh, et chaque voiture utilse son klaxon si une autre se trouve dans son chemin. Nous verrons également un joli panneau de fin pour afficher les places occupées par les voitures et leurs temps respectifs.

N'oubliez pas de lire tout d'abord l'article d'AUM32. Vous êtes prêts? Commençons par quelques définitions:

define
race_over skill20;
define counting_laps skill21;
define laps skill22;
define
max_speed skill23;
define min_speed skill24;
define distance skill25;
define scans
skill26;
define car_speed skill27;

J'ai nommé les variables d'entité afin de rendre les explications plus compréhensible, regardons leur signification:
- my.race_over est une variable qui est mise à 1 dès qu'une voiture a terminée la course;
- my.counting_laps valide ou invalide le compteur du nombre de tours;
- my.laps contient le nombre de tour pour cette voiture;
- my.max_speed contient la vitesse maximale pour la voiture; sa valeur change à chaque tour;
- my.min_speed contient la vitesse minimale pour la voiture; sa valeur change à chaque tour;
- my.distance contient la distance parcourue par la voiture;
- my.scans contient le nombre de balayage pour une voiture spécifique
- my.car_speed contient la vitesse actuelle de la voiture.

Inutile d'apprendre par coeur ce que font ces variables, nous les reverrons plus tard.

entity* car1;
// needed only for the panels
entity* car2;
entity* car3;

J'ai défini 3 pointeurs nommés car1, car2 and car3; le code de conduite ne nécessite pas de pointeurs du tout mais les textes qui apparaîssent à la fin de la course en ont, eux, besoin.

text
car1_txt
{
font = system_font;
pos_x = 180;
pos_y = 0;
string = car1_str;
layer = 15;
}

text
car2_txt
{
font = system_font;
pos_x = 180;
pos_y = 0;
string = car2_str;
layer = 15;
}

text
car3_txt
{
font = system_font;
pos_x = 180;
pos_y = 0;
string = car3_str;
layer = 15;
}

panel
finish_pan
{
bmap = finish_pcx;
pos_x = 100;
pos_y = 100;
alpha = 50;
layer = 10;
flags = overlay, refresh,
transparent;
}

Vous pouvez voir que ces définitions de texte et de panneaux sont simples; jetez un oeil à l'image ci-dessous pour les voir en action:

Et à présent une autre image pour montrer qui est qui:

Le texte apparaîtra sur le panneau finish_pan dans un ordre aléatoire dépendant en fait du classement de la coursete.

C'était l'introduction. Comment pouvons-nous créer un jeu de course qui inclut plusieurs voitures contrôlées par l'ordinateur? Vous pouvez utiliser quelque chose qui ressemble à ceci:

Vous pourriez en effet utiliser ces chemins concentriques mais il est hors de question que j'achète votre jeu. J'aime voir les voitures courrir les unes après les autres, utilisant leur avertisseur, etc... Cela ne se produira jamais avec les chemins ci-dessus. Alors que devons-nous faire? Nous dupliquons un bon cheminplusieurs fois, nous le tournons légèrement, nous éditons quelques uns de ces points et nous obtenons quelque chose comme ceci:

Vous pouvez voir que ces chemins se chevauchent à plusieurs reprises; Si votre piste est plus grande que la mienne vous pourrez obtenir des chemins se croisant de façon plus naturelle. Jetons à présent un oeil à la fonction principale (main):

function
main()
{
randomize();
fps_max = 30;
level_load (carai2_wmb);
wait (3);
camera.x = -3400;
camera.y = 1325;
camera.z = 840;
camera.pan = 326;
camera.tilt = -17;

La première ligne de code génère une séquence de nombre aléatoire qui est unique à chaque fois. Nous limitons le taux d'affichage à 30 fps, nous chargeons le niveau et nous donnons la bonne position et les bons angles à la caméra.

     while (1)
{
if (car1.race_over
== 0)
{
car1_time = total_frames / 30;
}
if (car2.race_over
== 0)
{
car2_time = total_frames / 30;
}
if (car3.race_over
== 0)
{
car3_time = total_frames / 30;
}
wait (1);
}
}

La boucle while ci-dessus calcule le temps de course pour chaque voiture aussi longtemps que la voiture continie d'avancer (race_over n'est pas mis à 1). Les temps sont stockés dans car1_time, car2_time et car3_time. Lisez l'article d'AUM32 pour en conna^tre plus sur cetastucieux système de chronométrage.

Ici commence la grande et effrayante action:

action
car
{
var waypoint_number = 0;
var target_coords;
var engine_handle;
var angle_diff;
var engine_offset;
my.race_over = 0;
my.counting_laps = 1;
my.laps = -1;
my.max_speed = 50;
my.min_speed = 50;
my.enable_scan = on;
my.event = make_way;
engine_offset = random(3);

Nous définissons des variables qui stockent le numéro du point de chemin actuel et les coordonnées de la prochaine cible (waypoint) sur le chemin. Engine_handle est le pointeur (l'identifiant) pour le son du moteur; angle_diff contient la difference entre l'angle pan actuel de la voiture et l'angle du prochain point à atteindrer sur le chemin. Engine_offset wcrée un son de moteur unique pour chaque voiture. La variable définie comme race_over est mise à zéro (la course est partie et n'est pas terminée) et le compteur de tour est enclenché (counting_laps = 1). Le nombre de tour est mis à moins 1 (il sera mis à 0 dès que la course va commencer). Les valeurs initiales pour max_speed et min_speed sont mises à 50 mais elles vont changer dans un moment, dès que les voitures croisent la ligne de départ (je sais, c'est un bloc, pas une ligne, mais nous dirons que c'est une ligne pour le moment).

La voiture est sensible au balayage (scanà; aussi lorsqu'elle est balayée par une autre voiture son évènement make_way est exécuté. Pour terminer, engine_offset est mis a une valeur aléatoire de 0 à 3.

     if (my.skill1 == 1) // don't forget to set skill1
= 1 for the first car, skill1 = 2 for the second car and so on
{
ent_path("car01");
// if skill1 = 1 the car will follow the path named "car01" without the quotes
car1 = me; // I'm
car 1
}
if (my.skill1 == 2) // same thing for the rest
of the cars
{
ent_path("car02");
car2 = me;
}
if (my.skill1 == 3)
{
ent_path("car03");
car3 = me;
}
engine_handle = ent_playloop (my, engine_wav,
50); // play the engine sound in a loop
ent_waypoint(target_coords, 1); // set the target coordinates to the first point
on the path

La première voiture ayant son skill1 de mis à 1 va utiliser le chemin "car01". Cette voiture est connue comme "car1" à partir de maintenant, car nous lui avons assigné le pointeur avec ce nom. La même chose se produit pour les autres voitures; n'oubliez pas de mettre leur skill1 à différentes valeurs!

Le son engine_wav est joué dans une boucle; le résultat de l'opération est stocké dans engine_handle. La ligne de code suivante met les coordonnées de la cible au premier point du chemin.

while (my.race_over == 0) // this loop will run until my.race_over = 1
{
temp.x = 60; // horizontal scanning angle
temp.y = 60; // vertical scanning angle
temp.z = 150; // scanning range
scan_entity (my.x, temp);

          temp.x = target_coords.x
- my.x;
temp.y = target_coords.y
- my.y;
temp.z = 0;
if (vec_to_angle
(angle_diff.pan, temp) < 150)
{
waypoint_number = ent_nextpoint(target_coords);
}
angle_diff.pan
= ang(angle_diff.pan - my.pan);
my.pan += my.car_speed
* angle_diff.pan * 0.005;

La boucle while ci-dessus s'exécute jusqu'à ce que my.race_over soit mis à 1, ce qui se produit uniquement à la fin de la course. La voiture balaye devant elle, détectant d'éventuelles voitures proches d'elle. Regardez l'image ci-dessous:

Si une autre voiture se trouve dans le cone de balayage, la fonction d'évènement associée à la voiture sera exécutée. Nous parlerons de cette fonction plustard pour l'instant revenons à notre code.

Nous prenons le vecteur qui pointe vers la cible et le stockons dans temp. Si la voiture est à moins de 150 quants de la cible, nous lui demandons de se diriger vers le prochain point du chemin (c'est ce que fait ent_nexpoint). Nous prenons la différence entre l'angle pan actuel de la voiture et l'orientation du prochain point du chemin et nous tournons doucement la voiture en direction de cette destination.

          if (((waypoint_number
> 65) || (waypoint_number < 8)) || ((waypoint_number > 34) &&
(waypoint_number < 49))) // horizontal areas of the track?
{
if (my.car_speed < (my.max_speed * time)) // and the speed is below max_speed?
{
my.car_speed += 1 * time; // increase the speed of the car
}
}
else // curved
areas of the track
{
if (my.car_speed > (my.min_speed * time)) // and the speed is above min_speed?
{
my.car_speed -= 0.5 * time; // decrease the speed of the car
}
}
my.skill28 = 0;
// second skill used for car_speed, we are using skill27..skill29 (skill27 was
defined as car_speed)
my.skill29 = 0;
// third skill used for car_speed
move_mode = ignore_you
+ ignore_passable + glide;
my.skill2 = ent_move
(my.car_speed, nullskill);
my.distance +=
my.skill2;
snd_tune (engine_handle,
50, (my.skill2 * 5 + engine_offset), 0);
vec_set (temp,
my.x);
temp.z -= 1000;
trace_mode = ignore_me
+ ignore_passable + scan_texture;
trace (my.x, temp);
// trace 1000 quants below the car
if (str_cmpi ("checker1",
tex_name))
{
add_laps( );
}

Si la voiture se déplace sur une portion horizontale de la piste et que sa vitesse est en dessous de max_speed, nous augmentons la vitesse de la voiture autrement si la voiture est dans une courbe et que sa vitesse est au dessus de min_speed, nous diminuons la vitesse de la voiture. Comme je vous sens un peu fainéant pour relire l'article de l'AUM32,j'ai remis l'image montrant ces nombves :

Nous mettons skill28 et 29 à zéro ( ce sont les composantes y et z du vecteur car_speed, qui est skill27...29). La voiture se déplace ignorant les entités passables et glissant le long des murs et des entités; la distance parcourue par la voiture pendant un cycle est stockée dans skill2 et la distance totale parcourue par la voiture est stockée dans my.distance. Le bruit du moteur est joué en fonction de la vitess réelle de la voiture, basée sur la distance couverte par elle. Notez que j'ai également ajouté engine_offset aléatoire afin que chaque voiture ait son propre bruit de moteur, ce dont je vous ai déjà parlé. Pour terminer nous traçons 1000 quants sous la voiture et nous obtenons le nom de la texture de la route. Si le nom est "checker1", la voiture passe sur le bloc de départ et la fonction add_laps() est appelée.

          if (my.laps ==
3) // end of race?
{
my.race_over = 1; // get out of the while loop after 3 laps
place += 1;
if (place == 1) // i'm the first car that finishes the race?
{
if (my.skill1 == 1)
{
str_cpy (car1_str, "1st place: Green car
Time: ");
str_for_num (temp_str, car1_time);
str_cat (car1_str, temp_str);
car1_txt.pos_y = 250;
car1_txt.visible = on;
}
if (my.skill1 == 2)
{
str_cpy (car2_str, "1st place: Red car
Time: ");
str_for_num (temp_str, car2_time);
str_cat (car2_str, temp_str);
car2_txt.pos_y = 250;
car2_txt.visible = on;
}
if (my.skill1 == 3)
{
str_cpy (car3_str, "1st place: Blue car
Time: ");
str_for_num (temp_str, car3_time);
str_cat (car3_str, temp_str);
car3_txt.pos_y = 250;
car3_txt.visible = on;
}
}

Si le nombre de tours est 3, la course est finie pour cette voiture, autrement nous mettons my.race_over à 1, nous incrémentons "place" (la place occupée par la voiture à la fin de la course) et nous vérifions la valeur de "place". Si place = 1, cette voiture est la première à finir la course. Si son skill1 est mis à 1, la première voiture est la voiture verte (je le sais parce que c'est moi qui ai mis son skill à 1 dans Wed). Nous écrivons "1ère place : voiture verte ("1st place: Green car.... ") dans la chaîne utilisée par car1_txt, nous convertissons car1_time de nombre en chaîne et nous l'ajoutons à notre chaîne "1ère place etc". Nous mettons la bonne valeur pour pos_y du texte et nous le rendons visible. Le résultat apparaît comme ceci (juste un exemple)):

1èret place: Voiture verte Temps: 50.342

Si my.skill1 = 2, alors c'est la voiture rouge qui gagne (encore une fois je le sais car j'ai mis son skill1 à 2 dans Wed. Nous utilisons une chaîne différente cette fois-ci : "1ère place: Voiture rouge....", nous convertissons car2_time de nombre en chaîne et nous l'ajoutons à notre chaîne "1ère place etc". Nous mettons la bonne valeur pour pos_y du texte et nous le rendons visible. Le résultat apparaît comme ceci :

1ère place: Voiture rouge Temps: 51.324

Si my.skill1 = 3, c'est la voiture bleue qui gagne et nous faisons exactement la même chose pour elle.

if (place == 2) // i'm the second car that finishes the race?
{
if (my.skill1 == 1)
{
str_cpy (car1_str, "2nd place: Green car
Time: ");
str_for_num (temp_str, car1_time);
str_cat (car1_str, temp_str);
car1_txt.pos_y = 280;
car1_txt.visible = on;
}
if (my.skill1 == 2)
{
str_cpy (car2_str, "2nd place: Red car
Time: ");
str_for_num (temp_str, car2_time);
str_cat (car2_str, temp_str);
car2_txt.pos_y = 280;
car2_txt.visible = on;
}
if (my.skill1 == 3)
{
str_cpy (car3_str, "2nd place: Blue car
Time: ");
str_for_num (temp_str, car3_time);
str_cat (car3_str, temp_str);
car3_txt.pos_y = 280;
car3_txt.visible = on;
}
}

Les choses se répètent exactement pour la deuxième voiture qui termine la course, la seule différence étant la valeur de pos_y utilisée pour afficher le texte, qui est plus grand de 30 pixels, aussi le deuxième texte s'affichera en dessous du premier texte.

if (place == 3) // i'm the third car that finishes the race?
{
if (my.skill1 == 1)
{
str_cpy (car1_str, "3rd place: Green car
Time: ");
str_for_num (temp_str, car1_time);
str_cat (car1_str, temp_str);
car1_txt.pos_y = 310;
car1_txt.visible = on;
}
if (my.skill1 == 2)
{
str_cpy (car2_str, "3rd place: Red car
Time: ");
str_for_num (temp_str, car2_time);
str_cat (car2_str, temp_str);
car2_txt.pos_y = 310;
car2_txt.visible = on;
}
if (my.skill1 == 3)
{
str_cpy (car3_str, "3rd place: Blue car
Time: ");
str_for_num (temp_str, car3_time);
str_cat (car3_str, temp_str);
car3_txt.pos_y = 310;
car3_txt.visible = on;
}
}
finish_pan.visible = on;
}
wait(1);
}

Les choses se répètent exactement pour la troisième voiture qui termine la course, la seule différence étant la valeur de pos_y utilisée pour afficher le texte, qui fait apparaître le texte très proche du bas de l'écran, après les 2 autres textes.

while (my.car_speed > 1) // decrease the speed from its current speed to 1
{
my.car_speed -= 1 * time;
snd_tune (engine_handle, 50, my.car_speed * 5, 0); // and tune the sound accordingly
my.skill28 = 0;
my.skill29 = 0;
move_mode = ignore_you + ignore_passable + glide;
ent_move (my.car_speed, nullskill);
wait (1);
}
snd_stop (engine_handle); // stop the engine sound
if (place == 3) // race finished for all the cars?
{
while (finish_pan.alpha < 95)
{
finish_pan.alpha += 2 * time; // increase alpha
wait (1);
}
finish_pan.transparent = off; // the panel is opaque now
}
}

Ici la course est terminée aussi nous diminuons la vitesse jusqu'à ce que sa valeur soit 1, jouant le son du moteur en conséquence. Dés que la vitesse atteint 1, nous arrêtons le bruit du moteur (la voiture n'avnce plus du tout). Si place = 3 (toutes les voitures ont terminé la course) nous augmentons l'alpha de finish_pan dans une boucle while puis nous le rendons opaque car toutes les voitures sont arrêtées à présent aussi il n'y a plus grand chose d'intéressant à voir sur l'écran.

Il est temps de voir 2 petites fonctions:

function
add_laps()
{
if (my.counting_laps == 0) {return;}
my.max_speed = 50 + random(10);
my.min_speed = 30 + random(10);
my.laps += 1;
snd_play (lap_wav, 100, 0);
my.counting_laps = 0;
sleep (10);
my.counting_laps = 1;
}

Vous savez que nous avons utilisé une instruction trace pour compter le nombre de tour, n'est-ce pas? Lorsqu'une voiture passe au dessus de la ligne de départ (unbloc texturé avec la texture "checker1") la fonction add_laps est appelée. Ce qui pose un petit problème : une voiture peut déclencher plus d'une fois l'appel de add_laps si sa vitesse est faible. Si la voiture se trouve sur ce bloc pendant 4 cycles, la fonction add_laps sera appelée 4 fois, nous sommes d'accord?

Pour éviter cela j'utilise my.counting_laps; si la voiture passe sur le bloc "checker1", le nombre de tours est incrémenté, mais cette fonction n'acceptera pas d'autres appels pendant 10 secondes (seule sa première ligne de code sera exécutée lors des appels intempestifs). Nous mettons des valeurs aléatoires à max_speed et min_speed pour chaque voiture à chaque tour, comme cela les résultats de la course ne sont pas prévisibles, y compris pour moi ;), nous ajoutons un son et comme je l'ai dit nous empêchons l'exécution intempestive de cette fonction pendant 10 secondes. Nous pouvons utiliser n'importe quelle valeur ici à la condition qu'elle soit inférieure au temps qu'il faut pour faire un tour complet.

La dernière fonction s'exécute lorsqu'une voiture est balayée par la voiture qui la suit:

function make_way()
{
my.scans += 1; // increase the number of scan
if (you.car_speed >= 5) // the speed of the car is above 5?
{
you.car_speed -= 0.5 * time; // decrease the speed of the car that has scanned me. Play with 0.5
}
if (my.scans > 5) // too many scans? play with "5"
{
ent_playsound (my, horn_wav, 2000); // I'll use my horn then
my.enable_scan = off; // stop reacting to scans
sleep (1); // for one second
my.enable_scan = on; // enable scans again
my.scans = 0; // reset the number of scans (prepare for a new horn)
}
}

Nous augmentons le nombre stocké dans my.scans et si la vitesse de la voiture qui a balayé cette voiture est au-dessus de 5, nous la diminuons lentement. De cette façon la voiture derrière évitera la plupart des collisions avec la voiture devant elle. Si my.scans est au-dessus de 5, nous jouons un bruit d'avertisseur sonore (horn_wav), nous cessons de réagir aux balayages pour une seconde et nous remettons my.scans à zéro, le préparant pour un autre horn_wav. C'est tout !

Ca me parait pas mal tout ça!. Ce code fonctionne bien avec A5 et A6, mais je vous encourage à acheter A6 parce qu'il emploie les collisions basées sur les polygones pour ldes modèles, ce qui signifie que les voitures ne se pénétreront pas si vous remplacez l'instruction ent_move par la nouvelle instruction de déplacement c_move de A6. Le mois prochain nous apprendrons à créer le code utilisé pour la voiture du joueur. D'ici là, bonnes courses


Untitled

Voiture avec IA partie 3

Ce mois-ci nous allons apprendre le code utilisé pour la voiture du joeur. Notre code crée une voiture qui passe les vitesse automatiquement (boite automatique), utilise un avertisseur sonore (klaxon), ses lumières de freins, a un compteur de vitesse et plus encore. Si encore une chose, la voiture utilise 3 caméras différentes.

Tout le nouveau code peut-être trouvé dans playercar.wdl, qui est inclu dans carai3.wdl, une version légèrement modifiée du fichier carai2.wdl. Commençons par quelques définitions simples:

panel speedo_pan
{
bmap = speedo_pcx;
pos_x = 640;
pos_y = 450;
layer = 10;
flags = overlay, refresh, visible;
}

entity
needle
{
type = <needle.tga>;
layer
= 20;
scale_x = 0.15;
scale_y = 0.25;
x = 200;
y = -91.5;
z = -65;
view = camera;
flags = overlay, nofilter, visible;
}

J'utilise un panneau et une entité aiguille (needle) pour le compteur de vitesse. Pourquoi j'utilise une entité pour l'aiguille?Tout simplement parce qu'un panneau ne peut pas tourner mais qu'une entité peut changer son angle roll pour faire le travail. Vous pouvez changer les valeurs de scale_x et scale_y pour ajuster la taille de l'aiguille; changer x pour mettre correctement l'aiguille devant la caméra et y et z pour mettre sa position à l'écran (les axes x et y).

starter
set_needle()
{
var needle_angle;
while (player
== null) {wait (1);}
while (1)
{
needle_angle = player.skill2 * 12.17 - 140; // -140...+140 degrees = 0...140 mph
needle.roll = max (-140, needle_angle);
wait (1);
}
}

La fonction ci-dessus s'exécute toute seule; son rôle est de mettre un angle roll correct à l'entité aiguille, fonction de la vitesse réelle de la voiture. Nous définissons une variable locale nommée needle_angle, nous attendons que le joueur soit chargé puis nous entrons dans une boucle while (1) qui contient de drôles de nombres. A qoui servent-ils?

L'intervalle de vitesse est de 0 à 140 m.p.h.(mph = miles par heure, vous pouvez convertir en km/h si vous le souhaitez) mais l'aiguille doit changer son angle de -140 à + 140 degrés. Je sais que player.skill2 va de 0 à 23 quants / frame. Comment je sais cela? J'ai utilisé un simple panneau avec un nombre qui affichait player.skill2 pour mesurer ces valeurs avec la voiture arrêtée et la voiture au maximum de sa vitesse. Regardons de nouveau cette formule horrible de plus près:

needle_angle = (0 to 23) * 12.17 - 140

a) needle_angle = 0 * 12.17 - 140 = -140 degrees

b) needle_angle = 23 * 12.17 - 140 = 139.91 degrees

La formule ci-dessus montre que l'aiguille change son angle de -140 à 139.91 degréss lorsque la vitesse va de 0 à 23 quants / frame. La ligne de code suivante dans la boucle s'assure que l'angle roll de l'aiguille n epuisse pas descendre en dessous de -140 degrés.

Je n'expliquerai pas la fonction qui utilise l'avertisseur sonore du joueur car elle est vraiment simple, jugez par vous même:

function
horn()
{
snd_play (plhorn_wav, 100, 0);
}

on_h = horn;

Ici se trouve une autre simple action appelée depuis l'action du joueur; c'est l'action qui nous permet de choisir l'une des 3 caméras dont je vous ai parlé:

function
car_camera()
{
if (key_1 == on) {camera_number = 1;}
if (key_2 == on) {camera_number = 2;}
if (key_3 == on)
{camera_number = 3;}
if (camera_number == 1) // top view
{
camera.x = player.x;
camera.y = player.y;
camera.z = player.z +
900; // play with this value
camera.pan = player.pan;
camera.tilt = -90;
}

Le jouueur peut choisir une de ces caméras en pressant les touches "1", "2" ou"3" sur le clavier. Si nous pressons "1", la caméra aura le même x et y que la voiture mais sera placée 900 quants au-dessus de la voiture, regardant vers le bas.

if (camera_number == 2) // simulated first person view
{
camera.x = player.x - 20 * cos(player.pan);
camera.y = player.y - 20 * sin(player.pan);
camera.z = player.z + 18;
camera.pan = player.pan;
camera.tilt = 0;
}

Si le joueur presse la touche "2", il passe en vue première personne. C'est en fait une vue isométrique qui est placée 20 quants derrière l'origine dela voiture et 18 quants au-dessus.

  if (camera_number == 3) // true isometric view
{
camera.x = player.x - 200 * cos(player.pan); // 200 =
distance
camera.y = player.y - 200 * sin(player.pan);
// same value here
camera.z = player.z + 150;
// above the player
camera.pan = player.pan;
camera.tilt = -27; // look down at the player
}
wait (1);
}

Si
le joueur presse la touche"3", il obtient une vue isométrique.
La caméra est placée 200 quants derrière la voiture et 150
quants au-dessus d'elle; son angle tilt de -27 degrés nous permet d'être
sur que le joueur aura une bonne vue de la route au devant.

C'était la partie facile de cet article. Préparez-vous pour rencontrer l'action qui dirige la voiture du joueur (insérer des rires jaunes ici).

action
players_car
{
var players_engine; // engine sound handle
var player_dist;
var player_speed;
var minimum_z;
player = my; // I'm the player
players_engine = ent_playloop (my, plengine_wav, 50);

J'ai déclaré un groupe de variables locales et j'ai associé le pointeur "player" à "my" l'entité voiture. La voiture jouera le son plengine_wav dans une boucle.

while (1)
{
player_speed.x = movement_speed * (key_cuu - 0.4 * key_cud); // backwards movement speed = 40%
my.skill1 = player_speed.x * time + max (1 - time * 0.02, 0) * my.skill1;
player_dist.x = my.skill1 * time;
player_dist.y = 0;

          vec_set (temp,
my.x);
temp.z -= 1000;
trace_mode = ignore_me + scan_texture;
player_dist.z = -trace (my.x, temp) - my.min_z;

Le joueur accélère / decélère lorsque nous pressons les touches de direction "haut" / "bas". Veuillez noter que la vitesse du mouvement arrière peut-être jusqu'à 0.4 = 40% de la vitesse maximale. Les lignes suivantes calculent la vitesse de la voiture; j'ai mis un facteur de friction de 0.02 mais vous pouvez essayer différentes valeurs ici. La voiture peut se déplacer en utilisant player_dist. (pondéré par time). La composante y de la vitesse est à zéro.

Les prochaines lignes de code mettent la bonne hauteur (z) pour la voiture. Nous utilisons une instruction trace pour trouver le niveau du sol et le nom de la texture sous la voiture.

La ligne rouge simule la trace alors qu ela flèche verte montre la distance retournée par trace. Finalement, nous soustrayons my.min_z, qui est la distance entre le point le plus bas de la voiture en z et son origine. Si vous prévoyez d'utilser un modèle de voiture animé vous devrez utiliser vec_for_min pour mettre la bonne valeur min_z à chaque cycle. Vous trouverez le code qui fait cela dans le fichier script playercar.wdl.

          if (str_cmpi (tex_name,
"tilegrass"))
{
if (movement_speed > 0.1)
{
movement_speed -= 5 * time;
}
else
{
movement_speed = 0.1;
}
}
else
{
if (movement_speed < 0.9)
{
movement_speed += 10 * time;
}
else
{
movement_speed = 0.9;
}
}

Si le nom de la texture sous les roues est "tilegrass", la voiture ralentira sa vitesse de mouvement jusqu'àêtre en dessous de 0.1.Si le nom de la texture détectée par trace n'est pas tilegrass", la voiture augmentera sa vitesse jusqu'à atteindre 0.9. De cette façon le joueur n'est pas encouragé à tricher en prenant des raccourcis.

         move_mode = ignore_passable
+ glide;
my.skill2 = ent_move
(player_dist, nullvector); // now ent_move the player
if (my.skill2 > 0) // change gears automatically, depending on
the value stored in skill2
{
engine_freq = 15;
}
if (my.skill2 > 8)
{
engine_freq = 20;
}
if (my.skill2 > 15)
{
engine_freq
= 22;
}
if (my.skill2 > 19)
{
engine_freq = 23.5;
}

La voiture se déplace en ignorant les entités passables et en glissant le long des obstacles trouvés sur son chemin; nous stockons le résultat (la distance couverte à chaque cycle) dans my.skill2. Si la valeur stockée ici est positive, la voiture choisira la bonne vitesse (boîte) en fonction de la vitesse de la voiture (my.skill2). Les valeurs pour le son ont été obtenues par tatonnements et essais divers, libre à vous de mettre les votres.

       player_speed.pan = rotation_speed * (key_cul
- key_cur) * my.skill2;
my.skill3 = time * player_speed.pan
+ max (1 - time * 0.7, 0) * my.skill3;
my.pan
+= my.skill3 * time;

my.skill4 = max (50,
my.skill2 * engine_freq); // play an engine sound at the minimum speed too
snd_tune (players_engine, 50, my.skill4, 0); // tune the engine sound

La voiture peut tourner (changer son angle pan) uniquement si my.skill2 est plus grand que zéro, ce qui signifie que la voiture est en mouvement. Nous utilisons une formule similaire pour la vitesse de rotation, utilisant cette fois une valeur de friction plus importante. La vitesse de rotation est également pondéré par time.

Nous voulons moduler le bruit du moteur en utilisant une valeur stockée dans my.skill4 - rejetons un oeil à la ligne ci-dessous pour voir comment cette valeur skill4 est calculée:

my.skill4 = max (50, my.skill2 * engine_freq); // play an engine sound at the minimum speed too

J'ai cherché à entendre un bruit de moteur même lorsque la voiture n'était pas en mouvement; c'est pourquoi my.skill4 choisira la plus grande valeur entre 50 et my.skill2 * engine_freq à chaque cycle. Si la voiture est à l'arrêt, le bruit du moteur sera joué en utilisant "50"; autrement le son sera joué en fonction de la vitesse de la voiture (my.skill2) et du numéro de la vitesse (boite) donné par la valeur mise pour engine_freq.

if (key_cud == on)
{
my.skin = 2;
}
else
{
my.skin = 1;
}

car_camera(); // update the camera position
wait(1);
}
}

Si nous pressons la touche de direction "bas" c'est que nous freinons aussi la voiture utilsera sa deuxième peau, celle qui montre les lumières de freins allumées. Si nous ne pressons pas la touche de direction "bas", la voiture utilise sa peau normale, la première. C'est une méthode simple et efficace mais elle utilise de la mémoire vidéo (environ 100Kb de plus pour la voiture du joueur). Les dernières lignes appelle la fonction car_camera et donne pour instruction au moteur d'attendre un cyccle.

Alors ? Finalement le code pour la voiture n'est pas si compliqué que cela! J'ai cherché à écrire un code pour un simple jeu d'arcade avant tout, pas un code pour une simulation! Le mois prochain nous nous remémorrerons tout ce que nous avons vu jusqu'ici puisnous ajouterons ce qui manque à notre jeu de course.


Untitled

Car AI

Cette foid-ci nous nous apprêtons à jouer à une démo de notre jeu, aussi ouvrez carai4.wmp, compilez-le et exécutez-le en utilisant carai4.wdl.

Les voitures attendent que "3" clignote à l'écran, mais vous avez déjà votre doigt qui s'énerve sur la touche de direction "haut"! Bien, les vitesses des voitures pilotées par l'ordinateur ne sont pas trop rapides aussi vous gagnez assez facilement de nombreuses courses. Jouez avec la vitesse des voitures jusqu'à ce que vous perdiez plus de courses et vendez cette version du jeu. Vous pouvez également mettre une valeur différente pour max_laps dans carai4.wdl pour obtenir un nombre de tours différents pour votre jeu.

J'ai commencé à écrire le code pour cette nouvelle série d'IA il y a déjà quelques mois : vous devez lire ces articles avnat de commencer celui-ci. Vous pouvez voir que le panneau de "fin" qui apparaît à la fin de la course inclut à présent le joueur, le code pour cela est identique à l'ancien code, aussi je ne vais pas ous l'expliquer de nouveau. Concentrons-nous plutôt sur le nouveau code qui permet d'obtenir :

- Nous avons une séquence d'introduction sympatique;

- La voiture prend un chemin aléatoire à la fin de chaque tour;

- La distance, la vitesse actuelle et le numéro du chemin sont affichés sur le panneau qui apparaît en haut de l'écran.

Il y a également quelques diiférences avec l'ancien code de l'IA voiture / joueur mais elles seront commentaires lorsque ce sera nécessaire. Commençons avec le code utilisé pour l'introduction:

starter
init_race()
{
sleep (2);
one_ent.visible
= on;
snd_play (lo_wav, 50, 0);
while (one_ent.x
> 100)
{
one_ent.x -= 50
* time;
wait (1);
}
one_ent.visible = off;
two_ent.visible = on;
snd_play (lo_wav, 50, 0);
while (two_ent.x > 100)
{
two_ent.x -= 50 * time;
wait (1);
}
two_ent.visible
= off;
three_ent.visible = on;
snd_play (high_wav,
100, 0);
while (three_ent.x > 100)
{
three_ent.x -= 150 * time;
wait (1);
}
race_started = 1;
three_ent.visible = off;
}

La fonction ci-dessus démarre par elle même lorsque nous démarrons le fichier script. La première ligne de code nous laisse 2 secondes pour admirer la vue :) puis nous rendons la première figure visible (one_ent). J'ai utilisé 3 définitions d'entité et 3 sprites pour les 3 chiffres qui apparaîssent au départ du jeu. Chaque entité (chiffre) est déplacée devant la caméra (son x est diminué dans une boucle "while") jusqu'à ce qui soit vraiment proche de la caméra; nous jouons un son puis nous cachons l'ancien chiffre pour faire appara^tre le suivant. Dès que le "3" est à moins de 100 quants de la caméra, race_started est mis à 1 et "3" est rendu invisible. Toutes les voitures (comprenant celle du joueur) ont une boucle "while" qui attend que race_started soit mis à 1, aussi elles démarrent leur moteur dés que cela se produit.

Si vous ave zlu l'article du Aum33, vous savez que chaque voiture conduite par l'ordinateur utilise un chemin pour se déplacer. Cette méthode présente plusieurs avantages, mais elle peut également mener à des simulations de conduite vite ennuyeuses; votre client peut découvrir que les voitures utilisent toujours le même chemin et il pourrait remarquer, par exemple, que la voiture verte ne prend jamais l'intérieur de la piste. Ne vous inquiétez pas, ce mois-ci les voitures utilisent en permanence un chemin aléatoire. Vous serez surpris d'apprendre que je n'utilise aucune valeur aléatoire et pourtant les chemins sont complétement aléatoires.

J'ai utilisé une définition de tableau qui ressemble à ceci :

var paths[12] = 3, 1, 2, 1, 3, 2, 3, 2, 1, 1, 3, 2;

J'ai utilisé des couleurs pour les différents jeux d valeurs pour que cela soit plus compréhensible pour vous. Les voitures démarrent avec avec le chemin par défaut donné par le bloc de variables ci-dessous:

var
car1_path = 1;
var car2_path = 2;
var car3_path = 3;

Regardons comment on choisit un chemin al"atoire à chaque tour. Chaque voiture IA utilise un code comme celui-ci:

if
(waypoint_number == 66)
{
pick_new_path();
}

Dés qu'une voiture approche de la fin d'un tour (waypoint_number = 66), il exécute la fonction pick_new_path, qui choisit un nouveau chemin pour la voiture. Les différents résultats de la fonction se trouvent ci-dessous.

function
pick_new_path()
{
if (my.skill40 == 1) {return;} // la voiture
a déjà choisi son chemin
my.skill40 = 1;
index += 1; // avance encore d'une case dans le tableau
index %= 12; // limite l'index aux 12 premiers (0..11) éléments
dans le tableau
if (my.skill1 == 1)
{
car1_path = paths[index];
}
if (my.skill1 == 2)
{
car2_path = paths[index];
}
if (my.skill1 ==
3)
{
car3_path = paths[index];
}
if (paths[index] == 1)
{
ent_path("car01");
}
if (paths[index] == 2)
{
ent_path("car01");
}
if (paths[index] == 3)
{
ent_path("car03");
}
sleep (10); // wait for 10 seconds
my.skill40 = 0; // la voiture
peut choisir de nouveau un chemin aléatoire (waypoint_number = 66 pendant
plusieurs cycles)
}

Vous devez me croire lorsque je vous dit que n'importe quelle voiture a son waypoint_number de mis à 66 pendant plusieurs cycles. Cela se produit parce que la vitesse de la voiture n'est pas extrêmement grande et que la distance entre deux noueuds consécutifs du chemin n'est pas extrêment petite. :) Nos voitures ne doivent changer de chemin qu'une fois par tour, c'est pourquoi nous commençons par mettre skill40 à 1, ce qui nous permet de rejeter toutes les autres fonctions appelées si skill40 est mis à 1. Nous augmentons l'index, nous limitons sa valeur à 11 car notre tableau de "chemins" contient 12 éléments (0 à 11) puis nous prenons le numéro du chemin depuis "paths" pour la voiture qui a appelée la fonction. Permettez moi de vous rappeler que la première voiture a son skill1 de mis à 1 dans Wed, la deuxième a son skill1 de mis à 2, et la troisième a son skill1 de mis à 3. L'embranchement "if" suivant assigne un des 3 chemins (car01, car02 ou car03) à la voiture, en fonction de la valeur qui a été lue à partir de "paths". Puis nous attendons 10 secondes (pour permettre à waypoint_number d'avoir à présent une valeur différente) puis nous remettons skill40 à zéro, permettant de nouveau l'appel de cette fonction au prochain tour.

Peut-être vous vous demandez comment s'effectue ce travail d'aspect aléatoire ; pourquoi est-ce que j'ai dit que les chemins sont aléatoires, quand je lis leurs valeurs d'un tableau prédéfini ? Bien, le truc ici est que nous ne connaissons pas quelle voiture est la gagnante. Les voitures emploient des valeurs aléatoires de vitesse, à chaque tour, aussi n'importe quelle voiture pourrait sélectionner le deuxième chemin, par exemple. Tout qce ue vous devez faire, c'est de vous assurer que chaque triplet des valeurs à l'intérieur du tableau inclut "1", "2" et "3" parce que vous ne voudrez pas que vos voitures se cognent l'une dans l'autre trop souvent.

panel
race_pan
{
bmap = race_pcx;
pos_x = 0;
pos_y = 0;
layer = 10;

   digits = 323, 18, 1, system_font, 1, car1_path; // the path
used by the first car
digits = 510, 18, 1, system_font, 1, car2_path;
// the path used by the second car
digits = 703, 18, 1, system_font,
1, car3_path; // the path used by the third car

   digits = 100, 18, 3, system_font, 1, players_speed; // the
speed of the 1st car
digits = 280, 18, 3, system_font, 1, car1_speed;
// the speed of the 2nd car
digits = 470, 18, 3, system_font,
1, car2_speed; // the speed of the 3rd car
digits = 660, 18, 3,
system_font, 1, car3_speed; // the speed of the player

digits = 64, 18, 3, system_font, 0.01, player.distance; // the distance covered by player's car
digits = 244, 18, 3, system_font, 0.01, car1.distance; // the distance covered by the 1st car
digits = 434, 18, 3, system_font, 0.01, car2.distance; // the distance covered by the 2nd car
digits = 624, 18, 3, system_font, 0.01, car3.distance; // the distance covered by the 3rd car

flags = overlay, refresh, visible;
}

Vous pouvez voir quele tableau utilisé dans la démo n'est pas très compliqué, il inclut des "digits" pour les chemins, les vitesses et les distanc"es couvertes par les voitures. Les chiffres pour les vitesses ne paraissent pas très bons : 20 quants par cyccle n'est pas vraiment ce que vous aimeriez voir affiché sur votre compteur de vitesse, aussi j'ai écrit une petite fonction qui convertit la vitesse en quelque chose de plus décent.

starter
compute_speeds() // just makes the speeds look good, really
{
while ((car1 == null) || (car2 == null) || (car3 == null) || (player == null))
{wait (1);}
while (1)
{
car1_speed = car1.car_speed * 6; // 6 = experimental value (same read on the panel
and on the speedo)
car2_speed = car2.car_speed
* 6;
car3_speed = car3.car_speed * 6;
players_speed = player.skill2 * 6; // player.skill2 =
player's "car_speed"
wait (1);
}
}

Cette fonction attend que les voitures et le joueur soient créés puis multiplie les vitesses en quants / frame par 6 (c'est une valeur expérimentale); ce qui permet d'avoir la même valeur sur le tableau et le compteur de vitesse. Le dernier jeu de chiffres affiche les distances (multipliées par 0.01) couvertes par les voitures, c'est valeurs sont stockées dans skill25 en tant que "distance".

Cet article termine la série des voitures avec IA. Cette démo n'est pas une tueuse de NFS, mais elle vous montre comment concevoir un jeu de course d'arcade à partir de zéro. Le point faible de cette démo est le système de collision car les voitures peuvent s'entre-pénétrer. Je sais que les utilisateurs de la version A6 seront heureux d'apprendre que le nouveau système de collison est normalement prévue pour la version A6.3 (sous réserves) mais que les possesseurs dela version A5 devront faire avec ce problème.

Attendez, je crois que j'ai trouvé une solution pour les possesseurs de la version A5 également! Pourquoi ne pas construire votre voiture en utilisant une entité wmb? Essayez cette méthode et faite partager votre expérience avec le reste de la communauté sur les différents forums. Bonne chance!


Untitled

New car

Je ne me souviens pas combien de vous ont demandé ce morceau de code sur le forum - j'ai décidé de créé le wdl pour cela. Ce que nous allons essayer de faire ici : déplacer le joueur en mode 1ère personne -> touche un modèle de voiture -> monte dans la voiture et la conduit en mode 1ère personne -> appuyez sur espace et vous descendez de la voiture et continuez à marcher..

Cela peut vous paraître une tâche difficile, mais pas du tout; tout ce que nous avons à faire et de basculer les fonctions player_to_car et car_to_player. Nous plaçons un modèle de voiture dans le niveau et lui attachons l'action suivante:

action my_car
{
my.enable_impact = on;
my.event = player_to_car;
}

Vous pouvez voir que la voiture réagit lorsqu'on la touche; voyons ce qui se passe :

function
player_to_car()
{
wait (1);
ent_remove
(me);
player.shadow = off;
player._MOVEMODE
= _MODE_DRIVING;
player._FORCE = 1.5;
player._BANKING = 0.1;
player.__SLOPES = on;
player.__WHEELS = on; // tourne uniquement si en mouvement
player.__JUMP = off; // la voiture ne peut pas sauter (ce n'est pas une kangou
:) )
player.__STRAFE = off; // can't strafe
player.__TRIGGER = on;
astrength.pan = 1.5; // réduit
la vitesse de rotation(pan)
car_entity.visible = on;
while (key_space == 0) {wait (1);} // appuyez sur espace pour
passer de conducteur à marcheur
temp.x = player.x
- 100 * sin(player.pan); // vérifie s'il y a un espace de libre (près
de la porte de la voiture) pour que le joueur puisse se déployer
temp.y = player.y + 100 * cos(player.pan) ;
temp.z = player.z;
ent_create (car_mdl, player.pos, create_car);
vec_set (player.pos,
temp);
car_entity.visible = off;
car_to_player();
}

entity car_entity
{
type = <rallycar.mdl>;
layer = 10;
view = camera;
x = 20;
y = 0;
z = -60;
}

La voiture est enlevée quand le joueur la cogne et le joueur est transformé en voiture (vous verrez la plupart de ces paramètres dans l'action player_drive). J'ai défini un car_entity qui regarde exactement comme la voiture qui a disparu et je la rends visible; maintenant le joueur peut conduire tant qu'il veut. Si nous appuyons dur espace, le joueur est déplacé près de la porte et le modèle de voiture est créé de nouveau :

function create_car()
{
wait (1);
vec_set(temp, my.x);
my.pan
= player.pan;
temp.z -= 2000; // trace 2000 quants below
the car
trace_mode = ignore_me + ignore_passable + ignore_models
+ ignore_sprites;
my.z -= trace (my.x, temp); // place
la voiture sur le sol
my_car(); //re-démarre l'action
}

J'utilise "trace" pour être sur que la voiture est placée sur le sol; action my_car est appelé de nouveau.

Encore un petit détail. Nous demandons au joueur de se comporter comme un humain et non comme une voiture, c'est fait avec ::

function
car_to_player()
{
player._MOVEMODE = _MODE_WALKING;
player._FORCE = 0.75;
player._BANKING = -0.1;
player.__SLOPES = off;
player.__WHEELS = off;
player.__JUMP = on;
player.__DUCK = on;
player.__STRAFE = on;
player.__BOB = on;
player.__TRIGGER = on;
astrength.pan = 7; // remets la
valeur originale dans move.wdl
}

Voilà un petit bout de code qui fait parfaitement ce que nous lui avons demandé. Bien entendu il peut être amélioré et je compte sur vous pour cela.


Untitled

Traduction Olivier Burton - correction AB

Yeux laser

Cet article vous apprendra comment créer des rayons de particules et comment les attacher aux yeux de vos modèles.

Je sais que je ne voudrais pas rencontrer ce type sur une allée sombre... Je parle du petit type rouge, bien sûr :)

Le modèle joue son animation d'attente ("stand") et ses deux rayons laser viennent continuellement de son mouvement d' yeux quand il tourne la tête.

action
night_monster
{
while (1)
{
ray_size = 2;
ray_width = 10;
ent_cycle("stand", my.skill1); // joue les frames de l' animation "stand"
my.skill1 += 0.4 * time;
// vitesse de l' animation
my.skill1 %= 100; // boucle de l' animation
vec_for_vertex (left_eye,
my, 235); // obtient les coords de vertes pour l' oeil gauche
vec_for_vertex (right_eye, my, 247); // obtient les coords de vertes pour l' oeil
droit
effect (attach_rays,
1, left_eye, normal);
effect (attach_rays, 1, right_eye, normal);
while (ray_size > 0)
{
vec_for_normal (temp_left, my, 235);
vec_for_normal (temp_right, my, 247);
vec_normalize (temp_left, ray_size);
vec_normalize (temp_right, ray_size);
vec_add (left_eye, temp_left);
vec_add (right_eye, temp_right);
effect (attach_rays, 1, left_eye, normal);
effect (attach_rays, 1, right_eye, normal);
ray_size -= 0.01;
}
wait (1);
}
}

Le modèle joue son animation d'attente ("stand") dans une boucle. Nous obtenons les coordonnées de vertex pour l' oeil gauche (vertex #235) dans left_eye et nous stockons les coordonnées pour l' oeil droit (vertex #247) dans right_eye. Nous créons deux fusées de signalisation en employant left_eye et right_eye comme leurs points de départ. Ceux de vous qui n' ont pas besoin de plus d' une paire d' yeux rougeoyant doivent mettre en commentaire la boucle while comme dans le code ci-dessous :

action
night_monster
{
while (1)
{
ray_size = 2;
ray_width = 10;
ent_cycle("stand", my.skill1); // joue les frames de l' animation "stand"
my.skill1 += 0.4 * time;
// vitesse de l' animation
my.skill1 %= 100; // boucle de l' animation
vec_for_vertex (left_eye,
my, 235); // obtient les coords de vertes pour l' oeil gauche
vec_for_vertex (right_eye, my, 247); // obtient les coords de vertes pour l' oeil
droit
effect (attach_rays,
1, left_eye, normal);
effect (attach_rays, 1, right_eye, normal);
//  while
(ray_size > 0)
//  {
//
vec_for_normal (temp_left, my, 235);
//       vec_for_normal (temp_right, my, 247);
//
vec_normalize (temp_left, ray_size);
//       vec_normalize (temp_right, ray_size);
//
vec_add (left_eye, temp_left);
//       vec_add (right_eye, temp_right);
//
effect (attach_rays, 1, left_eye, normal);
//       effect (attach_rays, 1, right_eye, normal);
//
ray_size -= 0.01;
//
}
wait (1);
}
}

Et maintenant, pour le reste d' entre nous qui voulons employer le plein effet jetons un autre regard dans la boucle while :

while (ray_size > 0)
{
vec_for_normal (temp_left, my, 235);
vec_for_normal (temp_right, my, 247);
vec_normalize (temp_left, ray_size);
vec_normalize (temp_right, ray_size);
vec_add (left_eye, temp_left);
vec_add (right_eye, temp_right);
effect (attach_rays, 1, left_eye, normal);
effect (attach_rays, 1, right_eye, normal);
ray_size -= 0.01;
}

Nous obtenons les normales résultantes pour les visages qui entourent l' oeil gauche et l' oeil droit, nous mettons leur longueur à ray_size et nous les ajoutons à l' original left_eye et right_eye, produisant des particules tant que ray_size est supèrieur à 0. Voyons une image qui explique ce qui arrive :

J' ai employé moins de particules dans cet exemple pour rendre les choses plus faciles pour vous; la première particule est créée quand ray_size à 2, la deuxième particule après la première soustraction à l' intérieur de la boucle while (ray_size à 1.99) etc. Le processus s' arrête quand ray_size = 0.

Et maintenant voyons les fonctions associées aux particules :

function
attach_rays()
{
my.bmap = flare_pcx;
my.flare = on;
my.bright = on;
my.size = ray_width;
my.function = remove_flares;
ray_width -= 0.01;
}

function
remove_flares()
{
my.lifespan = 0; // enlève la particule d' éclat
}

Les particules vivent pendant la durée d' un simple cycle d'affichage ; la seule chose qui doit être mentionnée ici est que la taille des particules est variable; les premières particules (celles qui sont loin des yeux) sont les plus grandes tandis que les particules postérieures (placées près de l' oeil) deviennent de plus en plus petites. Cet effet est réalisé en diminuant ray_width avec 0.01 pour chaque particule qui exécute la fonction. Ray_width est la variable qui contrôle la taille des particules donc c' est tout que nous devons faire pour réaliser l' effet ci-dessous.

Je vous encourage à jouer avec les valeurs initiales pour ray_size, ray_width et à changer le bitmap si vous voulez créer quelques effets intéressants. N' oubliez pas d' éditer la valeur numérique à l' intérieur des boucles while (0.01 dans mon exemple).

Ces rayons laser ne peuvent tuer personne mais à votre place je les combinerais avec mon "cône de vision" le code est dans Aum28 (pour employer un cône étroit pour chaque oeil). Mettez enable_scan = on pour le joueur et quand l' événement est déclenché blesse le joueur. C' est ce que je ferais pour créer un bon monstre de boss pour mon jeu!


Untitled

"Achetez maintenant!"

Votre jeu est terminé et vous avez mis une belle démo en téléchargement sur votre site. Mais n'avez-vous rien oublié? Et bien si, vous avez oublié d'ajouter un bouton dans votre démo pour que l'utilisateur puisse acheter votre produit..

Ce sera une des rares situations où vous aurez besoin d'utiliser le sdk; ne soyez pas désolé, j'ai inclu la dll, aussi vous aurez juste une ligne de code à ajouter si vous ne voulez pas utiliser le sdk. Si vous voulez savoir comment fonctionne le sdk repportez vous à l'aum 31.

Jetons un coup d'oeil au fichier cpp:

// buynow.cpp : Defines the entry point for the DLL application.
//

#include "stdafx.h"
#include "a5dll.h"
#include
"a5funcs.h"

int nSuccess;

BOOL APIENTRY DllMain( HANDLE hModule, DWORD ul_reason_for_call, LPVOID
lpReserved)
{
return TRUE;
}

DLLFUNC fixed Exequte(long command)
{
fixed *temp =
(fixed *)a5dll_getwdlobj("temp");
A4_STRING *my_command = (A4_STRING
*)command;
nSuccess = WinExec (my_command -> chars, SW_MAXIMIZE);
return INT2FIX(nSuccess);
}

Les lignes rouges sont ajoutées par moi, le reste du code est ajouté par l'assistant MS VC++ 6.0 . Ma fonction "Exequte" prend la variable prédéfinie "temp" (lisez Aum31 pour voir pourquoi nous avons besoin de tout cela) puis prend un pointeur nommé my_command à la chaîne qui est passée à la fonction. Puis nous utilisons WinExec pour exécuter la chaîne (programme + arguments) qui est passée à Exequte, retournant le résultat de l'opération. La variable nommée nSuccess retournera une valeur supérieure à 31 uniquement si WinExec s'est bien passé.

Certains programmes avancés voudront utiliser CreateProcess parce que WinExec est obsolète, mais il fait très bien son boulot ici (c'est un secret que vous n'avez pas besoin de connaître) WinExec appelle CreateProcess. Regardons le contenu du script à présent:

var success = 0;
var_nsave dll_handle;

text error_txt
{
pos_x = 200;
pos_y
= 200;
layer = 30;
font = std_font;
string = "Error while trying to run the command!";
}

panel outro_pan // exit panel
{
bmap = outro_tga;
layer = 20;
pos_x = 120;
pos_y = 100;
button = 133, 100, buyme2_tga, buyme1_tga, buyme2_tga, buy_now, null, null;
button = 133, 180, nothanks2_tga, nothanks1_tga, nothanks2_tga, shut_down, null,
null;
flags = overlay, refresh, visible;
}

J'ai défini un simple texte qui affichera un message d'erreur si WinExec n'aboutit pas. "Achetez maintenant!" est un bouton clasique sur un panneau et il doit exécuter la fonction buy_now lorsqu'il est pressé:

function buy_now()
{
dll_handle = dll_open ("buynow.dll");
success = Exequte("C:\\Program Files\\Internet Explorer\\iexplore.exe
www.randombyte.com");
if (success <= 31) // WinExec failed?
{
error_txt.visible = on; // show an error
message
}
dll_close (dll_handle);
}

function shut_down()
{
exit;
}

La fonction ci-dessus ouvre la dll, et exécute Internet Explorer avec mon site donné en paramètre. Notez la notation C-Style "\\" pour le chemin. Si WinExec est en erreur (success <= 31) nous affichons le message d'erreur. Si l'utilisateur presse le bouton "non merci!", la fonction shut_down arrêtera le moteur.

C'est tout. Vous pouvez rendre outro_pan visible seulement lorsque le client potentiel essaye de fermer le jeu. Autre chose, vous pouvez utiliser cette dll pour exécuter n'importe quelle autre application àà laquelle vous pouvez penser. laissez moi essayer ceci...

success = Exequte("C:\\Program Files\\Microsoft Office\\Office10\\winword.exe office.wdl");

Ca marche! Cette ligne démarre Microsoft Word et ouvre le fichier existant office.wdl pour être édité.


Untitled

Arc et flèche

Cette fois je vais vous montrer toutes les étapes qui sont nécessaires pour créer le code pour un arc et des flèches; notez s'il vous plaît que cet exemple peut être employé pour créer (presque) n'importe quel type d'arme et ses balles.

Avant toute chose nous définissons une entité qui sera utilisée pour l'arc :

entity bow_entity

{

type = <bow.mdl>;

layer = 10;

view = camera;

x = 30;

y = 8;

z = -10;

}

Nous utilisons le même modèle (bow.mdl) pour l'arc qui est placé dans notre niveau dans Wed. L'arc tourne s'il n'a pas été pris jusque là; lorsque le joueur touche l'arc, l'arc est enlevé et bow_entity est rendu visible. Bow_entity.frame = 2montre la 2ème frame qui contient l'arc et les flèches.

action bow

{

my.enable_impact = me;

my.event = get_bow;

while (my != null) // or while (my)

{

my.pan += 3 * time;

wait (1);

}

}

function get_bow()

{

wait (1);

ent_remove (me);

bow_entity.frame = 2;

bow_entity.visible = on;

}

Nous appelons la fonction init_bowarrow() dans main:

function init_bowarrow()

{

while (1)

{

if ((mouse_left == 1 || key_ctrl == 1) && bow_entity.frame < 7)

{

bow_entity.frame += 2 * time;

}

else

{

if (bow_entity.frame >= 7 && arrow_fired == 0)

{

while (mouse_left == 1 || key_ctrl == 1) {wait (1);}

vec_set (temp, player.pos);

temp.z += 20;

ent_create (arrow_mdl, temp, move_arrow);

arrow_fired = 1; // une simple flèche

}

else // tle joueur ne tire pas

{

arrow_fired = 0; // enable firing

bow_entity.frame = 2;

}

}

wait (1);

}

}

Si nous pressons le bouton gauche de la souris ou la touche Ctrl,nous animons bow_entity en changeant ses paramètres de frame. Si bow_entity.frame >= 7, nous attendons que le bouton gauche de la souris ou la touche Ctrl soit relachée (nous devons faire cela pour éviter le mode autofire - tir à répétition) et nous créons une flèche 20 quants au dessus de l'origine du joueur. J'ai choisi cette valeur parce qu'elle faisait plus vraie avec ma combinaison joueur + arc + flèche; vous devez bien entendu jouez avec cette valeur en fonction de votre modèle. Si le joueur ne tire pas de nouveau, nous validons le tir et mettons de nouveau la frame de bow_entity à 2.

La fonction associée à la flèche est :

function move_arrow()

{

wait (1);

my.enable_entity = on;

my.enable_block = on;

my.event = hurt_you;

my.passable = on;

my.pan = camera.pan;

my.tilt = camera.tilt;

my.skill12 = 50; // skill12, 13, 14 act like a var

my.skill13 = 0;

my.skill14 = 0;

my.skill12 *= time;

while (my.skill12 != 0) // se déplace jusqu'à renconter quelque chose

{

if (vec_dist (my.x, player.x) < 100) {my.passable = on;}

// pas de collision avec le joueur

else {my.passable = off;}

ent_move (my.skill12, nullvector);

wait (1);

}

}

La flèche est sensible aux entités et aux blocs du niveau; si elle frappe un bloc elle y restera et si elle frappe une entité elle disparaitra. J'utilise skill12, 13, 14 comme de simples variables nommées skill12;J'ai besoin d'être sûr que la première variable soit divisible par 3 (aussi je peux utiliser skill3, 6, 9, 12, 15, etc). Si la distance entre le joueur et la flèche est plus petite que 100 quants, la flèche est passable pour éviter qu'elle frappe son créateur.

La dernière fonction est :

function hurt_you()

{

wait (1);

if (event_type == event_entity)

{

my.skill12 = 0; // arrête le déplacement

you._HEALTH -= 80; // dommage = 80;

ent_remove (me);

}

if (event_type == event_block)

{

my.skill12 = 0; // arrête la flèche

my.passable = on; // elle ne doit pas nous empêcher de passer

}

}

Si la flèche frappe une entité elle cause 80 points de dommage. Si c'est un arbre cela n'a aucune importance mais si c'est un ennemi, robot2 dans mon exemple vous allez le voir déguerpir après le premier coup.

Un petit truc que j'utilise ici: j'ai utilisé le même modèle pour bow_entity et pour l'arc que j'ai placé dans Wed de cette façon j'ai une flèche et une main d'attachées tout le temps. Bien entendu nous ne voulons pas voir l'arc, la flèche et la main qui lui sont attachées lorsqu'il attend d'être pris. Le modèle doit avoir le même nombre de triangles à chaque frame, aussi j'ai décidé de cacher la main et la flèche dans l'arc (beaucoup plus petits bien entendu mais ils sont là). J'utilise la frame un pour l'arc en attente d'être pris et les frames 2..7 pour les animations arc + flèche + main..

Quel rapport avec ces cris perçants dont je parle dans le sommaire?

Je plaisantais mais vous pouvez facilement les ajouter avec une instruction ent_playsound placée dans le code du robot2..


Untitled

Caractéristique du mois: aspect

Acknex inclut un nombre de grandes caractéristiques qui ne sont pas exploitées correctement; ce mois je veux vous montrer l'une d'elle, ce que fait "aspect".

Utilisation pratique: inverser l'écran tête-bèche (utile pour des jeux d'espace), créer des sorts impressionnants et plus encore!

Le mot clé "aspect" détermine le ration de la taille verticale par rapport à la taille horizontale en pixels. En d'autres mots si vous placer un modèle carré sur votre écran pour pourrez faire qu'il devienne rectangle en utilisant son "aspect". J'ai créé deux fonctions simples qui utilsent cette grande particularité du moteur:

function flip_level()
{
while (camera.aspect > -1)
{
camera.aspect -=
0.1 * time;
wait (1);
}
}

function
grow_level()
{
while (camera.aspect < 5)
{
camera.aspect +=
0.2 * time;
wait (1);
}
}

on_f = flip_level;
on_g = grow_level;

La première fonction s'exécute lorsqu'on presse la touche "F" ; il diminuera camera.aspect de 1 (sa valeur normale au démarrage du jeu) à -1. Lorsque camera.aspect = -1, l'image est inversée tête-bèche.

Laseconde fonction s'exécute lorsque nous pressons la touche "G". Elle augmentera camera.aspect de 1 à 5, changeant l'aspect du niveau : chaque pixel en x correspond à 5 pixels en y. Regardez comment mes rochers sont magnifiques avec aspect = 5! Je peux utiliser cet effet pour une potion magique par exemple, etc.


Untitled

Bain de sang

Nous allons faire en sorte que votre ennemi perde son sang lorsqu'il meurt. J'ai essayé de faire le code aussi simple que possible:

define number_of_hits skill1;

action
enemy_blood
{
var blood_coords; // local var
my.enable_entity = on;
my.enable_impact = on;
my.event = enemy_hit;
my.number_of_hits = 0;
while(my.number_of_hits == 0) // standing
{
ent_cycle("stand",
my.skill20); // play "stand" animation
my.skill20 += 3
* time;
my.skill20 %= 100;
// loop
wait (1);
}

J'ai définiune variable locale nommée blood_coords. Je sais que vous êtes bien trop timide pour me demander ce que c'est mais je expliquer ici ce que sont les variables globales et les variables locales. Une variable globale est définie en dehors de toute action et définition - elle n'est pas enfermée par des accolades.. Une variable globale peut être accédée par n'importe quelle action ou fonction. Il est bon de savoir que vous pouvez taper ammo1 += 100 à la console et donner 100 extra balles pour les armes qui utilisent ammo1, n'est-ce past? Une variable locale est définie exactement comme une variable globale mais sa défnition est placée à l'intérieur d'une action ou d'une fonction, comme celle dans l'action enemy_blood. Une variable locale est connue seulement à l'intérieur de l'action / function qui l'a défini. Si vous voulez taper blood_coords.x = 100; à la console cela ne fonctionnera pas! Les variables locales sont comme les variables d'entités (skill) : vous pouvez les utiliser uniquement à l'intérieur de l'action ou de la fonction qui l'a défini. Revenons à présent à nos moutons...La variable locale nommée blood_coords sera mise à l'origine de la tâche de sang. J'aurai pu bien entendu utiliser une variable globale mais si vous voulez attacher la même action à plusieurs modèles, chaque modèle changerait la valeur de blood_coords parce que cette variable serait connue de toutes les actions et fonctions. Une variable locale blood_coords est reconnue uniquement à l'intérieur de sa propre action ou définition aussi la valeur blood_coords pour un ennemi donné ne peut interférer sur les autres valeurs blood_coords value, utilisées par les autres ennemis.

L'ennemi est sensible à l'impact; tire avec une roquette pour déclencher son évènement enemy_hit. J'ai utilisé number_of_hits comme nom pour skill1; tant que number_of_hits est à zéro, l'ennemi joue son animation "stand" dans une boucle.

// the enemy was hit by a rocket - it must die
my.skill20 = 0;
while(my.skill20 < 60) // one shot animation, 60% of "death" because the last "death" frame is damaged - open the model in Med to see for yourself
{
ent_cycle("death", my.skill20); // play "death" animation
my.skill20 += 2 * time;
wait (1);
}
my.event = null; // don't react to other events from now on
// the enemy is dead here - time to create the blood pool
vec_set(blood_coords, my.x);
blood_coords.z -= 500;
trace_mode = ignore_me + ignore_models + ignore_sprites;
blood_coords.z = my.z - trace (my.x, blood_coords) + 2; // place the blood sprite 2 quants above the ground
ent_create(blood_pcx, blood_coords, create_blood);
}

Lorsque number_of_hits est plus grand que zéro, l'ennemi est mort; nous jouons son animation de mort (uniquement 60% de ses frames de mort car la dernière frame est endommagé, vous pouvez le vérifier dans Med). Nous mettons event à null, nous traçons 500 quants sous l'ennemi pour obtenir la position du sol, nous mettons blood_coords.z 2 quants au-dessus le sol et nous créons le sprite sang en utilisant la fonction create_blood:

function
create_blood()
{
my.passable = on;
my.oriented = on;
my.tilt = 90;
my.flare = on;
my.ambient = 100;
my.bright = on;
while (my.scale_x < 5)
{
my.scale_x += 0.05
* time;
my.scale_y = my.scale_x;
wait (1);
}
}

Le sprite sang (blood) est passable, orienté et ses drapeaux flare et bright sont mis. Le sprite augmentera ses dimensions x et y 5 fois.

La fonction qui déclenche toute cette chose effrayante est :

function
enemy_hit()
{
if (event_type == event_entity || event_type == event_impact)
{
if (you == player)
// collided with the player?
{
return; // do nothing
}
else // hit by
a rocket
{
my.number_of_hits += 1;
}
}
// add other events here
}

Si l'ennemi cogne une autre entité, il vérifie pour voir si cette entité est le joueur. Si l'ennemi a été touché par une roquette, number_of_hits sera augmenté jusqu'à ce que l'ennemi meurt.


Untitled

Balles énergétiques

Il y a des centaines d'années de cela, un magicien maléfique vivait dans une grotte. Pour survivre il utilisait des balles énergétiques toute la journée mais après quelques années ces balles perdirent leur pouvoir et le magicien mourut. C'était il y a très longtemps...attendez, ne perdez pas espoir, j'ai retrouvé cette grotte et la formule magique était inscrite sur un mur. OK, c'était écrit en ancien code wdl, mais j'ai dépoussiéré le code, remis au gout du jour et voici ce que ça donne à présent :)

action wizard
{
clip_size
= 0;
while (1)
{
while (my.skill10 <
95)
{
ent_cycle
("attack", my.skill10); // play the attack animation frames
my.skill10
+= 3 * time;
wait (1);
}
eball_pos.x = my.x;
eball_pos.y = my.y;
eball_pos.z = my.z
+ 25; // the energy ball appears betwen wizard's hands
ent_create (energyball_mdl, eball_pos,
energy_ball);
snd_play (energy_snd,
70, 0);
waitt (16);
while (my.skill10 >
2)
{
ent_cycle
("attack", my.skill10); // play the attack animation frames
my.skill10
-= 2 * time;
wait (1);
}
waitt (32);
}
}

La première ligne de l'action demande au moteur de dessiner tous les triangles de tous les modèles first line in the action tells the engine to draw all the triangles for all the models. Le magicien bouge ses mains ("attack"); la balle énergétique est créée 25 quants au dessus de l'origine du magicien. Le magicien bouge ses mains de nouveau (cette fois ci c'est l'animation "attack" inversée) et on répète ceci toutes les 2 secondes.

function
energy_ball()
{
wait (1);
my.enable_entity = on;
my.enable_block = on;
my.event = remove_eball;
my.pan = you.pan;
my.tilt = you.tilt;
my.lightred = 250;
my.lightgreen = 150;
my.lightrange = 200;
eball_speed.x = 20;
eball_speed.y = 0;
eball_speed.z = 0;
eball_speed *= time;
while (my != null)
{
my.roll += 20 * time;
move_mode = ignore_you
+ ignore_passents;
ent_move (eball_speed, nullvector);
wait (1);
}
}

function
remove_eball()
{
wait (1);
if (you == player)
{
player._health -= 60; // decrease player's
health
}
ent_remove
(me);
}

La balle est sensible aux entités et aux blocs du niveau. Si la balle vient à entrer en collision avec une entité (le joueur, un ascenseur etc) ou avec un bloc de niveau (mur par exemple) son évènement remove_eball est exécuté. La balle a les mêmes angles pan et tilt que son créateur (le magicien) et génère une lumière orange dans un rayon de 200 quantst. Aussi longtemps que la balle existe, elle se déplace à la viitesse donnée par eball_speed.x et tourne en changeant son angle roll.

Si la balle entre en collision avec quelque chose, on vérifie si c'est avec le joueur. Si c'est le cas, le joueur perd 60 points de santé. A la fin de la fonction évènement, la balle est enlevée.


Untitled

Balle bondissante

Si vous aimez les différentes copies de jeux de billes alors vous allez pouvoir dès à présentcréer votre propre clone. Nous allons créer le code pour une balle qui sera controlée avec les flèches de direction et qui rebondira tout le temps. Je ne vais pas du tout utiliser le moteur de physique pour cela aussi le code fonctionnera avec toutes les éditions du moteur Acknex.

function
main()
{
fps_max = 60;
level_load (level1_wmb);
wait (3);
while (1)
{
camera.x = ball.x - 200;
camera.y = ball.y;
camera.z = 100;
camera.tilt
= -45;
wait (1);
}
}

A chaque fois que vous voyez une fonction main() dans un des cripts AUM vous savez qu'il s'agit d'un projet autonome, n'est-ce pas? Ce qui signifie que vous pouvez ajouter d'autres fonctionnalités au code existant et il devrait (je l'espère) continuer à fonctionner. Notre fonction limitele taux d'affichage à 60 fps, charge un simple niveau, attend qu'il soit chargé et met ses propres paramètres pour les angles et la position de la caméra. Notez que la caméra a une hauteur fixe (100 quants) et se tient 200 quants derrière la balle en permanence.

action
bouncing_ball
{
ball = my;
my.enable_block
= on;
my.enable_entity = on;
my.event = reverse_speed;
while (1)
{
if (key_cuu == on)
{
ball_speed.x = 5 * time;
my.tilt -= 8 * time;
}
if (key_cud == on)
{
ball_speed.x = -5 * time;
my.tilt += 8 * time;
}
if ((key_cuu == off) && (key_cud == off))
{
ball_speed.x =
0;
}
if
(key_cul == on)
{
ball_speed.y = 5 * time;
my.roll
+= 8 * time;
}
if (key_cur
== on)
{
ball_speed.y = -5 * time;
my.roll -=
8 * time;
}

Vous pouvez voir que j'ai donné au pointeur de la balle un nom très original (ball); elle est sensible aux blocs de niveau et aux entités, exécutant sa fonction reverse_speed si elle cogne quelque chose. Si nous pressons la flèche haute du clavier la balle se déplacera vers l'avant à la vitesse donnée par 5 * time et changera son angle tilt angle à la vitesse donnée par -8 * time car nous voulons donner l'illusion que la bille tourne. La même chose se produit sui vous pressez les autres flèches de direction sur le clavier bien entendu avec les bonnes vitesses et axes de rotation et déplacement.

     if ((key_cuu == off) && (key_cud == off))
{
ball_speed.x = 0;
}
if ((key_cul == off) && (key_cur
== off))
{
ball_speed.y
= 0;
}
vec_set (temp,
my.x);
temp.z -= 10000;
trace_mode = ignore_me + ignore_passable + use_box;
trace_dist
= trace(my.x, temp);
if (trace_dist > max_height)
// the ball has reached its maximum height?
{
ball_speed.z = -abs(ball_speed.z); // then give
the ball a negative speed
}
ent_move
(nullvector, ball_speed);
wait(1);
}
}

Si le joueur ne presse aucune touche de direction ball_speed.x et / ou ball_speed.y est remis à zéro; la balle doit s'arrêter si nous ne pressons aucune touche. Les lignes suivantes utilisent une instruction trace pour détecter le niveau du sol. J'ai décidé d'utiliser un petit truc en utilisant une boîte comme rayon (use_box) car je ne voulais pas gérer le faite que la bille cogne une arrête de la rampe ou une situation du même type.

Si trace_dist est plus grand que max_height (mis à 70 quants au début du fichier script) la balle a atteitn sa hauteur max aussi elle doit changer sa viteese de positive en négative parce que nous voulons que la bille tombe vers le sol à présent. J'ai choisi 70 quants pour la hauteur maximale par rapport au niveau du sol mais vous pouvez bien entendu chosir n'importe quelle autre valeur ici.

function
reverse_speed()
{
ball_speed.z *= -1;
}

La fonction évènement change simplement le signe de la vitesse (de positive à négative ou de négative à positive) au moment de l'impact.


Untitled

Bannière "commentaire" (Comment tags)

Maintenant voici ce que j'appelle une particularité sympathique! Vous, le programmeur, ajouez quelques lignes de code (principalement des commentaires) et le reste de l'équipe (vous encore?) peut éditer leur valeur, choisir différents sons, images etc.sans toucher une seule ligne de script.

Regardons le code qui crée une lampe clignotante:

define red_light skill1;
define green_light skill2;
define
blue_light skill3;
define light_range skill4;
define light_on skill5;
define
light_off skill6;

Rien de nouveau jusqu'ici? Nous avons donné de nouveaux noms aux six premiers skills. Continuons:

// entry: "light is on" sound
// help: This sound is played when the light is on
sound lighton_wav = <lighton.wav>;

Les lignes ci-dessus créent une nouvelle "entrée" qui vous permettront de choisir n'importe quel fichier son qui se trouve dans vitre répertoire, remplaçant le fichier "lighton.wav" par défaut. Mais regardons comment cette "entrée" apparaît et comment nous l'obtenons:

Regardons ce que chacune des lignes ci-dessous font:

// entry: "light is on" sound crée une nouvelle "entrée" pour notre fichier son et place le son "light is on" sous l'entrée. Vous pouvez créer des "entrées" pour des variables, des entités, des images, des sons et des chaînes.
// help: This sound is played when the light is on crée le fichier d'aide qui est affiché lorsque l'utilisateur clique sur le bouton d'aide "?" et clique sur la nouvelle entrée créée;
sound lighton_wav = <lighton.wav>; c'est votre définition de son habituelle, il met le son par défaut (lighton.wav).

Cliquez sur le bouton sur le côté droit de l'entrée et vous verrez une liste avec les sons qui peuvent être chargés, remplaçant le fichier son lighton.wav.

Examinons le reste du fichier script tags.wdl:

//
title: Flashing light
// image: flashing.pcx
// uses: red_light, green_light,
blue_light, light_range, light_on, light_off
// desc: Set color, range, on/off
intervals for any flashing light
action flashing_light
{
my.invisible = on;
my.passable = on;
if
(my.light_on < 0.1) {my.light_on = 0.1;}
if (my.light_off
< 0.1) {my.light_off = 0.1;}
my.red = my.red_light;
my.green = my.green_light;
my.blue = my.blue_light;
while (1)
{
snd_play
(lighton_wav, 50, 0);
my.lightrange = my.light_range;
sleep (my.light_on);
my.lightrange = 0;
sleep (my.light_off);
}
}

Cette partie de code change l'aspect de la fenêtre qui apparaît lorsque vous cliquez avec le bouton droit sur une entité et choisissez "Behavior".

Le premier commentaire donne le titre de la fenêtre. Le deuxième commentaire affiche l'image qui se trouve sous le titre. La ligne "uses" red_light, green_light etc affiche ces noms sur le panneau des comportements (behavior) remplaçant les affreux skill1... skill6. Vous pouvez éditer les valeurs et même les sauver. Le dernier commentaire décrit le texte qui apparaîtra sous l'image ou le texte qui apparaîtra lorsque vous choisisrez une action pour une entité donnée et vous pouvez choisir l'aide si vous ne savez pas ce que fait cette action.

Le code pour le clignotement de la lampe est rès simple; il cache l'entité, s'assure que light_on et light_off se termine bien à 0.1 et met les couleurs de la lumière. La boucle joue un son et bascule la surface d'éclairage de "Light_range" à zéro et vice versa créant ainsi l'effet de clognotement


Untitled

Bombe à retardement

Vous connaissez la petite maison dans le niveau office? Imaginez qu'il y ait une bombe sur un de ses murs, attendant avec angoisse que l'on vienne cliquer dessus ? Attendant avec angoisse qu'elle détruise quelque chose dans la zone où elle se trouve ? Quoi, vous avez déjà chargé mon niveau d'office et vous avez testé la bombe ? Mais c'est de la triche mec!

L'action attachée à timer.mdl est réellement simple:

action
timer
{
my.skin = 2;
my._DAMAGE = 200; // mettez
differentes valeurs à damage pour les différents timers
my.enable_click = on;
my.event = start_timer;
}

Vous pouvez voir que j'utilise un modèle avec 2 peaux; ses dommages sont mis à 200. La bombe à retardement est rendue sensible au clic de souris; si cet évènement se produit, sa fonction start_timer est exécutée:

function start_timer()
{
wait (1);
second_counter = 10; // explose
après 10 secondes
while (my != null)
{
second_counter
-= 1;
my.skin = 2;
ent_playsound (my,
timer_snd, 300);
waitt (14);
my.skin = 1;
waitt (2);
if (second_counter
< 1)
{
range = my._DAMAGE * 2;
damage = my._DAMAGE;
temp.pan = 360;
temp.tilt = 180;
temp.z = range;
indicator = _EXPLODE; // ce n'est pas un balayage amical
scan_entity (my.x, temp);
morph (explo_sprite, me);
my.oriented = on;
my.pan = 0;
while (my.frame < 7)
{
my.frame += 3 * time;
wait (1);
}
actor_explode(); // enlève le sprite de l'explosion
}
}
}

Second_counter contient le nombre de secondes avant le démarrage de l'explosion; Je l'ai mis à 10 secondes dans mon exemple. Aussi longtemps que la bombe n'a pas explosé (my != null) la bombe joue un son et change entre ses peaux(1 et 2) chaque seconde parceque waitt (14) + waitt (2) = waitt (16) = 1 seconde. Lorsque second_counter arrive à 0, la bombe explosera, s'autotransformant en un sprite orienté parce que le joueur a besoin de voir une superbe explosion. Le sprite explosion exécute toutes ses frames et il est enlevé.

Vous pouvez améliorer ce code de bombe à retardement en permettant au joueur de sélectionner le nombre de seconde sur un panneau par exemple etc etc.


Untitled

Machine à brouillard

J'aime tout simplement le brouillard. Il rend bien à l'écran et il est extrèmement pratique si vous créer de très grands niveaux. Mais comment met-on sa couleur et son "épaisseur"? Nous ouvrons le niveau dans Wed, nous choisissons File -> Map Properties -> Fog et nous donnons une couleur. Nous compilons le niveau, nous l'exécutons et nous n'aimons pas ce que nous voyons, nous devrions recommencer...Ce morceau de code vous permettra de mettre le brouillard pendant son exécution, cliquez quelques boutons et lorsque vous aimez le résultat notez simplement les valeurs et reportez-les dans votre script!

Let's take a better look at the panel that controls the fog:

Tout parait normal n'est-ce pas? Nous pouvons changer 5 valeurs (red = rouge, green = vert, blue = bleu, start = départ et end = fin) en utilisant 5 paires de boutons qui augmete ou diminue ces valeurs.using 5 pairs of buttons that increase or decrease the values. Cette interface simple mais pratique a été créée en utilisant un texte et un panneau:

text
fog_txt
{
font = terminal9_font;
pos_x = 10;
pos_y = 10;
string = "red (d3d_fogcolor1.red):\ngreen
(d3d_fogcolor1.green):\nblue (d3d_fogcolor1.blue):\nstart (camera.fog_start):\nend
(camera.fog_end):";
flags = d3d, visible;
}

Le
texte a une grande chaîne, il affiche red (d3d_fogcolor1.red):
puis \n (retour à la ligne) le déplace
à la ligne suivante. Le processus se répète et nous obtenons
5 lignes différentes de texte en utilisant une seule chaîne. Jetons
un oeil à la définition du panneau:

panel fog_pan
{
pos_x = 0;
pos_y = 0;
button = 190, 10, arrowup_pcx,
arrowup_pcx, arrowup_pcx, change_fog, null, null;
button = 202, 10, arrowdown_pcx,
arrowdown_pcx, arrowdown_pcx, change_fog, null, null;
button = 190, 22, arrowup_pcx,
arrowup_pcx, arrowup_pcx, change_fog, null, null;
button = 202, 22, arrowdown_pcx,
arrowdown_pcx, arrowdown_pcx, change_fog, null, null;
button = 190, 34, arrowup_pcx,
arrowup_pcx, arrowup_pcx, change_fog, null, null;
button = 202, 34, arrowdown_pcx,
arrowdown_pcx, arrowdown_pcx, change_fog, null, null;
button = 190, 46, arrowup_pcx,
arrowup_pcx, arrowup_pcx, change_fog, null, null;
button = 202, 46, arrowdown_pcx,
arrowdown_pcx, arrowdown_pcx, change_fog, null, null;
button = 190, 58, arrowup_pcx,
arrowup_pcx, arrowup_pcx, change_fog, null, null;
button = 202, 58, arrowdown_pcx,
arrowdown_pcx, arrowdown_pcx, change_fog, null, null;
digits = 155, 10, 3, terminal9_font,
1, d3d_fogcolor1.red;
digits = 155, 22, 3, terminal9_font, 1, d3d_fogcolor1.green;
digits = 155, 34, 3, terminal9_font, 1, d3d_fogcolor1.blue;
digits = 145, 46, 5, terminal9_font, 1, camera.fog_start;
digits = 145, 58, 5, terminal9_font, 1, camera.fog_end;
flags = overlay, refresh, d3d, visible;
}

Ce panneauparaît bien compliqué mais ce n'est pas le cas, sa définition crée:
- 10 boutons (5 paires) qui peuvent changer les 5 valeurs;
- 5 nombres, utilisés pour afficher les 5 valeurs du brouillard.

Vous pouvez voir que je n'utilise que 2 images bitmaps (arrowup_pcx et arrowdown_pcx) et tous les boutons utilisent la même fonction lorsqu'on les clique : change_fog.

function change_fog(button_number)
{
if (button_number == 1)
{
d3d_fogcolor1.red
= min(255, (d3d_fogcolor1.red + 5));
}
if (button_number == 2)
{
d3d_fogcolor1.red
= max(0, (d3d_fogcolor1.red - 5));
}
if (button_number == 3)
{
d3d_fogcolor1.green
= min(255, (d3d_fogcolor1.green + 5));
}
if (button_number == 4)
{
d3d_fogcolor1.green
= max(0, (d3d_fogcolor1.green - 5));
}
if (button_number == 5)
{
d3d_fogcolor1.blue
= min(255, (d3d_fogcolor1.blue + 5));
}
if (button_number == 6)
{
d3d_fogcolor1.blue
= max(0, (d3d_fogcolor1.blue - 5));
}
if (button_number == 7)
{
camera.fog_start
= min(5000, (camera.fog_start + 50));
}
if (button_number == 8)
{
camera.fog_start
= max(0, (camera.fog_start - 50));
}
if (button_number == 9)
{
camera.fog_end
= min(10000, (camera.fog_end + 50));
}
if (button_number == 10)
{
camera.fog_end
= max(0, (camera.fog_end - 50));
}
}

Si nous cliquons le premier bouton sur le panneau (le bouton associé à la première définition "button" dans fog_pan) button_number sera mis à 1. Si nous cliquons le 6ème bouton, button_number sera mis à 6 etc. Ne me remerciez pas pour cela, J.C. Lotter a créé (à l'intérieur du moteur) le code pour ces panneaux futés.De cette façon nous pouvons utiliser une seule fonction pour tous nos boutons et nous pouvons faire différentes choses en fonction de button_number.

Si nous cliquons le premier bouton, d3d_fogcolor1.red sera augmenté jusqu'à ce que sa valeur atteigne 255. Chaque clic de souris ajoute 5 à d3d_fogcolor1.red mais si vous voulez un pas plus petit, utiliser une valeur plus petite ici.. Si nous cliquons le deuxième bouton, d3d_fogcolor1.red sera décrémenter jusqu'à atteindre 0. La même chose se produit pour le reste des boutons ils mettent d3d_fogcolor1.green, d3d_fogcolor1.blue, camera.fog_start et camera.fog_end. Les 2 dernières valeurs pouvant être vraiment grandes, chaque clic de souris augmente ou diminue leur valeur par pas de 50.

Vous avez peut être noté que nous jouions avec d3d_fogcolor1 - c'est le premier des 4 brouillards dans Wed.is the first of the four fog colors in Wed. Nous avons encore besoin de faire une chose, dire au moteur que nous utilisons le premier brouillard et cette fonction fait cela parfaitement:

starter
init_fog()
{
fog_color = 1;
}

La fonction "starter" est une fonction spéciale, elle s'exécute toute seule au démarrage du niveau.

Exécutez votre niveau puis mettez les valeurs appropriées pour le brouillard. Lorsque vous serez fier du résultat, notez les valeurs et reportez le à la fin de votre fonction principale (main):

function main()
{
..............
..............
fog_color = 1; // use the first fog color
d3d_fogcolor1.red = 85; // your values here
d3d_fogcolor1.green = 45; // your values here
d3d_fogcolor1.blue = 95; // your values here
camera.fog_start = 400; // your values here
camera.fog_end = 2500; // your values here
}

Si vous ne voulez pas utiliser de boutons vous pouvez les remplacer par des curseurs.


Untitled

Brouillard de guerre

Si vous avez créé un jeu de stratégie alors vous aimeriez peut-être pouvoir lui ajouter un brouillard de guerre. Cet article va vous apprendre à en créer un qui soit décent et qui ne soit pas trop consommateur de ressources systèmes; en fait il vous appartient de choisir entre la performance ou la qualité en fonction du pc cible requis pour votre jeu.

Comment cela fonctionne-t-il? Nous créons une couche opaque qui recouvre intégralement la carte; nous la construisons en utilisant de petits sprites puis nous rendons transparent seulement les sprites proches du joueur.

Vous voyez tous ces carrés rouges? Chacun d'entre eux est un sprite. Et si vous pensez que j'ai placé tous ces carrés un par un dans le niveu et que donc je suis une personne très patiente eh bien vous vous trompez, je suis tout sauf patient...

starter
create_fog_sprites()
{
while (player == null) {wait (1);}
var row_index = 0;
var column_index = 0;
while (row_index < 32)
{
while (column_index < 32)
{
column_index += 1;
temp.x
= column_index * 128 - 2000; // -2000 to 2000 on x
temp.y = row_index * 128 - 2000; // -2000 to 2000 on y
temp.z = -1000;
ent_create
(fogsprite_tga, temp.x, manage_fog);
}
column_index = 0;
row_index += 1;
}
}

Le code attend que le joueur soit créé, puis il utilise 2 boucles while imbriquées pour placer tous les sprites sur le sol. Mon niveau de démo fait 4000 x 4000 quants et j'ai utilisé un sprite de 128 x 128 pixels pour le brouillard, aussi nous allons créer 4000 / 128 = 31.25 (j'ai choisi 32) rangs et colonnes.Nous avons besoin de 2 variables qui augmentent leur valeur de 0 à 31 (donc 32 étapes) et nous mettons la bonne position temp en multipliant l'index actuel par la largeur et la longueur de notre sprite (qui sont égaux dans notre exemple). Temp.z = -1000 places le sprite brouillard au dessus de la tête du joueur (vous pouvez tester une autre valeur expérimentale), et la prochaine ligne crée le sprite brouillard à la position donnée par temp et lui attache la fonction "manage_fog":

function
manage_fog()
{
my.tilt = 90;
my.alpha = 100;
my.oriented = on;
my.transparent = on;
my.passable = on;
while (1)
{
if (vec_dist(player.x, my.x) < 350)
{
my.alpha = 0;
}
else
{
my.alpha = 100;
}
wait (1);
}
}

La fonction ci-dessus met la bonne prientation pour le sprite, le rend passable et met sa valeur alpha en fonction de la disatance qui le sépare du joueur. Libre à vous de jouer avec la valeur "350",et n'oubliez pas que vous pouvez faire un système de brouillard encore plus réaliste si vous utilisez des sprutes plus petits.


Untitled

Traduction Olivier Burton

Brouillard volumétrique

Acknex emploie un système de brouillard simple qui est soutenu par toutes les cartes 3D qui sont disponibles sur le marché, même par les plus vieilles. Le moteur dresse la carte de la valeur de z des pixels depuis une fonction qui contrôle la couleur de brouillard, la force, etc. Le résultat est bien comme le brouillard que nous connaissons tous(lisez Aum25 pour plus de renseignements). Mais si nous voulons mettre du brouillard seulement dans un certain secteur de notre niveau, par exemple sur le plancher de ce cachot souterrain gluant ? Nous devrions employer le brouillard volumétrique.

Je suis sûr que certains d'entre vous ont déjà demandé cela plusieurs fois : comment puis-je ajouter du brouillard volumétrique à mes niveaux ? Cet article vous apprendra comment truquer le brouillard volumétrique en employant le moteur Acknex. La vue est d'un bon résultats et la chute de performance est pratiquement inexistante.

1) Créez un modèle simple qui ressemble à cela dans Med :

Ajoutez un cube Med et changez sa taille pour qu'il s'adapte à vos besoins. N'oubliez pas de le réduire au maximum. Employez une couleur plate pour la peau; j'ai employé une texture blanche de 32 x 32 pixels.

2) Placer le modèle dans votre niveau, près du plancher, ajustez son échelle (si vous avez besoin) et 'attachez l'action nommée fog_layer.

3) Ajouter plus de modèles et attachez leur la même action, en employant une valeur de z différente pour chaque d'entre eux. L'image ci-dessous montre 5 "couches" (5 modèles) placé au-dessus du niveau de plancher; mon niveau de démonstration emploie 10 couches.

Il est temps de jeter un coup d'oeil au code :

action
fog_layer
{
my.passable = on;
my.transparent = on;
while (1)
{
my.alpha = max
(0, (camera.z - my.z) / 20);
wait (1);
}
}

Je sais que cela pourrait paraître étrange, mais c'est tout le code qu'il est nécessaire pour créer le brouillard volumétrique! Le modèle est mis transparent et passable; ses changements alpha dépendent de la distance (sur l'axe z) entre la caméra et le modèle employé comme une couche de brouillard. Quand la caméra vient tout près d'une couche, cette couche deviendra de plus en plus transparente, jusqu'à ce qu'elle disparaisse.

Vous pouvez ajuster le facteur de transparence en changeant "20", vous pouvez changer la couleur du brouillard en changeant simplement la couleur de la peau, vous pouvez créer plusieurs actions avec des noms différents et différentes valeurs "20"si vous avez besoin de plusieurs effets de brouillard différents dans le même niveau, vous pouvez employer plus ou moins de modèles (plus mince ou plus épais) pour ajuster la qualité du brouillard, vous pouvez... Bien, vous pouvez faire beaucoup de choses.


Untitled

Bruit de pas

Si vous avez déjà voulu changer le bruit des pas du joueur selon la surface sur laquelle il marche, ce morceau de code est tout ce que vous avez besoin. J'ai employé deux types de surface dans mon exemple (herbe et pierre) mais vous pouvez ajouter autant de types de surface que vous voulez.

Tout d'abord, nous devons nous débarasser du bruit de pas par défaut. Ouvrez camera.wdl dans votre répertoire template, et trouvez cette ligne

play_sound(thud,30);

et mettez la en commentairel en plaçan t// devant elle. Incluez footstep.wdl dans votre fichier principal de jeu et appelez texture_scan dans la fonction Main.

function
texture_scan()
{
while (player == null) {wait (1);}
while (1)
{
vec_set(temp, player.x);
temp.z -= 100;
trace_mode = ignore_me + ignore_passable + ignore_models + ignore_sprites + scan_texture;
trace (player.x, temp);
if (str_cmpi ("grass1", tex_name))
{
texture_sound(grass);
}
else
{
texture_sound(stone);
}
step_length += 1 + 2 * key_shift;
waitt (4);
}
}

Nous attendons que le joueur soit créé et alors nous commençons à tracer quatre fois par seconde (c’est ce que veut dire le waitt (4) ). Nous traçons entre la position du joueur et la position située 100 quants sous le joueur. Après trace, tex_name contient le nom de la texture du plancher. Nous comparons le nom de texture de plancher à '"grass" et nous appelons la fonction texture_sound(grass) ou la texture_sound(stone) selon le type de surface.

La fonction texture_sound est appelée quatre fois par seconde, mais nous ne voulons pas avoir quatre pas donnant l’impression que notre joueur court comme Gabriela Szabo (vous n’avez jamais entendu parler lui? ça ne fait rien. ..). Nous employons la variable step_length pour cela; nous additionnons 1 à sa valeur initiale (zéro) chaque secondes ou 3 si la touche MAJ. est appuyée, quatre fois par seconde.

jetons un coup d'oeil à:

function texture_sound(sound_type)
{
vec_set (player_pos1, player.pos);
wait (2);
vec_set (player_pos2, player.pos);
if (player_pos1 != player_pos2)
{
if ((sound_type == grass) && (step_length % 6 == 0))
{
play_sound (grass_snd, 60);
}
if ((sound_type == stone) && (step_length % 6 == 0))
{
play_sound (stone_snd, 50);
}
if (step_length >= 6) {step_length = 0;}
}
}

Tout d'abord, nous devons savoir si le joueur se déplace. Je n'ai pas voulu modifier les calibres aussi j'ai décidé d'employer une méthode simple, stocker la position du joueur dans player_pos1 et encore la position du joueur dans player_pos2 deux frames plus tard. Si ces valeurs sont égales, le joueur se tient tranquile mais si player_pos1! = player_pos2, le joueur se déplace et nous devons jouer les bruits de pas.

Si le joueur fait un pas sur l'herbe et que step_length % 6 = 0 (ceci signifie step_length est multiple de 6 - 0, 6, 12. ) ..nous jouons le son grass_snd. La même chose se produit quand le plancher est fait en pierre. Mais que vient faire step_lengt h% 6? Vous vous rappelez que la fonction texture_sound est appelée quatre fois en une seconde lieu, mais nous n'avez pas besoin de quatre bruits de pas par seconde. J'ai choisi 6 parce qu'il est divisible par 1 (step_length + = 1 quand le MAJ. n'est pas appuyée) et par 3 (step_length + = 3 quand MAJ. est appuyée). Vous pouvez employer step_length % 3 pour doubler le nombre de pas si vous voulez. Avec step_length % 6, vous entendrez un bruit chaque 6/4 = 1. 5 secondes.

Quand step_length est plus grand que 6, il est remis à 0; de cette façon nous nous assurons que nous entendons les bruits de pas même si step_length est égal à 4 et nouslui additionnons constamment 3 (du step_length % 6 = 0 pourraient ne jamais se produire).


Untitled

Boussole

Cet article va vous apprendre à créer une boussole qui garde sa taille indépendamment de la résolution de l'écran.Si vous avez essayé de travailler avec les panneaux auparavant vous avez remarqué qu'ils chnangent leur taille relative à l'écran lorsque vous modifiez la résolution de celui-ci.Plus grande est la résolution et plus petit apparaîtra votre panneau. Mais pourquoi? Les panneaux ont conservés leur taille en pixels mais le nombre de pixels à l'écran a quant à lui augmenté. A présent vous pouvez retailler les panneaux en utilisant les nouvelles instructions scale_x et scale_y, qui feront parfois paraître vos panneaux plus laids ou qui vous permettront de placer des éléments 2 D dans un monde 3D en tant que définition d'entité.

entity
compass {  type = <compass.tga>; layer = 10; scale_x = 0.25; scale_y
= 0.25; x = 190; y = -85; z = -65; view = camera; flags = overlay, visible; }

entity needle { type = <needle.tga>; layer = 20; scale_x = 0.15; scale_y
= 0.15; x = 190; y = -85; z = -65; view = camera; flags = overlay, nofilter, visible;
}

Ces définitions d'entités utilisent deux fichiers tga. Vous pouvez voir que j'ai paramétré leur taille ainsi que leur position en monde 3D (x donne la distance à la caméra, y la position sur l'axe x de l'écran et z la position sur l'axe des y de l'écran. L'aiguille a une couche (layer) de 20 et apparaîtra donc par dessus la boussole qui a un layer de 10. Et la touche finale : l'aiguille a son drapeau "nofilter" de mis, ce qui permetrta un affichage brut, sans lui donner un aspect de flou sur les bords.

starter
set_compass()
{
 while (player == null) {wait (1);} // wait until the player
is loaded in the level
 while (1)
 {
 needle.roll = -player.pan; wait
(1);
 }
}

La fonction starter ci dessus attend que le joueur soit créé et il met l'angle roll en phase avec l'angle pan du joueur multiplié par -1.


Untitled

Fonctionnalité du mois : caméras à distance

Ce mois je veux vous montrer un dispositif "pro" qui est extrêmement utile dans des jeux d'action, jeux de sport, jeux d'aventure, jeux de stratégie... en fait, je pense que n'importe quel jeu pourrait employer une caméra à distance.
Acknex s'est amélioré beaucoup récemment et c'est l'une des nouvelles fonctionnalités du moteur : la capacité de rendre une vue sur un sprite.

Utilisation pratique : montrez votre jeu duefootball sur un grand écran, gardez un oeil sur l'entrée de vos forteresses, créez les rétroviseurs pour votre voiture et plus encore !

Tout d'abord, nous définissons une vue :

view
remote
{
pos_x = 0;
pos_y = 0;
size_x = 128; // size of
the view on x and y
size_y = 64;
layer = 5;
flags = visible;
}

Nous devons mettre sa taille à la taille du sprite (128 x 64 pixels dans mon exemple). J'ai chois layer = 5 mais vous pouvez choisir une valeur plus petite. Ce sera notre écran virtuel mais nous avons également besoin d'une caméra qui nous montrera ce qui se passe:

action
camera1
{
my.invisible = on;
my.passable = on;
remote.x = my.x;
remote.y = my.y;
remote.x = my.x;
remote.pan = my.pan;
remote.tilt = my.tilt;
remote.roll = my.roll;
}

Vous pouvez utiliser n'importe quel modèle pour la camera: placez le dans Wed, mettez sa position et ses angles puis transmettez-les à la vue à distance. Ca devrait être toutl... oh, attendez, nous avons besoin de montrer l'image capturée par l'écran en utilisant un sprite:

action monitor
{
my.oriented = on; // oriented sprite
remote.bmap = bmap_for_entity (my, 0);
}

J'utilise un sprite orienté. la vue peut être rendue sur "my" bitmap = l'image bitmap utilisée pour le sprite avec l'action qui lui est attachée.


Untitled

Caméras pour jeu d'aventure

Certains d'entre vous utilisent les caméras à la "Resident Evil" -du magazine Aum4. Petit rappel sur le fonctionnement de ce type de caméras : vous déplacez le joueur dans le niveau et la caméra la plus proche du joueur prend le contrôle. Je suppose que cela fonctionne très bien avec la majorité des jeux mais certains d'entre vous ont des problèmes avec des histoires utilisant de nombreux batiments, pendant que d'autres aimeraient pouvoir spécifierune zone d'activité pour chacune des caméras. Le code de cet article fixe tous ces problèmes.

view adv_camera{ }

starter
init_cameras()
{
sleep (0.5);
camera.visible
= off;
adv_camera.pos_x = 0;
adv_camera.pos_y
= 0;
adv_camera.size_x = screen_size.x;
adv_camera.size_y
= screen_size.y;
adv_camera.visible = on;
adv_camera.x
= 820;
adv_camera.y = -1150;
adv_camera.z =
620;
adv_camera.pan = 120;
adv_camera.tilt =
-25;
adv_camera.roll = 0;
}

J'ai défini une nouvelle vue nommée adv_camera. La fonction starter attend que le niveau soit chargé, désactive la vue "camera par défaut, met la position et la taille pour adv_camera et la rend visible. Les dernières lignes de code mettentl a position initiale et les angles pour adv_camera; c'est ce que voit le joueur (le point de départ pour votre jeu) lorsque vous démarrez le niveau.

//
uses cam_number, cam_range
action detect_player
{
my.invisible
= on; // hide the model
my.passable = on;
while
(player == null) {wait (1);} // wait until the player is loaded
if (my.cam_number == 0) {beep; beep; beep; exit;}
if (my.cam_range
== 0) {beep; beep; beep; exit;}
while (1)
{
temp.x = 360; // horizontal scanning angle
temp.y = 180; // vertical scanning angle
temp.z = my.cam_range; // scanning range
scan_entity (my.x, temp);
if (result > 0) // got something?
{
set_camera(my.cam_number);
// then set the proper camera position and angles
}
wait (1);
}
}

Placez plusieurs modèles dans le niveau, attachez leur l'action "detect_player" et ils se transformeront en éléments invisibles de détections du joueur. Vous devez paramétrer leur champ d'action (cam_range) et leur numéro (cam_number) dans Wed: le premier élément aura son cam_number de mis à 1, le second élément aura son cam_number = 2 etc. Oh, et si vous oubliez de paramétrer cam_number et / ou cam_range pour un des éléments, le moteur du jeu s'arrêtera. Comment cela fonctionne-t-il? Ils observent tout simplement autour d'eux; lorsque le joueur s'approche par exemple du 3eme élément, nous appelons la fonction set_camera(3) qui prendra la position et les angles de la 3eme caméra.

function
set_camera(number)
{
if (number == 1)
{
adv_camera.x = -40;
adv_camera.y = 340;
adv_camera.z = 1260;
adv_camera.pan = 270;
adv_camera.tilt = -72;
adv_camera.roll = 0;
}
........................
if (number
== 5)
{
adv_camera.x = 1900;
adv_camera.y = 1460;
adv_camera.z = 800;
adv_camera.pan = 220;
adv_camera.tilt = -35;
adv_camera.roll = 0;
}
}

La fonction ci-dessus met la bonne position et les bons angles pour adv_camera en fonction du numéro qui lui est passé comme paramètre. Si le joueur est proche de la caméra qui porte le numéro 1 pour son "cam_number" (1 mis dans Wed) la caméra prendra les valeurs de x = -40, y = 340, z = 1260, pan = 270, tilt = -72 et roll = 0, incroyable? Je suis sur que vous vous demandez comment j'ai bien pu trouver les valeurs utilisées dans la fonction set_camera, et bien voici la réponse : j'ai créé le niveau, je l'ai exécuté en tant que fichier wmb (en n'utilisant aucun script) et j'ai déplacé la caméra à la bonne position pour la première adv_camera. F11 m'a montré les valeurs actuelles de x, y, z, pan, tilt, roll angles, j'ai rnoté ces valeurs et j'ai poursuivi à la prochaine caméra etc.


Untitled

Caméras de surveillance

J'ai vi ces caméras dans Duke Nukem en 1997 - nous allons les voir en vrai dans cet article!

Je veux obtenir des caméras de surveillance "réelles", donc je commute entre la vue par défaut (camera) et ma nouvelle vue, surv_camera.

La fonction init_surveillance () est appelée dans main; son but est de créer une vue qui a la même taille et la même position que l'originale "camera"

"

view surv_camera {}

function init_surveillance()

{

surv_camera.size_x = screen_size.x;

surv_camera.size_y = screen_size.y;

surv_camera.pos_x = 0;

surv_camera.pos_y = 0;

}

Chaque caméra de surveillance (fish.mdl dans mon exemple, bien camouflée n'est-ce pas ?) a cette action qui lui est attachée :

action surveillance_camera

{

if (my.skill1 + my.skill2 + my.skill3 + my.skill4 + my.skill5 + my.skill6 == 0) {beep; beep; return;}

while (1)

{

my.skill10 = abs(ang(player.pan) - ang(my.pan));

if ((vec_dist(player.x, my.x) < 100) && (my.skill10 > 150) && (my.skill10 < 210))

{

camera.visible = off;

surv_camera.x = my.skill1;

surv_camera.y = my.skill2;

surv_camera.z = my.skill3;

surv_camera.pan = my.skill4;

surv_camera.tilt = my.skill5;

surv_camera.roll = my.skill6;

surv_camera.visible = on;

my.skill11 = 0;

}

else

{

if (my.skill11 == 0)

{

surv_camera.visible = off;

camera.visible = on;

my.skill11 += 1;

}

}

wait (1);

}

}

Si nous oublions de remplir au moins une valeur de ces skill1 ... skill6, nous entendons deux signaux sonores et ensuite le modèle de caméra est enlevé. Nous stockons l'angle de distance entre fish.mdl (le déclenchement de lacaméra) et le joueur dans skill10. Si le joueur est plus proche que 100 quants et si l'angle stocké dans skill10 est entre 150 et 210 degrés, le joueur qui fait face déclenche la caméra et la caméra de surveillance correspondante est validée.

Si une des caméras de surveillance est validée, la caméra standard est cachée et surv_camera est rendu visible. Les coordonnées de Surv_camera sont lues à partir des variables d'entité skill1..6 (x, y, z, pan, tilt, roll) aussi vous devez renseigner ces valeurs dans Wed. Si vous voulez désactiver la caméra de surveillance vous devez simplementy faire tourner le joueur ou le faire reculer.

Si vous voulez améliorer ce code de caméra vous pourriez ajouter un panneau qui ressemble à un encadrement de TV quand vous êtes dans le mode de surveillance, jouer quelques sons, etc.


Untitled

La caméra suit un chemin

Pour être honnête avec vous j'ai piqué l'idée à Doug (j'ai jeté un oeil attentif à son code de "oiseau suivant un chemin). Le résultat de mon travail est différent ici mais je souhaitais le citer ici.

Comme nous ne voulons pas modifier les scripts prédéfinis, nous définnissons une nouvelle vue nommée cutcamera. Nous avons placé une entité (fish.mdl?) dans le niveau et lui avons attaché l'action cutcamera_init :

action
cutcamera_init
{
vec_set (cut_center, my.pos);
my.skill20 = max_speed;
camera.visible = off;
cutcamera.tilt -= 20;
cutcamera.x = 0;
cutcamera.y = 0;
cutcamera.size_x = screen_size.x;
cutcamera.size_y = screen_size.y;
ent_path("campath");
ent_waypoint(my._target_x, 1);

my.x = my._target_x;
my.y = my._target_y;
my.z = my._target_z;

cutcamera.visible = on; // cutcamera devient visible

La caméra tournera pour faire face à l'entité poissons tout le temps. Le poisson peut être placé n'importe où dans le niveau parce qu'il est invisible et passable. Avant tout nous stockons les coordonnées de poisson et la vitesse maximum. Nous neutralisons la vue par défaut (caméra) et nous mettons le nouveau cutcamera. Le poisson se déplacera au premier point dans le chemin "campath" (c'est pourquoi nous stockons ses coordonnées initiales).

while(1)
{
cutcamera.x = my.x;
cutcamera.y = my.y;
cutcamera.z = my.z;
cutcamera.pan = my.pan;

       vec_set(my._speed, my._target_x);
vec_sub(my._speed, my.x);
if (vec_to_angle(my.pan, my._speed)
< 15)
{
ent_nextpoint(my._target_x);
}

       vec_set (temp, cut_center.x);
vec_sub (temp, my.x);
vec_to_angle (my.pan, temp);
vec_normalize(my._speed, (max_speed * time));

if (key_s != 1)
{
ent_move(nullskill,
my._speed);
}

Le poisson se déplace sur le chemin 3D et la vue cutcamera lui est attachée. Il y a deux choses que nous devons considérer :
1) nous devons mettre _speed de sorte que le poisson se déplace sur le chemin
2) que nous devons tourner le poisson (la vue) de sorte qu'il se dirige toujours vers cut_center.

Vous pouvez voir que cut_center ne doit pas être au centre du chemin et que le chemin peut avoir n'importe quelle forme (n'oubliez pas que c'est un chemin 3D, après tout).

Si nous voulons tourner l'objet my vers l'objet you nous devons employer 3 lignes de code selon le manuel :

vec_set (temp, you.x);
vec_sub (temp, my.x);
vec_to_angle (my.pan, temp)

J'ai employé ces lignes (je pourrais aussi avoir employé vec_rotate) pour placer le vecteur _speed dans la direction du prochain but et pour tourner la vue vers cut_center (tout les points rouges pointent vers cut_center mais je n'ai pas voulu surcharger l'image). Nous normalisons le vecteur _speed ; nous coupons la longueur et gardons sa direction.

Vous pourriez vous demander ce que sont ces lignes :

if
(vec_to_angle(my.pan, my._speed) < 15)
{
ent_nextpoint(my._target_x);
}

J'ai employé un truc ici : Je suppose que vous êtes paresseux et que votre chemin a moins de mille points, ainsi la caméra secoue quand elle atteint la cible (un des points sur le campath) et doit changer sa direction (jetez un coup d'oeil).

Que pouvons-nous faire ? Les lignes ci-dessus calculent la distance entre le poisson et le prochain but. Quand nous devenons assez proches du point de destination, nous arrêtons de nous déplacer vers lui et plaçons le nouveau point de destination - voici la différence :

Le chemin vert est plus lisse ; jetez un coup d'oeil aux angles rouges - de plus grands angles = des transitions plus douces. Avant que j'oublie, vous pouvez pressez S pour arrêter le mouvement.

Que reste-t-il à ajouter à ce morceau de code fonctionnant admirablement ? Nous pourrions abaisser sa vitesse quand elle atteint un certain secteur (disons que nous voulons jeter un bon coup d'oeil au noyau nucléaire dans notre niveau) puis accélérer de nouveau lorsqu'on a quitté ce secteur.

J'ai créé ce type de mouvement dans mon exemple office.wmp quand le poisson va en-dessous de 190 quants (j'a obtenu cette valeur dans Wed)

if
(my.z < 190)
{
max_speed = max (my.skill20
/ 2, max_speed * 0.995);
}
else
{
max_speed = min (my.skill20, max_speed * 1.005);
}

Nous avons stocké max_speed dans skill20 et maintenant nous le diminuons de 7 (valeur initiale) à 3.5 ; veuillez noter que nous ne plaçons pas simplement max_speed à 3.5 mais nous le diminuons constamment (nous le multiplions par 0.995) jusqu'à ce qu'il atteigne 3.5. Quand nous sortons de notre secteur spécial (my.z > 190) max_speed est constamment augmenté (multiplié par 1.005) de 3.5 à 7 qui a été stocké dans skill20, vous vous souvenezt ?

Naturellement vous pouvez ajouter des conditions multiples comme ceci :

if (my.x < 100 && my.y > 250 && my.y < 300 && my.z > 400)
{
..........................................................
}


Untitled

Cash and carry

Vous n’avez jamais voulu avoir un script qui vous permette d'obtenir de l'argent et d'acheter des articles dans votre jeu? Le voilà.

Le code se compose de deux parties: l'action + la fonction qui vous donne de l'argent et l'action + la fonction qui vous aide à les dépenser.

action
get_cash
{
my.enable_impact = on;
my.event = add_money;
}

function add_money()
{
wait (1);
if (my.skill1 == 0) {my.skill1 = 20;} // par défault
= 20 pièces
if (cash == 0) {cash_pan.visible = on;}
cash += my.skill1;
snd_play (gotcash_snd, 70,
0);
ent_remove (me);
}

J'ai employé lamp.mdl (une lampe) pour le modèle qui vous donne de l'argent comptant. Quand le joueur (ou autre chose, comme la balle du joueur) cogne avec la lampe, la fonction add_money() est exécutés. Si nous oublions de donner une valeur à la variable skill1 de la lampe, elle sera mise à 20 - ceci signifie que nous obtenons 20 pièces. Nous augmentons la variable argent comptant, nous jouons un bruit et ensuite nous enlevons l'entité

action
buy_items
{
my.enable_click = on;
my.event = buy_me;
}

function buy_me()
{
wait (1);
if (event_type != event_click
|| cash < my.skill1) {return;}
cash -= my.skill1;
snd_play (spentcash_snd, 70,
0);
ent_remove (me);
}

L'action qui vous aide à dépenser l'argent (je devrais l'avoir appelé action épouse:) est semblable, mais utilise la souris pour plus de fantaisie. Pour acheter un objet clic droit de la souris lorsque le pointeur est dessus, puis autant de clic gauche que quantité d’objets que vous voulez (poissonsle mdl) et vous "les achèterez" si vous avez assez d'argent comptant. Placez skill1 pour les objets qui peuvent être achetés; si skill1 = 0, les objets sont gratuits (vous n'avez pas besoin d'argent comptant pour les acheter).

J'emploie un panneau simple pour montrer la quantité d'argent comptant disponible au joueur:

panel
cash_pan
{
bmap = cash_map;
layer = 20;
pos_x = 0;
pos_y = 0;
digits = 40, 22, 4, arial_font, 1, cash;
flags = transparent, overlay, refresh,
d3d;
}

Ce panneau est rendu visible dès que vous prendrez le premier objet qui vous donne l'argent comptant.


Untitled

Champ de mines

Imaginez la situation: vous êtes poursuivis par plusieurs ennemis. Votre santé est au plus bas et chaque nouvelle balle vous rapproche d'une mort peu honorable Et ce long couloir qui semble ne jamais vouloir finir (Bien que vous soyez blessé, vous ne pouvez vous empêcher de penser qu'Acknex est quand même un superbe moteur...) Les ennemis sont tout près derrière vous, mais attendez, c'est quoi cette porte? Vous ouvrez la porte puis vous entendez 2 courts beep. A saut spectaculaire vous sauve la vie. Le champ de mines explose sans vous blesser du tout, mais ce n'est pas le cas pour vos ennemis...

Si cela vous semble bien, soyez heureux : cet article va vous apprendre comment créer un champ de mines qu peut vous tuer vous ou vos ennemis. Vous pouvez faire en sorte que les ennemis vous suivent et lorsqu'ils seront près du champ de mines ils mourront.

Le code n'est pas compliqué du tout:

action
landmine
{
my.skill1 = 0;
my.enable_detect = on;
my.event = hurt_them;

Nous mettons skill1 à zéro; sa valeur restera à zéro aussi longtemps que la mine n'a pas détecté d'entités. Nous mettons enable_detect à 1; de cette façon la mine sera sensible à n'importe quel entité qui aura son enable_scan de mis: le joueur et l'ennemi normal (actor_ai_one, par exemple). Si une de ces entités approche le champ de mines la fonction hurt_them s'exécutera.

     while (my.skill1 == 0)
{
temp.pan = 360;
temp.tilt = 180;
temp.z = 200;
scan_entity(my.x,
temp);
if ((result >
0) && (result < 200))
{
my.ambient = 100;
ent_playsound (my, beephigh_wav, 500);
sleep (0.3);
my.ambient = 0;
sleep (0.3);
my.ambient = 100;
ent_playsound (my, beephigh_wav, 500);
my.skill1 = 1; // get out of the while loop
}
sleep (0.2); //
scan 5 times a second
}

Tant que skill1 = 0, nous balayons tout autour du champ de mines,comme sur l'image ci-dessous, 5 fois par seconde:

Personne ne peut échapper à notre balayage; mieux encore, nous pouvons simplement mettre temp.z a une très grosse valeur pour augmenter l'étendue du balayage. Si la mine a détecté une entité, elle augmente son ambient, joue un son puis attend 0.3 secondes. Ambient est mis à zéro, nous attendons pour 0.3 seconds et la séquence se répète. Jusqu'à ce que le second beep soit joué, nous mettons skill1 à 1 aussi nous pouvons sortir de la boucle while. Sleep (0.2) fait que le moteur balaye 5 fois par seconde; si vous pensez que vos ennemis sont plus rapides que cela, vous pouvez baisser cette valeur.

Vous avez pu noter que nous n'avons encore blessé personne jusqu'à présent, le code qui fait cela se touve dans la fonction hurt_them():

function hurt_them()
{
sleep (1);
you._health -= 200;
}

Nous attendons une seconde, l'entité est déjà morte à présent aussi nous donnons l'illusion que il ou elle peut s'échapper. Cela ne peut pas se produire car la ligne suivante diminue de 200 points de santé. Un champ de mines doit tuer quelqu'un, n'est-ce pas?


Untitled

Traduction Olivier Burton

Champ de vision

N' avez-vous jamais joué au jeu Commandos? Ils ont mis en oeuvre un effet propre à ce jeu : vous pouvez voir le champ de vision des ennemis, autrement dit vous pouvez voir si un certain ennemi peut vous voir ou non. Je suis un viel utilisateur de moteur mais je suis quand même impressionné de voir comment il est facile de mettre en oeuvre un si grand effet en employant Acknex et le C-script.

J' emploierai les templates donc je devrai définir une nouvelle vue parce que je ne veux pas éditer trop de code des templates :

view
bird_view
{
layer = 15;
pos_x = 0;
pos_y = 0;
size_x = 640;
size_y = 480;
arc = 80;
flags = visible;
}

J' ai créé une vue de dessus qui a 640x480 pixels; si votre jeu emploie une résolution différente changez ces valeurs. J' ai choisi un commode facteur de zoom (80), mais si vous n' êtes pas heureux avec cela n' hésitez pas à choisir une autre valeur pour cela.

J' ai employé le guard.mdl standard qui patrouille sur son chemin comme "ennemi"; cette entité détectera le joueur et quatre... lampes employées comme des camarades de travail d' équipe du joueur. Le seul changement qui doit être fait à vos modèles ennemis doit être de mettre une ligne de code comme la première ligne dans l' action qui est attachée à l' ennemi :

action
patrol_prog
{
init_sight();
guard = me;
// watched
= me;
..................
}

Vous pouvez voir comment j' ai ajouté la ligne à patrol_prog; j' ai inclus le fichier modifié office.wdl aux archives téléchargeables. Voyons ce que cette fonction d' init_sight fait :

function
init_sight()
{
LensFlare_Stop(); // désactive
lens flares si présentes
camera.visible =
off;
clip_size = 0;
my._health = 100; // si besoin
ent_create (cone_mdl,
my.pos, move_cone);
bird_view.x = 0;
bird_view.y = 0;
bird_view.z = 1200;
bird_view.tilt = -90;

Nous arrêtons l' effet d' éclat de lentille parce que nous ne le voulons pas dans une vue de dessus; si vous obtenez des erreurs alors ne l' employez pas du tout et supprimez la première ligne dans init_sight (). Nous cachons la vue par défaut (caméra), nous montrons tous les triangles pour tous les modèles à l' intérieur du niveau. Nous mettons la santé à 100 pour le garde de patrouille (nous en aurons besoin plus tard) et ensuite nous créons le modèle de cône de vision et nous lui assignons la fonction de move_cone.

Finalement, notre vue d' oiseau est placée dans l' origine, 1200 quants au-dessus de la terre et regarde en bas à cause de la valeur angulaire de tilt(inclinaison).

while (my._health > 0)
{
my.skill40 = 40; // pan
my.skill41 = 180; // tilt
my.skill42 = 300; // range
scan_entity (my.x, my.skill40);
if ((result > 0) && (!snd_playing (alert_handle)))
{
alert_handle = snd_play (alert_wav, 50, 0);
}
wait (1);
}
}

Tant que le garde est vivant (il ne peut pas mourir mais vos ennemis doivent être capables de le faire), nous mettons un secteur de balayage de 40 degrés horizontalement et 180 degrés verticalement et uneétendue de balayage de 300 quants.

Le garde est prêt à commencer à parcourir maintenant; s' il détecte quelque chose dans la gamme il jouera un son d' alarme dans une boucle.

Ok, ainsi à ce point nous avons un ennemi qui peut parcourir pour voir d' autres entités et s' il les détecte il joue un son. Voyons comment le cône de vision réel travaille :

function
move_cone()
{
proc_late();
my.passable = on;
my.albedo = 0;
my.transparent = on;
my.alpha = 20;
my.z += 25;

La première ligne de code s' assure que le garde et son cône se déplacent ensemble sans à-coup. Le cône sera passable et transparent; son z sera placé 25 quants au-dessus du z du garde (qui est la valeur nécessaire pour le niveau de l' oeil si vous employez le modèle garde).

while (you._health > 0) // aussi longtemps que le créateur (l' ennemi)
est vivant
{
my.x = you.x;
my.y
= you.y;
my.pan =
you.pan;
my.tilt =
you.tilt;
my.scale_x
= you.skill42 / 10;
my.scale_y = 2 * you.skill42 * tan(you.skill40 / 2) / 10;
wait (1);
}
// l'
ennemi est mort ici-> détruire le cône
ent_remove (me);
}

Tant que l' ennemi est vivant, le cône existe avec lui et hérite aussi de son angle tilt. L' échelle du cône sur x (la gamme) est donnée par you.skill42 / 10. Maintenant pourquoi cela?

Démarrez Med et ouvrez cone.mdl :

Vous pouvez voir que le modèle de vision du cône est en fait un triangle qui a une taille de 10 quants. Si nous mettons une gamme de balayage de 300 nous devons mettre scale_x pour ce modèle à 30 parce que cela nous donnerait 30 * 10 quants = 300 quants. J' aurai pu employer un triangle avec une taille de 1 quant mais nous ne pouvons pas employer des valeurs de plus de 100 pour scale_x... Scale_z et cela aurait limité notre gamme de balayage à 100 quants! C' est pourquoi nous divisons skill42 par 10. La ligne de code suivante semble même plus effrayante :

my.scale_y = 2 * you.skill42 * tan(you.skill40 / 2) / 10;

Voici quelque conseil pour les plus jeunes utilisateurs : apprenez à aimer les maths! Je les détesté quand que j' étais au lycée et à l' université, mais si vous projetez de gagner votre existence comme programmeur vous devez en savoir beaucoup sur les maths!

Vous pouvez voir comment j' ai inventé cette formule en regardant l' image ci-dessus; scale_y dépend de scale_x ( you.skill42) et l' angle de balayage horizontal (you.skill40).

Quand l' ennemi perd sa santé le cône disparaîtra. Si vous évaluez la démonstration soigneusement vous remarquerez que mon triangle travaille bien, mais si vous voulez avoir plus de précision vous devez employer un modèle qui ressemble à la section verticale d' un cône, pas à un triangle simple. Jetez un coup d' oeil à l' image ci-dessous pour voir comment vous devez concevoir votre modèle de cône.

J'ai oublié de vous dire quelque chose d'important : les entités qui doivent / peuvent être scannées doivent avoir leur drapeau enable_scan de mis à 1. Le joueur a le sien de mis mais jetons un coup d'oeil à l'action attachée à nos 4 lampes:

action
scan_me
{
my.enable_scan = on;
// ajoutez votre code ci-dessous
}

C'est tout. Bien entendu vous pouvez (et vous devez) ajouter du code pour vos alliés mais n'oubliez pas de mettre leur drapeau enable_scan de mis à 1 autrement ils ne seraient pas détectés.


Untitled

Traduction Olivier Burton

Code de changement de niveau

Acknex emploie un système simple de chargement de niveau; une simple ligne de code vous permet de vous déplacer d' un niveau à un autre. Si vous avez essayé de le faire vous avez remarqué que le nouveau niveau "efface" les armes, des articles, etc donc vous devez les créer de nouveau d' une façon ou d' une autre. Il y a seulement deux règles d' or donc il serais mieux que vous les apprenniez tout de suite:
1) Les variables globales ne sont pas effacées;
2) Une fonction continuera à tourner après chargement d' un nouveau niveau seulement si vous y ajoutez cette ligne de code :

me = null;

Que sont les variables globales? Elles ne sont rien de plus que vos variables normales définies au début de vos fichiers de C-script, à L' EXTÉRIEUR de n' importe quelle définition de fonction :

var number_of_mice;

Ces variables tiennent leurs valeurs identiques après une instruction level_load donc vous pouvez les employer pour stocker le nombre de balles, nombre de vies, etc - Elles réchapperont au changement de niveau.

Qu' est-ce que cela àa à voir avec les fonctions ? Quand vous chargez un nouveau niveau, toutes les entités de l' ancien niveau sont enlevées. Cela signifie que les actions ou les fonctions attachées à n' importe quelle entité donneraient des messages d' erreur parce que leur entité parentale a disparu. Avec my = null la fonction diras au moteur : "hé, je suis toute seule ici! Je n' ai pas d' entité attachée à ma suite donc ne me fermez pas!"

J' ai utilisé le standard office.wmp comme premier niveau et une version légèrement modifiée (incluant un énorme chiffre "2" ) pour le deuxième niveau. Le premier niveau (office.wmp) inclut 2 armes et une entité qui déclenchera le changement de niveau; jetons d' abord un coup d' oeil au code pour les armes :

action pistol
{
my.__rotate = on; // tourne avant d' être pris
my.__repeat = off; // désactive le tir automatique
my.__silent = on; // n' afficher aucun message de prise
my._offs_x = 30; // position du pistolet
my._offs_y = -10; // sur x, y, z
my._offs_z = 15;
my._ammotype = 1.20; // utilise ammo type 1, fournit avec 20 balles
my._weaponnumber = 1; // presser "1" pour sélectionner cette arme
my._bulletspeed = 1000.01; // speed.recoil
my._firetime = 8; // temps de rechargement (entre deux balles)
my._firemode = damage_shoot + hit_hole + gunfx_brass + gunfx_animate + 0.10; // hit holes on the walls, damage = 10 points / shot
gun(); // appelle la réelle action pistol
}

action
machine_gun
{
my.__rotate = on; // tourne avant
d' être pris
my.__repeat = on; // désactive
le tir automatique
my.__silent = on; // n' afficher
aucun message de prise
my._offs_x = 35; // position
de la mitrailleuse
my._offs_y = 10; // on x, y,
z
my._offs_z = 15;
my._ammotype = 2.99; // utilise ammo type 2, fournit avec 99 bullets
my._weaponnumber = 2; // presser "2" pour sélectionner cette arme
my._bulletspeed = 5000.01; // speed.recoil
my._firetime = 2; // temps de rechargement (entre deux balles)
my._firemode = damage_shoot + hit_hole + gunfx_brass + gunfx_animate + 0.20; //
hit holes on the walls, damage = 20 points / shot
gun(); // appelle la réelle action gun
}

J' ai créé un pistolet qui vient avec 20 balles et une mitrailleuse qui vient avec 99 balles. Ppressez"1" ou"2" pour choisir les armes après que vous les ayez prises. L' entité qui déclenche le changement de niveau a ce morceau de code qui lui est attaché :

action
level_12
{
my.enable_impact = on;
my.event = change_levels_12;
}

function
change_levels_12
{
wait (1);
if (you != player) {return;}
me = null;
level_load (level2_wmb);
sleep (1);
restore_12();
}

L' entité est sensible à l' impact; quand une autre entité entre en collision avec elle, sa fonction d' événement change_levels_12 courra. Si l' entité qui est entrée en collision avec le trigger (you) n' est pas le joueur, nous ne changerons pas le niveau; autrement nous mettons me = null pour nous assurer que cette fonction réchappera de l' instruction level_load, nous attendons pendant une seconde et ensuite nous appelons la fonction restore_12 () qui est supposée rétablir dans le niveau 2 les articles pris dans le niveau 1.

function
restore_12()
{
while (player == null) {wait
(1);}
if (weapon_1 == 1) // si le joueur a pris
l' arme 1
{
ent_create (weapon1_mdl, nullvector, pistol); // créer de nouveua le pistolet
}
if (weapon_2 ==
1) // si le joueur a pris l' arme 2
{
ent_create (weapon2_mdl, nullvector, machine_gun); // create the gun again
}
gun_loaded = 1; // s'
assurre que c' est chargé
gun_select(); //
et le sélectionne
}

Cette fonction attend que le joueur soit créé; si le joueur a pris weapon1 dans level1, l' arme est créée de nouveau et dirige la même action (le pistolet); si le joueur a pris weapon2 dans level1, cette arme sera créée aussi. Nous nous assurons que l' arme à feu est chargée et ensuite nous choisissons la dernière arme qui a été employée dans le premier niveau.

J' ai employé quelques variables qui sont définies à l' intérieur de weapons.wdl pour rendre les choses plus simples. Si vous projetez d' écrire votre code d' arme / inventaire / etc à partir de zéro voici ce que vous devez faire pour vous assurer que vous pouvez porter tous les articles dans tous les niveaux :
1) Mettre une certaine variable à 1 quand vous avez pris(acheté) un certain objet (weapon_1 à 1 ou key_blue à 1, etc);
2) Ajouter-me = null au corps de la fonction qui fait le changement de niveau AVANT la nouvelle instruction level_load;
3) Attendre un peu (sleepl (1) dans mon exemple mais vous pourriez attendre (2) etc);
4) Appeler la fonction qui rétablit les articles;
5) La fonction qui rétablit les articles vérifiera quels articles ont été pris(achetés) dans le niveau précédent (key_blue à 1 ou 0 ?) et les créera de nouveau (utilisation de ent_create) si leur variable s associées ont été mise à 1.
6) Répéter le même processus pour tous vos niveaux (restore_23, restore_34, etc).


Untitled

Chronomètre

Si vous voulez créer un jeu de course vous aurez certainement besoin d'un bon chronomètre. La fonction start_counter() s'exécutera tout le temps que durera votre niveau; regardons le code de plus près :

string temp_str;
string counter_str;

function
start_counter()
{
str_cpy (counter_str, "0.000"); // show this string at start
while (1)
{
if (start == 1)
{
current_ticks = total_ticks - temp_ticks;
seconds = (current_ticks / 16) % 1000;
miliseconds = (current_ticks * 1000 / 16) % 1000;
str_for_num (temp_str, seconds);
str_cpy (counter_str, temp_str);
str_cat (counter_str, ".");
str_for_num (temp_str, miliseconds);
str_trunc (counter_str, 1);
}
else
{
temp_ticks = total_ticks;
}
wait (1);
}
}

function
toggle_counter()
{
start = (start == 0);
}

on_s = toggle_counter;

Tout d'abord nous copions "0.000" dans la chaîne qui sera utilisée pour afficher le temps; c'est toujours plus sympa de montrer le chrono comme cela avant le départ de la course. Start = 1 devrait être mis à 1 par notre jeu de course au moment du départ mais comme nous n'avons pas de course nous ferons un départ artificiel à l'aide de la fonction toggle_counter qui mettra start à 1 ou à 0 lorsque la touche "S" est pressée.

Nous utilisons current_ticks pour stocker le nombre de frames (ticks) qui se sont passés depuis le démarrage du compteur. Je vais illustrer mon propos avec quelques nombres pour que ce soit plus clair pour vous.

Disons que le jeu a démarré depuis 10 seconde et que votre taux d'affichage est de 60 fps. Ce qui signifie u'à ce point, total_ticks = 10 secondes * 60 fps = 600, puisque total_ticks compte le nombre de frame qui se sont passées depuis que le jeu est lancé. Temp_ticks est à zéro au départ du jeu, aussi current_ticks = 600. Nous calculons le npmbre de secondes et milisecondes, nous convertissons cela en chaîne et nous la stockons dans counter_str de cette façon: secondes.milisecondes. Si nous pressons "S" de nouveau, start = 0 et l'embranchement "else" est exécuté. Nous savons qu'à ce point total_ticks = 600 aussi temp_ticks (une variable temporaire) sera mise à 600 aussi.

Total_ticks continue de compter et compte même si nous mettons le jeu en pause, si nous chargeons ou nous sauvons etc noous ne pouvons pas l'arrêter! Lorsque nous pressons "S" de nouveau pour relancer le chronomètre, start = 1 et current_ticks = total_ticks - temp_ticks = 0, parce que l'embranchement "else" a copié total_ticks dans temp_ticks lorsque le jeu a été arrêté. De cette façon notre compteur démarre de zéro à chaque fois que nous le resettons.

Le temps est affiché à l'écran par l'intermédiaire d'une simple définition de texte:

text
counter_pan
{
layer = 20;
pos_x = 0;
pos_y = 0;
font = speed_font;
string = counter_str;
flags = visible;


Untitled

Chute d'eau

Je pense qu'une bonne chute d'eau peut-être créée en utilisant la recette suivante : mélangez une entité wmb transparente avec un peu de code qui décale la texture et qui génère quelques particules. C'est exactement ce que nous allons faire.

action
waterfall
{
my.transparent = on;
while (1)
{
my.v -= 30 * time;

L'action waterfall est attachée à l'entité wmb waterfall. Vous pouvez voir que l'entité est transparente et sa texture est mouvante car nous changeons son paramètre v., regardons l'entité telle qu'elle est dans Wed:

Je vous entends dire: qu'a-t-elle de spécial cette entité?

Bien, nous avons besoin de connaître la largeur de la chute d'eau (130 quants dans mon exemple) et nous devons placer son origine telle que vous la voyez sur la vue arrière (back). De cette façon les particules pourront être créées dans la région séparant la chute d'eau et le bassin d'eau sans à avoir à écrire un code compliqué ou d'ajouter d'autres entités comme un générateur de particules.

          vec_set (temp,
my.pos);
temp.y += 60 -
random(120);
effect (water_splash,
1, temp, normal);
wait (1);
}
}

Nous utilisons temp pour stocker la position de l'entité waterfall puis nous ajoutons une valeur aléatoire entre -60 et 60 quants au y de l'entité waterfall. Cela créera plusieurs particules water_splash avec une position aléatoire en y. Les particules sont produites avec l'instruction habituelle "effect" , avec une particule de dréée à chaque cycle. Je n'ai pas limité le nombre d'éclaboussures : si votre pc fonctionne à 100 fps vous obtiendrez 100 particles chaque seconde. De cette façon, les PC non performants auront une chance d'afficher des éclaboussures comme elles le peuvent. Si vous voulez,vous pouvez émettre plusieurs partiucles en même temps et utiliser sleep(x) à la place de wait (1) dans la boucle.

'Regardons la fonction qui génère les particules:

function
water_splash()
{
temp.x = random(4) - 2;
temp.y = random(2) - 1;
temp.z = random(1) + 1;
vec_add (my.vel_x, temp);
my.alpha = 10 + random(10);
my.bmap = water_pcx;
my.size = 20 + random(10);
my.flare = on;
my.move = on;
my.bright = on;
my.lifespan = 200;
my.function = splash_fade;
}

Les particules ont une petite vitesse aléatoire en x (-2..2) et y (-1..1) et une vitesse positive en z (1..2) parce qu'ellesrmonteront un petit peu. Vous pouvez voir que les particules commencent leur vie avec un alpha et une taille aléatoires et avec une durée de vie de 200 ticks. La fonction qui dirige chaque particule est présentée ci-dessous:

function
splash_fade()
{
my.size += 5 * time;
my.alpha -= 1 * time;
if (my.alpha < 0)
{
my.lifespan = 0;
}
}

La particule augmente sa taille et diminue son alpha durant sa vie. Lorsque la particule est devenue invisible (alpha < 0) elle est enlevée parce que nous ne voulons pas gaspiller des ressources systèmes inutilement.

Vous devez être capable de créer de superbes chutes d'eau en utilisant ce morceau de code comme exemple. Vous pouvez choisir entre différentes images bitmap d'éclaboussure, jouer avec l'alpha, la taille, les drapeaux etc. Bonne chance!


Untitled

Code d'accès

Imaginez que vous vous trouviez dans cette situation : vous avez tué des dizaines d'ennemis et vous vtouchez presque au but, la fin du niveau. Vous prenez un dernier virage et brusquement vous vous trouvez nez à nez avec une porte qui va vous permettre de sortir de cette zone toxique. Vous essayez d'ouvrir la porte mais elle ne veut pas s'ouvrir. Mais attendez! Il y a portier électronique à côté sur le mur. Vous essayez le seul mot de passe que vous connaissez (1234) mais il ne fonctionne pas...

string password_str = "6239";

Je jette un rapide coup d'oeil à la ligne de code ci-dessus et maintenant je suis pratiquement sur que le code d'accès est 6239. Laissez-moi cliquer le bouton droit de la souris (pour montrer le pointeur) et ensuite je vais pouvoir cliquer sur les boutons du portier électronique.

Le mot de passe était bon puisque la porte s'est ouverte! Pressez la touche astérisque du portier électronique pour le cacher et jetons un oeil sur le code.

J'ai utilisé unpetit sprite pour symboliser le portier electronique sur le mur; l'action attachée ausprite est vraiment simple:

action
dial_small
{
my.oriented = on;
my.passable = on;
my.enable_click = on;
my.event = show_dialpad;
}

Le sprite est orienté, passable et sensible aux clics de la souris; à chaque fois que je cliquerais dessus, sa fonction d'évènement sera exécutée:

function
show_dialpad()
{
number_of_digits = 0;
str_cpy (guess_str, ""); // reset the string
dialpad_pan.visible = on;
}

La fonction ci-dessus met number_of_digits à zéro, vide guess_str (la chaîne qui contiendra le mot de passe tapé par le joueur) et affiche le panneau dialpad_pan:

panel
dialpad_pan
{
bmap = dialpad_pcx;
pos_x = 0;
pos_y = 0;
layer = 10;

     button = 143, 8, one_pcx, one_pcx, one_pcx, check_code,
null, null;
button = 270, 8, two_pcx, two_pcx, two_pcx, check_code, null, null;
button = 398, 8, three_pcx,
three_pcx, three_pcx, check_code, null, null;
button = 143, 128, four_pcx, four_pcx, four_pcx,
check_code, null, null;
button = 272, 128, five_pcx, five_pcx, five_pcx, check_code, null, null;
button = 397, 129, six_pcx,
six_pcx, six_pcx, check_code, null, null;
button = 144, 248, seven_pcx, seven_pcx, seven_pcx, check_code, null, null;
button = 273, 249, eight_pcx,
eight_pcx, eight_pcx, check_code, null, null;
button = 397, 250, nine_pcx,
nine_pcx, nine_pcx, check_code, null, null;
button = 146, 368, zero_pcx, zero_pcx, zero_pcx,
check_code, null, null;
button = 272, 370, asterisk_pcx, asterisk_pcx, asterisk_pcx, check_code, null,
null;

flags = overlay, refresh;
}

Le portier electronique consiste en un simple panneau avec 11 boutons. Chaque fois que nous cliquons un bouton, la fonction check_code s'exécute et prend le numéro du bouton qui a été cliqué:

function check_code(button)
{
number_of_digits += 1;
snd_play (dial_wav, 30, 0);
if (button == 1)
{
str_cat (guess_str,
"1");
}

..............................................

     if (button == 9)
{
str_cat (guess_str,
"9");
}
if (button == 10)
{
str_cat (guess_str,
"0");
}
if ((button == 11) || (number_of_digits
> 4))
{
while (mouse_left
== 1) {wait (1);}
sleep (0.5);
snd_play (error_wav,
70, 0);
dialpad_pan.visible
= off;
}
}

Chaque fois que nous cliquons un des boutons sur dialpad_pan, nous incrémentons number_of_digits (le nombre de caractères dans guess_str). Un son est joué et un caractère ("1", "2", ... , "0") est ajouté à guess_str. Si nous pressons le 11ème bouton ( * ) ou si le nombre de touches cliquées dépasse la longueur de notre mot de passe (6239 a 4 chiffres) nous attendons que le bouton gauche de la souris soit relâché puis nous jouons un son error_wav et cachons dialpad_pan.

entity* locked_door;

action new_door
{
locked_door = me;
}

La porte a une simple actionqui lui est attachée; nous mettons son nom à "locked_door" utilisant un pointeur d'entité préalablement défini.La dernière fonction compare password_str et guess_str dans une boucle while:

starter compare_strings()
{
while (1)
{
if (str_cmp (password_str,
guess_str) == 1) // pass ok?
{
snd_play (success_wav, 20, 0);
dialpad_pan.visible = off;
while (locked_door.pan < 180) // unlock the door!
{
locked_door.pan += 2 * time;
wait (1);
}
locked_door.pan = 180;
break; // get out of the while loop
}
wait (1);
}
}

C'est une fonction starte, aussi elle s'exécutera toute seule au début du jeu. Si password_str (mis à "6239") et guess_str sont identiques, nous jouons success_wav, nous cachonsle panneau du portier electronique puis nous utilisons une boucle while pour changer l'angle pan de 90 à 180 degrés. Vous pouvez changer 180 par un autre angle fonctionnant pour votre porte en fonction de son orientation bien entendu. Nous remettons l'angle à 180 degrés (il pourrait être à 180.2 degrés, etc) puis nous sortons de la boucle en utilisant "break". De cette façon nous sommes sur que la porte ne sera ouverte qu'une fois.

C'est tout! Vous pouvez utiliser la boucle qui tourne la porte par une porte coulissante, changer le x et / ou y de votre porte coulissante dans une boucle while et vous êtes prêt de nouveau!


Untitled

Codes de triche

Cette fois-ci j'ai décidé de vous montrer comment ajouter des codes de triche dans votre jeu. J'ai implémenté la triche pour la santé et l'armure mais vous pouvez ajouter autant de code de triche que vous voulez.

Tout d'abord nous définissons quelques chaînes de carctères, si vous regardez de plus près les définitions ci-dessous, vous verrez une chaîne vide, une chaîne qui stockera ce que nous frapperons au clavier, une autre qui affichera "Unknown command" si nous tapons quelque chose qu'il ne reconnait pas et 2 chaînes qui contiennent les codes de triche pour la santé et l'armure.

string
empty_str = "
";
string cheat_str = "
";
string unknown_str = "Unknown command";
string health_str = "gimmehealth";
string armor_str = "gimmearmor";

Nous définissons deux objets text; un qui sera utilisé pour la triche et un autre pour le message "Unknown command".

text
cheats_txt
{
pos_x
= 0;
pos_y = 0;
layer = 20;
font = standard_font;
string = cheat_str;
}

text
unknown_txt
{
pos_x
= 0;
pos_y = 0;
layer = 20;
font = standard_font;
string = unknown_str;
}

La fonction ci-dessous est appelée à partir du main aussi elle est exécutée au démarrage du jeu:

function
init_cheats()
{
player._health
= 30;
while
(1)
{
if (key_alt + key_p == 2)
{
cheats_txt.visible
= on;
str_cpy
(cheat_str, empty_str);
inkey
cheat_str;
if
((player != null) && (str_cmpi (cheat_str, health_str) == 1))
{
player._health = 100;
}
if ((player
!= null) && (str_cmpi (cheat_str, armor_str) == 1))
{
player._armor = 100;
}
if (str_cmpi
(cheat_str, health_str) + str_cmpi (cheat_str, armor_str) == 0)
{
unknown_txt.pos_x = screen_size.x / 2;
unknown_txt.pos_y = screen_size.y / 2;
unknown_txt.visible = on;
beep;
waitt (32);
unknown_txt.visible = off;
}
cheats_txt.visible
= off;
}
wait (1);
}
}

Nous avons mis les points de santé à 30 car nous voulons les voir se transformer :) lorsque nous taperons "gimmehealth". Les codes de triche (mots de passe si vous préférez) peuvent être saisis après que vous ayiez pressé Alt + P. Si cela se produit nous montrons cheats_txt,nous effaçons l'entrée précédente et nous montrons le curseur; jusqu'à ce que nous tapions sur Enter, les caractères saisis sont stockés dans cheat_str.

Si le joueur existe (il peut être enlevé depuis le niveau) et que cheat_str = health_str, le joueur obtient 100 points de santé; si cheat_str = armor_str le joueur obtient 100 points d'armure. J'ai utilisé str_cmpi qui comparent les chaînes sans s'occuper de la casse (majuscule / minuscule indifférent) donc GimmEHeAltH fonctionnera aussi.

Si nous tapons quelque chose de mauvais (ou si nous ne connaissons pas le code tout simplement) unknown_txt est placé au milieu de l'écran et rendu visible pendant 2 secondes et cheats_txt pourra alors disparaître. Vous pouvez ajouter autant de code de triche que vous le souhaitez en ajoutant simplement leur chaîne et leur vérification:

if
((player != null) && (str_cmpi (cheat_str, yourcheat_str) == 1))
{
your_cheat_here;


Untitled

Couperet tranchant

Elles sont grandes, elles sont mauvaises, elles sont...malicieuse, ces lames tranchantes. Elles vous tueront si elles vous touchent. Comment cela est-il possible ?

Tout d'abord vous devez créer quelques lames. Assurez vous que l'origine de la lame wmp est bien au centre, sinon ce ne sera pas génial lorsqu'elle vont pivoter.

Ici une image de la lame en action:

Vous pouvez voir que si nous obtenons une lame qui se déplace comme sur l'image, alors nous sommes prêts à y aller. Tout le code qui nous intéresse est dans cette action (attachez la à une ou plusieurs lames.wmb (blades.wmb):

action slicing_blades
{
my.enable_impact = on;
my.enable_entity = on;
my.enable_push = on;
my.push = -5;
my.event = kill_player;
while (1)
{
my.skill20 += my.skill1
* time;
my.roll = ang(my.skill2
* sin(my.skill20));
wait (1);
}
}

La lame est sensible à l'impact, une entité ou une poussée Si son évènement est déclenché, la fonction function kill_player() sera exécutée. Mais qu'est ce morceau de code?

while (1)
{
my.skill20 += my.skill1 * time;
my.roll = ang(my.skill2 * sin(my.skill20));
wait (1);
}

Je ne voudrais pas avoir un long corridor avec ( disons) 10 lames tranchantes qui se déplacent à la même vitesse; ne serait-ce pas ennuyeux? C'est pourquoi j'ai décidé d'employer skill1 pour placer la vitesse et skill2 pour placer l'amplitude (valeur maximum pour l'angle roll de la lame) pour chaque lame. Skill20 est employé pour garder la même vitesse de mouvement sur les PCS lents et rapides (j'ai expliqué comment time fait cela dans Aum6 à la rubrique FAQ). J'ai employé la fonction sinus en raison de ses variations sans heurt; vous pouvez voir que skill20 accroît tout le temps ausi sin(my. skill20) change sa valeur tout le temps, comme dans l'image:

Si quelque chose (ou quelqu'un) est touché par la lame, la fonction kill_player() est exécutée:

function kill_player()
{
if (you == player)
{
beep;
player._health
= 0;
}
}

Si l'entité qui est touchée par la lame est le joueur, un beep retentit (vous pouvez le remplacer par une instruction play_sound ) et le joueur meurt. (Nous mettons sa santé à zéero).

Voici quelques idées en plus:
- mettez le drapeau "light" pour les lames, rendait les transparentes et jouer avec leurs lumières rouge,vert et bleue (mettez lightrange à 0)
- augmentez leur ambiance et placez les dans un corridor complètement sombre
- ajoutez des sons aux lames (avec play_entsound et / ou placez quelques sons dans wed près des lames)


Untitled

Traduction Olivier Burtonn

Coups à la tête, coups au ventre ou coups aux pieds

Si vous cherchez une méthode simple pour faire que vos modèles réagissent de différentes façons selon le secteur d'impact de la balle, vous êtes venus au bon endroit. Il est tout à fait normal d'avoir un soldat ennemi mort si nous avons réussi à friser ses cheveux avec une décharge à la tête, non ? D'autre part, il n'est pas normal de tuer un ennemi en tirant une fois sur sa jambe.

Je vais révéler ma méthode - c'est rapide et elle fonctionne impeccablement. J'aime l'appeler la méthode(mets quelque chose ici) .

L'idée est simple : nous mettons quelque vertices clés à notre modèle d'ennemi et ensuite nous obtenons leur hauteur dans la boucle while. Quand une balle frappe le modèle, nous obtenons la hauteur de la balle et ensuite nous comparons cette valeur avec la hauteur de chaque vertex clé. Le vertex le plus proche déclenchera son animation spécifique. Le code est aussi simple qu'il semble l'être (est-ce de bonnes ou de mauvaises nouvelles ?)

J'emploierai un texte pour montrer une chaîne à l'écran et deux panneaux : un pour le viseur et un pour le vertice clé et pour la coordonnée de balle.

text
info_txt
{
layer = 20;
pos_x = 0;
pos_y = 0;
font = _a4font;
string = "head vertex:\nmiddle vertex:\nleg
vertex:\nhit coord:";
flags = visible;
}

panel
crosshair_pan
{
bmap = crosshair_pcx;
layer = 20;
pos_x = 0;
pos_y = 0;
flags =
transparent, refresh, d3d, visible;
}

panel
coords_pan
{
layer = 20;
pos_x = 80;
pos_y = 0;
digits = 0, 0, 4, _a4font, 1, vertex_coords.x;
digits
= 0, 9, 4, _a4font, 1, vertex_coords.y;
digits =
0, 18, 4, _a4font, 1, vertex_coords.z;
digits =
0, 27, 4, _a4font, 1, bullet_pos.z;
flags = refresh,
d3d, visible;
}

La fonction main met hors service la touche "D" (le panneau de mise au point) parce que nous emploierons ZSQD pour déplacer le joueur et ensuite elle charge le niveau :

function
main()
{
on_d = null;
level_load (damage_wmb);
}

C'est un projet autonome donc nous avons besoin d'une action pour le joueur :

action
player_moves
{
my.invisible = on; // pas besoin
de voir le joueur parce que nous sommes en mode 1ère personne
crosshair_pan.pos_x = (screen_size.x - bmap_width(crosshair_pcx)) / 2;
crosshair_pan.pos_y = (screen_size.y - bmap_height(crosshair_pcx)) / 2;
while (1)
{
vec_set (camera.pos, my.pos);
camera.tilt += 20 * mouse_force.y * time;
camera.pan -= 20 * mouse_force.x * time;
my.pan = camera.pan;
my.tilt = camera.tilt;
player_speed.x = 15 * (key_w - key_s) * time; // déplacement avant / arrière
player_speed.y = 10 * (key_a
- key_d) * time; // tourne gauche / droite
vec_set (temp, my.x);
temp.z -= 1000;
trace_mode
= ignore_me + use_box;
player_speed.z = -trace (my.x, temp);
move_mode = ignore_you + ignore_passable;
ent_move(player_speed, nullvector);
if (mouse_left == 1)
{
ent_create (bullet_mdl, player.pos, shoot_bullet);
snd_play (bullet_wav, 50, 0);
while (mouse_left == 1) {wait (1);}
}
wait (1);
}
}

Nous ne devons pas voir le joueur parce que nous sommes dans le mode 1ère personne; le panneau viseur est placé au centre de l'écran. La caméra est placée dans la position du joueur et change son pan et tilt en déplaçant la souris. Le joueur hérite son pan et tilt de la caméra et des mouvements employant les touches ZSQD. Le code de mouvement inclut la gravité; nous traçons 1000 quants au-dessous des pieds du joueur et ensuite nous déplaçons le joueur avec ses pieds sur le terrain. Si nous pressons le bouton gauche de la souris (LMB) nous créons une balle, un son est joué et ensuite nous attendons que le bouton de souris soit relaché parce que nous voulons mettre hors service le tir en rafale.

Voyons le code associé à l'ennemi :

action
enemy
{
my.enable_impact = on;
my.enable_shoot = on;
my.enable_entity = on;
my.event = compute_shot_pos;
while (my.skill5 == 0)
{
ent_cycle ("idle", 0);
vec_for_vertex(temp, my, 222); // vertex de la tête de l'ennemi
my.head_pos = temp.z;
vertex_coords.x = my.head_pos;
vec_for_vertex(temp, my, 235); // vertex du ventre de l'ennemi
my.middle_pos = temp.z;
vertex_coords.y = my.middle_pos;
vec_for_vertex(temp, my, 171); // vertex de la jambe de l'ennemi (une des deux)
my.leg_pos = temp.z;
vertex_coords.z =
my.leg_pos;
wait (1);
}
}

L'ennemi est sensible à l'impact, au tir ou à d'autres entités; si un de ces événements arrive, il exécutera la fonction compute_shot_pos. Tant que l'ennemi n'a pas été tué (skill5 à 0), il se tiendra debout. J'ai employé une frame simple pour "idle" mais vous pouvez employer une animation réelle "idle" ici. J'ai choisi 3 vertices clé pour la tête, le ventre et le pied en employant Med mais vous pouvez en choisir autant que vous voulez; leur coordonnée z est stocké dans head_pos (un autre nom pour skill1), middle_pos (skill2) et leg_pos (skill3). Nous devons obtenir ces valeurs dans la boucle while parce que si l'ennemi se déplace (saute, rampe, etc) ces coordonnées changeront.

Ok, imaginons que notre balle a frappé l'ennemi - > la fonction compute_shot_pos () s'exécutera :

function
compute_shot_pos()
{
my.skill5 = 1;
vec_set(bullet_pos, you.pos);
wait (1);
my.event = null;

Skill5 sera mis à 1, stoppant la boucle while dans l'action ennemi. Nous stockons les coordonnées de la balle avant qu'elles ne soient perdues (avant que la balle ne disparaisse), nous attendons un cycle et ensuite nous mettons my.event à nul.

if (abs(bullet_pos.z - my.head_pos) < abs(bullet_pos.z - my.middle_pos)) //
nous avons une décharge à la tête, aucun besoin de vérifier
les pieds
{
my.skill5 = 0;
while
(my.skill10 < 80) // saute la dernière frame
{
ent_cycle("head", my.skill10);
my.skill10 += 4 * time;
wait (1);
}
}

Si la distance entre la balle et le vertex de la tête est plus petite que la distance entre la balle et le vertex du milieu, nous avons une décharge à la tête donc nous jouons l'animation "head".

else // milieu ou pieds
{
if (abs(bullet_pos.z - my.middle_pos) < abs(bullet_pos.z - my.leg_pos)) //
tir au milieu
{
my.skill10 = 0;
while (my.skill10 < 80) // saute la dernière frame
{
ent_cycle("middle", my.skill10);
my.skill10 += 2 * time;
wait (1);
}
}
else // tir aux pieds
{
my.skill10 = 0;
while (my.skill10 < 80) //saute la dernière frame
{
ent_cycle("leg", my.skill10);
my.skill10 += 4 * time;
wait (1);
}
}
}
}

Si la distance entre la balle et le vertex de la tête est plus grande que la distance entre la balle et le vertx du milieu, cela pourrait être une décharge au milieu (au ventre) ou une décharge au pied donc nous jouons l'animation correcte. J'ai animé le modèle aussi prenez si garde... Vous allez voir quelque chose d'effrayant!

Les deux dernières fonctions créent les balles et les enlèvent - voici le code :

function
shoot_bullet()
{
my.enable_entity = on;
my.enable_block = on;
my.event
= remove_bullet;
my.passable = on;
my.pan = player.pan;
my.tilt = camera.tilt;

La balle est sensible à d'autres entités et aux blocs du niveau. Quand elle entre en collision avec une autre entité (notre ennemi) ou un bloc du niveau, elle sera enlevée après un cycle, nous permettant d'obtenir ses coordonnées avant qu'elle ne disparaisse. La balle a la même orientation que le joueur et la caméra.

my.skill20 = 0;
my.skill1 = 300;
my.skill2 = 0;
my.skill3 = 0;
my.skill1 *= time;

while (my.skill20 < 250)
{
if (my.skill20 < 1)  // n'entre pas en colision avec le joueur
{
my.passable = on;
}
else
{
my.passable = off;
}
my.skill20 += 1
* time;
ent_move (my.skill1,
nullvector);
wait
(1);
}
remove_bullet();
}

function
remove_bullet()
{
wait (1);
ent_remove(me);
}

Nous mettons skill20 à zéro et ensuite nous augmentons cette valeur dans la boucle while jusqu'à qu'elle atteigne 250. Si la balle n'a rien frappé quand skill20 > = 250, elle sera enlevée parce que nous ne voulons pas gaspiller du temps CPU. Skill1.. 3 sont employées comme variables locales pour déplacer la balle. Un petit truc : la balle est créée à l'intérieur du modèle du joueur donc elle doit être rendu passable au début parce qu'autrement elle entrerait en collision avec le joueur. Quand skill20 > = 1 nous pouvons supposer que la balle est assez loin et ensuite nous la rendons impassable.

Vous pouvez employer plus de vertices clé si vous voulez; vous pourriez ajouter un autre vertex clé à l'autre pied, encore 2 sur les bras, etc.


Untitled

Maîtrise des caméras

Je sais que certains d'entre vous aimeraient employer quelques codes simples de caméra c'est donc ce que je me propose de vous apprendre. Assurez-vous juste que vous incluiez cameras.wdl lorsque vous exécutez votre niveau et pressez les touches de 1 à 4 pour changer la vue : nous avons la vue supérieure, la vue de côté, la vue isométrique et la vue de marqueur. La vue de marqueur est employée pour une des "antennes" dans la première démonstration de I A et j'ai pensé que vous pourriez la trouver utile dans un de vos projets.

Je ne sais pas si vous avez noté que toutes mes démonstrations sont des projets autonomes ou des dossiers qui ne modifient pas les calibres - c'est pourquoi j'ai neutralisé la vue de défaut (camera). J'ai créé une nouvelle vue appelée aum_camera - ses paramètres sont placés dans cette fonction :

function
set_cameras()
{
camera.visible = off;
aum_camera.size_x = screen_size.x;
aum_camera.size_y = screen_size.y;
aum_camera.pos_x = 0;
aum_camera.pos_y = 0;
aum_camera.visible = on;
select_camera();
}

Vous pouvez voir que la taille pour aum_camera est lue de screen_size ; donc nous n'avons pas besoin de connaître la résolution d'écran. Nous rendons la nouvelle vue visible et nous appelons select_camera ; une simple boucle while(1) vérifie si nous avons appuyé sur les touches 1 à 4 et change la vue en conséquence :

Vue de dessus

if
(camera_number == 1)
{
aum_camera.x = player.x;
aum_camera.y = player.y;
aum_camera.z = player.z + 300;
aum_camera.pan = player.pan;
aum_camera.tilt = -90;
aum_camera.roll = 0;
}

La première caméra est vraiment simple : elle suit le joueur tout le temps, elle est placée au dessus du joueur (aum_camera.z = player.z + 300) et elle regarde par dessus le joueur (aum_camera.tilt = -90)

Vue de côté

if
(camera_number == 2)
{
aum_camera.x = player.x + 200 * sin(player.pan);
aum_camera.y = player.y - 200 * cos(player.pan) ;
aum_camera.z = player.z + 10;
aum_camera.pan = player.pan + 90;
aum_camera.tilt = 0;
aum_camera.roll = 0;
}

Cette camera demande un peu de trigonométrie, c'est pourquoi je préfère un schéma a un long discours:

Vous pouvez remplacer 200 par d'autres valeurs: plus la valeur est petite plus la caméra sera près du joueur. J'ai mis la caméra légèrement au dessus du sol (player.z + 10) et je l'ai tournée vers lui (player.pan + 90).

Vue Isométrique

if
(camera_number == 3)
{
aum_camera.x = player.x - 200 * cos(player.pan);
aum_camera.y = player.y
- 200 * sin(player.pan);
aum_camera.z = player.z + 200;
aum_camera.pan = player.pan;
aum_camera.tilt = -30;
aum_camera.roll = 0;
}

C'est une caméra pour un jeu à la 3ème personne. Elle fonctionne bien mais ne gère nullement les collisions. Si vous êtes intéressés par un bon code pour une 3eme personne, peut-être que j'en écrirais undans une prochaine édition. Voici le schéma explicatif:

La caméra est légèrement inclinée (aum_camera.tilt = -30)

Vue pour démo I A (marqueur)

if
(camera_number == 4)
{
aum_camera.x = player.x + 200 * cos(player.pan)
+ 150 * sin(player.pan);
aum_camera.y = player.y + 200 * sin(player.pan)
- 150 * cos(player.pan);
aum_camera.z = player.z;
aum_camera.pan = player.pan
+ 130;
aum_camera.tilt = 0;
aum_camera.roll = 0;
}

C'est la dernière caméra. Si vous avez regardé votre écran sur la droite, la caméra est placée dans le coin supérieur gauche de votre moniteur, vous regardant en permanence et se déplaçant avec vous. J'ai utilisé un code similaire avec 2 antennes pour la premère démo d'IA. Ici 2 schémas ::

Maintenant que vous avez entendu parler des antennes (la caméra est placé comme si c'était l'antenne avant droite dans la prochaine image), vous êtes disposé à voir le code pour l'IA...


Untitled

Disco

Ce bout de code combine musique et lumières colorées. J'ai créé un projet autonome mais vous pouvez facilement l'insérer dans le code de vos propres projets.

Jetons un oeil sur la fonction main:

function
main()
{
d3d_lightres
= 1;
fps_max = 50;
level_load (disco_wmb);
wait (2); // wait for the level to be
loaded
set_camera();
waitt (80);
while (1)
{
if (sound_handle ==
0)
{
snd_loop
(music_snd, 80, 0);
sound_handle = result;
}
wait (1);
}
}

Avant tout nous mettons d3d_lightres à 1; ceci améliore la qualité des lumières dynamiques. Nous bloquons la vitesse d'affichage à 50 fps, nous chargeons le niveau, nous positionnons la caméra et nous jouons notre fichier musical dans une boucle. J'utilise un petit exemple musical mais vous pouvez le remplacer par votre fichier wav.

function
set_camera()
{
camera.tilt = -90;
camera.y = -100;
camera.z = 800;
wait (1);
}

La fonction set_camera choisit une bonne position et un angle convenable pour la caméra; jouez avec ces valeurs pour obtenir une position et un angle différents.

Mais qu'y-a-t'il de si spécial avec la lumière disco? Je pourrais utiliser une entité invisible qui marcherait le long d'un chemin et génèrerait de la lumière ou quelque chose du même genre, non?
Bien sur que vous pourriez faire cela, mais ce morceau de code place la lumière sur le sol à une position qui est fonction de la lumière du spot mural, comme sur cette image:

Caque couleur à deux entités et deux actions; nous avons disco_red (l'action attachée à la lumière rouge sur le sol) et red_spot (l'action attachée au spot lumineux rouge), etc.

Regardons l'action qui gère la lumière rouge sur le sol:

entity* redspot_pnt;

action
disco_red
{
my.invisible
= on;
my.passable = on;
my.lightrange = 0;
my.lightred = 250;
waitt (16 + random(32));
snd_play (poweron_snd, 30, 0);
my.lightrange = 200;
while (1)
{
my.x = redspot_pnt.z
* tan (redspot_pnt.pan);
my.y = redspot_pnt.z * tan (redspot_pnt.roll);
wait (1);
}
}

L'entité est invisible, passable et génère une lumière rouge (lightred = 250). Après 1..3 secondes nous jouons le son poweron et nous donnons une portée de 200 quants à notre lumière. La lumière se déplace sur le sol en changeant ses coordonnées x et y en fonction de l'angle de redspot_pnt, un pointeur pour le spot lumineux rouge.

action red_spot
{
redspot_pnt
= me;
my.light = on;
my.lightrange = 0;
my.lightred
= 250;
waitt (80);
while (1)
{
my.pan = 30 - random(60);
my.roll = -10 - random(50);
waitt (2);
}
}

Le spot rouge luit rouge; J'utilise le même modèle pour tous les spots lumineux (rouge, vert, bleu) et j'utilise des valeurs différentes de lightred, lightgreen, lightblue values pour changer leur apparence.

Le spot attend 5 secondes (j'utilise la même valeur dans main, souvenez-vous?) puis je change ses angles pan et roll dans une boucle while (1) ;)
Les valeurs de pan et roll dépendent de notre niveau aussi vous serez certainement appelé à modifier ces valeurs en fonction de votre niveau. Vous pouvez contrôler n'importe lequel des autres angles (pan et tilt, tilt et roll, etc) à partir du moment ou vous utilisez les mêmes angles dans l'action disco_red. Avant que j'oublie, voici les maths utilisées pour ce code:

Le code pour le vert et le bleu est identique.


Untitled

Démo Temps

La nouvelle variable time_factor rend plus facile le contrôle de la vitesse de nos jeux. Voulez-vous créer tous ces temps qui donnent ces effets si particuliers dans Max Payne? C'est du gâteau!

Tout d'abord nous allons créer un panneau avec un curseur pour ajuster la vitesse dans le jeu Regardez comme c'est simple?

panel
speed_factor
{
bmap = slider_pcx;
layer = 20;
pos_x = 10;
pos_y = 25;
hslider = 10, -12, 200, cursor_pcx, 0, 10, time_factor;
flags = d3d, overlay,
refresh, visible;
}

Time_factor variera entre 0 et 10 lorsque vous bougerez le curseur. J'ai placé une arme dans le niveau. Regardez le déplacement de la roquette avec time_factor = 0 et time_factor = 10.

Mais comment obtient-on ces effets de variation de vitesse? Je vous ai préparé 2 exemples:

function
init_timedemo()
{
while (1)
{
if (key_q == 1)
{
while (key_q == 1) {wait (1);} // attend que la touche soit relâchée
time_factor = 1; // reset it
while (time_factor > 0.01)
{
time_factor -= 0.05 * time;
wait (1);
}
}
if (key_a == 1)
{
while (key_a == 1) {wait (1);} // attend que la touche soit relâchée
time_factor = 1; // reset it
while (time_factor < 10)
{
time_factor += 0.2 * time;
wait (1);
}
}
wait (1);
}
}

Lorsque nous pressons la touche "A" time_factor sera décrémentée de 1 à 0.01; vous verrez l'action s'exécuter de plus en plus lentement. Si vous pressez la touche "Q", time_factor augmentera rapidement vers 10 et toutes les actions du niveau s'exécuterons plus vite que vous ne les avez jamais vues.


Untitled

Eau réaliste

Il est temps que nous apprenions à créer un effet d'eau qui soit superbe. Jetez un oeil à l'image ci-dessus pour cimprendre ce que je veux dire.

Avant tout nous créons un terrain dans Med. J'ai utilisé les valeurs par défaut (33 x 33 vertices, 5 quants / triangle) et j'ai tiré vers le bas le premier vertex du terrain comme sur l'image ci-dessous:

Souvenez-vous de faire cela à chaque terrain utilisé pour de l'eau, sinon les sommets du terrain ne pourront pas se déplacer du tout. Choisissez n'importe quelle peau qui ressemble à de l'eau pour votre terrain et vous êtes prêt pour la suite!

define amplitude skill1;
define water_speed skill2;

define number_of_vertices 1089;

var
vertex_array [number_of_vertices];
var counter;
var index;

Mon terrain a 33 * 33 = 1089 sommets; changez cette valeur si votre terrain a une taille différente. J'ai défini un tableau qui stockera différentes valeurs pour chaque sommet et 2 variables qui sont utilisées comme compteur.

action nice_waves
{
my.transparent = on;
my.passable = on;
my.alpha = 75;
my.ambient
= -100;
if (my.amplitude == 0)
{
my.amplitude = 2; // default wave amplitude value
}
if (my.water_speed == 0)
{
my.water_speed = 10; // default wave speed value
}

Le terrain est mis transparent et passable; son facteur de transparence est mis à 75 mais vous pouvez utiliser n'importe quelle autre valeur ici. La peau utilisée dans ma démo n'est pas très sombre aussi j'ai mis ambient à une valeur négative. Si vous oubliez de mettre skill1 dans Wed, amplitude sera mis à 2; si vous oubliez water_speed, sa valeur par défaut sera 10.

    while (counter < number_of_vertices)
{
vertex_array[counter] = random(360); // set
random values in vertex_array
counter += 1;
}

TLa première boucle while remplit vertex_array avec des valeurs aléatoires; ici un résultat possible pour my.amplitude = 5:

Nous allons utiliser la fonction sinus pour nos vagues parce que ça rend bien et qu'elle est facile à utiliser. Je vous donne une rapide explication ici.

Avant tout vous pouvez noter que cette fonction ressemble à une vague, c'est plutôt bon signe! Vous pouvez voir que quelque soit la valeur de x, la valeur de sinus s'étend de -1 à +1. Nos vagues auront une amplitude donnée par la valeur sinus (de -1 à 1) multiplié par my.amplitude (skill1) mis dans Wed. Si je mets amplitude = 5 dans Wed, les sommets changeront leur valeur z de -5 à 5?

Nous avons généré des valeurs aléatoires comme point de départ pour nos vagues; jetez un oeil à l'image ci-dessous pour voir comment elles apparaîssent sur la vague sinus.

Je ne peux pas dessiner tous les points parce que il y a 1089 valeurs sur cette sinusoide! Chaque sommet démarre de sa valeur stockée dans vertex_array et ses coordonnées z (sa hauteur) change continuellement comme indiqué sur l'image ci-dessus. Revoyons de nouveau notre action:

  while (1)
{
index = 0;
while (index < number_of_vertices)
{
vec_for_mesh(temp, my, index); // store the vertex coordinates in temp
temp.z = sin(counter + vertex_array[index])
* my.amplitude; // change the z component
vec_to_mesh(temp, my, index); // deform the terrain entity
index += 1;
}
counter += my.water_speed * time;
wait(1);
}
}

Nous démarrons avec le premier sommet; nous stockons ses coordonnées dans temp en utilisant vec_for_mesh, nous mettons temp.z à la nouvelle valeur donnée par notre fonction sinus multiplié par my.amplitude puis nous changeons la hauteur du sommet en utilisant vec_to_mesh. Nous répétons le processus pour tous les sommets, sans utiliser d'instruction wait (1) dans la boucle while car nous avons besoin de 1089 cycles (5...20 secondes) pour passer par tous les sommets. C'est une de ces situations ouon peut utiliser une boucle while sans "wait".

Nous sortons de l'intérieur de la boucle dès que tous les sommets sont mis à leur nouvelle hauteur puis nous augmentons le compteur en lui ajoutant water_speed pondéré par time. Ceci déplace tous les sommets vers la droite le long du graphique de la fonction sinus; comme je l'ai dit nous pouvons augmenter le compteur encore et encore parce que la fonction sinus n'ira jamais en dessous de -1 et au dessu de 1.

C'est la fin de cet article. N'oubliez pas de mettre votre terrain à la bonne dimension dans Wed.


Untitled

Effet aquatique du pauvre

Bien, nous savons comment créer une surface d'eau qui soit agréable à regarder. Mais que se passe-t-il lorsque le joueur se trouve dessous? Voit-il (ou elle) le même bel effet de vague? Cet article vous explique comment créer un simple effet aquatique qui fonctionne avec n'importe qulle édition du moteur. L'idée est simple; nous contrôlons la hauteur du joueur dans une boucle while(1) et si elle atteint une certaine valeur nous mettons sa tête dans une spère tournante bleue:)

starter create_water()
{
sleep (1); // wait until the level is loaded
ent_create (sphere_mdl, nullvector, move_sphere);
}

Nous commençons par une fonction starter qui attend que le niveau soit chargé puis qui crée une sphère et lui attache la fonction move_sphere() :

function
move_sphere()
{
while (player == null) {wait (1);}
proc_late();
my.passable = on;
my.transparent
= on;
my.unlit = on;
my.bright = on;
my.ambient = -100;
my.alpha = 90; // play with this value
while (1)
{
my.x =
player.x;
my.y = player.y;
my.z = player.z + 40; // 40 = experimental value
my.pan = sin(total_frames * water_speed) * amplitude;
my.tilt = sin(total_frames * water_speed) * amplitude;
my.roll = cos(total_frames * water_speed) * amplitude;
if (my.z < -38) // experimental value, depends on the height of the water surface
{
my.invisible = off;
}
else
{
my.invisible = on;
}
wait(1);
}
}

Cette fonction attend que le joueur soit chargé et rend le modèle de sphère passable, transparent, unlit et bright. J'ai choisi une valeur négative pour ambient, mais vous pouvez également noircir la peau avec un logiciel de peinture. Vous pouvez également vouloir jouer avec le facteur de transparence alpha. Cette sphère est située 40 quants autour de la tête du joeur, valeur expérimentale dépendant de la hauteur de votre joueur.

J'utilise les fonctions sin et cos pour créer la sphère autour; libre à vous d'utiliser d'autres combinaisons de sin et cos pour les angles, et n'oubliez pas de jouer avec les valeurs de water_speed et. Comme je l'ai dit le code vérifie si le z du joueur est inférieur à 38 ou pas pour rendre la sphère visible ou invisible. Cette valeur dépend de la hauteur de la surface de l'eau et à besoin d'être ajustée en fonction de vos besoins.

J'ai utilisé un simple sprite transparent pour la surface de l'eau mais vous pouvez utiliser n'importe quelle autre méthode y compris le système basé sur le terrain d'eau présenté dans un AUM il y a quelqu temmps déjà.


Untitled

Effets basés sur les sommets

Cet effet spécial parait génial! Vous pouvez l'utiliser pour la téléportation, rendre des monstres affreux, etc. L'idée est très simple : chaque sommet se comportera comme générateur de particules:

L'action attachée au modèle a besoin d'exécuter une fonction qui initialise le code; j'ai appliqué cet effet basé sur les sommets au modèle qui utilise patrol_path dans la démonstration office et voici ce que j'ai modifié dans l'action patrol_path:

action
patrol_path
{
actor_init();
init_vertex(); // new line
// attach next path
temp.pan = 360;
temp.tilt = 180;
temp.z = 1000;
result = scan_path(my.x,temp);
if (result == 0) { my._MOVEMODE = 0; } // no path found
...................
}

Vous pouvez noter que ce que j'ai seulement changé dans le code, c'est la ligne rouge qui appelle init_vertex(). J'ai inclu le fichier actors.wdl modifié mais pensez à faire une sauvegarde du fichier actors.wdl actuel avant de le remplacer par celui que je vous fournis. Il se peut qu'au moment où vous lisez ce code, un nouveau template soit sorti et il se peut que vous ne retrouviez pas certaines fonctionnalités si vous décidiez de modifier vous même le fichier d'origine..

Ajoutez simplement la ligne en rouge à n'importe quelle action qui est attachée au modèle et vous serez ok. Regardons ce que fait la fonction init_vertex() does:

function
init_vertex()
{
var vertices; // local var
my.invisible = on;
while (1)
{
vertices = ent_vertices
(my);
while (vertices
> 0)
{
vec_for_vertex (temp, my, vertices);
effect (attach_flares, 1, temp, normal);
vertices -= 1; /
}
wait (1);
}
}

Nous définissons une variable locale nommée vertices et nous cachons le modèle; bien entendu vous pouvez montrer le modèle également mais je pense que l'effet est beaucoup mieux si on ne le voit pas. Nous obtenons le nombre de sommets pour le modèle puis nous prenons les coordonnées de chaque sommet à l'intérieur d'une boucle while sans wait(1) car la boucle a besoin de mettre à jour les sommets le plus vite possible. Nous utilisons la même boucle pour créer un signal lumineux puis nous passons au sommet suivant (en décrémentant).

La fonction attach_flares est une fonction classique de particules:

function
attach_flares()
{
my.bmap = flare_pcx;
my.flare = on;
my.bright = on;
my.size = 10;
my.function = remove_flares;
}

function remove_flares()
{
my.lifespan = 0; // remove the flare particle
}

Les particules utilisent l'image bitmap flare_pcx avec ses drapeaux flare et bright de mis; sa taille est 10. La fonction qui gère le signal lumineux (remove_flares) les enlève instantanément, en clair leur durée de vie n'est que d'un cycle. Vous pouvez utiliser une valeur plus grande pour lifespan si vous voulez créer des trainées de particules, etc mais quand (et si) vous le faites, jetez un oeil sur le compteur de particules qui apparaît dans le panneau de mise au point.


Untitled

Empreintes de pas :

Employez ce code et vous pourrez voir les empreintes de pas du warlock lorsqu’il il marche dans la neige. Vous devez commencer 'init_footprints() dans Main:

function init_footprints()
{
while (player == null) {wait (1);}
vec_set(temp, player.x);
temp.z -= 500;
trace_mode = ignore_me + ignore_passable + ignore_models
+ ignore_sprites;
leftfoot_coords.z = player.z
- trace (player.x, temp) +
5; // place l’empreinte 5 quants au dessus du
sol
rightfoot_coords.z = leftfoot_coords.z;
// nous n’avons pas besoin de 2 traces ici
while (1)
{
leftfoot_coords.x = player.x
- left_dist * sin(player.pan);
leftfoot_coords.y = player.y
+ left_dist * cos(player.pan);
rightfoot_coords.x = player.x
+ right_dist * sin(player.pan);
rightfoot_coords.y = player.y
- right_dist * cos(player.pan);
if (player_pos1 != player_pos2) // the player is moving
{
if (anim_str == anim_walk_str)
{
if (player.frame % 5 < 0.2)
{
ent_create(footprint_pcx,
rightfoot_coords, create_footprints);
}
if (player.frame % 7 < 0.2)
{
ent_create(footprint_pcx,
leftfoot_coords, create_footprints);
}
}
}
vec_set (player_pos1, player.pos);
wait (1);
vec_set (player_pos2, player.pos);
}
}

Cette fonction est commencée dans Main aussi nous devons nous assurer que le joueur existe avant de l’exécuter; ce qui est fait à la première ligne . Nous exécutons une trace pour obtenir la taille extérieure, comme dans cette image :

La trace renvoie la distance entre le joueur et le sol; nous soustrayons cette distance de la taille du joueur et nous obtenons la hauteur du sol. Nous ajoutons 5 quants au résultat pour nous assurer que notre image d'empreinte ne soit pas placée au-dessus du sol. Joliment simple, non? Nous n'avons pas besoin de faire une autre trace pour le pied droit, aussi nous copions la distance de leftfoot_coords.

Maintenant que nous savons la coordonnée z pour les empreintes de pas, nous devons calculer les coordonnées x et y. Warlock.mdl est un modèle étrange parce que la distance entre ses pieds et l'origine n'est pas égale, mais j'ai voulu employer ce modèle bien qu'il ait exigé plus de travail:

J'ai employé une formule simple pour calculer les positions xy pour les empreintes de pas; vous devrez choisir différentes valeurs de left_dist et de right_dist si vous changez de modèle afin de faire apparaître les empreintes naturelles.

Vous savez que je n'aime pas utiliser les calibres (à moins que je sois forcé de le faire) aussi j'emploie ma méthode originale pour vérifier si le joueur se déplace; Je stocke ses coordonnées dans un vecteur et les compare aux coordonnées qui sont stockées une frame plus tard. Si ces valeurs sont égales, le joueur se tient tranquille; si non, le joueur se déplace.

if
(anim_str == anim_walk_str)
{
if (player.frame % 5 < 0.2)
{
ent_create(footprint_pcx,
rightfoot_coords, create_footprints);
}
if (player.frame % 7 < 0.2)
{
ent_create(footprint_pcx,
leftfoot_coords, create_footprints);
}
}

Si le joueur marche (anim_str = anim_walk_str) et son paramètre de frame est de près de 5 (le pied droit touchera le sol bientôt) nous créons l’empreinte de pas droite. La même chose se produit avec l'empreinte de pas gauche quand player.frame est près de 7. Mais pourquoi n’ avons-nous pas employé if (player.frame == 5)? La réponse est simple: player.frame peut ne pas être égal à 5; nous pourrions avoir player.frame = 4. 98 et après une frame player.frame = 5. 02 aussi l'empreinte de pas ne seraient jamais créées.

Ces empreintes de pas sont créées uniquement lorsque le joueur marche; vous devrez ajouter un code semblable pour la course, le crowch, etc..

if (anim_str == anim_run_str)
{
....................
}

et utilisez les valeurs correctes de frame pour chaque type d'animation.

Jetez un regard à la fonction qui crée les empreintes de pas:

function create_footprints()

{
my.passable = on;
my.oriented = on;
my.scale_x = 0.15;
my.scale_y = 0.15;
my.tilt = 90;
my.pan = player.pan;
my.flare = on;
while (my.alpha > 0)
{
my.alpha -= 0.5 * time;
wait (1);
}
ent_remove (me);
}

Nos empreintes de pas sont passables et ont le même angle pan que le joueur ainsi elles semblent normales. J'ai employé des grandes images pour les empreintes de pas et je les ai beaucoup réduites pour m'assurer qu'elles semblent bonnes. Les empreintes de pas s’effacent lentement et quand elles deviennent invisibles nous les enlevons.

Hé! Que se passe-t-ili si mon joueur escalade une colline? La taille pour ces empreintes de pas est calculée seulement une fois!

Vous devrez déplacer le code de trace à l'intérieur de la boucle while et tout sera ok.


Untitled

Escalator

Si vous avez besoin de transporter votre joueur en utilisant un escalator alors ce code est pour vous.

Nous avons une action simple qui est attachée à notre escalator wmb.

var escalator_speed = 2;

action
escalator
{
while (1)
{
my.v += escalator_speed * time;
wait (1);
}
}

Cette action change tout simplement le "v"de la texture bois qui est appliquée sur l'escalator, donnant ainsi l'illusion d'un mouvement. J'aurais du utiliser une autre texture, plus appropriée mais la seule qui se trouve dans le wad standard et qui est la plus appropriée est "woodflhori".

starter trace_below()
{
var trace_pos;
while (player == null) {wait (1);}
while (1)
{
vec_set(trace_pos.x, player.x);
trace_pos.z -= 1000;
trace_mode = ignore_me + ignore_passable + ignore_models + ignore_sprites + scan_texture;
trace (player.x, trace_pos.x);
if (str_cmpi ("woodflhori", tex_name))
{
player.y += escalator_speed * time;
player.z += 0.5 * time; // experimental
value
}
wait (1);
}
}

La fonction ci-dessus attend que le joueur soit créé et "trace" 1000 quants sous les pieds du joueur à chaque cycle. Si trace détecte la texture nommée "woodflhori" sous les pieds du joueur, il augmente le x et le z du joueur. Notez au passage que le y du joueur est incrémenté de la même valeur que celle utilisée pour le "v" afin de synchroniser lemouvement de l'escalator avec celui du joueur. Vous pouvez utiliser des valeurs négatives pour y ou replacer le mouvement sur l'axe des y par le mouvement sur l'axe des x en fonction de l'orientation de l'escalator. Regardez les images ci-dessous pour voir tout cela en détail.

Nous augmentons également lla hauteur du joueur (z); ceci l'aide à monter plus facilement avec un petit montant et laisse le code de la gravité faire le reste du boulot, tirant le joueur vers le sol du niveau. Jouez avec la valeur 0.5 jusqu'à ce que vous soyez heureux du résultat et n'oubliez pas de jouer également avec la vitesse de l'escalator.


Untitled

Champ d'étoiles

MPlusieurs de vos jeux peuvent utiliser un champ d'étoiels; l'effet rend bien et le code n'est pas compliqué du tout.

Je reconnais que l'image ne rend pas très bien l'action mais exécutez la démo et vous l'aimerez; sinon vous pouvez demander à être remboursé ;). Ici une copie d'écran du niveau pris dans Wed:

La caméra apparaît à l'origine et pointe dans la direction montrée par la flèche jaune. uoi d'autre? Le point rouge est une entité invisible qui génère des particules et ces particules se déplacent en direction de la caméra, générant l'effet de champ d'étoiles. J'ai utilisé une texture normale pour les murs, mais j'ai ajouté une petite source de lumière près de l'un des coins du cube, de cette façon le niveau paraît réellement sombre.

function main()
{
fps_max = 50;
level_load
(stars_wmb);
wait (3);
camera.pan = 0;
}

La fonction principale (main) limite le taux d'affichage à 50, charge le niveau, attend jusqu'à ce que le niveau soit chargé puis nous mettons une orientation correcte pour la caméra.

action particle_generator
{
my.invisible = on;
my.passable = on;
while(1)
{
effect (particle_init, 5, my.pos, normal);
wait (1);
}
}

L'action ci-dessus est attachée au modèle (rouge) qui génère les particules : elle fait le modèle invisible et passable puis nous générons 5 particules chaque cycle, utilisant les coordonnées du modèle (my.pos) au point de départ pour les particules.

function particle_init()
{
temp.x -= 2 + random
(1) ;
temp.y += 1 - random (2);
temp.z +=
1 - random (2);
vec_normalize (temp, 5);
vec_add
(my.vel_x, temp);
my.bmap = star_pcx;
my.alpha
= 50; // play with this value
my.size = 0.1;
my.flare = on;
my.bright = on;
my.move = on;
my.lifespan = 1000;
my.function = kill_particles;
}

La fonction qui gère les particules génère une vitesse positive aléatoire sur l'axe des x et vitesse aléatoire (positive ou négative) sur l'axe des y et des z. La magnitude du vecteur temp" est mis à 5; vous aurez à changer une seule valeur (5) si vous voulez augmenter ou diminuer temp.x, temp.y et temp.z simultanément.

Les prochaines lignes de code ajoute la vitesse résultante "temp" à la vitesse de la particule et met star_pcx comme image bitmap utilisée pour les étoiles. La particule a une valeur initiale alpha de 50 et une petite taille (0.1); ses drapeaux flare, bright et move sont mis. Finalement, la durée de vie (lifespan) de la particule est mis à 1000 et la fonction associée à la particule est kill_particles.

function kill_particles()
{
my.size += 0.01 * time;
if (my.x <= 0)
{
my.lifespan = 0;
}
}

La taille de la particule augmente lorsqu'elle s'approche du joueur, si la particule se retrouve derrière la caméra (my.x <= 0) elle est terminée. Voici 2 images du même champ d'étoiles avec le drapeau "beam" et "streak" mis pour les particules. Le code dans cet article inclut les lignes nécessaires; si vous utilisez A5 pro, A6 commerciale ou A6 pro vous pouvez enlever les 2 // de commentaires.


Untitled

Explosion de barils

Ces barils sont mortels; ils éclateront quand ils seront frappés par des balles, des fusées, etc..; plus que cela, ils éclateront si un autre baril a éclaté tout près. Pouvez-vous imaginer ce que vous pouvez faire à un dépôt de carburant en tirant un seul coup de feu? N’approchez pas trop d’un de ces barils parce que les ennemis peuvent tirer dessus aussi.

Vous pouvez avoir autant de barils que vous voulez; placez leurs modèles dans votre niveau et attachez-leu l'action de barrel:

action barrel
{
my._DAMAGE = 300;
my._SIGNAL = 0;
my.enable_scan = on;
my.enable_shoot = on;
my.enable_detect = on;
my.event = barrel_explodes;
}

J'emploie un peu de codes des calibres ici; _ DAMAGE est plus partlant que skill16, n'est-ce pas? Prochaine ligne, my. _ SIGNAL = 0; dit à l'ennemi que le baril n'est pas le joueur, ainsi il ne devrait pas attaquer le baril:). Le baril est sensible au scan_entity et aux balles, il peut détecter des entités et si son événement est déclenché il exécutera la fonction barrel_explodes

function
barrel_explodes()
{
if ((event_type == event_scan && indicator == _EXPLODE)
|| (event_type == event_shoot && indicator == _GUNFIRE))
{
waitt (3);
range = my._DAMAGE * 2;
damage = my._DAMAGE;
temp.pan = 360;
temp.tilt = 180;
temp.z = range;
indicator = _EXPLODE;
scan_entity (my.x, temp);
morph (barrel_sprite, me);
while (my.frame < 7)
{
my.frame += 3 * time;
wait (1);
}
actor_explode();
}
}

Si le baril est frappé par une roquette (ou une grenade, etc..) ou par une balle, nous attendons (waitt(3)) pour éviter l'avertissement « dangerous instruction » et pour obtenir un petit retard, utile pour des explosions enchaînées. Nous nous préparons à un balayage total du secteur (pan = 360, tilt = 180, distance = 600 quants) et nous plaçons l'indicateur à _ EXPLODE – c’est de cette façon que nous disons aux autres que ce n'est pas un balayage amical (pour ouvrir des portes ou équivalent).

J'ai défini un sprite d'explosion de baril (il est le sprite explo+7 qui vient avec les calibres mais j'ai changé son nom) et nous remplaçons barrel.mdl par barrel_sprite en utilisant le morphing. Le sprite d'explosion joue ses frames d'animation et alors il éclate en utilisant l'action prédéfinie d'actor_explode() dans les calibres.


Untitled

Traduction Olivier Burton

Fondu enchaîné

Pourquoi auriez-vous besoin 'un fondu enchainé? Bien, peut-être parce que vous voulez créer une transition lisse entre deux scènes différentes, montrer les crédits, montrez au joueur et cetera. Les bonnes nouvelles sont que le code à l' intérieur de cet article est simple et n' emploie pas trop de mémoire vidéo (moins de 3Ko). N' employez pas de panneaux; ils auront besoin dec beaucoup plus de mémoire!

C' est le coin du débutant donc l' idée est simple : nous créons une entité, nous la plaçons devant la caméra et nous mettons un facteur alpha variable pour cela. Jetons un coup d' oeil au code qui crée l' entité :

entity
fader_ent
{
type = <fader.mdl>;
layer = 40;
alpha = 0;
pan = 90;
flags = transparent,
visible;
view = camera;
x = 250; // 250 quants devant
la vue
y = 0;
z = 0;
}

J' ai employé un modèle nommé fader.mdl et j' ai changé son angle pan pour m' assurer qu' il couvre la vue. L' entité est transparente et visible et elle est placée 250 quants devant la vue. Maintenant voyons les fonctions qui créent les effets fade in et fade out :

function
fade_in()
{
while (fader_ent.alpha
> 5)
{
fader_ent.alpha -= 5 *
time;
wait (1);
}
fader_ent.alpha = 0;
}

function
fade_out()
{
while (fader_ent.alpha < 95)
{
fader_ent.alpha += 5 * time;
wait (1);
}
fader_ent.alpha = 100;
}

on_i fade_in;
on_o fade_out;

La première fonction (fade_in) s'exécute quand nous appuyons sur la touche "i" et elle diminue fader_ent.alpha jusqu' à ce que sa valeur soit en dessous de 5, le met ensuite à zéro (comme si l' entité n' existait pas). Utilisez cette fonction quand vous avez employé fade_out, l' écran est complètement noir et vous voulez retourner au jeu. La fonction fade_out obscurcira l' image, cachant l' écran progressivement; elle augmente fader_ent.alpha jusqu' à ce que sa valeur devienne plus grande que 95 et la met ensuite à 100 (complètement sombre). Commencez votre niveau et appuyer sur "o" pour un fade out et appuyer ensuite sur "i" pour un fade in . Ajoutez une de ces lignes de code à une action ou à la fonction employée dans votre jeu :

Fade_in ();

Fade_out ();

Demandez fade_in ou fade_out en fonction de vos besoins. Bien sûr vous pouvez changer la couleur de la peau du modèle d' entité pour simuler le sang, l' eau, la pluie, etc. Jetez un coup d' oeil à l' image ci-dessous pour voir un exemple qui emploie un modèle fader avec une peau complètement rouge.


Untitled

Fusées éclairantes

Je dois admettre qu'un niveau sombre apparaît beaucoup mieux lorsqu'une lumière l'éclaire de temps en temps. Vous pouvez ajouter un peu de "spectacle" en faisant une zone complètement noire et permettre au joueur de,lancer des éclats qui génèreront une lumière pour un temps limité comme une fusée éclairante par exemple. C'est ce que nous allons voir à présent

starter
init_flares()
{
while (player == null) {wait (1);}
while (1)
{
if (key_f
== 1)
{
while (key_f == 1) {wait (1);}
ent_create (flare_pcx, player.x, create_flare);
}
sleep (0.1);
}
}

La fonction ci-dessus s'exécute par elle-même au début du jeu; elle attend que le joueur soit créé puis nous créons un éclat chaque fois que la touche "F" est pressée. La dernière ligne fait que la boucle est suspendue pendant 100ms, ce qui fait que nous pouvons créer 10 éclats par seconde, ce qui est suffisant pour votre clavier :).

function
create_flare()
{
my.burn_time = 10; // this flare lasts for
10 seconds
my.passable = on;
my.flare = on;
my.ambient = 100;
my.pan = camera.pan;
my.tilt = camera.tilt;
my.enable_entity = on;
my.enable_impact = on;
my.enable_block = on;
my.event = show_flare;

Chaque éclat (flare) a 10 secondes d'existance; burn_time n'est rien d'autre qu'un autre nom pour skill20. Le sprite utilisé pour l'éclat est passable au moment de sa création, a son drapeau flare de mis et son ambient est à 100. L'éclat hérite des angles pan et tilt utilisés par la caméra et il est rendu sensible aux impacts avec les autres entités et blocs de niveau. La fonction d'évènement associée à l'éclat est la fonction show_flare.

  while (my.skill10 == 0)
{
if (vec_dist (my.x, player.x) > 50) {my.passable = off;}
my.skill1 = 50 * time;
my.skill2 = 0;
if (my.skill40 < 100)
{
my.skill3 = 0;
}
else
{
my.skill3 = - 5 * (my.skill40 / 100) * time;
}
move_mode = ignore_you + ignore_passable;
my.skill30 = ent_move (my.skill1, nullvector); // get the distance that was covered
this frame in skill30
my.skill40 += my.skill30;
// store the distance covered so far in skill40
wait (1);
}
}

L'éclat se déplacer tant que son skill10 n'est pas à zéro, et il devient impassable dès qu'il est à plus de 50 quants de son créateur (le joueur). Le sprite se déplace avec une vitesse donnée 50 * time; son mouvement inclut la gravité (vitesse négative en z mise par skill3) si l'éclat a parcouru une distance supérieure à 100 quants. Regardez l'image ci-dessous pour comprendre.

L'éclat ignore toutes les entités passables qu'il peut trouver sur son chemin (eau etc). La distance parcourue par l'éclat pendant un cycle est stockée dans skill40. Si cette valeur est plus grande que 100 quants, nous commençons à diminuer la hauteur du sprite éclat. Que se passe-t-il lorsque l'éclat touche quelque chose. Sa fonction d'évènement show_flare est exécutée:

function
show_flare()
{
my.skill10 = 1; // stop moving
my.passable = on; // become passable
while (my.burn_time
> 0)
{
my.lightred = 255;
my.lightgreen = 200 + random(55);
my.lightblue = 230;
my.lightrange = 200 + random(50);
my.scale_x = 0.9 + random(1) / 10; // scale = 0.9 to
1
my.scale_y = my.scale_x;
my.scale_z = my.scale_x;
my.burn_time
-= time / 16;
wait (1);
}

Dès que l'éclat touche un bloc de niveau ou une autre entité son skill10 est mis à 1; de cette façon la boucle while de la fonction précédente est automatiquement terminée et du coup cela stoppe le déplacement de l'éclat. Nous rendons l'éclat passable car nous ne voulons pas qu'il puisse géner les déplacements du joueur puis nous lui faison sgénérer une lumière dynamique blanche avec de légères variations de rayons et de couleurs. La taille du sprite éclat change de 0.9 à 1, et nous otons "1" de burn_time chaque seconde. Notez que je change également les dimensions de scale_z (inutile sur un sprite) mais cela vous permet d'utiliser un modèle 3D à la place du sprite si vous le souhaitez.

while (my.scale_x > 0.1)
{
my.scale_x -= 0.1 * time;
my.scale_y = my.scale_x;
my.scale_z = my.scale_x;
my.lightrange -= 3 * time;
wait (1);
}
ent_remove (me);
}

Cette nouvelle boucle démarre lorsque burn_time descend en dessous de zéro. L'échelle du dprite et son lightrange sont décrémentés continuellement jusqu'à ce que scale_x soit inférieur à 0.1, et le sprite est enlevé. N'hésitez pas à faire des essais avec différents sprites et valeurs de rayon et couleur pour vos éclats.


Untitled

Goutte d'eau

Cet effet donne beaucoup plus de valeur à votre jeu. Je l'utilise dans un de mes niveaux pour un tuyau qui fuit et (pourquoi pas) pour de la pluie!

Mon exemple montre des gouttes qui tombent depuis un tuyau percé et produit des ondulations lorsqu'elles touche l'eau en dessous. Nous avons besoin de placer un générateur de gouttes à l'intérieur de notre niveau et c'est cette action qui lui est attachée:

action
drop_generator
{
my.invisible = on;
my.passable = on;
while (1)
{
ent_create(drop_mdl,
my.pos, falling_drop);
sleep (4 + random(2));
}
}

Le générateur de gouttes sera invisible et passable aussi nous pouvons utiliser n'importe quel modèle pour lui; j'utilise le modèle de goutte lui-même. Le générateur crée une goutte à sa position chaque 4...6 secondes. La goutte qui est créée exécute la fonction nommée falling_drop:

function
falling_drop()
{
my.flare = on;
vec_set(temp, my.pos);
vec_set(drop_height, my.pos);
temp.z -= 5000; // trace
5000 quants below the drop generator
trace_mode = ignore_me + ignore_models + ignore_sprites;
drop_height.z = my.z - trace(my.x,
temp) + 2; // place the ripple sprite 2 quants above the water surface
while (my.z > drop_height.z)
{
my.z -= 3 * time;
wait (1);
}
ent_create(ripple_pcx, my.pos, ripple_effect);
ent_remove(me); // remove
the drop
}

Le modèle de goutte a son drapeau flare de mis. Nous traçons entre la position actuelle de la goutte et une position placée 5000 quants en dessous, et stockons le résultat (lahauteur jusqu'à la surface de l'eau) dans drop_height.z. Nous ajoutons 2 quants au résultats car nous voulons placer le sprite utilisé pour l'ondulation au-dessus de la surface de l'eau.

Nous faisons tomber la goutte en changeant son z dans une boucle while puis lorsque nous sommes à la même hauteur que la surface de l'eau nous créons l'effet donde (en utilisant le sprite nommé ripple_pcx) puis nous enlevons la goutte.

function ripple_effect()
{
ent_playsound (my, drip_wav, 500);
my.oriented = on;
my.flare = on;
my.alpha = 60;
my.tilt = 90;
while (my.scale_x < 5)
{
my.scale_x += 0.03
* time;
my.scale_y = my.scale_x;
if (my.scale_x
> 3)
{
my.alpha -= 1 * time;
}
wait (1);
}
ent_remove(me); // remove the sprite
}

Nous jouons le son drip_wav , nous mettons les drapeaux "oriented" et "flare" pour le sprite et nous choisissons le bon angle "tilt" et la bonne valeur "alpha" pour lui. Nous commençons à augmenter la taille du sprite dans une boucle while, diminuons son alpha lorsque que sa taille est 3 fois supérieure. Nous augmentons sa taille jusqu'à 5 fois puis nous l'enlevons.


Horloge et instructions de chaîne

J'ai été étonné de voir que créer une horloge temps réel était si facile; j'ai créé deux chaînes de caractères:

STRING clock_string, " "; // hh:mm ss = 8 caractères qui contiennent l'heure sous la forme : 17:34 56
STRING temp_string, " "; // 2 caractères pour stocker temporairement la donnée qui sera ajoutée à clock_string le temps au temps.

J'utilise clock_text qui montre la chaîne clock_string (l'heure) à l'écran; la fonction start_clock fait tout le travail.

function
start_clock()
{
WHILE (1)
{
str_for_num(clock_string, SYS_HOURS);
str_cat(clock_string, ":");
IF (SYS_MINUTES < 10)
{
str_cat(clock_string, "0");
}
str_for_num(temp_string, SYS_MINUTES);
str_cat(clock_string, temp_string);
str_cat(clock_string, " ");
IF (SYS_SECONDS < 10)
{
str_cat(clock_string, "0");
}
str_for_num(temp_string, SYS_SECONDS);
str_cat(clock_string, temp_string);
WAIT (1);
}
}

Parcourons cette fonction ligne par ligne :

str_for_num(clock_string, SYS_HOURS);
Lit SYS_HOURS qui est un nombre, le convertit en chaîne et le mets dans clock_string (les deux premiers caractères)

str_cat(clock_string, ":");
On ajoute le caractère ':' à la chaîne (en troisième position) pour créer une horloge qui ressemble à ceci 12:32

IF (SYS_MINUTES < 10) // ajoute un "0" si les minutes ne sont que sur un caractère
{
str_cat(clock_string, "0");
}
SYS_MINUTES montrera 12: 7 au lieu de 12:07pour SYS_MINUTES inférieur à 10, donc nous ajoutons 0 quand c'est nécessaire

str_for_num(temp_string, SYS_MINUTES);
Lit SYS_MINUTES et le convertit en chaîne (nous utilisons temp_string puis nous le copierons dans clock_string)

str_cat(clock_string, temp_string);
Nous ajoutons les minutes à clock_string; les minutes doivent être en 4ème et 5ème position dans clock_string

str_cat(clock_string, " ");
Ajoute un espace à la sixième position sur l'écran

IF (SYS_SECONDS < 10) // ajoute un "0" si les secondes ne sont que sur un caractère
{
str_cat(clock_string, "0");
}
Nous ajoutons un "0" si les secondes sont inférieures à 10

str_for_num(temp_string, SYS_SECONDS);
Nous lisons SYS_SECONDS et le convertissons en chaîne; le résultat est placé dans temp_string

str_cat(clock_string, temp_string); // add seconds to clock_string
Nous ajoutons les secondes à clock_string (en 7ème et 8ème position))

Tout ceci est placé dans une boucle WHILE(1), donc est actualisé continuellement.


Untitled

Première démonstration d' IA (Intelligence Artificielle)

Vous rappelez-vous l'action standard de patrouille qui est fournie avec les calibres ? Vous avez dû placer quelques positions au niveau et ajuster leur angle pan pour créer un chemin virtuel. J'aime beaucoup cette action mais c'est une manière trop compliquée pour être expliqué ici, aussi j'ai dû la récrire. J'ai ajouté un peu de code d'IA et le résultat est le FAID, la démonstration qui fonctionne avec A4 et A5.

Avant que je commence à expliquer ce que j'ai fait, veuillez charger le niveau aidemo1 dans wed et jetez lui un coup d'oeil : il y a seulement quatre positions près des coins et beaucoup de boîtes sur le chemin du garde. Peut-il le faire ? Peut-il éviter tous ces obstacles ? Pourquoi est-ce que vous n'exécuteriez pas ce niveau en ce moment et voir pour vous-même ? Exécutez aidemo1.wdl.

J'ai jeté un bon coup d'oeil (au moins trois fois) et je suis encore fier du résultat. J'ai employé une idée simple : la garde a deux "antennes" qui sont placées comme ceci :

Si au moins une des antennes (front_right ou front_left) est à l'intérieur d'un bloc plein (content_solid), la garde évite l'obstacle jusqu'à ce que l'antenne sorte du bloc plein.

Maintenant vous devriez charger le niveau aumai1 ; n'oubliez pas d'exécuter le fichier ai1.wdl. Déplacez le bloc près du garde et voyez comment (et si) il parvient à éviter le bloc. Vous pouvez voir les antennes - svp noter qu'elles ne sont pas nécessaires parce que nous employons des vecteurs (pas entités) pour vérifier '"content" ; J'ai créé ces antennes visibles pour vous montrer comment cet algorithme fonctionne. Quand vous voulez enlever les antennes mettez simplement ces deux lignes en commentaires dans ai1.wdl :

create (<marker.mdl>, front_right, frlaction);
create (<marker.mdl>, front_left, frlaction);

J'ai essayé d'employer des noms décents et signicatifs pour les variables aussi si vous voulez utiliser plusieurs unités "intelligentes" vous devrez renommer quelques variables en my.skillxx, copiez leur valeur dans temp (au besoin) et ainsi de suite.

Veuillez noter que les explications suivantes se rapportent à ai1.wdl. La fonction init_demo() est appelée dans main et place la position et les angles de la caméra selon nos besoins :

function
init_demo()
{
camera.x = 0;
camera.y = 0;
camera.z = 440;
camera.tilt = -90;
camera.pan = 270;
}

Tout le code qui nous intéresse est dans l'action guard_patrol. Avant tout nous devons créer le bloc qui sera utilisé pour les tests:

block_pos.x
= 0;
block_pos.y = 0;
block_pos.z = 0;
create (<block.wmb>, block_pos, move_block);

Nous plaçons le cube (block.wmb) à l'origine au début du jeu; l'action qui contrôle ses mouvements (move_block) est très simple et n enécessite pas d'explication ici.

La fonction create_markers() crée les“antennes”:

function
create_markers()
{
while (1)
{
front_right.x = guard_syn.x + 50
* cos(guard_syn.pan) + 20 * sin(guard_syn.pan);
front_right.y = guard_syn.y + 50
* sin(guard_syn.pan) - 20 * cos(guard_syn.pan);
front_right.z = guard_syn.z;

       front_left.x = guard_syn.x + 50 *
cos(guard_syn.pan) - 20 * sin(guard_syn.pan);
front_left.y = guard_syn.y + 50 *
sin(guard_syn.pan) + 20 * cos(guard_syn.pan);
front_left.z = guard_syn.z;

       fright_marker = content(front_right);
fleft_marker = content(front_left);
if (fright_marker == content_solid)
{
obstacle_right
= 1;
}
else
{
obstacle_right
= 0;
}
if (fleft_marker == content_solid)
{
obstacle_left
= 1;
}
else
{
obstacle_left
= 0;
}
wait (1);
}
}

N'oubliez pas que j'utilise des fonctions et des actions simplifiées pour que vous puissiez comprendre plus facilement.

Nous créons les vecteurs antenne (front_right and front_left) au devant du joueur puis nous vérifions si leur contenu est content_solid. Si une des antennes est à l'intérieur d'un bloc solide, la variable correspondante (obstacle_right or obstacle_left) est mise à 1.

Ici le reste de l'action guard_patrol:

my_pos.x
= destination.x - my.x;
my_pos.y
= destination.y - my.y;
result
= vec_to_angle(my_angle, my_pos);
if
(result < 30)
{
vec_set (my_pos, destination);
my_angle.pan = my.skill12;
temp.pan = 60;
temp.tilt = 90;
temp.z = 2000;
scan_pos (my_pos,my_angle,temp);
if(result > 0)
{
vec_set (destination,
my_pos);
my.skill12
= my_angle.pan;
}
}
my.skill13
= ang (my_angle.pan - my.pan);
if
(obstacle_right != 1 && obstacle_left != 1)
{
my.pan += 0.2 * my.skill13 * time;
}

Le garde calcule la distance entre ses coordonnées et la prochaine position, se tourne vers elle et si il est proche d'elle, (result < 30) il cherche une nouvelle position. Si il trouve une nouvelle position (result > 0) et que les antennes ne sont pas dans un bloc solide, il se tourne tranquillement vers la cible. Skill13 conserve la différence entre l'angle pan du garde et celui de la cible.

if
(obstacle_right == 1)
{
my.skill30 = my.pan;
while (my.pan < my.skill30 + 80)
{
marker_distance
= 0;
my.pan +=
5 * time;
move_guard();
wait (1);
}
}
if
(obstacle_left == 1)
{
my.skill30 = my.pan;
while (my.pan > my.skill30 - 80)
{
marker_distance
= 0;
my.pan -=
5 * time;
move_guard();
wait (1);
}
}
if
(obstacle_right == 1 && obstacle_left == 1) {my.pan = random(360);}
if (marker_distance < 50) {marker_distance
+= 2 * time;}
move_guard();

Si une des antennes est à l'intérieur d'un bloc plein, nous stockons l'angle pan du garde dans skill30 et nous lui ajoutons (ou soustrayons) 80 degrés dans une boucle while. Vous pouvez essayer d'ajouter de plus petites ou plus grandes valeurs d'angle pan selon vos niveaux. Nous devons placer marker_distance à 0 pendant une période courte pour améliorer la détection autour des coins (si vous exécutez le niveau de test et jouez avec le bloc vous comprendrez pourquoi). Si les deux antennes sont à l'intérieur d'un bloc plein, la garde choisit un angle pan aléatoire qui permettra d'avoir une des antennes hors du bloc plein. Vous savez que marker_distance est la distance entre la garde et ses antennes. Tant que cette distance est plus petite que 50, elle est lentement augmentée.

function
move_guard()
{
move_mode = ignore_you + ignore_passable
+ activate_trigger;
ent_move(temp, nullvector);
ent_cycle("walk",my.skill11);
my.skill11 += 5 * time; // skill11 = animation
frame
if (my.skill11 >= 100) {my.skill11 = 0;}
}

Cette fonction déplace et anime le joueur. J'ai employé des fonctions semblables dans Aum2 et Aum3 vous savez comment ceci fonctionne.

J'ai voulu montrer les antennes aussi j'ai créé deux modèles qui sont placés aux coordonnées des vecteurs front_right et de front_left.

create (<marker.mdl>, front_right, frlaction);
create (<marker.mdl>, front_left, frlaction);

function frlaction()
{
my.passable = on;
waitt (2);
remove me;
}

La fonction frlaction crée les modèles pour les antennes, les rendant passables (pour permettre qu'elles pénètrent dans des blocs solides), les affiche pendant 1/8 seconde puis les enlève.

Cette première démonstration de AI est-elle parfaite ? Je ne pense pas ; si elle était parfaite je l'aurais appelée démonstration finale d'IA. Voici ses points faibles :

- le code ne fonctionnera pas très bien dans des secteurs serrés (vous pouvez changer marker_distance et marker_width pour lui rendre le travail meilleur pour vos niveaux) ;
- si le secteur près d'une des positions est bloqué, le garde essayera de l'atteindre toute la journée ;
- la garde se coincera de temps en temps ;

Si je vois que vous êtes intéressé par lui, je continuerai à améliorer ce code.


Untitled

lampe torche avec piles

Le code de la lampe torche utilise cette idée : créer un objet invisible à une grande distance au devant du joueur et tracer entre le joueur et cet objet invisible. Si quelque chose est heurté (comme un mur par exemple) déplacer l'objet invisible au point d'impact et générer une lumière comme sur l'image ci-dessous.

Jetons un oeil à l'action associée à la lampe torche:

var max_dist = 10000; // use it for distances smaller than 10,000 quants

action flashlight_pickup
{
while (player == null) {wait
(1);}
my.passable = on;
while (vec_dist (player.x,
my.x) > 100) {wait (1);}
my.invisible = on;
my.lightred = 250;
my.lightgreen = 250;
my.lightblue = 170;
while (1)
{
while (energy >
0 && key_f == 1) // press the "F" key to use the flashlight
{
my.lightrange = 200;
temp.x = max_dist * cos(player.pan) * cos(camera.tilt) + player.x;
temp.y = max_dist * sin(player.pan) * cos(camera.tilt) + player.y;
temp.z = max_dist * sin(camera.tilt) + player.z;
trace_mode = ignore_models + use_box;
my.skill1 = trace (player.x, temp);
if (my.skill1 != 0)
{
temp.x = (my.skill1 - 30) * cos(player.pan) * cos(camera.tilt) + player.x;
temp.y = (my.skill1 - 30) * sin(player.pan) * cos(camera.tilt) + player.y;
temp.z = (my.skill1 - 30) * sin(camera.tilt) + player.z;
vec_set (my.pos, temp);
}
energy -= 0.5 * time;
wait (1);
}
my.lightrange =
0;
wait (1);
}
}

Le modèle lampe torche est passable; dès que le joueur suffisemment près, la lampe torche devient invisible. Si nous avons assez d'énergie avec nos piles (energy > 0) et que nous pressons la touche "F" (celle qui sert à allumer la lampe torche) nous calculons une position à 10 000 quants au devant du joueur. SI vous aimez les maths vopus pouvez verifier les formules. J'expliquerai la plus facile:

temp.z = max_dist * sin(camera.tilt) + player.z;

Nous traçons entre le joueur et l'objet lampe torche et nous stockons le résultat dans skill1. Si le résultat est différent de zéro, nous avons touché quelque chose, aussi nous calculons une nouvelle position pour le point lumineux de la lampe torche, remplaçant max_dist par la distance retournée par trace. Nous ne voulons pas que notre point lumineux soit dans le miur aussi nous soustryons 30 quants de la distance donnée par skill1. Vous avez noté que j'utilisais le modèle lampe torche pour mon objet torche; vous pouvez utiliser un autre objet mais comme il est invisible, je ne vois pas ce que ça changerait.

L'énergie baisse si vous laissez la touche F maintenue; energy decreases if the "F" key is pressed; Si nous relâchons "F" l'e lightrange is est mis à zéro -> la lampe torche est éteinte..

L'action assignée aux piles est simple:

action
battery_pickup
{
my.passable = on;
while (player == null) {wait (1);}
while (vec_dist (player.x, my.x) > 100) {wait (1);}
energy += battery;
ent_remove (me);
}

Lorsque le joueur est tout près, l'énergie est incrémentée et la l'objet pile est enlevé.


Untitled

Un ascenseur façon "Maison de la terreur"

Comment créer un grand ascenseur qui opère à grande vitesse ? Vous pouvez soit créer un niveau énorme et utiliser de la mémoire supplémentaire et consommer du taux d'affichage, soit utiliser mon code ci-dessous.

Si vous avez chargé l'exemple office.wmp que je fournis avec ce magazine, vous avez du voir que les planchers étaient proches l'un de l'autre. En fait j'utilise un truc ici : je déplace la texture des murs de l'ascenseur vers le haut de cette façon le joueur croit descendre. Je peux déplacer la texture pendant des heures et le joueur croierait qu'il s'est dépacé sur des milliers de kilomètres.

action
platform
{
my.skill11
= 100;
my.skill10 = 100;
while (vec_dist (player.x, my.x) >
50) {wait (1);}
while (player.z > 50)
{
snd_tune (snd_handle, 100, my.skill10,
0);
if (snd_handle == 0)
{
snd_loop
(elevator_snd, 100, 0);
snd_handle = result;
}
my.z -= start_speed
* time * 1.1;
my.skill10 += 5 * time;
// augmente la fréquence
player.z = my.z + 40;
wait (1);
}
while (my.skill11
> 0)
{
my.skill11 -= 1 * time;
elevator_pnt.v += max_speed
* time;
wait (1);
}
while (my.z > -210)
{
snd_tune (snd_handle, 100, my.skill10,
0);
my.z -= start_speed
* time * 1.1;
my.skill10 -= 5 * time;
// réduit la fréquence
player.z = my.z + 40;
wait (1);
}
snd_stop (snd_handle);
}

La platforme se déplacera si la distance entre elle et le joueur est en dessous de 50 quants; je sais comme cela que le joueur est près du centre de la plateforme. La plateforme doit descendre jusqu'à ce que la coordonnée z du joueur soit proche de 50 (un point près du milieu de l'ascenseur dans mon exemple). Je change la fréquence du son pour l'ascenseur (avec snd_tune et skill10) pour faire plus vrai. La plateforme est déplacée brusquement en utilisant my.z plutôt que ent_move afin que le joueur soit déplacé en même temps que la plateforme; c'est pourquoi le joueur est placé 40 quants au dessus de la platforme (une valeur approximative; il se peut que vous ayiez à changer cette valeur en fonction du modèle qu evous allez utiliser. Jusqu'à présent nous n'avons rien fait d'autre que le déplacement d'un ascenceur ordinaire. C'est lorsque le z du joueur va passer end essous de 50 que cette boucle va fonctionner:

while (my.skill11 > 0)
{
my.skill11 -= 1 * time;
elevator_pnt.v += max_speed
* time;
wait (1);
}

J'ai défini un pointeur pour le mur de l'ascenseur et je déplace les textures du mur vers le haut encvhangeant leur v. C'est cette partie qui donne la "longueur" de l'effet: si vous mettez une valeur plus grande pour skill11 au début de l'action, l'ascenseur descendra plus profond, si vous mettez des valeurs plus petites,l'ascenseur descendra moins profond. Lorsque le temps est passé, nous exécutons la boucle suivante:

while (my.z > -210)
{
snd_tune (snd_handle,
100, my.skill10, 0);
my.z -= start_speed * time * 1.1;
my.skill10 -= 5 * time;
// réduit la fréquence
player.z = my.z + 40;
wait (1);
}

L'ascenseur ralentira jusqu'à ce qu'il atteigne un certain point où il s'arrêtera (-210 dans mon exemple, obtennez cette valeur dans Wed). Le bruit de l'ascenseur "à grande vitesse" revient au bruit "de vitesse normale".

Vous devriez cacher l'ascenseur sinon votre client va voir le truc; J'ai employé une plateforme simple pour vous permettre de voir comment cela fonctionne.


Untitled

Travailler avec les matériaux (materials)

Une des particularités merveilleuses de A6 est la possibilité d'utiliser les matériaux. Les différences entre A5 et A6 peuvent ne pas para^tre trop évidentes mais lorsque qu'on en arrive aux matériaux, alors oui, il y a une vraie différence Regardez cette copie d'écran ci-dessous:

Me croyez-vous si je vous dis que tous ces modèles sont des copies du modèle "normal" mais qui utilsent différents matériaux? Ce qui signifie que j'ai à chaque fois utilisé le même modèle avec la même peau et j'ai simplement changé son aspect en utilisant quelques variables. N'est-ce pas merveilleux? L'autre bonne nouvelle c'est que ce projet vous permettra de créer n'importe quel type de matériau auquel vous pourriez penser.

Regardons ce que cette démo puet faire pour nous; ouvrez materials.wmp, compilez-le et exécutez-le en utilisant materials.wdl. Vous pouvez voir l'image avec les 4 modèles de crystal; Je pense que l'auteur de ce modèle est TMW, mais il n'a pas inclu de fichier lisezmoi avec le modèle, donc je peux me tromper d'auteur. Bien le premier modèle "normal" n'a aucun matériau qui lui est assigné, le deuxième utilise le matériau de Conitec "smaragd", et j'ai créé 2 autres matériaux ("lava" ) lave et "marble" = marbre).

Regardez bien ces modèles puis pressez la touche "R" (rotation) une première fois:

Vous pouvez voir le même modèle "normal" (celui qui n'utilise aucun matériau) un modèle clair "shiny" et un modèle sombre "dark". Je le répète encore une fois, j'ai utilisé le même modèle avec la même peau et tous ces superbes représentations ne sont rien d'autre que des matériaux différents utilisés pour ces modèles. Prêt pour encore plus de plaisir? Pressez de nouveau la touche "R".

Cette fois-ci vous pouvez voir le modèle "normal" et deux modèles prédéfinis ("metal" et "unlit"). Pressez "R" encore une fois pour arriver sur notre laboratoire de création de matériaux.

C'est là que nous voulons placer notre modèle dans le but de créer le matériau parfait pour lui en ajustant tous les paramètres sur le panneau. Cliquez sur chacunne des flèches pour voir les changements; certains d'entre eux produiront des effets apparents seulement dans certaines circonstances, mais nous y arriverons bientôt.

Peut-être avez-vous remarqué le cercle noir en haut à gauche (cerclé de jaune sur l'image pour que vous le repériez). C'est une lumière dynamique que vous pouvez déplacer dans votre niveau.

Certaines flèches qui ont pu vous paraître inutiles jusque là montrent toute leur puissance en présence de la lumière du soleil ou d'une lumière dynamique. Vous pouvez voir que la position du curseur de la souris et celle ne la lampe ne sont pas en phase, j'ai décidé d'utiliser cette méthode car je ne veux pas que vous (ou moi) perdions la lumière lors de son déplacement en dehors des limites de l'écran.

Que sont ces matériaux, après tous ? Ce ne sont rien d'autre qu'une collection de paramètres qui commandent les propriétés de réflexion de la lumière d'un modèle, d'un sprite, d'un terrain ou d'une surface. La lumière qui affecte l'entité peut être reçue du niveau ou peut être produite par l'entité elle-même.

Après cette brillante intriduction, il est temps de passer à la compréhension du code. Regardons en premier la définition du panneau:

panel
material_pan
{
bmap = menu_pcx;
pos_x = 0;
pos_y = 0;
layer = 10;

     button = 90, 12, up_pcx, up_pcx, up_pcx, change_material,
null, null;
button = 105, 12, down_pcx, down_pcx,
down_pcx, change_material, null, null;
button = 90,
37, up_pcx, up_pcx, up_pcx, change_material, null, null;
button = 105, 37, down_pcx, down_pcx, down_pcx, change_material, null, null;
button = 90, 62, up_pcx, up_pcx, up_pcx, change_material, null, null;
button = 105, 62, down_pcx, down_pcx, down_pcx, change_material, null, null;

...................................

digits = 125, 12, 3, arial_font, 1, e_red; // emissive_red
digits = 125, 37, 3, arial_font, 1, e_blue; // emissive_blue
digits = 125, 63, 3, arial_font, 1, e_green; // emissive_green

....................................

flags = overlay, refresh, visible;
}

Le panneau utilise un bouton pour chaque flèche noire, ce qui nous donne 30 boutons et 15 nombres (digit) qui montrent les valeurs pour chaque paramètre matériau. Notez que dès que lorsque nous cliquons un des 30 boutons nous exécutons toujours la même fonction : change_material().

La fonction principale (main) est fort simple:

function
main()
{
fps_max = 30;
sun_light = 1;
level_load (materials_wmb);
mouse_map = pointer_pcx;
mouse_mode = 2;
while (1)
{
mouse_pos.x = pointer.x;
mouse_pos.y = pointer.y;
wait (1);
}
}

La fonction ci-dessus limite
le taux d'affichage à 30 fps, valide la lumière du soleil et charge
le niveau materials_wmb. Nous mettons pointer_pcx comme pointeur de souris, nous
montrons le curseur et nous autorisons le déplacement de la souris en mettant
ses coordonnées mouse_pos.x et mouse_pos.y dans une boucle "while".

Vous pouvez être sur que materials.wdl contient les définitions pour de nombreux matériaux, mais nous en étudierons uniquement que quelques uns. La raison en est très simple, ils utilisent des fonctions similaires avec des valeurs différentes. Démarrons avec le matériau utilisé pour la lave1:

material
mat_lava
{
emissive_red = 200;
emissive_green = 70;
emissive_blue = 20;
ambient_red = 255;
ambient_green = 150;
ambient_blue = 50;
diffuse_red = 200;
diffuse_green = 150;
diffuse_blue = 50;
specular_red = 255;
specular_green = 255;
specular_blue = 255;
alpha = 60;
albedo = 70;
power = 10;
}

Les 3 premiers paramètres (emissive) mettent la puissance et la couleur de la lumière qui est générée par l'entité elle-même. Vous pouvez être sur que le matériau clair (shiny") d'une des images ci-dessus est réalisé en mettant sces paramètres emissive à leurs valeurs maximales (255), tandis que le matériau sombre ("dark") est obtenu en mettant ses paramètres emissive à leurs plus petites valeurs (0).

Le prochain jeu de paramètre (ambient) met la couleur et la force de la lumière qui sera reçue par l'entité à partir d'une lumière statique dans le niveau, sa propre ambience ou l'ambiance de la caméra. Imaginons que votre jeu ait lieu sur Mars et la plupart des objets, y compris ces cristaux précieux, doivent tirer sur le rouge. Ainsi votre soleil est rouge, la terre est peinte en rouge, mais ces cristaux ne sont pas tout à fait rouges. Vous pouvez employer ses 3 paramètres ambiants pour créer un matériel qui absorbe plus rde ouge des sources lumineuses statiques du u niveau ; jetez un coup d'oeil à ce que j'ai obtenu en jouant avec les valeurs "ambiantes" :

Vous pouvez noter que ce matériel n'emploie pas ses paramètres emissive, parce qu'il ne produit pas de la lumière ; il reçoit simplement plus de rouge du blanc des sources lumineuses statiques. Naturellement ce vous pouvez (et vous devez) c'est combiner ses valeurs "emissive" appropriées avec ses bonnes valeurs "ambient".

Le prochain jeu de paramètres (diffuse) place l'influence des lumières dynamiques (y compris da lumière du soleil). Faisons une petite expérience : exécutez materials.wdl, mettez diffuse_green à 255 et jettez un bon coup d'oeil au cristal défini par utilisateur ("user defined"):

Je ne peux voir aucun changement malgré tous mes efforts! Il est temps pour nous de déplacer le petit rond noir (la lumière dynamique) et de l'approcher du modèle crystal:

Vous pouvez voir que ma lumière dynamique blanche illumine le vert de mon modèle en cristal ; le matériau ignore les composants rouges et bleus de la lumière blanche parce que diffuse_red et diffuse_blue ont été mis à zéro. Le dernier jeu de paramètres (spécular) est semblable à "diffuse" , mais cette fois l'effet dépend des angles de la caméra.

Cette fois j'ai mis specular_blue to 255. Le résultat est évident : seulement une partie du modèle en cristal est affectée par la lumière dynamique ; la grande valeur de specular_blue colore le cristal dans le bleu, mais vous pouvez voir l'effet seulement quand la camera a un certain angle pan. Je vous encourage à appuyer sur la touche "zéro" tandis que le moteur s'exécute; de cette façon vous pourrez vous déplacer et tourner à l'intérieur du niveau. Une chose encore : vous pouvez cliquer le cristal défini par utilisateur pour démarrer / arrêter sa rotation autour de son angle pan.

Les 3 derniers paramètres ("power", "alpha" et "albedo") Les 3 derniers paramètres ("puissance", "alpha" et "albedo") sont assez francs : ils mettent la puissance/acuité de l'effet (0...10), l'opacité du matériel (0... 100) et l'influence de la lumière du soleil (0... 100).

Vous noterez que certaines des définitions de matériau à l'intérieur de material.wdl n'incluent pas tous les paramètres, parce que parfois nous n'aurons pas besoin d'eux tous. Ok, maintenant nque ous savons définir un matérieau Comment l'utilisons-nous?

action test_lava
{
my.material = mat_lava;
..................................
}

C'est vraiment très simple, n'est-ce pas? Nous créons l'action qui sera attachée à notre entité puis nous utilisons une simple ligne de code pour assigner notre matériau précédemment défini à notre entité. Le moteur sait comment travailler avec plusieurs matériaux prédéfinis comme mat_flat, mat_shaded, mat_metal etc; notez que vous pouvez également changer leurs valeurs. Ici l'action attachée au crystal de métal "metal" dans la démo:

action test_metal
{
my.metal = on;
mat_metal.emissive_red = 50;
mat_metal.emissive_green = 50;
mat_metal.emissive_blue = 80;
}

La première ligne de code met metal = on pour mon crystal; de cette façon mat_metal est mis automatiquement pour mon modèle. A présent je peux contrôler tous les paramètres du matériau prédéfini mat_metal ; je choisis de le faire un peu plus métallique en ajoutant un petit peu de lumière, particulièrement de la lumière bleue, pour cela. Jetons à présent un oeil à l'action attachée à la lumière dynamique:

action
dynamic_light
{
my.lightrange = 80;
my.red
= 100;
my.green = 100;
my.blue = 100;
my.enable_click = on;
my.event = move_me;
}

La lumière a une zone d'action de 80 quants et génère une lumière blanche car ses composantes rouge, vert et bleu ont la même valeur (100). L'entitéest sensible au clic. Cliquez-la une fois et sa fonction d'évènement move_me event sera exécutée:

function move_me()
{
while (mouse_left == on)
{
temp.x = pointer.x;
temp.y = pointer.y;
temp.z = 100;
vec_for_screen (temp, camera);
my.x = temp.x;
my.z = temp.z;
wait (1);
}
}

Cette fonction déplace l'entité qui génère la lumière aussi longtemps que le bouton gauche de la souris est maintenu pressé. Jetez un oeil sur l'image ci-dessous pour voir la zone qui peut être couverte par l'emmeteur de la lumière pour temp.z = 100; changez cette valeur si vous souhaitez couvrir une zone différente. Les coordonnées stockées dans temp sont converties en coordonnées 3D en utilisant vec_for_screen, et l'entité est déplacée vers sa nouvelle position.

maintenant voici l'action attachée au modèle définissable par l'utilisateur ("user defined"):

action
test_user
{
my.material = mat_user;
my.enable_click
= on;
my.event = rotate_me;
}

function rotate_me()
{
rotation += 1;
while ((rotation % 2) == 1)
{
my.pan += 5 * time;
wait (1);
}
}

Le modèle utilise le matériau mat_user, est sensible aux clics et exécute sa fonction d'évènement rotate_me lorsqu'il est cliqué avec le bouton gauche de la souris. Le modèle tournera autour de son angle pan tant que son angle de rotation aura une valeur impaire: 1, 3, 5, 7, etc.

Vous souvenez-vous de cet immense définition de panneau dont nous avons parlé au début de cet article? Chaque bouton (flèche noire) sur le panneau exécute la même fonction lorsque l'on clique dessus:

function
change_material(button_number)
{
if (button_number == 1)
{
while (mouse_left == 1)
{
e_red += 1;
e_red
= min(max(e_red, 0), 255);
wait (1);
}
wait (1);
}
if (button_number == 2)
{
while (mouse_left == 1)
{
e_red -= 1;
e_red = min(max(e_red, 0), 255);
wait (1);
}
wait (1);
}

......................
}

Cette fonctionest immense car elle est utilisée par les 30 boutons (flèches) sur le panneau. J'ai décidé de vous montrer le code pour les 2 premiers boutons mais le reste du code est à l'identique.

Si vous êtes un habitué des AUM vous savez que la fonction change_material prends "1" comme paramètre lorsque nous pressons le premier boputon du panneau, "2"lorsque nous pressons le 2ème bouton du panneau etc.Les 2 pemiers boutons augmentent et diminuent e_red, une variable qui contrôle le paramètre emissive_red.

Si nous pressons le premier bouton (flèche) sur le panneau, nous augmentons incrementing e_red tant que le bouton gauche de la souris reste enfoncé. La ligne de code ci-dessous:

e_red = min(max(e_red, 0), 255);

s'assure que la valeur pour e_red reste dans son intervalle: 0...255. Si nous pressons le 2ème bouton (flèche) du panneau, e_red est décrémenté tant que le bouton gauche de la souris reste appuyé.

regardez notre définition de matériau "user defined":

material mat_user
{
emissive_red = 0;
emissive_green = 0;
emissive_blue = 0;
ambient_red = 0;
ambient_green = 0;
ambient_blue
= 0;
diffuse_red = 0;
diffuse_green = 0;
diffuse_blue = 0;
specular_red = 0;
specular_green
= 0;
specular_blue = 0;
power = 0;
}

Tous les paramètres de mat_user sont contrôlés par une fonction starter:

starter set_material()
{
while (1)
{
mat_user.emissive_red = e_red;
mat_user.emissive_green = e_green;
mat_user.emissive_blue = e_blue;
mat_user.ambient_red
= a_red;
mat_user.ambient_green = a_green;
mat_user.ambient_blue = a_blue;
mat_user.diffuse_red = d_red;
mat_user.diffuse_green
= d_green;
mat_user.diffuse_blue = d_blue;
mat_user.specular_red = s_red;
mat_user.specular_green = s_green;
mat_user.specular_blue
= s_blue;
mat_user.power = m_power;
mat_user.alpha = m_alpha;
mat_user.albedo = m_albedo;
wait (1);
}
}

La boucle while ci-dessus met à jour en permanence tous les paramètres de matériau en utilisant les valeurs qui sont mises par le panneau. La dernière fonction est celle qui fait tourner la caméra de 90 degrés à chaque fois que l'on presse la touche "R":

function
rotate_camera()
{
camera.pan -= 90;
}

on_r = rotate_camera;

That's all, folks! (je l'ai laissé en anglais car c'est ce que l'on voit à la fin des dessins animés) Maintenant à vous de jouer. Remplacez le modèle "user defined" par votre propre modèle, pressez quelques boutons et émerveillez-vous devant cet étonnante possibilité de créer ses propres matériaux!


Untitled

Meilleur scores

Certains jeux n'ont pas besoin d'un système compliqué de sauvegarde / chargement; le p'tit bout de code consiste en un jeu très simple qui sauvegarde le plus haut score. Vous pouvez utiliser mon exemple pour apprendre comment sauver le plus haut score, les paramètres du jeu etc. Il peut remplacer aisément la fonction de sauvegarde / chargement implémentée dans le moteur A5.

Le but du jeu et d'arriver au plus gros score en cliquant sur le plus de panneaux possibles. Les panneaux apparaîssent à des positions aléatoires, aussi vous devez être très rapide..

Ici ma fonction principale main:

function
main()
{
fps_max =
50;
level_load (black_wmb);
wait (2); // attendre pour que le niveau
soit chargé
init_mouse();
start_game();
}

Je charge un niveau vide et après 2 frames j'initialise la souris et je démarre le jeuI initialize the mouse and start the game. La fonction init_mouse est classique:

function init_mouse()
{
mouse_map
= arrow_pcx;
mouse_mode = 2;
while (1)
{
mouse_pos.x = pointer.x;
mouse_pos.y = pointer.y;
wait (1);
}
}

Nous
choisissons le pointeur, nous mettons mouse_mode à une valeur qui montre
la souris et nous permettons à la souris de se déplacer librement
en utilisant une boucle.

function start_game()
{
highscore_handle = file_open_read("hiscore.dat");
max_clicks
= file_var_read (highscore_handle);
while (timer
> 0)
{
timer -= 1;
target_pan.pos_x = random(736); // 800
- 64
target_pan.pos_y =
random(536); // 600 - 64
target_pan.visible = on;
waitt (16);
target_pan.visible
= off;
waitt (32);
}
if (current_clicks
> max_clicks) // new high score
{
highscore_handle =
file_open_write("hiscore.dat");
file_var_write (highscore_handle, current_clicks);
file_close(highscore_handle);
}
while (mouse_right == 0) {wait (1);}
current_clicks = 0;
timer = 20;
main();
}

La fonction start_game fait (presque) tout le travail. Avant tout, elle ouvre le fichier hiscore.dat et met max_clicks à la valeur qui a été lue depuis hiscore.dat. Ce fichier est un simple fichier texte, comme votre fichier wdl. Je l'ai créé dans un éditeur de texte, je l'ai nommé hiscore.dat et j'ai tapé 0 (zero) à l'intérieur. Ce qui fait que lorsque vous démarrez le jeu pour la première fois le score à battre est de zéro.

J'ai initialisé timer à 20 donc la boucle s'exécutera 20 fois:

while (timer > 0)
{
timer -= 1;
target_pan.pos_x =
random(736); // 800 - 64
target_pan.pos_y = random(536); // 600
- 64
target_pan.visible
= on;
waitt (16);
target_pan.visible
= off;
waitt (32);
}

Le code à l'intérieur de la boucle génère une position aléatoire pour le panneau et le rend visible pour une seconde. Si le joueur clique sur le panneau, la fonctionassociée au panneau (on_click) est exécutée:

function add_clicks()
{
current_clicks += 1;
}

La fonction ajoute simplement unautre clic à la variable current_clicks var. A la fin du jeu, le panneau cible a été montré 20 fois et nous continuons par :

if (current_clicks > max_clicks) // nouveau meilleur score-> on le sauvegarde
{
highscore_handle = file_open_write("hiscore.dat");
file_var_write (highscore_handle, current_clicks);
file_close(highscore_handle);
}
while (mouse_right == 0) {wait (1);}
current_clicks = 0;
timer = 20;
main();
}

A ce point le jeu est terminé; Si nous avons fait un score recor il sera écrit dans le fichier hiscore.dat, remplaçant les données qui s'y trouvaient. Le jeu attend que vous cliqiez le bouton droit de la souris pour remettre à zéro le score (pas le meilleur score) et redémarre.

Vous pouvez crypter les données que vous sauvegardez dans le fichier également. La meilleure façon est de convertir le nombre en chaîne de caractère, d'utiliser un simple algorithme d'encryptage et de sauvegarder la chaîne sur votre disque. Lorsque vous démarrez le jeu, lisez la chaîne, décriptez la, convertissez la en nombre et vous êtes prêts à jouer.


Untitled

Message électronique (NDT : la version anglaise est : Data sticks, littéralement bâton de données)

Vous avez du 'aimer les messages electroniques! Vous savez ce sont ces messages disséminés tout au long d'un jeu qui vous donnent de précieuses informations. Certains pouvant être pathétiques (comme "aujourd'hui 2 hommes sont encore morts, je suis seul à présent") d'autres pouvant être vraiment utiles (comme : "vous lisez un message électronique).

Le code de cet article peut être utilisé pour 2 objets, mais vous pouvez l'étendre et l'utiliser pour un nombre illimité de messages électroniques. Avant-tout nous définissons 2 chaînes et un texte:

string data1_str = "The new access code is 6239. Memorize it!";
string data2_str = "The vents will be shut down tonight.";

text data_txt
{
pos_x = 200;
pos_y = 20;
font = std_font;
}

Chaque message électronique (qui peut être un modèle, un sprite, une entité wmb) a cette action qui lui est attachée:

action data_stick
{
my.ambient = -50;
my.enable_click = on;
my.event = show_data;
}

La première ligne assombrit légérement le modèle, vous pouvez utilser une valeur positive ou négative ici fonction du modèle utilisé comme message électronique. L'entité est sensible au clic de souris, aussi pensezà presser le bouton droit de la souris pour faire apparaître le pointeur de la souris si vous utilisez les templates. Chaque fois que nous cliquerons un message électronique, sa fonction évènement (show_data) sera exécutée:

function
show_data()
{
if (my.skill1 == 1)
{
data_txt.string
= data1_str;
data_txt.visible
= on;
sleep (3);
data_txt.visible
= off;
}
if (my.skill1 == 2)
{
data_txt.string
= data2_str;
data_txt.visible
= on;
sleep (3);
data_txt.visible
= off;
}
ent_remove (me);
}

Chaque message électronique a son skill1 de mis à une valeur différentes dans Wed. Si l'entité que j'ai cliquée à son skill1 de mis à 1 c'est qu'il s'agit dupremier message électronique, nous mettons data1_str comme chaîne pour le texte nommé data_txt, nous montrons le texte pendant 3 secondes puis nous le cachons. La même chose se produit si nous cliquons sur le deuxième message électronique (son skill1 = 2. Vous pouvez ajouter plus de branchement "if" si vous avez besoin de plus de messages électroniques.

La dernière ligne de code dans la fonction enlève le message électronique (vous l'avez lu aussi il peut disparaître). J'ai utilisé le message électronique pour indiquer au joueur le code d'accès pour une porte particulière, le nombre de soldats quigardent un objectif militaire spécifique etc.


Untitled

Min-carte

3DGS nous permet d'avoir plusieurs vues sur l'écran en mêm temps; j'ai utilisé cette merveilleuse possibilité pour créer une vue de dessus de votre écran.

Regardons d'abord la définition de la vue:

view
top_view
{
layer = 15;
pos_x = 0;
pos_y = 0;
size_x = 200;
size_y = 150;
tilt = -90;
arc = 80;
}

La vue est de 200 x 150 pixels et apparaît dans le coin supérieur gauche de l'écran.and appears in the upper left corner of the screen. La vue regarde vers le bas car son angle tilt est mis à -90 et son facteur de zoom est mis à 80 (jouez avec cette valeur pour voir plus ou moins de votre niveau sur la vue de dessus.

J'ai utilisé une fonction "starter" (une fonction qui s'exécute par elle-mêm lorsque nous démarrons le niveau) pour montrer / cacher la mini-carte:

starter init_topcam()
{
while (player == null) {wait (1);}
while (1)
{
if (key_t == 1)
{
while (key_t == 1) {wait (1);}
if (top_view.visible == on)
{
top_view.visible = off;
}
else
{
top_view.visible = on;
}
}
top_view.x = player.x;
top_view.y = player.y;
top_view.z = 1000;
// play with this value
wait (1);
}
}

Nous attendons que le joueur apparaîsse dans le niveau puis nous montrons ou cachons la mini-carte lorsque la touche "T" est pressée. La vue a le mêm x et y que le joeur et est placée à une hauteur de 1,000 quants. Votre niveau peut avoir une hauteur diffétente aussi bvous pouvez jouer avec cette valeur.

Vous devez ajouter un petit cadre à votre vue mini-carte (utilisez 2 ou 4 raies minces).


Untitled

Miroirs verticaux

Si vous êtes l'heureux possesseur d'une version A5 pro, vous êtes familier avec le miroir horizontal qui est placé sur le sol dans le niveau office. Le code pour un miroir vertical est similaire mais il y a quelques précautions à prendre:
1) Le miroir ne doit pas être tourné dans votre niveau. Si vous voulez placer un miroir dans votre niveau sur une face sud, le miroir doit être horizontal.

2) Le code du miroir change en fonction de son orientation. Ici le code du miroir utilisé dans mon exemple:

function
init_mirror()
{
camera.portal = mirror;
mirror.noshadow = on; // suppress shadows in the mirror
mirror.noparticle = on; // suppress particles
in the mirror
mirror.portalclip = on; //
clip at portal plane
while (1)
{
proc_late(); //
place it at the end of the function list
mirror.genius =
camera.genius;
mirror.aspect =
camera.aspect;
mirror.arc = -camera.arc;
mirror.fog = camera.fog;
mirror.x = camera.x;
mirror.y = camera.portal_y
- (abs(camera.portal_y - camera.y));
mirror.z = camera.z;
mirror.pan = -camera.pan;
mirror.tilt = camera.tilt;
mirror.roll = camera.roll;
wait(1);
}
}

Si vous placez le miroir sur un mur ouest, le code doit être modifié de cette façon :

function
init_mirror()
{
camera.portal = mirror;
mirror.noshadow = on; // suppress shadows in the mirror
mirror.noparticle = on; // suppress particles
in the mirror
mirror.portalclip = on; //
clip at portal plane
while (1)
{
proc_late(); //
place it at the end of the function list
mirror.genius =
camera.genius;
mirror.aspect =
camera.aspect;
mirror.arc = -camera.arc;
mirror.fog = camera.fog;
mirror.x = camera.portal_x
- (abs(camera.portal_x - camera.x));
mirror.y = camera.y;
mirror.z = camera.z;
mirror.pan = -camera.pan;
mirror.tilt = camera.tilt;
mirror.roll = camera.roll;
wait(1);
}
}

3) Lorsque vous créez l'entité miroir, mettez"flat" sur toutes les faces qui ne servent pas et "mirror" sur la face qui doit servir de miroir.

4) Le dos de votre miroir doit avoir assez de place parce que ce que vous voyez dans le miroir est "créé" artificiellement de l'autre coôté du mur, à travers le trou en utilisant une vue caméra imaginaire.

5) C'est également une bonne idée de placer un bloc invisible devant votre miroir si le joueur peut s'en approcher, ça évitera qu'il pénètre un peu dans le miroir. C'est ce que j'ai fait dans mon niveau d'exemple.

6) N'oubliez pas de mettre "none" sur toutes les surfaces du niveau qui entoure le miroir.

J'ai inclu un répertoire avec une démo pour un miroir vertical; copiez le répertoire \mirrors dans votre répertoire Gamestudio, ouvrez mirrors.wmp (pas mirror.wmp!), compilez et exécutez.

Joyeux miroir!


Untitled

Programme de mise au point instantané

"Programme de mise au point instantané" paraît trop bien pour être vrai, n'est-ce pas? IJe vous laisse juger si ce programme est utile ou pas : exécuez le niveau, pressez la touche "I" pour afficher le programme de mise au point, clic droit pour montrer le curseur de la souris puis déplacez le curseur sur n'importe quelle entité dans le niveau. Vous verrez s'afficher ses valeurs x, y, z, pan, tilt, roll, red, green, blue, ambient, alpha, skill1... skill8 et frame. Si une de ces valeurs change, vous pourrez voir le changement en temps réel : de cette façon vous pouvez tester et améliorer votre code sans avoir d'autres outils difficiles d'utilisation. Je suis un garçon timide mais j'aurai pu ajouter le reste des variables, les drapeaux et plein d'autres choses sur le même panneau, faites moi confiance!

Si vous aimez ce programme, peut-être impatient de voir comment il fonctionne; mais si vous vous attendez à voir beaucoup de codes compliqués, vous allez être déçu. Le code inclut un texte et un panneau:

text info_txt
{
font = terminal9_font;
pos_x = 10;
pos_y = 10;
string = "x:\ny:\nz:\npan:\ntilt:\nroll:\nred:\ngreen:\nblue:\nambient:\nalpha:\nskill1:\nskill2:\nskill3:\nskill4:\nskill5:\nskill6:\nskill7:\nskill8:\nframe:";
flags = d3d, visible;
}

Le panneau a une grande chaîne car j'affiche tout le texte avec la même chaine en utilisant des retours à la ligne (\n). J'utilise un panneau avec des nombres (digits) pour afficher les valeurs:

panel
info_pan
{
pos_x = 0;
pos_y = 0;
digits = 50, 10, 5, terminal9_font,
1, mouse_ent.x;
digits = 50, 22, 5, terminal9_font, 1, mouse_ent.y;
digits = 50, 34, 5, terminal9_font, 1, mouse_ent.z;
digits = 50, 46, 5, terminal9_font,
1, mouse_ent.pan;
digits = 50, 58, 5, terminal9_font, 1, mouse_ent.tilt;
digits = 50, 70, 5, terminal9_font, 1, mouse_ent.roll;
digits = 50, 82, 5, terminal9_font,
1, mouse_ent.red;
digits = 50, 94, 5, terminal9_font, 1, mouse_ent.green;
digits = 50, 106, 5, terminal9_font, 1, mouse_ent.blue;
digits = 50, 118, 5, terminal9_font,
1, mouse_ent.ambient;
digits = 50, 130, 5, terminal9_font, 1, mouse_ent.alpha;
digits = 50, 142, 5, terminal9_font, 1, mouse_ent.skill1;
digits = 50, 154, 5, terminal9_font, 1, mouse_ent.skill2;
digits = 50, 166, 5, terminal9_font, 1, mouse_ent.skill3;
digits = 50, 178, 5, terminal9_font, 1, mouse_ent.skill4;
digits = 50, 190, 5, terminal9_font, 1, mouse_ent.skill5;
digits = 50, 202, 5, terminal9_font, 1, mouse_ent.skill6;
digits = 50, 214, 5, terminal9_font, 1, mouse_ent.skill7;
digits = 50, 226, 5, terminal9_font, 1, mouse_ent.skill8;
digits = 50, 238, 5, terminal9_font, 1, mouse_ent.frame;
flags = refresh, d3d, visible;
}

J'ai utilisé de nombreuses instruction s"digit".I'm using a lot of "digits" instructions. Toutes les valeurs sont affichées sur 5 chiffres possibles, avec une police de caractère nommée terminal9_font. Les valeurs sont multipliées par 1 (en fait elles ne sont pas changées). Maintenant voici la partie étrange : toutes les valeurs apparaissent comme ceci : mouse_ent.x, mouse_ent.roll, mouse_ent.skill2... mais qu'elle est cette chose appelée mouse_ent? Si on se reporte au manuel, mouse_ent est l'entité sur laquelle se trouve le pointeur de la souris. Si nous mettons le pointeur de la souris sur une porte, mouse_ent pointera sur la porte aussi nous verrons les x, y, z, pan, tilt, roll, ... pour cette porte. Le processus fonctionne de la même façon avec chaque entité du niveau; si nous ne touchons aucune entité avec le curseur de la souris, toutes les valeurs sont à zéro.

J'utilise une seule fonction mais j'utilise une "starter" car j'ai besoin qu'elle s'exécute au démarrage du jeu:

starter
debug_init()
{
while (1)
{
if (key_i == 1)
{
while (key_i == 1) {wait (1);}
info_txt.visible = (info_txt.visible == off); // toggle visibility
info_pan.visible = (info_txt.visible == on); // same thing here
}

Si nous pressons la touche "I" nous montrons / cachons la mise au point (le texte et le panneau qui montrent les valeurs). Nous attendons que la touche "I" soit relâchée autrement le panneau appara^trait et disparaîtrait à chaque cycle jusqu'à ce que nous relachions la touche.

         if (mouse_ent != null)
// entity detected?
{
if (total_frames % 100 == 1) // play a sound every 100 frames
{
snd_play(entity_wav, 10, 0);
}
}
wait (1);
}
}

Si nous déplaçons le pointeur de la souris sur une entité, mouse_ent est assigné à l'entité aussi il n'est pas null. Si cela se produit, nous jouons un petit son chaque 100 frames, avertissant le joueur que le pointeur est placé sur une entité et que donc les valeurs affichées sont bonnes.

Si vous voulez afficher plus de valeurs, ajoutez plus de nombres (instructions "digit") à la définition du panneau et c'est tout!


Untitled

Nouveau code de mise au point

Je suis presque sur que vous connaissez les rangées de nombres qui s'affichent lorsque vous pressez la touche "D" pendant qu'un niveau s'exécute. I'm pretty sure that you know the rows of numbers that appear when you press the "D" key while running a level. J'avoue que j'ai été assez effrayé lorsque j'ai vu toutes ces valeurs la première fois (je suis toujours effrayé mais surtout ne le dites à personne) la plupart du temps parce que je suis incapable de savoir à quoi elles correspondent. Ce nouveau code de mise au point montre un panneau avec les nombres les plus importants et les textes qui expliquent leur rôle. J'espère que vous êtes capabled'optimiser votre niveau avec ce nouvel outil.

Jetons un oeil à toutes ces valeurs intéressantes fournies par le moteur:
- num_actions, nombre d'actions s'exécutant actuellement;
- num_visents, nombre d'entités visibles;
- num_visentpolys, polygones visibles pour les entités et sprites;
- num_vismappolys, polygones visibles pour les cartes et wmb;
- d3d_texskins, mémoire utilisée par les modèles et les sprites;
- d3d_texbmaps, mémoire utilisée par les panneaux et les définitions d'entités;
- d3d_texsurfs, mémoire utilisée par les entités wmb;
- d3d_texsmaps, mémoire utilisée par la carte d'ombres;
- d3d_texfree, mémoire vidéo disponible.

Toutes ces valeurs et mêm d'autres encore peuvent être affichée sur un panneau qui a cette définition:

panel debug_panel
{
bmap = debug_pcx;
layer = 30;
pos_x = 0;
pos_y = 0;

digits = 40,20,5,arial10_font,1,num_actions;
digits = 40,60,5,arial10_font,1,num_visents;
digits = 40,100,5,arial10_font,1,num_visentpolys;
digits = 40,142,5,arial10_font,1,num_vismappolys;
digits = 40,186,5,arial10_font,1,temp_texskins;
digits = 40,230,5,arial10_font,1,temp_texbmaps;
digits = 40,274,5,arial10_font,1,temp_texsurfs;
digits = 40,321,5,arial10_font,1,temp_texsmaps;
digits = 40,364,5,arial10_font,1,temp_total;
digits = 40,410,5,arial10_font,1,temp_free;
digits = 40,455,5,arial10_font,16,temp_fps;

flags = overlay, refresh, d3d;
}

Vous pouvez voir que certaines valeurs dans la définition du panneau apparaissent telles quelles alors que d'autres valeurs sont calculées dans cette bocle while:

function
toggle_debug()
{
on_d = null; // deactivate the standard debug panel
while (1)
{
temp_texskins =
d3d_texskins / 1024;
temp_texbmaps =
d3d_texbmaps / 1024;
temp_texsurfs =
d3d_texsurfs / 1024;
temp_texsmaps =
d3d_texsmaps / 1024;
temp_total = (d3d_texskins
+ d3d_texbmaps + d3d_texsurfs + d3d_texsmaps) / 1024;
temp_free = d3d_texfree
/ 1024;
temp_fps = 0.9
* temp_fps + 0.1 / time;
if (key_d == 1)
{
while (key_d == 1) {wait (1);}
debug_panel.visible = (debug_panel.visible == off);
}
wait (1);
}
}

La première ligne désactive le panneau de mise au point standard qui apparait en pressant la touche "D". Toutes les valeurs relatives à la mémoire vidéo sont en Ko aussi nous les divisons par 1024 pour les avoir en Mo. Nous ajoutons toutes ces valeurs pour obtenir la mémoire utilisée dans le niveau et nous calculons le taux d'affichage (1 / time) rapidement pour voir ce taux se modifier. Le panneau apparait / disparait avec la touche "D".

Please note that d3d_texfree can show a value that is bigger than the memory installed on your video card, because many modern 3D cards use different compression algorithms. Some of your customers will use older cards so it is better to keep an eye on "Total memory used" instead of "Texture free".


Untitled

Code demandant un mot de passe

C'est le code pour votre npc (non-playing character). Vous vous approchez de lui, il vous demande "Quel est le mot de passe?", vous tapez le mot de passe et si vous le connaissez, vous pourrez ouvrir certaines portes ou déclencher telle action ou telle fonction.

J'utilise 3 chaînes de carctères: la première affiche la chaîne qui réclame le mot de passe, la seconde qui stocke les infos que vous taperez au clavier (10 caractères mais vous pouvez changer cette valeur) et la dernière contient le vrai mot de passe.

string
passreq_str = "Quel est le mot de passe?";
string pass_str[10];
string password_str = "geronimo";

J'utilise la définition classique d'affichage de chaînes à l'écran et une simple action de demande de mot de passe qui sera attachée à mon npc.

text
pass_txt
{
pos_x = 20;
pos_y = 20;
layer = 15;
font standard_font;
}

action
request_pass
{
my.enable_impact = on;
my.enable_push = on;
my.event = ask_pass;
}

Un petit code sympa dans la fonction ask_pass:

function
ask_pass ()
{
if (you == player)
{
if (pass_txt.visible
== on) {return;}
pass_txt.string
= passreq_str;
pass_txt.visible
= on;
waitt (32);
pass_txt.string
= pass_str;
while (pass_txt.visible
== on)
{
inkey pass_str;
if(result == 13)
{
pass_txt.visible = off;
}
}
if (str_cmpi (pass_str,
password_str) == 1)
{
key1 = 1;
new_event();
}
}
}

Cette fonction s'exécute uniquement si l'entité qui entre en collision avec le npc est le joueur. Si vous avez un très bon pc, plusieurs évènements pourraient se déclencher en mêm temps et ce n'est pas ce que nous voulons. nous pouvons donc utiliser exclusive_global ou ma méthode:

if (pass_txt.visible == on) {return;}

Si pass_text est déjà visible , la fonction s'arrête car ça signifie qu'elle s'exécute déjà par ailleurs, la première ayant rendu pass_text visible. Nous affichons la chaîne passreq_str pendant 2 secondes puis nous attendons la frappe du joueur; ceci dure tant que le joueur n'a pas pressé la toche entrée (result is 13 si Enter est pressée). Lorsque nous avons fini de taper le mot de passe er que nous avons appuyé sur entrée, le texte est rendu invisible et une simple instruction str_cmpi est exécutée. Si pass_str == password_str (l'utilisateur a bien tapé le bon mot de passe) et que nous utilisons les scripts prédéfinis, key1 = 1 permet à la porte qui peut être déverrouillée avec cette clé, d'être ouverte, key2 = 1 permet à la porte qui peut être déverrouillée avec cette clé, d'être ouverte etc. Nous pouvons également appeler la fonction new_event qui peut être utilisée pour exécuter d'autres fonctions, etc.


Untitled

Musique aléatoire

Différents jeux utiise plusieurs pistes sonores, les jouant de façon aléatoire dans chaque niveau. Le code ci-dessous vous permettra de faire la même chose dans votre jeu.

starter
random_tunes()
{
var current_track = 0;
randomize();
while (1)
{
current_track
= int(random(5)) + 1;
if (current_track ==
1)
{
media_play("track1.mid", null, 100);
}
if (current_track == 2)
{
media_play("track2.mid",
null, 100);
}
if (current_track == 3)
{
media_play("track3.mid", null, 100);
}
if (current_track == 4)
{
media_play("track4.mid",
null, 100);
}
if (current_track == 5)
{
media_play("track5.mid", null, 100);
}
while (snd_playing(media_handle) != 0) {wait (1);}
sleep (1);
}
}

Nous avons mis une fonction starter ici aussi elle démarrera aussitôt que le niveau sera chargé. Nous définissons une variable locale nommée current_track, puis nous nous assurons qu'il y aura bien un choix aléatoire différent à chaque fois que nous exécuterons le script en appelant la fonction prédéfinie "randomize". La première ligne de code à l'intérieur de la boucle while génère un nombre aléatoire dans entre 1 et 5 ce qui nous permettra de jouer la piste sonore correspondant à cette valeur "current_track". Les dernières lignes de code attendent que les dernières paroles soient terminées et crée une pause d'une seconde entre chaque piste. Libre à vous de changer ces valeurs, y compris le temps d'attente entre 2 pistes.


Untitled

Musique intelligente

Essayez d'imaginer ce scénario: vous pénétrez dans un couloir sombre où quelques torches créent plus d'ombre que de lumière. Une musique est joué sur un ton gentil aussi vous n'êtes pas angoissé. Encore quelques pas et ... bang! La porte se referme derrière vous et la musique change du tout au tout, de gentille elle devient intense pendant qu'une horde de monstres vous attaque. La musique continue à être dense jusqu'à ce que vous aperceviez de nouveau la lumière du jour.

Ce que je vais vous dire va dédramatiser un peu la situation, le code qui joue la musique de cette façon est en fait très simple - voyez par vous même

action music_trigger
{
my.passable = on;
music_handle = snd_loop (gentle_wav, 50, 0);
while (vec_dist
(player.x, my.x) > 100) {wait (1);}
temp_volume = 50;
while (temp_volume > 5)
{
snd_tune (music_handle, temp_volume, 0, 0);
temp_volume -= 1 * time;
wait (1);
}

J'ai utilisé un modèle garde pour déclencher le changement; vous pouvez mettre ce modèle invisible lorsque vous serez satisfait du résultat. J'ai choisi l'instruction snd_loop pour jouer la piste son car media_play crée un léger blanc lors de la boucle. La première boucle while attend que le joueur s'approche à moins de 100 quants, et utilise snd_tune à l'intérieur d'une autre boucle while pour diminuer le volume de la piste son. Nous utilisons une variable nommée temp_volume qui diminue sa valeur de 50 (le volume initial de la piste son) à 5, et cela permet de faire un fondu avec la piste son en douceur.

    snd_stop (music_handle);
music_handle = snd_loop (tense_wav, 50, 0);
while (vec_dist
(player.x, my.x) < 1000) {wait (1);}
temp_volume = 50;
while (temp_volume > 5)
{
snd_tune (music_handle, temp_volume, 0, 0);
temp_volume -= 1 * time;
wait (1);
}
snd_stop (music_handle);
wait (1);
music_handle = snd_loop (gentle_wav, 50, 0);
}

Gentle_wav est arrêté pour de bon à présent aussi nous pouvons démarrer tense_wav . Cette piste sera jouée tant que la distance entre le joueur et le garde sera supérieure à 1000 quants (près de la sortie du couloir dans mon exemple). Lorsque le joueur quitte la zone dangereuse, tense_wav est en fondu en douceur et la bonne vieille piste son gentle_wav est jouée de nouveau.


Untitled

niveau infini

Cet article va vous apprendre à créer un niveau qui ne se termine jamais. Bien entendu pour pouvez décider d'arrêter le jeu quand vous voulez mais vous pouvez aussi que l'action ne se termine jamais. J'ai appelé ce concept le "tapis roulant"et voici comment il fonctionne:

- Nous créons un niveau avec 9 préfab qui sont placés comme sur la figure ci-dessous. Chaque préfab a une textyre différente (nommée une, deux ...neuf ou en anglais one, two ...nine dans cette démo), en fait dans ma démo chaque texture a un chiffre d'imprimé vous permettant de comprendre comment fonctionne le tapis roulant. A noter que les lignes vertes et les gros chiffres en haut à droite de chaque carré de cette image n'apparaissent pas à l'écran, ils ont juste été ajouté pour votre compréhension de ce magazine.

- Nous plaçons notre joueur (une voiture ici) au milieu (ici le 5ème préfab)

- Le code fait le reste :)

Si vous voulez créer un nouveau niveau vous devrez également ajouter un bloc quelque part autrement le moteur génèrera une erreur car un niveau ne peut pas être uniquement d'entités wmb.

Examinons le code pour le moment:

function
main()
{
level_load(start_wmb);
fps_max =
60;
}

entity*
tile1;
entity* tile2;
entity* tile3;
entity* tile4;
entity* tile5;
entity*
tile6;
entity* tile7;
entity* tile8;
entity* tile9;

action
tile_1 // just name these prefabs
{
tile1 = me;
}

..................

action
tile_9
{
tile9 = me;
}

La fonction principale (main) est commune. Chaque carré a une simple action qui lui est attachée, cette action donne à chaque carré (le préfab) un nom significatif (tile1... tile9).

action
car
{
var car_speed;
while ((tile1 == null)
|| (tile1 == null) || (tile3 == null) || (tile4 == null) || (tile5 == null) ||
(tile6 == null) || (tile7 == null) || (tile8 == null) || (tile9 == null)) {wait
(1);}
player = me;
while (1)
{
if(car_speed != 0) // can't rotate the car
if it isn't moving
{
my.pan -= 0.2 * key_force.x * car_speed * time;
}
car_speed = 3 * key_force.y * time + max
(1 - time * 0.2, 0) * car_speed;
ent_move (car_speed,
nullvector);
if (key_t == 0) // normal 3rd
person camera view
{
camera.x = player.x - 300 * cos(player.pan);
camera.y = player.y - 300 * sin(player.pan);
camera.z = player.z + 100;
camera.pan += 0.2 * ang (player.pan - camera.pan);
camera.tilt = -10;
}
else // press and hold "T" to get a top view
{
camera.x = player.x;
camera.y = player.y;
camera.z = 8000;
camera.pan
= player.pan;
camera.tilt
= -90;
}
if (total_frames % 31) // trace twice a second, starting with the 1st frame
{
trace_below(); // trace the name of the texture below the car
}
wait (1);
}
}

L'action co-dessus est attachée à notre joueur (une voiture); elle attend que le dernier niveau soit chargé puis nous permet de contrôler la voiture à l'aide des touches de direction. J'ai utilisé une simple action pour la voiture car j'ai souhaité que vous puissiez voyager et noter les changements rapidement. Vous pouvez bien entendu utiliser votre propre action pour le joueur. Oh et vous pouvez bien entendu utiliser la touche shift en la maintenant enfoncé pour doubler la vitesse de déplacement.

L'action inclut 2 caméras différentes : une vue isométrique et une vue de dessus, pressez et maintenez la touche "T" pour voir l'action à partir d'une perspective "yeux d'oiseau" (bird'seye).

La même "action car" appelle la fonction nommée "trace_below" 2 fois par seconde:

function
trace_below()
{
vec_set(temp.x, player.x);
temp.z -= 10000; // trace 10,000 quants below the car
trace_mode
= ignore_me + ignore_passable + ignore_models + ignore_sprites + scan_texture;
trace (player.x, temp);
if (str_cmpi ("one",
tex_name)) // if the texture below the car is named "one" in wad
{
tile2.x = tile1.x;
tile2.y = tile1.y - 2048;
tile3.x = tile1.x
- 2048;
tile3.y = tile1.y - 2048;
tile4.x = tile1.x + 2048;
tile4.y = tile1.y;
tile5.x = tile1.x + 2048;
tile5.y = tile1.y - 2048;
tile6.x = tile1.x - 2048;
tile6.y = tile1.y;
tile7.x = tile1.x + 2048;
tile7.y = tile1.y + 2048;
tile8.x = tile1.x;
tile8.y = tile1.y + 2048;
tile9.x = tile1.x - 2048;
tile9.y = tile1.y
+ 2048;
}
.....................
if (str_cmpi ("nine", tex_name)) // if the texture below the car is named
"nine" in wad
{
tile1.x
= tile9.x + 2048;
tile1.y = tile9.y - 2048;
tile2.x = tile9.x;
tile2.y = tile9.y - 2048;
tile3.x = tile9.x
- 2048;
tile3.y = tile9.y - 2048;
tile4.x = tile9.x + 2048;
tile4.y = tile9.y;
tile5.x = tile9.x - 2048;
tile5.y = tile9.y + 2048;
tile6.x = tile9.x - 2048;
tile6.y = tile9.y;
tile7.x = tile9.x + 2048;
tile7.y = tile9.y + 2048;
tile8.x = tile9.x;
tile8.y = tile9.y + 2048;
}
}

Cette fonction fait le travail le plus dur, faites-moi confiance! Elle trace entre l'origine du joueur et un point situé 10,000 quants en dessous, retournant le nom de la texture qui est frappée par le rayon traceur. Si le nom de la texture est "one", le joueur se trouve sur le prelier préfab, aussi nous déplaçons des préfab se trouvant à l'arrière vers l'avant du joueur. Chaque préfab fait 2048 x 2048 quants, aussi si vous ajouez cette valeur à une tuile elle rempalcera une autre tuile, exactt? Ceci se répète pour les 9 préfabs et le code déplace les préfabs appropriés à chaque fois, donnant l'illusion (ou la réalité) d'un niveau qui ne finit jamais. Si vous voulez comprendre comment fonctionne le tapis roulant, maintenez simplement la touche T enfoncée puis déplacez la voiture dans le niveau à l'aide des touches de direction.

Le joueur démarre sur le 5eme carré (prefab); ci-dessous ce qui se passe pour chacun des carrés voisins:

Coment vous ne comprenez-pas? Ok, prenons l'exemple de la première image. Le joueur est sur le 5eme carré à un moment donné alors qu'il conduit la voiture, il arrive sur le 1er carré. A ce moment les carrés qui se trouvent derrière lui (3, 6, 9, 8 et 7), qui ne peuvent pas être vue sont déplacés devant le joueur - ce sont les carrés rouges 3, 6, 9, 8, 7. De cette façon le joueur ne verra rien d'anormal et c'est justement l'effet recherché - un niveau infini. Que se passe-t-il si le joueur décide de revenir sur le carré 5? Rien de spécial, les pièces sont tout simplement remise à leur place d'origine.

Pour le mêùe prix je vous donne encore 2 petits trucs:

action
sky_sphere
{
my.passable = on;
while (player
== null) {wait (1);}
while (1)
{
my.x = player.x;
my.y
= player.y;
wait (1);
}
}

La prmière idée est d'avoir un immense dôme de ciel qui se déplace en mêm temps que le joueur, donnant l'illusions d'un énorme niveau. La sphère, passable, attend que le joueur soit chargé puis elle lui prend ses coordonnées x et y à chaque cycle.

function toggle_fog() // toggles fog if you press the "F" key
{
fog_color = (fog_color == 0); // set fog_color to 0 (no fog) or 1
camera.fog_start = 500; // the fog starts at 500 quants
camera.fog_end = 2500; // and ends at 2500 quants
}

on_f toggle_fog;

Le 2eme truc est l'utilisation du brouillard; le brouillard est votre ami car il cache les transitions. Dans mon exemple le brouillard peut être bascué à l'aide de la touche "F". Si vos carrés font plus que 2048 quants (et ikl faut qu'ils le soient) vous devez être capable d'utiliser un mince brouillard (donc une plus grande valeur pour camera.fog_end) et obtenir ainsi de meilleurs résultas.

Que pouvons-nous faire avec tout ceci?? Bien, nous pourrions utiliser des terrains "recouvrables" pour nos préfabriqués de niveau et/ou nous pouvons créer différentes parties de niveau utilisant le même carré. Voici un début de ce que ç apourrait donner.

Supposons que notre voiture se trouve sur le carré 1 et se dirige vers la droite comme indiqué par la flèche jaune. A un moment le carré 1 va être déplacé vers le 1', qui inclut 3 arbres (les points bleus) puis comme le joueur avance toujours, le carré 1 va être déplacé vers le 1'' qui inclut 4 arbres. Ces arbres sont placés dans Wed, à la bonne hauteur, dans l'air mais lorsque le carré 1 est placé sous eux (à sa nouvelle position) cela parait naturel. Vous pouvez de cette façon ajouter des arbres, des maisons, tout ce que vous voulez trouver dans votre niveau. Une bonne méthode de conception de niveau consiste à copier/coller le carré 1 plusieurs fois, mettre les détails souhaités sur la copie du carré puis d'enlever toutes les copies du carré à l'exception de l'original.


Untitled

NPC parle (NPC signifie Non Player Character soit personnage qui n'est pas le joueur)

Ce morceau de code permet de faire parler n'importe qui à partir de n'importe quel modèle! Tout ce que vous devez faire c'est vous approcher du modèle et il commencera à "dire" les lignes une par une. Ces lignes sont stockées dans des chaînes de caractères; il y a 5 chaînes dans mon exemple (npc1_str... npc5_str) mais vous pouvez en ajouter plei d'autre bien en tendu. Les chaînes sont affichées en utilisant un text:

text npc_txt
{
layer
= 22;
pos_x = 0;
pos_y = 0;
font
= standard_font;
strings = 5;
string = null;
flags
= d3d;
}

Action npc1 est attachée au modèle (guard.mdl):

action
npc1
{
clip_size
= 0;
while (1)
{
temp.x = player.x - my.x;
temp.y
= player.y - my.y;
temp.z
= 0;
vec_to_angle(my.pan, temp);

Clip_size = 0 permet de montrer tous les triangles de tous les modèles mais il réduit un peu la vitesse. Les autres lignes sont utilisées pour tourner le NPC vers le joeur en permanence. (vous n'aimeriez pas parler avec le NPC et voir qu'il regarde ailleurs)

        ent_cycle("stand",my.skill9);
my.skill9 += 1.5 * time;
if (my.skill9
>= 100) {my.skill9 = 0;} // joue l'animation "stand"

Les lignes ci-dessus sont utilisées pour animer le garde avec son animation "stand"; j'ai déjà expliqué de nombreuses fois comment cela fonctionnait.

        my.skill10 = vec_dist(my.x,
player.x);
if (my.skill10
< 200)
{
if (my.skill11 == 0)
{
npc_talks();
my.skill11 = 1; // le npc dit une simple"ligne" (équivaut à exclusive_global)
}
}
else
{
my.skill11 = 0;
}
wait (1);
}
}

Lorsque le joueur s'approche à moins de 200 quants du npc, le npc lui dit une simple ligne (npc_talks est exécuté seulement une fois) parce que my.skill11 est mis à 1 aussitôt après l'approche, aussi my.skill11 = 0 n'est désormais plus vrai, comme sur l'image:

Mais pourquoi avoir choisi de faire un code aussi compliqué? N'aurait-il pas étét aussi simple de faire un code comme ceci?

if
(my.skill10 < 200)
{
npc_talks()
}

La réponse est non bien entendu. Lorsque le joueur est à moins de 200 quants, npc_talks() serait exécuté tout le temps et le npc dirait toutes ses lignes une par une en quelques instants. Ce que nous voulons c'est que le npc dise une ligne, puis attende que nous le quittions, puis dire une nouvelle ligne etc.

La fonction npc_talks() est simple:

function npc_talks()
{
if (my.skill12 < 5) {my.skill12 += 1;}

Nous utilisons 5 chaînes pour ce npc; si le joeur approche le npc plus de 5 fois, le npc répétera la dernière ligne.

      if (my.skill12 == 1) {npc_txt.string =
npc1_str;}
if (my.skill12 == 2)
{npc_txt.string = npc2_str;}
if (my.skill12 == 3) {npc_txt.string =
npc3_str;}
if (my.skill12 == 4)
{npc_txt.string = npc4_str;}
if (my.skill12 == 5) {npc_txt.string =
npc5_str;}

npc_txt.visible = on;
waitt (48);
npc_txt.visible = off;

}

Npc_text est montré (visible = on) pendant 3 secondes.


Untitled

Oiseaux volants

Je me rappelle avoir joué à Unreal1 et réussir à passer les premiers niveaux d'intérieur. J'ai été emprisonné et je suis parvenu à m'échapper et je suis sorti d'une caverne. J'ai été stupéfié ! L'énorme niveau extérieur avec des arbres , des oiseaux volant haut dans le ciel... Aujourd'hui je vais vous apprendre à créer le code pour un oiseau qui peut voler et prendre soin de lui-même. Regardons cela de plus près:

action move_bird
{
var init_pos; // initial position
of the bird
var bird_speed; // flying speed
var flying_distance;
flying_distance = my.skill1;
if (flying_distance == 0) {flying_distance = 2000;} // default flying distance
bird_speed = my.skill2;
if (bird_speed == 0) {bird_speed
= 5;} // default flying speed
my.passable = on;
vec_set (init_pos.x, my.x); // store the initial position of the bird
while (1)
{
temp.x
= bird_speed * time;
temp.y = 0;
temp.z = 0;
move_mode
= ignore_passable;
my.skill40 += ent_move (temp,
nullvector); // move in the direction given by the pan angle in Wed
if (my.skill40 > flying_distance)
{
my.skill40
= 0;
vec_set
(my.x, init_pos.x);
}
ent_cycle("walk", my.skill1); // animate the bird
my.skill1 += 3 * time; // animation speed
my.skill1 %= 100; // loop animation
wait (1);
}
}

Nous avons défini quelques variables ; init_pos stocke la position initiale de l'oiseau, bird_speed stocke la vitesse de l'oiseau et flying_distance stocke la distance que peut couvrir l'oiseau avant de revenirà sa position initiale. Vous pouvez mettre flying_distance en utilisant skill1 et bird_speed en utilisant skill2 dans Wed;autrement si vous oubliez de le faire le code se chargera demettre des valeurs prédéfinies de 2000 quants et 5. Nous rendons l'oiseau passabele (pas vraiement nécessaire) et nous stockons sa position initiale dans init_pos (qui est la position que vous avez donné à votre oiseau dans Wed) avant d'entrer dans une boucle while. La boucle conserve la vitesse corrigée de l'oiseau et déplace l'oiseau, stockant la distance couverte dans skill40; si cette valeur est plus grande que flying_distance, l'oiseau retourne vers sa position initiale et le processus recommence. Les dernières lignes animent l'oiseau bien qu'il ait un drôle de nom pour son animation (marche ou walk) mais vous pouvez bien entendu mettre ici le nom de votre animation.

Comment utiliser ces oiseaux? Placez-les dans le niveau avec des tailles légèrements différentes, choisissez leur orientation et donnez leur une bonne vitesse et une distance de vol. La distance doit être assez doit être assez grande pour que le joueur ne s'aperçoive pas de la supercherie (boucle). Assurez-vous pour cela que les oiseaux commencent et terminent leur vol dans une zone qui n'est pas visible par le joueur. Bon amusement!


           Panneaux, textes et boutons

Les panneaux, textes et boutons sont utilisés par un moteur 2D qui se trouve à l'intérieur de A4 / A5 pour donner un peu de saveur à vos jeux 3D; ne les sous-estimez pas - vous pouvez créer plein de jeux 2D ou truquer des jeux 3D avec le moteur 2D. J'ai créé un jeu éducatif sans intérêts qui fait une utilisation lourde de panneaux, des textes et des boutons et devinez-quoi ? Je l'ai appelé devinezquoi...(Guessme)

La fonction principale est vraiment trés simple :

function
main()
{
D3D_PANELS
= ON;
LOAD_LEVEL (<guess.wmb>);
start_game();
}

J'ai mis à 1 D3D_PANELS pour permettre une accélération matérielle des panneaux. J'ai également chargé un niveau simple (n'est pas nécessaire, mais utile si vous voulez ajouter un peu plus de matières ou si votre panneau ne couvre complètement votre écran).

J'ai créé un panneau qui couvre l'écran entier (800x600 pixels) et qui a son drapeau son drapeau VISIBLE à 1, donc il apparaîtra au début de jeu. J'ai deux boutons qui servent à commencer le jeu et à le quitter. Si vous vous rappelez la définition de bouton dans le manuel, quand je déplace la souris sur un des boutons, il changera son image bitmap à starthi_map ou quithi_map. Quand je clique sur un des boutons, son action correspondante (start_guessing ou quit_game) est exécutée.

PANEL
guessmain_pan
{
BMAP guess800_map;
LAYER 10;
POS_X 0;
POS_Y 0;
BUTTON 20,
20, start_map, start_map, starthi_map, start_guessing, NULL, mouse_over;
BUTTON 20,
70, quit_map, quit_map, quithi_map, quit_game, NULL, mouse_over;
FLAGS OVERLAY,
REFRESH, VISIBLE;
}

La fonction quit_game est vraiment simple - elle éteint le moteur; la fonctionstart_guessing est plus complexe. Avant que nous continuions , notez s'il vous plaît que les textes sont assez semblables aux panneaux; ils ont des couches et peuvent être déplacés sur l'écran.

function start_guessing() // version simplifiée
{
intro_text.VISIBLE = ON; // affiche le texte stocké dans intro_string ("Essayez de deviner un nombre...")
WAITT (16); // attend une seconde, puis démarre le défilement du texte vers le haut
WHILE (intro_text.POS_Y > -30) // s'assurent que les trois rangées ont disparu
{
intro_text.POS_Y -= TIME * 0.05 * (570 - intro_text.POS_Y); // nous modifions le POS_Y de intro_text pour défiler de plus en plus vite
WAIT (1);
}
intro_text.VISIBLE = OFF; // enlève le texte d'introduction
random_number = int(RANDOM(9)+1); // choisit un nombre aléatoire entre 1 et 9
click_text.VISIBLE = ON; // affiche le texte stocké dans in click_string ("Cliquer sur le clavier...")
keypad_pan.VISIBLE = ON; // affiche le panneau clavier
WHILE (success == 0) // si je trouve le nombre, success = 1
{
IF (random_number == your_guess) // découvert
{
smaller_pan.VISIBLE = OFF; // enlève le panneau 'flêche'
bigger_pan.VISIBLE = OFF;
PLAY_SOUND thanks_snd, 100; // joue le son merci
success = 1; // sort de la boucle while
}
ELSE
{
IF (random_number < your_guess) // mon nombre est plus grand
{
bigger_pan.VISIBLE = OFF;
smaller_pan.VISIBLE = ON; // donc nous rendons visible le panneau petite flêche
}
ELSE
{
smaller_pan.VISIBLE = OFF; // mon nombre est plus petit
bigger_pan.VISIBLE = ON; // donc nous rendons visible le panneau grande flêche
}
}
WAIT (1);
}
click_text.VISIBLE = OFF; // enlève le texte ("Cliquer sur le clavier...")
keypad_pan.VISIBLE = OFF; // et le reste des panneaux excepté guessmain
smaller_pan.VISIBLE = OFF;
bigger_pan.VISIBLE = OFF;
}

Finallement voici start_game()

function
start_game() // version  simplifiée
{
MOUSE_MAP = cursor_map;
// nous choisissons le curseur de souris, le plaçons dans le centre de l'écran
et laissons le curseur libre et mobile
MOUSE_POS.X = SCREEN_SIZE.X / 2;
MOUSE_POS.Y =
SCREEN_SIZE.Y / 2;
MOUSE_MODE = 2;
WHILE (1)
{
MOUSE_POS.X
= POINTER.X; // déplacement de la souris en x et y
MOUSE_POS.Y
= POINTER.Y;
IF
(MOUSE_LEFT == 1) // si le bouton gauche de la souris (lmb) est pressé,
nous vérifions s'il est au dessus de keypad_pan.
{
IF (MOUSE_POS.X > 650 && MOUSE_POS.X < 700)
{
IF (MOUSE_POS.Y > 0 && MOUSE_POS.Y < 50)
{
your_guess = 1;
}
IF (MOUSE_POS.Y > 50 && MOUSE_POS.Y < 100)
{
your_guess = 4;
}
IF (MOUSE_POS.Y > 100 && MOUSE_POS.Y < 150)
{
your_guess = 7;
}
}
IF (MOUSE_POS.X > 700 && MOUSE_POS.X < 750)
{
IF (MOUSE_POS.Y > 0 && MOUSE_POS.Y < 50)
{
your_guess = 2;
}
IF (MOUSE_POS.Y > 50 && MOUSE_POS.Y < 100)
{
your_guess = 5;
}
IF (MOUSE_POS.Y > 100 && MOUSE_POS.Y < 150)
{
your_guess = 8;
}
}
IF (MOUSE_POS.X > 750 && MOUSE_POS.X < 800)
{
IF (MOUSE_POS.Y > 0 && MOUSE_POS.Y < 50)
{
your_guess = 3;
}
IF (MOUSE_POS.Y > 50 && MOUSE_POS.Y < 100)
{
your_guess = 6;
}
IF (MOUSE_POS.Y > 100 && MOUSE_POS.Y < 150)
{
your_guess = 9;
}
}
}
WAIT (1);
}
}

Vous pourriez vous demander pourquoi j'emploie ce type de vérification pour le clavier et pas des boutons classiques. Tout d'abord, c'est un Tutorial et vous devez apprendre autant de tours que possible. En plus de cela, vous pouvez créer "un bouton" de pratiquement n'importe quelle forme et taille et employer ce type de code pour y cliquer.

Vous pouvez voir que j'ai placé le panneau de clavier dans le coin supérieur droit.

IF (MOUSE_LEFT == 1) // si le bouton gauche de la souris (lmb) est pressé, nous vérifions s'il est au dessus de keypad_pan.
{
IF (MOUSE_POS.X > 650 && MOUSE_POS.X < 700) // première colonne (1, 4, 7) démarre à 650 et termine à 700 pixels pour X
{
IF (MOUSE_POS.Y > 0 && MOUSE_POS.Y < 50) // premier rang(1, 2, 3) démarre à 0 et termine à 50 pixels pour Y
{
your_guess = 1; // vous avez cliqué sur 1
}
IF (MOUSE_POS.Y > 50 && MOUSE_POS.Y < 100) // deuxième rang (4, 5, 6) démarre à 50 et termine à 100 pour Y
{
your_guess = 4;
}
IF (MOUSE_POS.Y > 100 && MOUSE_POS.Y < 150) // troisième rang (7, 8, 9) démarre à 100 et termine à 150 pour Y
{
your_guess = 7;
}
}
..............................................

Nous n'avons pas besoin d'un panneau de clavier pour ce type d'action; keypad_pad est placé sur l'écran pour montrer au joueur où cliquer.


Untitled

Se sauver furtivement (comme un voleur) dans l'obscurité

Je me rappelle avoir jouer au jeu du voleur - vous devez voler différents objets pour obtenir de l'argent comptant. Vous partez furtivement dans l'obscurité, utilisant des flèches pour tuer les gardes et ainsi de suite. L'idée est intéressante et peut facilement valoriser n'importe quel jeu d'action. Ce morceau de code utilise 'trace 'pour mettre en application un voleur - comme comportement : essayez de rester dans l'obscurité et vous ne serez pas vu. Entrez dans la lumière et une alarme retentira.

function
scan_texture()
{
while (1)
{
vec_set(temp, player.x);
temp.z -= 300;
trace_mode = ignore_me + ignore_passable + ignore_models + ignore_sprites + scan_texture;
trace (player.x, temp);
if (tex_light > 50)
{
alarm();
}
waitt(8);
}
}

Trace trace une ligne entre deux points et retourne plusieurs paramètres. Le premier point est les coordonnées du joueur, le deuxième point est localisé 300 quants au-dessous du joueur. Nous faisons ceci parce que nous sommes intéressés à obtenir le tex_light, qui nous donne l'éclat de carte d'ombre dans ce secteur. Si tex_light est plus grand que 50 (le joueur est dans un secteur lumineux), l'alarme retentira deux fois une seconde - waitt(8).

J'emploie un panneau simple qui montre le tex_light - vous apprendrez comment les faire un peu plus tard. N'oubliez pas que votre niveau d'essai doit avoir des secteurs foncés et des secteurs lumineux - si le niveau est trop lumineux, l'alarme retentira tout le temps.


Untitled

peindre des peaux et des sprites

A6 a des particularités intéressantes, l'une d'elles est la possibilité de dessiner n'importe quoi sur n'importe quelle image bitmap en utilisant des instructions C-Script. J'ai choisi de peindre une croix rouge par dessus l'armure du garde dans cet article:

bmap* heros_skin; // bitmap pointer, will be used for the skin

action
my_hero
{
heros_skin = bmap_for_entity(my, 0); // get a pointer
to guard.mdl's skin
temp.red = 255; // paint the skin using the
red color
temp.green = 0;
temp.blue = 0;
format = bmap_lock (heros_skin, 0); // lock the skin bitmap
pixel = pixel_for_vec(temp, 100, format);
draw_cross(31, 108);
bmap_unlock (heros_skin); // unlock the bitmap
// put
the rest of the code for your hero here
}

Nous sommes dans la rubrique "coin des débutants" aussi le code ne doit pas être trop compliqué: nous mettons le pointeur préalablement défini nommé heros_skin à la peau de guard.mdl (c'est ce que fait bmap_for_entity) puis nous mettons temp.red à 255. Cela signifie que nous allons dessiner tous nos pixels en utilisant la couleur rouge. Nous "bloquons" la peau en utilisant bmap_lock, puis nous créons un pixel de couleur rouge, avec un facteur de transparence de 100 (donc complètement opaque) et le bon format de pixel pour notre dessin. Les 2 dernères lignes appelle la fonction qui dessine la croix rouge et débloque la peau.

Ici le code pour la fonction draw_cross:

function draw_cross(coords_x, coords_y)
{
pixel_to_bmap(heros_skin, coords_x + 3, coords_y + 0, pixel);
pixel_to_bmap(heros_skin, coords_x + 4, coords_y + 0, pixel);
pixel_to_bmap(heros_skin, coords_x + 5, coords_y + 0, pixel);
pixel_to_bmap(heros_skin, coords_x + 6, coords_y + 0, pixel);
pixel_to_bmap(heros_skin, coords_x + 3, coords_y + 1, pixel);
pixel_to_bmap(heros_skin, coords_x + 6, coords_y + 1, pixel);
pixel_to_bmap(heros_skin, coords_x + 3, coords_y + 1, pixel);
pixel_to_bmap(heros_skin, coords_x + 6, coords_y + 1, pixel);
pixel_to_bmap(heros_skin, coords_x + 0, coords_y + 2, pixel);
pixel_to_bmap(heros_skin, coords_x + 1, coords_y + 2, pixel);
pixel_to_bmap(heros_skin, coords_x + 2, coords_y + 2, pixel);
pixel_to_bmap(heros_skin, coords_x + 3, coords_y + 2, pixel);
pixel_to_bmap(heros_skin, coords_x + 6, coords_y + 2, pixel);
pixel_to_bmap(heros_skin, coords_x + 7, coords_y + 2, pixel);
pixel_to_bmap(heros_skin, coords_x + 8, coords_y + 2, pixel);
pixel_to_bmap(heros_skin, coords_x + 9, coords_y + 2, pixel);
pixel_to_bmap(heros_skin, coords_x + 0, coords_y + 3, pixel);
pixel_to_bmap(heros_skin, coords_x + 9, coords_y + 3, pixel);
pixel_to_bmap(heros_skin, coords_x + 0, coords_y + 4, pixel);
pixel_to_bmap(heros_skin, coords_x + 9, coords_y + 4, pixel);
pixel_to_bmap(heros_skin, coords_x + 0, coords_y + 5, pixel);
pixel_to_bmap(heros_skin, coords_x + 1, coords_y + 5, pixel);
pixel_to_bmap(heros_skin, coords_x + 2, coords_y + 5, pixel);
pixel_to_bmap(heros_skin, coords_x + 3, coords_y + 5, pixel);
pixel_to_bmap(heros_skin, coords_x + 6, coords_y + 5, pixel);
pixel_to_bmap(heros_skin, coords_x + 7, coords_y + 5, pixel);
pixel_to_bmap(heros_skin, coords_x + 8, coords_y + 5, pixel);
pixel_to_bmap(heros_skin, coords_x + 9, coords_y + 5, pixel);
pixel_to_bmap(heros_skin, coords_x + 3, coords_y + 6, pixel);
pixel_to_bmap(heros_skin, coords_x + 6, coords_y + 6, pixel);
pixel_to_bmap(heros_skin, coords_x + 3, coords_y + 6, pixel);
pixel_to_bmap(heros_skin, coords_x + 6, coords_y + 6, pixel);
pixel_to_bmap(heros_skin, coords_x + 3, coords_y + 7, pixel);
pixel_to_bmap(heros_skin, coords_x + 4, coords_y + 7, pixel);
pixel_to_bmap(heros_skin, coords_x + 5, coords_y + 7, pixel);
pixel_to_bmap(heros_skin, coords_x + 6, coords_y + 7, pixel);
}

Oui je sais c'est une longue fonction mais il ne vous aura pas échappé que toutes les lignes sont quasiment identiques. Examinons-là en détail:

pixel_to_bmap(heros_skin, coords_x + 3, coords_y + 0, pixel);

Cette instruction utilise pixel_to_bmap pour dessiner par dessus over heros_skin, aux coordonnées données par (coords_x + 3, coords_y + 0), utilsant la valeur pixel rouge et 100% opaque. Donc nous appelons la fonction draw_cross:

draw_cross(31, 108);

qui dessine un pixel pour chacune des lignes de la fonction, la première ligne dessinant un pixel rpuge à x = 34, y = 108 (31 + 3, 108 + 0). Mais comment ai-je obtenu cette valeur pour l'appel de la fonction draw_cross? Jetons un oeil sur la peau:

Le reste est simple: vous décidez de ce que vous voulez afficher (une étoile, un symbole etc) et vous le dessinez par exemple au préalable sur un papier quadrillé. Ici ma croix telle qu'elle apparaît sur le papier (les lignes grises sont justes pour vous montrer le quadrillage du papier):

Utilisez une instruction pixel_to_bmap pour chaquepixel, et n'oubliez pas que vous pouvez peindre également un sprite! A vous d'imaginer tout ce que vous pouvez faire avec ce code.


Untitled

Pluie intelligente

Nous aimons les particules, n'est-ce pas? Elles rendent vraiment bien et sont si rapides! La mauvaise nouvelle c'est qu'elle ne peuvent entrer encollision avec rien -c'est pour cela qu'elles sont rapides. Que se passe-t-il si vous voulez avoir de la pluie ou de la neige uniquement à l'extérieur de la maison ? Vous pouvez utiliser plusieurs générateurs de particules placés délicatement autour de la maison ou plus simplement vous pouvez utiliser le code de la pluie intelligente présenté ci-dessous.

L'idée est en fait toute simple, nous n'utilisons pas les particules mais des sprites et nous contrôlons si ils pénètrent quelque chose de solide. Si oui, leur chute cesse. Jetons un oeil à l'action qui génère la pluie qui tombe:

define area_x skill1;
define area_y skill2;
define speed skill3;
define number_of_drops skill4;

action
rain_maker
{
rain_ptr =
me;
my.invisible = on;
my.passable = on;
if (my.skill1 + my.skill2 + my.skill3
+ my.skill4 == 0)
{
my.area_x = 1000;
my.area_y = 1000;
my.speed = 20;
my.number_of_drops
= 300;
}
while (my.skill5 < my.number_of_drops)
{
temp.x = my.x
+ my.area_x / 2 - random(my.area_x);
temp.y = my.y + my.area_y / 2 - random(my.area_y);
temp.z = my.z;
ent_create(rain_pcx,
temp, create_drops);
my.skill5 += 1;
wait (1);
}
}

Vous pouvez attacher
l'action rain_maker à n'importe quelle entité car nous la rendons
invisible et passable. J'ai donné des noms un peu plus parlant pour skills1..4:
la zone de pluie en x et en y (relative à l'entité rain_maker),
la vitesse de descente de l'eau et la quantité d'eau. Si vous oubliez de
mettre ces variables dans Wed, leur somme sera égale à zéro
et le code basculera sur les valeurs par défaut: 1000 quants en x et y,
vitesse = 20 et 300 gouttent qui tombent.

Temp stocke une position aléatoire dans une zone rectangulaire définie par area_x et area_y; un nouveau sprite goutte d'eau sera créé à cette position. Skill5 est incrémenté à chaque goutte, aussi la boucle continue jusqu'à ce que toutes les gouttes soient créées La fonction qui s'exécute pour chaque goutte est :

function create_drops()
{
my.scale_x
/= 2;
my.transparent
= on;
my.passable
= on;
my.flare
= on;
while (1)
{
my.z -= (random(rain_ptr.speed)
+ rain_ptr.speed) * time;
if (content(my.x) == content_solid)
{
my.z
= rain_ptr.z;
}
wait (1);
}
}

Vous pouvez utiliser n'importe quel sprite mais impossible d'avoir un sprite moins large que 1 pixel. Je pense néanmoins que 1 pixel est unpeu trop gros lorsque vous êtes près de la goutte c'est pourquoi la première ligne divise la largeur par 2. La goute est transparente, passable et a son drapeau flare de mis. La goutte se déplace dans une boucle while (1) en changeant son z. J'utilise une valeur aléatoire afin que les gouttes ne descendent pas à la même vitesse.

Si la goutte frappe un obstacle (comme un plafond, etc) son contenu sera content_solid; la goutte sera remontée et nous pourrons la faire tomber de nouveau.De cette façon nous n'avons pas à créer des gouttes en permanence ce quipermet d'avoir un taux d'affichage satisfaisant.

Assurez-vous de n epas utiliser des milliers de sprites pour vos gouttes de pluie et n'oubliez pas d'augmenter le nexus si vous ajoutez plus de particules. Si vous prévoyez d'utiliser 400 gouttes de pluie le nexus à besoin d'être au moins à 50.

Si quelques gouttes traversent le plafond(cela arrive dans le niveau office) faites le plafond plus épais, les gouttes plus petites ou réduisez la vitesse des gouttes.


Untitled

Pianissimo

Pianissimo est un petit piano stéreo avec lequel on peut jouer de la musique à la souris; il utilse de vrais sons obtenus à partir de mon synthétiseur Yamaha. Utilisez-le comme instrument d'accompagnetou pour impressionner vos voisins.

Ce projet est autonome - regardons la fonction principle (main):

function
main()
{
level_load (dummy_wmb);
wait (2);
mouse_map = mouse_pcx;
mouse_mode = 2;
while (1)
{
mouse_pos.x = pointer.x;
mouse_pos.y = pointer.y;
wait (1);
}
}

Nous chargeons un niveau bidon (dummy), nous mettons le pointeur de souris, nous rendons le curseur visible et autorisons les mouvements de la souris dans une boucle while.

Le piano est en fait un panneau avec plusieurs boutons:

panel
main_pan // main panel
{
bmap = main_pcx;
pos_x = 190; // close to
the center of the screen
pos_y = 120; // on x and y
layer = 10;
button = 24, 54, keypressed_pcx,
key_pcx, key_pcx, key_pressed, null, null;
button = 45, 54, keypressed_pcx,
key_pcx, key_pcx, key_pressed, null, null;
button = 67, 54, keypressed_pcx,
key_pcx, key_pcx, key_pressed, null, null;
button = 89, 54, keypressed_pcx,
key_pcx, key_pcx, key_pressed, null, null;
button = 110, 54, keypressed_pcx,
key_pcx, key_pcx, key_pressed, null, null;
button = 132, 54, keypressed_pcx,
key_pcx, key_pcx, key_pressed, null, null;
button = 154, 54, keypressed_pcx,
key_pcx, key_pcx, key_pressed, null, null;
button = 175, 54, keypressed_pcx,
key_pcx, key_pcx, key_pressed, null, null;
button = 197, 54, keypressed_pcx,
key_pcx, key_pcx, key_pressed, null, null;
button = 219, 54, keypressed_pcx,
key_pcx, key_pcx, key_pressed, null, null;
flags = refresh, d3d, visible;
}

Le piano est placé près du centre de l'écran et a 10 boutons - les 10 touches sur le clavier. Lorsque vous pressez l'une de ces touches avec la souris, la fonction key_pressed est exécutée:

function key_pressed(key_number)
{
if (key_number == 1)
{
snd_play(do_wav,
70, -100);
}
if (key_number == 2)
{
snd_play(re_wav,
70, -70);
}
...........................................
if (key_number == 10)
{
snd_play(mi2_wav,
70, 100);
}
}

Lorsque nous pressons l'un des boutons sur le panneau, key_number est mis à une certaine valeur : 1 pour le premier bouton, 2 pour le second etc. De cette façon nous pouvons utiliser une seule fonction pour tous les boutons (toutes les touches du clavier). Si nous pressons un des boutons, une note est jouée en utilsiant une simple instruction snd_play. L'effet stéréo est obtenu en mettant différentes valeurs de balance pour les touches, démarrant à -100 pour la première touche et terminant à + 100 pour la dernière touche.


Untitled

Piège

Ce code a été créé pour vous, les utilisateurs qui ont aimé mon article paru dans le magazine Aum38 "musique intelligente". C'est une porte qui se ferme derrière le dos du joueur dès qu'il marche dessus un des pièges placés dans le sol.

action trap_trigger
{
while (player == null) {wait (1);}
while (vec_dist (player.x, my.x) > 50) {wait (1);} // play with 50
player_close = 1;
}

Les déclencheurs (triggers) attendent que le joueur soit créé puis ils attendent que le joueur soit à moins de 50 quants. C'est une valeur critique dépendant de la hauteur du joueur et de la position z du déclencheur, aussi vous devez jouer avec cette valeur pour un fonctionnement optimal. Si le joueur marche sur un des pièges, une variable nmmée "player_close" est mise à 1.

action
trap_player
{
while (player_close == 0) {wait (1);}
snd_play (gate_wav, 100, 0);
while (my.z > 170)
{
my.z -= 30 * time;
wait (1);
}
}

L'action ci-dessus est attachée à l'entité porte, elle attend que player_close soit à 1, joue le son gate_wav, et diminue son z jusqu'à ce qu'il soit à 170 quants (valeur expérimentale). Assurez vous que les déclencheurs ne soient pas trop près de la porte sinon le joueur risuqe d'avoir bobo à la tête ou de pouvoir s'échapper avant qu'elle se referme. Bien entendu vous pouvez modifier la vitesse de la dezcente en remplaçant 30 * time par 50 * time ou 100 * time, en fonction de vos besoins.


Untitled

Traduction Olivier Burton

Poussière

Vous ne voyez pas trop de poussière dans les jeux d' aujourd' hui, n' est-ce pas ? Toute cette puissance d' ordinateur tient les rues propres, mais peut-être parfois, quand vous voyagez dans un désert, vous pourriez employer quelques poussières; si c' est le cas, lisez çà.

La poussière est créée en employant des particules qui commencent au niveau des pieds du joueur et s' éléve, lentement et disparait. Bien sûr vous pouvez ajouter le même effet à vos monstres ou (même mieux) à vos véhicules.

Nous commençons par une fonction de démarrage, une fonction qui court elle-même au début du jeu :

var
pos1;
var pos2;
var start_pos;

starter
init_dust()
{
while (player == null) {wait (1);}
while (1)
{
vec_set (pos1, player.pos);
wait (1);
vec_set (pos2, player.pos);
if (pos1 != pos2)
{
start_pos.x = player.x;
start_pos.y = player.y;
start_pos.z = player.z - 30;
effect (dust, 1, start_pos, normal);
}
}
}

Le code attend que le joueur soit créé et ensuite nous stockons la position actuelle du joueur dans une variable nommée pos1. Nous attendons une frame et ensuite nous stockons la position du joueur de nouveau, cette fois dans une variable nommée pos2. Il y a deux possibilités :
a) Le joueur ne s' est pas déplacé pendant la dernière frame. Si c' est le cas, rien n' arrive;
b) Le joueur s' est déplacé pendant la dernière frame. Si c' est vrai, pos1 et pos2 tiennent des positions différentes donc nous mettons start_pos à une position près des pieds du joueur et nous commençons à émettre des particules en employant la fonction nommée dust :

function
dust()
{
temp.x = (random(2)
- 1) * 0.3;
temp.y
= (random(2) - 1) * 0.3;
temp.z = (random(1) + 1) * 0.2;
vec_add (my.vel_x, temp);
my.alpha = 10 + random(5);
my.bmap = dust_pcx;
my.size = random(5) + 10;
my.flare = on;
my.move
= on;
my.lifespan =
20;
my.function = fade_dust;
}

function
fade_dust()
{
my.alpha -= 0.3 * time;
if (my.alpha < 0) {my.lifespan = 0;}
}

La poussière a une vitesse aléatoire sur x et y et une vitesse aléatoire mais positive sur z pour s' assurer que la poussière se déplace vers le haut. Le bitmap employé pour les particules de poussière est dust_pcx et son facteur de transparence, aussi bien que ses gammes de taille de 10 à 15. La fonction qui dirige les particules (fade_dust) diminue leur alpha avant qu' elles ne soient rendues invisibles et ensuite elle les enlève.

Vous pouvez jouer avec la taille de la particule, son facteur alpha, sa vitesse et bien sûr avec la particule bitmap pour créer la poussière qui s' adapte à vos besoins. Oh et la touche F7 plusieurs fois pour commuter la caméra sur le côté si vous voulez être en mesure d' admirer la poussière.


Untitled

Utiliser plusieurs objets physiques

Si vous avez acheté la version A6 commerciale vous vous dites peut-être que "1 seul objet physique" ça n'est pas génial. Heureusement il y a une façon légale decontourner cet obstacle et cet article vous apprendra comment faire. Ceci intéressera également les utilisateurs de la version pro parce que je vais également apprendre à créer des sources lumineuses sur lesquelles on peut tirer

J'ai cherché à vous montrer ces lampes en action même si vous n'avez pas la version commerciale ou professionnelle c'est pourquoij'ai créé un petit film; assurez-vous d'avoir Media Player 9 d'installé sur votre PC et cliquez ici pour le voir.

L'article inclu 2 projets, commencez par charger physics1.wmp, compilez-le et exécutez-le en utilisant physics1.wdl. Vous devriez voir un corridor obscur; déplacez la souris et vous devriez voir la première lampe:

Assurez-vous que le curseur soit placé sur la lampe et tirez une balle orange en pressant le bouton gauche de la souris; vous pouvez voir que la lampe se met en mouvement, créant de jolis effets de lumière et d'ombre sur le sol et sur les murs:

Il y a 3 lampes comme celle-ci dans le premier niveau et chacune d'elle contrôle un intervalle de 700 quants, se transformant en objet "physique" dès que le joueur s'en approche. Je vous invite à explorer le niveau; je vous montre ci-dessous la vue de dessus de WED pour la première démo.

Nous allons à présent regarder le code. La fonction principale (main) charge simplement le niveau physics1_wmb :

function
main()
{
level_load (physics1_wmb);
}

Le curseur est une simple image bitmap de 6 x 6 pixels qui est défini comme panneau et placée au centre de l'écran:

panel
crosshair_pan
{
bmap = pointer_pcx;
pos_x
= 397;
pos_y = 297;
flags = visible, overlay;
}

Voici l'action attachée au joueur:

action
player1
{
player = my;
my.invisible = on;
while (1)
{
vec_set (camera.pos, my.pos);
camera.pan =
my.pan;
camera.tilt += 10 * mouse_force.y
* time;
my.pan -= 10 * mouse_force.x * time;
temp.x = 10 * (key_w - key_s) * time;
temp.y = 5 * (key_a - key_d) * time;
temp.z
= 0;
ent_move (temp, nullvector);
wait (1);
}
}

Le modèle du joueur est rendu visible; la caméra hérite des positions x, y et z ainsi que de l'angle pan du joueur. Nous pouvons modifier l'angle tilt de la caméra en bougeant la souris verticalement, et l'angle pan en bougeant la souris horizontalement. Le joueur peut avancer / reculer à l'aide des touches "Z" et "S" et se déplacer sur les côtés avec les touches "Q" et "D". Ces valeurs sont stockées dans temp à chaque cycle et déplace le joueur avec l'instruction ent_move. Vous pouvez voir que temp.z est mis à zéro, aussi le joueur n'utilise pas la gravité dans ce projet.

Que se passe-t-il lorsque le joueur presse le bouton gauche de la souris? Regardons!

on_mouse_left = fire_bullet;

function fire_bullet()
{
ent_create (bullet_mdl, camera.x, move_bullet);
}

Chaque fois que nous pressons le bouton gauche de la souris, la fonction fire_bullet() est exécutée. Cette fonction crée une entité bullet_mdl à la position donnée par la caméra et de ce fait exécute la fonction nommée move_bullet():

function
move_bullet()
{
my.unlit = on;
my.enable_block = on;
my.enable_entity = on;
my.enable_impact = on;
my.pan = camera.pan;
my.tilt = camera.tilt;
my.event = remove_bullet;

L'entité balle n'est pas affectée par la lumière du niveau car nous voulons également la voir lorsqu'elle se déplace dans les parties sombres du niveau. La balle est sensible aux blocs du niveau et aux entités; elle hérite des angles pan et tilt de la caméra et exécute sa fonction remove_bullet() à chaque fois qu'elle entre encollision avec un bloc de niveau ou une autre entité.

while(1)
{
if (vec_dist (my.x,
camera.x) < 50)
{
my.passable = on;
}
else
{
my.passable = off;
}
temp.x = 30 * time;
temp.y = 0;
temp.z = 0;
move_mode = ignore_you + ignore_passable + glide;
ent_move (temp, nullvector);
wait(1);
}
}

La balle ne doit pas entrer en collision avec le joueur aussi elle est rendue passable jusqu'à ce que la distance entre elle et le joueur soit supérieure à 50 quants. TLa balle se déplace à la vitesse donnée par temp.x, ignorant les objets passables. Regardons ce qui se passe lorsque la balle touche un mur ou une lampe:

function remove_bullet()
{
wait (1);
ent_remove(me);
}

Cette simple fonction attend un cycle puis enlève la balle. Voici un morceau de code plus intéressant, celui attaché à chaque modèle de lampe:

action physics_lamp
{
while (player == null) {wait
(1);}
my.unlit = on;
my.ambient = 100;
my.lightrange = 300;
my.spotlight = on;
vec_set (d3d_spotlightcone, vector (75, 30, 4));

La lampe attend que le joueur soit créé, puis met son drapeau "unlit" à on et son ambient à 100. Nous faisons cela car nous avons besoin de voir notre lampe même dans le noir, sinon nos balles auraient peu de chance de trouver leur cible. La lampe génère une lumière dynamique dans un rayon de 300 quants et a son drapeau "spotlight" de mis à 1. Nous mettons d3d_spotlightcone à la bonne valeur par notre vecteur (75, 30, 4); vous pouvez jouer avec ces valeurs depuis le vecteur (1, 1, -5) au vecteur (90, 90, 5).

    ph_setgravity (vector (0, 0, -386));
phent_settype
(my, ph_rigid, ph_box);
phent_setmass (my, 10, ph_box);
phent_setdamping (my, 20, 50);
my.skill20 = phcon_add (ph_ball,
my, 0);
phcon_setparams1 (my.skill20, vector (my.x, my.y,
(my.z + 35)), nullvector, nullvector); // play with "35"

Les lignes ci-dessus transforme notre lampe classique en une entité physique. Avant tout nous mettons la gravité à 9.81 m/s2 si nous admettons que 1 quant = 1 pouce puis nous disons au moteur que notre lampe doit être traitée comme un objet physique dont la coque serait une boîte. La lampe a une masse de 10 et son comportement de rotation sera celui d'une boîte. Pour terminer nous mettons l'atténuation (damping) linéaire à 20 et l'atténuationangulaire à 50 pour notre lampe.

Nous avons besoin d'ajouter une contrainte à notre entité lampe car nous devons pouvoir la balancer comme sur l'image ci-dessous:

    my.skill20 = phcon_add (ph_ball, my, 0);
phcon_setparams1 (my.skill20, vector (my.x, my.y, (my.z + 35)), nullvector, nullvector);
// play with "35"

J'ai copié de nouveau les 2 lignes de code afin que vous voyez mieux de quoi il s'agit. La première ligne met une contrainte de balle à l'entité lampe et stocke l'identifiant pour la contrainte dans skill20 pour des manipulations futures; la seconde ligne de code met les paramètresde la contrainte dont l'identifiant (id) a été stocké dans skill20. Nous utilisons un vecteur qui donne les valeurs x et y de notre lampe et ajoutons 35 quants à l'axe des z; ce sont les coordonnées du point de balancement pour la lampe. Le reste des paramètres sont mis à nullvector car (conformément au manuel) ph_ball n'a besoin d'avoir que son point d'ancrage de mis.

C'est tout en ce qui concerne la partie physique! pour ceux d'entre vous qui sont toujours dans le brouillard, voici une petite explication amicale : nous disons que notre lampe doit se déplacer comme si elle tournait autour d'une sphère et nous mettons le centre de la sphère avec le vecteur (my.x, my.y, (my.z + 35). Ce qui signifie que le centre de rotation aura les mêmes positions x et y que la lampe et placée 35 quants au dessus de la lampe (le point rouge de la figure ci-dessus).

    while (1)
{
if (vec_dist (player.x, my.x) < 700) // 700 = experimental value
{
phent_enable
(my, 1);
}
else
{
phent_enable (my, 0);
}
wait (1);
}
}

La boucle while ci-dessus valide ou invalide l'entité physique en fonction de la distance entre le joueur et la lampe. Pensez à enlever la boucle si vous avez la version pro A6 car cette boucle ne vous aidera pas. Le reste de l'article ne s'adresse qu'au possesseur de la version A6 commerciale.

Toutes les lampes contrôlela distance entre elle et le joueur à chaque cycle; si le joueur s'approche à moins de 700 quants d'une lampe, cette lampe démarre son action pour devenir un objet physique pendant que les autres lampes se figent. Cela donnera un bon effet à la condition que le joueur ne voient pas toutes les lampes en même temps c'est pourquoi mon niveau à cette forme. Vous pouvez jouer avec la valeur "700"; j'ai choisi une petite valeur car je voulais que vous observiez le comportement des lampes; je pense que "1000" est une bonne valeur pour the niveau de démo.

La méthode ci-dessus vous permet d'utiliser plusieurs entités physiques disséminés dans votre niveau.; j'aiutilisé une lampe ici mais ça peut être tout autre chose comme un tonneau, un véhicule etc, assurez-vous simplement qu'ils ne soient pas visibles / utilisés en même temps. Exécutez le contrôle de distance pour tous les objets physiques et mettez phent_enable à 1 ou à 0 en fonction de la distance de cet objet au joueur.

Ok, je sais que je vous avais dit que vous pourriez utilisez plusieurs entités en même temps avec la version A6 commerciale et le truc ci-dessus ne le fait pas du tout, parce que chaque lampe doit être utilisée seule et donc pas en même temps. Alors pourquoi ne chargeriez-vous pas le niveau physics2.wmp, le compiler et exécuter physics2.wdl? Vous verrez une pièce avec 2 lampes:

Ces 2 lampes sont de véritables objets physiques et peuvent être utilisés en même temps même avec une version A6 commerciale! Comment cela se peut-il??

Ue grande quantité du code de physics2.wdl est identique à celui de physics1.wdl, aussi nous ne parlerons que de ce qui change ici. Tout d'abord nous avons une fonction starter qui ressemble à ceci:

starter
multiple_objects()
{
while (1)
{
ent_number = 1;
wait
(2);
ent_number = 2;
wait (2);
}
}

C'est une simple fonction qui met une variable nommée ent_number à 1 pour 2 cycles, puis met ent_number à 2 pour 2 cyccles etc. Cette variable est utilisée dans l'action attachée aux lampes:

action
physics_lamp
{
my.unlit = on;
my.ambient
= 100;
my.lightrange = 300;
my.spotlight = on;
vec_set (d3d_spotlightcone, vector (75, 30, 4));

   ph_setgravity (vector (0, 0, -386));
phent_settype (my, ph_rigid, ph_box);
phent_setmass (my, 10, ph_box);
phent_setdamping (my, 20, 50);
my.skill20 = phcon_add (ph_ball,
my, 0);
phcon_setparams1 (my.skill20, vector (my.x, my.y, (my.z
+ 35)), nullvector, nullvector);

Tout le code ci-dessus est le mêm que pour physics1.wdl, je ne ferai donc aucun commentaire.

  while (1)
{
if ((my.skill1 == 1) && (ent_number == 1))
{
phent_enable
(my, 1);
}
if ((my.skill1 == 2) && (ent_number == 2))
{
phent_enable
(my, 1);
}
wait (1);
phent_enable (my, 0);
}
}

Cette foisla boucle rend l'entité en objet physique pour 2 cyccles (c'est le temps pendant lequel ent_number est mis à 1) puis il autorise la deuxième lampe à devenir une entité physique pour 2 cyccles car chaque lampe a un skill1 de mis à une valeur différente (1, 2, ...) dans Wed. Si vous vouliez 3 entités physiques en même temps nous utiliserions quelque chose qui ressemblerait à ça :

starter multiple_objects()
{
while (1)
{
ent_number = 1;
wait (2);
ent_number = 2;
wait (2);
ent_number = 3;
wait (2);
}
}

action physics_lamp
{

   ....................................................
while (1)
{
if ((my.skill1 == 1) && (ent_number == 1))
{
phent_enable
(my, 1);
}
if ((my.skill1 == 2) && (ent_number == 2))
{
phent_enable
(my, 1);
}
if ((my.skill1 == 3) && (ent_number == 3))
{
phent_enable
(my, 1);
}
wait (1);
phent_enable (my, 0);
}
}

C'est comme cela que ça fonctionne. Nous continuons à avoir un seulobjet physique (qui est la limite dela version A6 commerciale) mais elle est partagée par plusieurs entités suffisemment rapidement pour donner l'impression d'avoir plusieurs entités physiques en même temps!

Combien d'objet sdifférents pouvons-nous utiliser en même temps? Et bien tout dépendr de la nature de votre projet;si le jeu tourne à 80 fps et que nous avons 4 lampes de visibles en mêm temps chacune d'elle se déplacera à 80 fps / 4 = 20 fps, ce qui est ok.Si vous prévoyer de faire un jeu de billard, avec toutes les balles comme entités physiques, elles bougeront à 10 fps.

Mes tests ont montré que vous pouviez utiliser 3-4 objets physiques sur un PC décent, ce qui est un grand progrès comparé à la limité d'un seul objet de la version A6 commerciale. Et pensez qu'en combinant les 2 projets ci-dessus vous pouvez avoir encore beaucoup plus d'objets physiques dans votre niveau.


Untitled

Raie de lumière

Lorsque vous démarrez un nouveau jeu Half-Life (HL à partir de maintenant), vous ne pouvez pas sauter l'intro. A un certain moment il y a une porte sur la gauche; lorsque cette porte s'ouvre, vous pouvez voir la lumière arriver de la porte - et c'est ce que nous allons réaliser à présent.

Je pense que cet effet doit être utilisé pour des scènes coupées ou pour des endroits qui ne peuvent pas être atteints par le joueur. Je vous entends vous demander : pourquoi cela? Simplement parce qu'il s'agit d'un trucagecomme dans HL.

HL utilise un sprite pour la lumière, nous utiliserons un modèle car c'est un objet 3D et il rendra mieux si en plus vous utilisez une bonne texture pour sa peau. J'ai utilisé un blanc uni pour la texture mais je ne suis pas modeleur, en fait je pourrai facilement vous apprendre comment "ne pas faire" un modèle.

L'idée est simple : lorsque la porte se lèvera, nous déplacerons l'objet lumière vers la droite; lorsque la porte se rabaissera, nous déplacerons le modèle lumière vers la gauche. Jetons tout d'abord un oeil à la fonction attachée au modèle lumière:

entity* light_ptr;

action light_model
{
light_ptr = me;
my.skill1 = my.y;
my.transparent = on;
my.passable = on;
my.alpha = 30;
}

J'ai créé un pointeur pour le modèle lumière, de cette façon je peux utilser ce modèle dans d'autres actions ou fonctions. Le modèle se déplace sur son axe des y dans mon niveau exemple; nous ne pouvons pas être sur que lorsqu'il devra revenir à sa position de départ , et ce plusieurs fois, qu'il retrouve sa position d'origine, c'est pourquoi nous stockpns cette position dans son skill1. Le modèle ess transparent, passable (les autres entités peuvent passer à travers la lumière, non?). Le facteur de transparence est mis à 30.

La porte fait
le reste du boulot- regardons son code:

action
hl_gate
{
my.skill1 = my.z;
while (light_ptr == null) {wait (1);}
while (player == null) {wait (1);}
while (1)
{
while (vec_dist
(player.x, my.x) > 600) {wait (1);}
while (my.z <
my.skill1 + 200)
{
my.z += 3 * time;
light_ptr.y -= 4 * time;
wait (1);
}
while (vec_dist
(player.x, my.x) < 700) {wait (1);}
while (my.z >
my.skill1)
{
my.z -= 5 * time;
light_ptr.y += 7.5 * time;
wait (1);
}
my.z = my.skill1;
// correct the displacement
light_ptr.y = light_ptr.skill1;
wait (1);
}
}

Nous stockons la coordonnée z de la porte dans skill1 car nous voulons la placer dans la bonne position à chaque fois. Nous attendons que le modèle de lumière et le modèle joueur soient chargés puis nous entrons dans une boucle while (1) - une boucle qui s'exécute en permanence.

Tant que la distance entre la porte et le joueur est supérieure à 600 quants, la porte n efait rien. Lorsque le joueur s'approche, la porte se déplacera vers le haut jusqu'à une hauteur supérieure de 200 quants à celle d'origine. Dans le même temps, la lumière est déplacée avec une vitesse donnée par 4 * time; jouez avec le 4 jusqu'à obtenir de bons résultats. La porte s'est ouverte, la lumière a jailli, quoi d'autre à présent? Tant que le joueur est à moins de 700 quants de la porte, rien ne se passe. Lorsque le joueur s'éloigne de la porte, elle redescend jusqu'à ce que sa hauetrur retrouve sa valeur d'origine. Dans le même temps, la lumière s'éloigne du joueur (vers la gauche dans l'image ci-dessus) avec une vitesse de 7.5 * time.

La dernière ligne corrige les déplacements pour la porte et la lumière. En effet si laporte s'est déplacée par exemple de 200 à 400 quants plusieurs fois, elle peut terminer à 199.2 au lieu de 200 quants.Ces lignes remettent donc la porte et la lumière à leur position d'origine.

Cet effet rend parfaitement bien la nuit. Pourquoi ne puis-je pas utiliser cet effet pour toutes les portes de mon niveau? Que se passe-t-il si le joue passe la porte?
Ca ne sera pas très joli si le joueur passe à travers le modèle de lumière. Tous les triangles qui composent le modèle de lumière doivent être 100% visibles pour être rendus. Mon modèle consiste en seulement quelques triangles mais si vous voulez vraiment utilser cet effet voici quelques idées (testées):
- coupez le modèle de lumière en autant de faces que possible : ouvrez Med, choisissez le mode triangle, sélectionnez une face - pressez "subdivide". De cette façon vous obtiendrez de nombreux petis triangles mais n'oubliez pas de jeter un oeil sur le compteur de polygones;
- rendez le modèle de lumière plus transparent lorsque vous vous approchez de lui;
- utilisez du brouillard blanc pour les murs à l'intérieur de la pièce et augmentez camera.fog en fonctionde la distance du joueur au modèle de lumière.


Untitled

Traduction Olivier Burton - correction AB

Réveil

Le code d'horloge ci-dessous vous permet de paramétrer une alarme en employant les heures et les minutes; l'alarme hurlera son son ennuyeux pendant une minute.

Le panneau principal montre l'image d'horloge et les quatre boutons (des flèches) utilisés pour paramétrer l'heure (et les minutes) de l'alarme: + - heures et + - minutes.

panel
main_pan // main panel
{
bmap = main_pcx;
pos_x = 0;
pos_y = 0;
layer = 10;

button = 260, 260, arrowup_pcx, arrowup_pcx, arrowup_pcx, null, hours_up, null;
button = 280, 260, arrowdown_pcx, arrowdown_pcx,
arrowdown_pcx, null, hours_down, null;
button =
420, 260, arrowup_pcx, arrowup_pcx, arrowup_pcx, null, minutes_up, null;
button = 440, 260, arrowdown_pcx, arrowdown_pcx, arrowdown_pcx,
null, minutes_down, null;

flags = overlay, refresh, d3d, visible;
}

Clock_text montre le temps actuel sous cette forme: hh:mm ss et alarm_text montrent le temps d'alarme sous cette forme hh:mm. Vous savez que h signifie Heures, m = Minutes et s = Secondes, OK ?

text
clock_text
{
layer = 20;
pos_x = 270;
pos_y = 200;
font = lcd_font;
string = clock_string;
flags = visible;
}

text
alarm_text
{
layer = 20;
pos_x = 300;
pos_y = 260;
font = lcd_font;
string = alarm_string;
flags = visible;
}

La partie la plus importante du code est incluse dans la fonction principale (main):

function
main()
{
screen_color.red = 0;
screen_color.green = 0;
screen_color.blue = 0;
mouse_map = mouse_pcx;
mouse_mode = 2;
while (1)
{
mouse_pos.x = pointer.x;
mouse_pos.y = pointer.y;

Cette fonction principale ne charge pas de niveau; nous mettons la couleur de fond à rgb = 000, nous employons mouse_pcx pour l'indicateur de souris et nous montrons le curseur. Les 2 premières lignes à l'intérieur de la boucle while s'assure que nous pouvons déplacer la souris sur le panneau.

str_for_num(clock_string, sys_hours);
str_cat(clock_string, ":");
if (sys_minutes < 10)
{
str_cat(clock_string, "0");
}
str_for_num(temp_string, sys_minutes); // lit sys_minutes et le convertit en string
str_cat(clock_string, temp_string); // ajoute les minutes à clock_string
str_cat(clock_string, " "); // laisse un espace blanc avant l'addition des secondes
if (sys_seconds < 10) // ajoute"0" si les secondes sont montrées avec un seul chiffre
{
str_cat(clock_string, "0");
}
str_for_num(temp_string, sys_seconds); // lit sys_seconds et le convertit en string
str_cat(clock_string, temp_string); // ajoute les secondes clock_string

Nous lisons sys_hours, nous le convertissons de nombre en chaîne de caractères et nous le mettons dans clock_string. Nous ajoutons ":" à la chaîne; jusqu'ici nous avons obtenu "hh :". Si sys_minutes < 10, l'horloge montrerait quelque chose du genre: "23 : 2" donc nous ajoutons un zéro après ":" pour que l'horloge montre quelque chose comme cela : "23:02". Maintenant nous pouvons lire sys_minutes, nous le convertissons en chaîne, nous ajoutons les minutes à clock_string et ensuite nous laissons un espace " " avant l'addition des secondes. Si sys_seconds < 10 nous ajoutons un autre zéro à la chaîne et ensuite nous lisons sys_seconds, nous le convertissons en chaîne et ensuite nous l'ajoutons à clock_string, obtenant quelque chose comme : "23:02 45".

Voyons le code qui s'occupe de l'alarme :

str_for_num(alarm_string, alarm_hours); // convertit alarm_hours en string
str_cat(alarm_string, ":"); // add :
if (alarm_minutes < 10) // ajoute"0" si les minutes sont montrées avec un seul chiffre
{
str_cat(alarm_string, "0");
}
str_for_num(temp_string, alarm_minutes); // convertit alarm_minutes en string
str_cat(alarm_string, temp_string); // ajoute alarm_minutes à alarm_string

if ((alarm_hours == sys_hours) && (alarm_minutes == sys_minutes)) // son
de l'alarme
{
start_alarm();
}

J'ai mis ces valeurs (par défaut) pour alarm_hours et alarm_minutes : 12 et 30. Nous convertissons alarm_hours en chaîne, nous ajoutons ":", nous ajoutons un zéro si alarm_minutes < 10, nous convertissons alarm_minutes en chaîne et ensuite nous l'ajoutons à alarm_string. Si alarm_hours (mis par nous) = sys_hours (l'horloge du PC) et alarm_minutes (mis par nous) = sys_minutes (l'horloge du PC) l'alarme commencera et fonctionnera une minute, jusqu'à alarm_minutes! = sys_minutes.

wait (1);
}
}

La fonction start_alarm est facile:

function start_alarm()
{
exclusive_global; // ne permet plusieurs instances si cette fonction tourne
while (snd_playing(alarm_handle)) {wait (1);}
alarm_handle = snd_play (alarm_wav, 100, 0);
}

Nous ne permettons pas plusieurs instances de cette fonction et nous jouons un son dans une boucle, attendant qu'il finisse avant de le jouer de nouveau.

Le reste du code est employé pour mettre le temps d'alarme lorsque nous pressons un des boutons ( flèches) :

function
hours_up()
{
if (alarm_hours < 23)
{
alarm_hours += 1;
}
}

function
hours_down()
{
if (alarm_hours > 0)
{
alarm_hours -= 1;
}
}

function
minutes_up()
{
if (alarm_minutes < 59)
{
alarm_minutes += 1;
}
}

function
minutes_down()
{
if (alarm_minutes > 0)
{
alarm_minutes -= 1;
}
}

Il n'y a rien de spécial ici; si vous cliquez sur la flèche des heures, alarm_hours sera incrémentée jusqu'à 23. Le reste des fonctions est semblable.


Untitled

Scènes coupées scénarisées

Le monde change! Les bons vieux films 256 couleuyrs qui étaient utilisés pour les scènes coupées il y a quelques années sont remplacées par des scènes coupés scénarisées qui sont créées en utilisant le moteur du jeu lui même. Et cet article vous expliquera cette technique

Commençons par le scénario:

1) Quelques soldats discutent près de 2 camions qu'ils sont censés protéger;

2) Un hélicoptère atterrit silencieusement tout près sans être vu. 2 hommes masqués sortent de l'hélicoptère et se dirigent vers les camions;

3) Une camionnette passant bruyamment devant les soldats, attire leur attention;

4) Un des hommes masqués saute à l'intérieur d'un des camions, pose l'explosif et se sauve;

5) Les camions explosent et les soldats sont tués.

Tout ceci est créé simplement avec des actions et des fonctions:

function
main()
{
fps_max = 40;
level_load (cutscene_wmb);
wait (3);
camera.x = -800;
camera.y =
-1100;
camera.z = 50;
camera.pan = 115;
camera.tilt = -10;
}

Il est fortement conseillé de vérouiller le taux d'affichage pendant la scène coupée; vous verrez pourquoi dans une minute. Nous chargeons le niveau, nous attend qu'il soit chargé et nous mettons la position initiale de la caméra:

Voici l'action qui est attachée au soldat 1:

action
soldier_1
{
soldier1 = my;
while (vec_dist
(my.x, soldier2.x) > 150)
{
ent_cycle("stand", my.skill20);
my.skill20
+= 0.25;
if (my.skill20 == 19)
{
snd_play (radio_wav,
20, 0);
}
wait (1);
}
my.skill20 = 0;
snd_play (allquiet_wav, 50, 0);
while (my.skill20 < 95)
{
ent_cycle("salut", my.skill20);
my.skill20 += 5 * time;
wait (1);
}
snd_play (isee_wav, 60, -30); // bigger volume for
the left channel

Le premier soldat attend jusqu'à ce que la distance entre lui et soldier2 soit inférieure à 150 quants, jouant son animation "stand" dans une boucle. Notez que je n'utilise pas la valeur de correction time mais une valeur fixe (0.25) parce que je veux pouvoir jouer un son à une frame d'animation spécifique.Nous n'avons pas besoin d'utiliser time puisque le taux d'affichage est bloqué.

L'officier s'est approché à présent (voir la première image) aussi le premier soldat dit la ligne qui contient allquiet_wav puis il salue l'officier. Le même soldat dit la phrase de l'officier (isee_wav) avec un plus fort volume sur la gauche avec le facteur de balance -30 pour donner l'impression que c'est l'officier qui parle. Ce que j'essaye de faire ici, c'est que ça ne sert à rien de faire quelque chose si ça ne doit pas paraître réel.

    sleep
(2);
media_play("soundtrack.wav", null, 20); // start the soundtrack
chopper_moves = 1;
camera.x = 300;
camera.y
= -2700;
camera.z = 50;
while (start_van ==
0) {wait (1);}
while (van1.x < 1000)
{
vec_set(temp, van1.x);
vec_sub(temp, my.x);
vec_to_angle (my.pan,
temp); // rotate the soldier towards the van
wait (1);
}
snd_play (itsnothing_wav, 80, 0);
while (start_explosion == 0) {wait (1);}
sleep (0.2);
while (my.y > -3000)
{
my.y -= 50 * time;
my.x -= 5 * time;
my.z += 30 * time;
my.roll += 50 * time;
wait (1);
}
sleep (9); // experimental value
beep; //
you would load a new (playable) level here
}

Deux secondes passent et la piste son commence à jouer. La variable nommée"chopper_moves" est mise à 1, disant à l'hélicoptère de commencer à se déplacer et nous mettons une nouvelle position pour la caméra. Rien ne se passe avec soldier1 jusqu'à ce que start_van soit mis à 1. C'est une variable utilisé par la camionnette qui va distraire les 2 soldats. Donc dès que la camionnette va démarrer les 2 premiers soldats la suivtont des yeux en permanence, nous pourrions utiliser l'animation par os ici si vous avez la version A6 mais j'ai préféré faire une version compatible également avec A5.

Le x de la camionnette est à présent supérieur à 1000 aussi le soldat dit la phrase itsnothing_wav. puis il ne fait plus rien en attendant que la variable "start_explosion" soit à puis après 0.2 seconde, il commence à voler :) comme sur l'image ci-dessous:

Nous attendons encore 9 secondes (valeur obtenue par tests) puis nous jouons un beep, c'est ici que vous pouvez inclure une instruction level_load qui charger le jeu proprement dit.

action
soldier_2
{
soldier2 = my;
sleep (6); //
wait for 6 seconds
while (vec_dist (soldier1.x, my.x) > 70)
{
ent_cycle("run", my.skill20);
my.skill20 += 4 * time;
my.skill20 %= 100; // loop the animation
my.y
+= 5 * time;
wait (1);
}
my.skill20 = 0;
while (my.skill20 < 95)
{
ent_cycle("salut", my.skill20);
my.skill20 += 3.5 * time;
wait (1);
}

L'officier attend 6 secondes; ce qui nous laisse assez de temps pour regarder le premier soldat. L'officier se dirige vers le soldat en augmentant son y dans une boucle while; il joue également son animation "run" . Lorsque l'officier est à moins de 70 quants du soldat, il s'arrête et joue son animation "salut" .

  while (start_van == 0) {wait (1);}
while (van1.x <
1000)
{
vec_set(temp, van1.x);
vec_sub(temp, my.x);
vec_to_angle (my.pan, temp); // rotate the soldier towards the van
wait (1);
}
while (start_explosion == 0) {wait
(1);}
sleep (0.3);
while (my.y > -3000)
{
my.y -= 50 * time;
my.x -= 10 * time;
my.z += 30 * time;
my.roll += 50 * time;
wait (1);
}
}

L'officier attend tant que start_van n'est pas mis à 1,et comme pourle premier soldat il suit la camionnette des yeux tant que l'x de la camionnette est inférieur à 10000. Puis il attend que l'explosion soit déclenchée et après 0.3 seconde, il se met à voler exactement comme le premier soldat

action
john_doe
{
my.skill20 = random(100); // set a random animation
start for every dummy soldier
while (start_explosion == 0)
{
ent_cycle("stand", my.skill20);
my.skill20 += 1 * time;
my.skill20 %= 100; // loop the animation
wait
(1);
}
sleep (0.5 + random(1) / 3);
snd_play (scream_wav, 80, 0);
my.skill10 = my.x;
while (my.x > my.skill10 - 1000)
{
my.x -= 50 * time;
if (my.x > my.skill10 - 500)
{
my.z += 20 * time;
if (my.tilt < 180) {my.tilt
+= 50 * time;}
}
else
{
ent_cycle ("death", 95);
my.z -= 20 * time;
}
wait (1);
}
my.z = 20;
}

Cette action est assignée aux 6 soldats qui se tiennent près du 2eme camion. On leur donne une frame de départ aléatoire puis ils jouent leur animation d'attente dans une boucle en attendant l'explosion. (start_explosion est mis à 1). Lorsque cela se produit, les soldats attendent entre 0.5 et 0.83 seconde, jouent le son scream_wav et se mettent à voler. Cette fois-ci j'ai voulu montrer leur corp s'écrasant sur le sol aussi j'ai choisi une méthode différente : les soldats stockent leur x initial en utilisant leur propre skill10 et ils volent tant que leur x est supérieur à 1000. La dernière ligne met leur z à une bonne valeur (20).

action
chopper
{
var chopper_handle;
var animation_speed
= 20; // chopper's animation speed
while (chopper_moves == 0)
{wait (1);} // wait until the chopper is allowed to move
while
(my.z > 35)
{
ent_cycle("fly",
my.skill20); // play "fly" animation
my.skill20
+= animation_speed * time;
my.skill20 %= 100;
// loop the animation
my.z -= 10 * time;
my.y -= 20 * time;
vec_set(temp, my.x);
vec_sub(temp, camera.x);
vec_to_angle (camera.pan, temp); // rotate the camera
towards the chopper
wait (1);
}
start_specops = 1;
while (animation_speed
> 0)
{
if (!snd_playing(chopper_handle))
{
chopper_handle = ent_playsound (my, chopper_wav, 100); // the sound is louder
this time
snd_tune
(chopper_handle, 0, 25 * animation_speed, 0);
}
ent_cycle("fly", my.skill20); // play "fly"
animation
my.skill20 += animation_speed * time;
my.skill20 %= 100; // loop the animation
animation_speed -= 0.15 * time;
wait (1);
}
}

L'hélicoptère est placé à une place adéquate et il attend que la variable chopper_moves soit mise à 1. Lorsque cela se produitl'hélicoptère l'hélicoptère joue son animation de vol dans une boucle, diminuant son z (se préparant à atterrir) et son y (s'approchant des camions). La caméra suit l'hélicoptèrr jusqu'à ce qu'il touche le sol (son z est inférieur à 35). Nous devons créer les 2 commandos aussi nous mettons la variable start_specops à 1. L'hélicoptère joue le son chopper_wav dans une boucle, diminuant la fréquence du son (l'instruction snd_tune) et la vitesse de l'animation "fly".

Nous avons utilisé une fonction starter pour les 2 commandos:

starter
create_specops
{
while (start_specops == 0) {wait (1);} //
wait until the chopper has landed
sleep (1);
temp.x = -300;
temp.y = 1500;
temp.z = 35;
ent_create (specops_mdl, temp, move_specop1);
temp.y +=
100;
ent_create (specops_mdl, temp, move_specop2);
}

La fonction attend que start_specops soit mis à 1, attendant ensuite 1 seconde puis choisit les positions de départ pour les hommes masqués, les crée et leur attache les fonctions move_specop1 et move_specop2:

function
move_specop1()
{
specop1 = my;
my.pan = 270;
while (my.y > -650)
{
ent_cycle("run", my.skill20);
my.skill20 +=
10 * time;
my.skill20 %= 100; // loop the
animation
my.y -= 15 * time;
if (my.x > -450) {my.x -= 1 * time;}
if
(my.y < 600)
{
camera.x = -145;
camera.y
= 2060;
camera.z =
200;
camera.pan = 260;
camera.tilt = -10;
}
wait (1);
}

C'est ce type qui fait tout le boulot, réellement, je ne sais pas pourquoi ils en ont embauché un deuxième! Il choisi tle bon angle pan puis il commence à courir jusqu'à ce que son y soit inférieur -650 quants (qui est proche de l'angle de la rue) Nous appliquons également un petit mouvement sur son x mais uniquement si x est plus grand que -450 quants. Si le y est plus petit que 600, nous donnons une nouvelle position à la caméra, celle qui est placée derrière l'hélicoptère:

  start_van = 1;
my.y = -710;
my.pan =
180;
while (my.x > -800)
{
ent_cycle("run", my.skill20);
my.skill20
+= 7 * time;
my.skill20 %= 100; // loop
the animation
my.x -= 8 * time;
wait (1);
}
ent_cycle("standcrouch",
90); // crouch behind the truck
sleep (5); // plant the bomb
:)
my.pan = 0;
while (my.x < 0)
{
ent_cycle("run", my.skill20);
my.skill20 += 7 * time;
my.skill20 %= 100; // loop the animation
my.x += 8 * time;
wait (1);
}
sleep (1); // wait a bit and then trigger the explosion
start_explosion = 1;
}

La camionnette a commencé à se déplacer à présent aussi le commando prend une nouvelle position et une nouvelle orientation puis commence à courrir vers le camion en diminuant sa valeur de x. Le mouvement continue jusqu'à ce que x soit inférieur à -800 quants; lorsque cela arrive l'homme s'accroupit ("standcrouch") attend 5 secondes (temps nécessaire pour poser la bombe). Ensuite il choisi tle bon angle pan et il court s'éloignant du camion.

Nous attendons une seconde puis nous mettons la variable start_explosion à 1, ce qui déclenchera le processus d'explosion.

function
move_specop2()
{
specop2 = my;
my.pan = 270;
while (start_van == 0)
{
my.x = specop1.x;
my.y = specop1.y + 100;
ent_cycle("run", my.skill20);
my.skill20 += 10 * time;
my.skill20 %= 100;
// loop the animation
wait (1);
}
}

Le deuxième commando prend un bon angle puis il suit le premier commando jouant son animation "run" jusqu'à ce que la variable start_van soit mise à 1. Qu'il vive en paix!

action
van
{
van1 = my;
while (start_van == 0) {wait
(1);}
camera.x = -150;
camera.y = -1700;
camera.z = 320;
camera.pan = 140;
camera.tilt
= -25;
snd_play (van_wav, 20, 0);
while (my.x
< 2000)
{
my.x += 20 *
time;
wait (1);
}
}

C'est une petite action. La camionnette attend que start_van soit mis à 1, puis elle donne une nouvelle valeur à la caméra (angle et position). Un bruit de moteur est joué et la camionnette augmente son x dans une boucle jusqu'à atteindre 2000 quants.

Le premier camion est composé de 2 modèles:

action
truck1_part1
{
while (start_explosion == 0) {wait (1);}
snd_play (explosion1_wav, 100, 0);
sleep (0.2);
snd_play (explosion2_wav, 100, 0);
while (my.y > -3000)
{
my.y -= 50 * time;
my.z += 30 * time;
my.roll += 50 * time;
camera.roll = 2 - random(4); // shake the camera a bit
wait (1);
}
camera.roll
= 0; // reset camera's roll angle
}

La première partie du camion attend que start_explosion soit mis à 1, puis on joue 2 sons d'explosion. La citerne (truck1_part1) commence à voler dans le ciel, augmentant son z à la vitesse donnée par 30 * time. La caméra choisit un angle roll aléatoire entre -2 et 2, la fin de l'explosion remettant le bon angle roll. Que se passe-t-il avec la 2eme partie du camion?

action
truck1_part2
{
while (start_explosion == 0) {wait (1);}
sleep (0.5);
snd_play (explosion1_wav, 100, 0);
while (1)
{
temp.x = my.x -70;
temp.y = my.y;
temp.z = my.z + 50;
effect (fire_fx, 1, temp, normal);
wait (1);
}
}

Il attend que start_explosion soit mis à 1, joue un son d'explosion puis crée un effet de particules en feu à la position donnée par temp.

action
truck2
{
while (start_explosion == 0) {wait (1);}
sleep (0.4);
snd_play (explosion1_wav, 100, 0);
sleep (0.1);
snd_play (explosion2_wav, 100, 0);
snd_play (explosion1_wav, 100, 0);
while (my.x > -1400)
{
my.x -= 20 * time;
my.z += 3 * time;
if (my.roll < 180) {my.roll += 50 * time;}
camera.roll
= 2 - random(4); // shake the camera a bit
wait (1);
}
camera.roll = 0; // reset camera's
roll angle
while (1)
{
temp.x = my.x -70;
temp.y = my.y;
temp.z = my.z + 80;
effect (fire_fx, 1, temp, normal);
wait (1);
}
}

Le 2eme camion attend que start_explosion soit mis à 1, puis il joue plusieurs son d'explosion et se déplace légèrement en arrière (c'est pourquoi nous jouons avec son x) et nous le faison srouler (roll de 0 à 180 degrés). La caméra est secouée un peu, la dernière boucle créant le même effet de particule mais à la bonne position.

function
fire_fx()
{
temp.x = random(20) - 10;
temp.y
= random(20) - 10;
temp.z = random(10);
vec_add
(my.vel_x, temp);
my.alpha = 20 + random(80);
my.bmap = fire_tga;
my.size = 200;
my.bright
= on;
my.move = on;
my.lifespan = 4;
my.function = fade_away;
}

function
fade_away()
{
my.alpha -= 2 * time;
if (my.alpha
< 0) {my.lifespan = 0;}
}

La fonction utilisée pour l'effet de feu est classique, elle donne des vitesse aléatoires aux flammes et chosit un facteur de transparence aléatoire dans la limite 20 à 100 puis assigne la fonction fade_away à chaque particule. Cete fonction diminue simplement l'alpha des particules les supprimant dès que leur alpha vaut 0. Vous devez ajouter de la fumée à cet effet, de moncôté j'ai souhaité la faire le plus simple possible..

Le déroulement de notre scénario est terminé. Il ne vous servirait à rien de faire un copier / coller de ce code dans votre projet car vous avez appris qu'il dépend pour beaucoup de notre scénarion, de la géométrie du niveau etc. La bonne nouvelle est que vous disposez de nombreux templates pour vous aider. Dernier truc à connaître, la caméra peut se trouver n'importe où vous voulez à la condition de ne pas changer ses x,y, z et angles pan, tilt et roll à l'intérieur d'une boucle while. Comprenez moi bien, les boucles while et les caméras vont très bien ensemble mais vous devez mettre la caméra en dehors d'une boucle si vous êtes appelé à la déplacer durant le déroulement de la scène.


Untitled

Utilisation du sdk - traduction Olivier Burton - correction AB

Nous tous savons que le C-script est un langage de programmation puissant; j' ai réussi à écrire le code pour plusieurs jeux sans toucher au sdk! Cependant, parfois vous pourriez vouloir employer le sdk, ce qui signifie que vous avez acheté le sdk et un de ces logiciel de déveleppement: Visual C ++, C ++ Builder ou Delphi. Vous voudrez employer le sdk si :
-Une fonction particulière n' est pas disponible dans C-script et vous en avez vraiment besoin;
-Vous voulez empaqueter quelque morceau intelligent de code de C-script comme "my.ambient = 100" parce que vous avez peur que quelqu' un d' autre vole votre code si vous ne l' empaquetez pas;
-Vous êtes un programmeur professionnel C ++ / Delphi et vous ne voulez pas employer le C-script parce que c' est une façon trop simple pour vous;
-Vous avez quelques fonctions critiques qui s'exécuteraient plus rapidement si vous les écriviez en C ++ ou Delphi;
-Vous voulez créer quelques effets impressionnants, jamais vu avant, ou peut-être un nouveau sous-système de rendu.

Créez un nouveau dossier nommé MyDlls à la racine de votre disque dur.

Démarrez votre logiciel de développement, (les exemples sont fait à partir de Microsoft Visual C++ 6.0) puis créez un nouveau projet de dll :

Pressez "OK" et vous verrez la fenêtre suivante :

Sélectionnez "A simple DLL project".

Pressez "Finish" puis "OK". Quittez le logiciel de développement et explorez le nouveau répertoire nommé \first à l'intérieur de \MyDlls.

Copiez les fichiers suivants de votre sdk dans \first : a5dll.h, a5funcs.h, a5dll.lib. Le résultat doit ressembler à l' image ci-dessous :

Double cliquez sur "first.dsw" pour charger le projet de nouveau. Assurez-vous que vous avez choisi "FileView", open "Source Files" et double cliquez ensuite sur "first.cpp".

Incluez les deux fichiers d'entête:

Il est temps de lier le fichier a5dll.lib au projet; cliquez "Project" et ensuite "Settings":

Choisissez "Category : Input", cliquez "Object / library modules", appuyez sur la touche "Fin" de votre clavier pour vous déplacer à la fin de la rangée et taper "a5dll.lib" sans les guillemets.

Pressez "OK" puis compilez le projet:

Félicitations! Vous venez de construire votre première dll! Jetez un coup d' oeil aux dossiers \debug ou \release qui ont été créés à l' intérieur de votre dossier \first et vous verrez un tout nouveau fichier nommé "first.dll"!

Ok, donc nous avons construit une dll capable de travailler mais elle ne fait rien d' utile! Qu' est-ce qui serait vraiment simple à coder et impossible à créer avec le C-script ? Hmm... (Environ 10 minutes plus tard) je sais! Créons une fonction qui peut supprimer n' importe quel fichier sur le disque dur! C-script nous permet de supprimer n' importe quel fichier du dossier "savedir", mais notre nouvelle fonction serait vraiment utile pour un système de protection de copie, ou si nous voulons supprimer quelques fichiers temporaires laissés par l' installateur employé pour notre jeu, cela peut-être l' ennuyeux fichier scandisk.log qui apparaît dans la racine du disque dur, etc.

Nous allons employer dans C ++ la fonction nommée "remove", qui a besoin de <stdio.h> ou <io.h> donc nous devons "inclure" un de ces fichiers. La fonction qui supprime les fichiers consiste en seulement trois lignes de code (ok, elle a seulement besoin de deux lignes, mais nous arriverons là bientôt). Voici le fichier first.cpp modifié :

Assurez-vous que le fichier first.cpp est exactement comme cela et compliez ensuite le projet; vous devez faire votre propre fichier first.dll, qui doit être copié à l' intérieur de votre dossier de jeu principal (\work si vous employez les templates).

La première ligne de code dans la fonction ne fait rien d' utile pour nous : elle obtient la variable nommée "temp" du C-script et la rend disponible pour notre fonction en tant que "temp". Je ne vais pas employer "temp" dans cette fonction, alors pourquoi fais-je cela ?

Le moteur ne travaillera pas avec les dlls "standard"; vous devez acheter le sdk si vous voulez les créer. La bibliothèque a5dll.lib doit être liée avec votre projet même si vos fonctions n' emploient pas d' objet de C-script; c' est pourquoi j' ai décidé de saisir "temp", bien que je n' en aie pas besoin. Vous devez employer une fonction factice ou vous devez ajouter une ligne de code qui ressemble à la mienne à chaque fois que vous créez un nouveau dll; autrement vous obtiendrez un message d' erreur qui ressemble à cela quand vous voudrez employer votre dll :

Jetons un coup d' oeil aux deux dernières lignes de code la fonction KillFile : nous obtenons un pointeur sur la chaîne passé à la fonction et ensuite nous essayons d' enlever le fichier, et rendons le résultat de l' opération comme un INT2FIX.

C' était tout pour la partie C ++! Voyons le fichier de C-script maintenant :

var success = 0;
var_nsave dll_handle;

font std_font = <ackfont.pcx>, 6, 9;

text deletion_txt
{
pos_x = 200;
pos_y = 200;
font = std_font;
flags = visible;
}

J' ai défini deux variables : une variable nommée success et une qui sera employé comme un pointeur dans le fichier first.dll; j' ai choisi var_nsave cette fois parce que sa valeur n' est pas sauvée sur le disque quand nous employons "game_save", donc elle ne sera pas changée par une instruction "game_load". J' ai aussi défini une police simple et un texte qui l'emploiera.

dllfunction KillFile(x);

Chaque fonction de votre dll doit être définie à l' intérieur de votre fichier de C-script; la ligne ci-dessus fait juste cela.

starter
terminate_file
{
dll_handle = dll_open ("first.dll");
while (key_t == 0) {wait (1);}
success = KillFile ("c:\\somefile.txt");
if (success == 0)
{
deletion_txt.string
= "The file was deleted successfully";
}
else // success = -1 here
{
deletion_txt.string
= "Error while trying to delete the file!";
}
sleep (3);
deletion_txt.visible = off;
dll_close (dll_handle);
}

J' ai employé une fonction "Starter", une fonction spéciale qui s'exécute isolément au début du jeu; elle ouvre la dll et ensuite elle attend que le joueur appuie sur la touche " T ". Aussitôt que cela arrive, la fonction KillFile de la dll est exécutée et son résultat est stocké dans la variable nommée "success". Si le fichier a été supprimé, success est mis à 0; autrement, si le fichier ne peut pas être supprimé, success est mis à -1. D' où ces valeurs sont-elles venues ? Lisez les renseignements au sujet de "remove" dans l' aide MS Visual C++.

Si le fichier peut être supprimé, nous mettons une chaîne de confirmation pour deletion_txt; autrement, nous mettons une chaîne d'erreur pour deletion_txt. Le texte sera visible pendant trois secondes et ensuite il disparaîtra. La dernière ligne de code ferme la dll; nous devons le faire chaque fois que nous finissons notre travail avec elle.

C' est tout! Copiez somefile.txt à la racine c:\ de votre dique, exécutez le niveau et appuyez ensuite sur " T "; vous devez voir l' image suivante

Quittez le moteur et exécutez ensuite le niveau de nouveau. Pressez la touche " T " et vous verrez l' image suivante :

La fonction a essayé de supprimer somefile.txt, mais ce fichier ne pouvait pas être supprimé parce qu' il n' était plus là! Bien sûr vous pourriez employer la fonction KillFile avec n'importe quel fichier, indépendamment de son nom et extension. N' oubliez pas d' employer deux antislashs quand vous spécifiez le chemin du fichier.

Employez une ligne de code qui ressemble à celle ci-dessous si vous voulez enlever un fichier du dossier de jeu principal; c' est le dossier qui contient le fichier first.dll et cela ne doit pas être le dossier "savedir".

success = KillFile ("somefile.txt");


Untitled

Traduction Olivier Burton

Shooter en écran partagé

Si vous avez un frère jumeau (ou une soeur) préparez vous pour la confrontation de votre vie!

Tout d'abord nous créons deux vues :

view
camera1
{
layer = 15;
pos_x = 0;
pos_y = 0;
size_x = 800;
size_y = 300;
arc = 80;
flags = visible;
}

view
camera2
{
layer = 15;
pos_x = 0;
pos_y = 300;
size_x = 800;
size_y = 300;
arc = 80;
flags = visible;
}

Ces définitions de vue diviseront l'écran en deux parties égales, comme dans l'image ci-dessous :

J'ai employé une définition de texte simple pour mettre un texte sur l'écran :

text
score_text
{
pos_x = 0;
pos_y = 0;
font = verdana_font;
string = "Player1's score:\n\n\n\n\n\n\n\n\n\n\n\n\nPlayer2's
score:";
flags = visible;
}

Je suis sûr que vous vous rappelez ce que "\n" fait : il ajoute une ligne. De cette façon je peux montrer "'le score du Player1" et "le score du Player2" en 'utilisant un texte simple. J'emploie une définition de panneau simple et deux chiffres pour montrer le score des deux joueurs :

Panneau(jury)
score_panel
{
Pos_x à 0;
Pos_y à 0;
Digits = 460, 0, 2, verdana_font,
1, score1;
Digits = 460, 300, 2, verdana_font, 1,
score2;
Flags = refresh, visible;
}

C'est un projet autonome, donc il a sa propre fonction principale :

function main()
{
fps_max = 40;
level_load(split_wmb);
wait (3);
camera.visible = off;
}

La seule chose qui doit être mentionnée ici est la dernière ligne de code : elle cache la vue par défaut (la caméra) parce que nous avons défini deux vues pour notre projet (camera1 et camera2).

J'ai fait un double de la plupart du code employé pour les deux joueurs, donc l'action et les fonctions associées au premier joueur sont identiques à celles associées au deuxième joueur.

action player_one
{
player1 = me;
my.enable_entity = on;
my.enable_impact = on;
my.event = kill_player1;
ent_create (handgun1_mdl, nullvector, weapon1);

L'entité attachée à cette action deviendra player1 et sera sensible à d'autres entités; quand une autre entité la frappe, son événement (kill_player1) courra. La dernière ligne créera l'arme associée à player1; l'arme qui a été créée est contrôlée par la fonction nommée weapon1 ().

while (p1_alive == 1)
{
my.pan += 5 * (key_a - key_d) * time;
player1_dist.x = 15 * (key_w - key_s) * time;
player1_dist.y = 0;
player1_dist.z = 0;
move_mode = ignore_passable + glide;
ent_move (player1_dist, nullvector);
camera1.pan = my.pan;
camera1.x = my.x;
camera1.y = my.y;
camera1.z = my.z + 30;

Tant que player1 est vivant (p1_alive est mis à 1 au début de jeu), il peut se déplacer en utilisant les touches "W", "S", "A" et "D". Player1 ignore les entités passables et glisse le long des murs pour un mouvement plus lisse. La vue nommée camera1 suit player1 tout le temps, en employant ses positions x et y et en étant placé 30 quants au-dessus de son origine.

if (key_w + key_s > 0) // if the player is moving
{
ent_cycle("run", my.skill46);
my.skill46 += 10 * time;
my.skill46 %= 100;
}
else // the player is standing or shooting
{
if (key_space == 1) // if this player is shooting
{
ent_cycle("attack", 100); // display the last "attack" animation frame
if (bullet1 == null)
{
wait (3);
ent_create (bullet_mdl, bullet1_coords, move_bullet1);
}
}
else
{
ent_cycle("idle", my.skill46); // play "stand" frames animation
my.skill46 += 2 * time; // "stand" animation speed
my.skill46 %= 100; // loop animation
}
}
wait (1);
}
}

Si le joueur se déplace (key_w ou key_s est appuyé), player1 joue son animation "run" dans une boucle. Si le joueur ne se déplace pas, il y a deux possibilités :
1) Le joueur tire (la touche espace est appuyée). Si c'est vrai, le joueur montrera sa dernière frame d'animation "attack", tirant une balle aussitôt que sa balle précédente a disparu.
2) Le joueur s'arrête (la touche espace n'est pas appuyée). Si c'est le cas, le joueur jouera son animation "idle" dans une boucle. Voyons la fonction qui s'occupe de l'arme employée par player1 : Fonction weapon1 ()

function
weapon1()
{
proc_late();
my.passable
= on;
while (1)
{
vec_for_vertex (bullet1_coords, my, 148); // get the vertex coords for the bullet
my.x = you.x;
my.y = you.y;
my.z = you.z;
my.pan = you.pan;
my.tilt = you.tilt;
my.roll = you.roll;
my.frame = you.frame;
my.next_frame = you.next_frame;
wait (1);
}
}

La première ligne de code s'assure que l'arme n'as pas une frame de retard sur le joueur. Nous employons la boucle while pour obtenir les coordonnées de vertex pour la balle tout le temps; de cete façon, la balle apparaîtra à la même position avec le vertex #148 employé pour ce modèle d'arme. L'arme a la même position, angles et frames que son créateur (player1) à chaque instant.

Quand nous pressons sur la touche espace, la fonction ci-dessous commence à
courir :

function move_bullet1()
{
bullet1 =
me;
my.skill10 = 1;
my.pan = you.pan;
my.enable_entity = on;
my.enable_impact = on;
my.enable_block = on;
my.event = remove_bullet;
while (my != null)
{
my.skill1 = 15 * time; // bullet speed
my.skill2 = 0;
my.skill3 = 0;
move_mode = ignore_you + ignore_passable;
ent_move (my.skill1, nullvector);
wait (1);
}
}

Chaque balle tirée par player1 est nommée "bullet1". Nous employons l'action associée à player1 pour nous assurer qu'une entité simple nommée "bullet1" peut exister à un certain moment; de cette façon, nous mettons hors de service le tir automatique.

Nous mettons skill10 à 1 pour donner un numéro d'id unique à la balle et ensuite nous nous assurons que le joueur et la balle ont le même angle à l'heure de création. La balle sera sensible aux entités et pourras rencontrer des blocs; quand elle entrera en collision avec quelque chose, son événement remove_bullet courra.

La balle se déplace par la boucle while, ignorant l'entité qui l'a créé (player1) et n'importe quels objets passables qu'elle pourrait rencontrer en avançant. Vous l'avez deviné; l'événement associé à la balle est vraiment simple :

function
remove_bullet()
{
snd_play (hit_wav, 100, 0);
wait (1);
ent_remove (me);
}

La fonction joue un son et ensuite elle enlève la balle.

Voyons le code pour la fonction d'événement associée à player1 :

 function
kill_player1()
{
if (you.skill10
!= 1) {return;}
my.event = null;
p1_alive = 0;
score2 += 1;
my.skill46 = 0;
snd_play (scream_wav, 100, 0);
while (my.skill46 < 80)
{
ent_cycle("death", my.skill46);
my.skill46 += 3 * time;
wait (1);
}
}

La première ligne de code vérifie si player1 a été frappé par une balle ou non; il ne serait pas agréable d'être capable de tuer votre adversaire en s'y heurtant simplement ! La deuxième ligne de code dit au joueur de s'arrêter d'être sensible à d'autres événements - le joueur est déjà mort maintenant. Nous mettons p1_alive au zéro (cela arrête les animations), nous augmentons score2 (player1 est mort), nous jouons un son et ensuite nous dirigeons l'animation "dead" une fois.

La dernière fonction qui doit être discutée est celle qui reprend le jeu :

function play_again()
{
snd_play (restart_wav, 100, 0);
p1_alive = 1;
p2_alive = 1;
wait (2);
main();
}

on_p play_again;

Cette fonction court quand vous pressez la touche "P". Elle joue un sons, ranime les deux joueurs (bien que seulement un d'entre eux soit mort) et lance ensuite la fonction principal () de nouveau. Notez s'il vous plaît que score1 et score2 ne sont pas changés ainsi vous pouvez jouer toute la journée sans perdre le score.


Untitled

Panneau à la Splinter Cell

Splinter Cell est un grand jeu! Ce n'est pas un jeu de tir classique, c'est un mélange entre Project IGI et Thief!. Quoi qu'il en soit, vous contrôlez un type qui doit parfois partir furtivement dans le noir (en fait la majeure partie du temps). Vousavez un indicateur qui montre le niveau de lumière dans n'importe quelle partie du niveau, et cet article vous explique comment ajouter cette fonctionnalité dans votre jeu.

Pour commencer nousdéfinissons deux panneaux:

panel back_pan
{
bmap = back_pcx;
pos_x = 5;
pos_y = 5;
layer = 20;
flags = overlay, refresh,
visible;
}

panel cursor_pan
{
bmap = cursor_pcx;
pos_x = 0;
pos_y = 10;
layer = 25;
flags = overlay, refresh,
visible;
}

Rien de bien difficile ici; cursor_pan apparaîtra par dessus r back_pan car sa valeur de couche (layer) est plus grande que la sienne. La fonction qui fait que cela fonctionne est vraiment simple:

function
player_light()
{
while(1)
{
vec_set (temp,
my.x);
temp.z -= 1000;
trace_mode = ignore_me
+ ignore_passable + scan_texture;
trace (my.x, temp);
cursor_pan.pos_x
= 10 + 0.38 * tex_light;
wait (1);
}
}

Cette fonction doit être appelée depuis l'intérieur de l'action assoxiée au joueur, en dehors d'une boucle, aussi la méthode la plus simple pourrait être ceci:

action players_action
{
player_light();
// le reste du code pour le joueur
.........................
}

J'ai attaché le fichier office.wdl qui contient l'action du joueur player_prog modifiéecomme exemple. Notre fonction player_light() trace sousles pieds du joueur, balayant l'ambient de la texture et retourne le résultat (0...255) dans la variable nommée tex_light. La seule ligne qui nécessite des explications est la suivante:

cursor_pan.pos_x = 10 + 0.38 * tex_light;

Que fait-on avec 10 + 0.38 * tex_light? D'où viennent ces valeurs?

Notre cursor_pan doit changer sa position x de 10 à 107 pixels en x (comme indiqué par la flèche rouge) si nous voulons quelque chose de réaliste pendant qu'il se déplace sur back_pan. Notre boulot est de faire que lorsque tex_light = 0, cursor_pan.pos_x = 10, et lorsque tex_light = 255, cursor_pan.pos_x = 107. Notre panneau doit être capable de se déplacer de 107 - 10 = 97 pixels lorsque tex_light change sa valeur de 0 à 255, aussi le facteur de multiplication sera de 97 / 255 = 0.38. Nous ajoutons 10 pixels (valeur minimale pour cursor_pan.pos_x) à 0.38 * tex_light et nous sommes prêt à rouler!


Untitled

Splitball

Splitball (split screen ball game ou jeu de balle sur 2 écrans) utilise une des particularités de Acknex: la capacité d'avoir plusieurs vues en même temps. Regardons la fonction set_cameras - elle est appelée dans main:

function set_cameras()
{
camera.size_x = 400;
camera.size_y = 600;
camera.pos_x = 0;
camera.pos_y = 0;
camera.x = first.x;
camera.y = first.y;
camera.z = first.z;
camera.tilt = -90;
camera.pan = first.pan;
camera.visible = on;

camera2.size_x = 400;
camera2.size_y = 600;
camera2.pos_x = 400;
camera2.pos_y = 0;
camera2.x = second.x;
camera2.y = second.y;
camera2.z = second.z;
camera2.tilt = -90;
camera2.pan = second.pan;
camera2.visible = on;
}

J'ai mis la résolution de l'écran à 800x600 ce qu fait que mes vues (nommées camera et camera2) font 400x600 pixels chacune. J'ai placé le (toujours) fameux modèle flèche dans le niveau; Sa position (xyz) et orientation (pan) sont transférées à la vue correspondante. Les flèches sont nommées first (première) et second (deuxième) et pour être honnête, c'est le nom des synonymes associés aux actions utilisées par player1eye et player2eye.

Les joueurs utilisent des actions similaires; jetons un coup d'oeil sur l'une d'elle:

action player1
{
my.push = 5;
my.enable_block = on;
while (1)
{
while (score1 ==
9 || score2 == 9)
{
wait (2);
}
if (key_z == 1
&& my.x > -270)
{
ent_move (move_left, nullvector);
}
if (key_x == 1
&& my.x < 270)
{
ent_move (move_right, nullvector);
}
move_left.x = -25
* time;
move_right.x =
25 * time;
wait (1);
}
}

Tout se passe dans une boucle while (1); si le jeu est terminé (score1 = 9 ou score2 = 9) les joueurs ne peuvent pas se déplacer, autrement le joueur 1 se déplace vers la gauche ou la droite selon la touche utilisée (w ou x). Finalement nous calculons la vitesse de déplacement de la raquette à chaque frame. C'est vrai que nous avons limité le taux d'affichage à 50 aussi ces valeurs ne varient pas beaucoup mais sur un système moins performant ou en mode logiciel le taux d'affichage peut descendre en dessous de 50 fps.

Le mur qui entoure le terrain est une simple entité qui a sa variable d'entité skill11 de mise à 1; Je fais cela car on doit pouvoir savoir si la balle touche le mur ou la raquette.

La balle à l'action suivante qui lui est attachée:

action ball
{
ball_syn = me;
my.skill10 = 1;
my.enable_entity = on;
my.event = change_direction;
if (random(1) > 0.5)
{
my.pan = 45 + random(90);
}
else
{
my.pan = 225 +
random(90);
}
while (1)
{
ball_speed.x =
30 * time;
ball_speed.y =
0;
ball_speed.z =
0;
ent_move (ball_speed,
nullvector);
wait (1);
}
}

La balle a sa variable d'entité skill10 de mise à 1 déclenche son évènement (change_direction) si elle entre en collision avec une autre entité (un des joueurs, le mur ou le but). Lorsque le jeu démarre la balle va vers le joueur 1 si random(1) > 0.5 ou le joueur 2 si random(1) <= 0.5. Chaque joueur a donc la même chance de commencer le jeu. J'ai choisi un bon angle pan. Voir schéma ci-dessous:

La balle se déplace tout le temps en utilisant quelques lignes de code. Rien à dire de spécial à ce sujet.

A chaque fois que vous marquez un but, l'action goal (qui est attaché à l'entité goal.wmb) déclenche son évènement: la fonction add_score:

action
goal
{
my.enable_impact = on;
my.event = add_score;
}

function
add_score()
{
if (you.skill10 == 1)
{
if (score1 == 9
|| score2 == 9) {return;}
play_sound goal_snd,
50;
if (my.flag1 ==
on)
{
score1 += 1;
wait (1);
ball_syn.x = 0;
ball_syn.y = 0;
ball_syn.pan = 225 + random(90);
}
if (my.flag2 ==
on)
{
score2 += 1;
wait (1);
ball_syn.x = 0;
ball_syn.y = 0;
ball_syn.pan = 45 + random(90);
}
}
}

Tout d'abord nous vérifions si la balle est entrée en collision avec l'entité but goal.wmb (you.skill10 = 1); si le jeu est déjà terminé (score1 = 9 or score2 = 9) on ne fait rien, sinon nous jouons le son goal_snd et nous ajoutons le score au joueur 1 ou 2 (si flag1 ou flag2 est mis pour goal.wmb dans wed). Si c'est le joueur un qui marque, la balle est placée au centre et est envoyé vers lui (c'est la seule chose à faire).

Le jeu est relancé chaque fois que nous mettons les scores à 0 - cela démarre les boucles while (1) pour les joueurs leur permettant de se déplacer de nouveau.


Untitled

Sprites animés

Il ne se passe pas une semaine où on ne me pose pas cette question : est-il possible d'avoir un sprite animé dans un panneau? Normalement je devrais répondre "non" mais ce magazine prouve une fois de plus qu'à l'impossible nul n'est tenu; nous pouvons créer une entité sprite et la placer n'importe où sur l'écran.

Avant toute chose nous créons le panneau:

panel
energy_pan // background panel
{
bmap = panel_pcx;
layer = 20;
pos_x = 0;
pos_y = 0;
flags = d3d, overlay, refresh,
visible;
}

Pui snus créons l'entité sprite, notre sprite animé:

entity energy_sprite
{
type = <energy+10.pcx>;
layer = 25;
view = camera;
x = 350;
y = 134;
z = 124;
flags = visible, flare, bright;
}

Il est important de choisir les valeurs correctes pour x y et z parce qu'elles nous aideront à placer notre sprite à la bonne place et avec la taille dont nous avons besoin. Tout ce que nous devons faire à présent c'est changer la frame d'animation en utilisant une simple fonction:

function init_animsprite()
{
while (1)
{
energy_sprite.frame += 0.3 * time;
if (energy_sprite.frame > 10)
{
energy_sprite.frame %= 10; // loop
}
wait (1);
}
}

Vous pouvez voir que le sprite a 10 frames d'animationqui s'exécute dans une boucle; la vitesse d'animation peut être changée si vous modifiez la valeur 0.3.


Untitled

Texte à la Star Wars

Si vous êtes un ancien fan de Star Wars (comme moi) vous avez du être impressioné par le texte qui défilait au début de chaque épisode. J'avais trouvé l'idée géniale aussi j'ai préparé quelques codes qui peuvent être utilisés pour cos intros.

Nous utilisons une entité pour le texte qui se déroulera devant nous. L'entité utilise une image bitmap nommée intro.pcx:

entity text_on_screen
{
type = <intro.pcx>;
layer = 1;
alpha = 100;
flags = transparent, visible;
view = camera;
x = 300;
y = 10;
z = -350;
tilt = 60;
}

L'entité est placé sur la première couche (layer = 1) et est transparente et visible. Son alpha est mis à 100 aussi bien que l'entité soit transparente, elle est opaque. L'entité doit être visible par la caméra, nos "yeux" à l'intérieur du niveau. Nous mettons l'entité 300 quants devant la caméra, 10 quants sur la gauche et 350 quants sous l'origine pour la cacher au départ. Pour terminer, nous inclinons le texte en mettant son angle tilt à 60 degrés.

Ce bout de code peu paraître petit mais j'ai créé un nprojet autonome pour cela; de cette façon vous pouvez simplement ajouter votre code existant à ce projet et vous êtes prêts à l'exécuter. Jetons un oeil à la fonction principale (main):

function
main()
{
fps_max = 50; // limit the frame rate to 50 fps
level_load (dummy_wmb);
media_loop ("music.wav",
null, 100);
while (text_on_screen.x <
2000)
{
text_on_screen.x
+= 2 * time; // move the text in front of the camera
text_on_screen.z
+= 0.7 * time; // and a little upwards
if ((text_on_screen.x
> 1000) && (text_on_screen.alpha > 5))
{
text_on_screen.alpha -= 0.2 * time;
}
wait (1);
}
}

Nous limitons le taux d'affichage à 50 fps, nous chargeons un niveau bidon (dummy) - un cube creux noir avec quelques étoiles sur les murs - puis nous jouons la musique dans une boucle. J'ai travaillé dur pour faire cette musique aussi je vous demande de l'écouter avec attention!

Le texte défilera jusqu'à ce que sa position x change de 300 à 2000 quants. Vous pouvez voir que j'augmente x et z dans le même temps, à l'intérieur de la boucle while. Lorsque le texte atteint 1000 quants en x, nous commençons à le rendre de plus en plus transparent, jusqu'à disparaître complètement (alpha < 5).

C'est tout, Mesdames et Messieurs


Untitled

Chaîne au dessus des entités

Peut-être vous êtes-vous déjà poser la question : comment pui-je ajouter un nom aux personnages dans mon jeu? Comment puis-je placer du texte au dessus de la tête de mes personnages? Cet article vous montre combien il est facile de faire cela, et il est inutile de vous mentionner que le code peut être employé pour n'importe quel type de jeux, y compris ceux qui emploient une caméra isométrique.

J'utilise des définitions standards pour les textes ::

text
name1_txt
{
font = system_font;
string = "Player";
}

text
name2_txt
{
font = system_font;
string = "Guard";
}

Vous pouvez utiliser autant de textes et autant d'entités que vous voulez. La fonction qui fait tout le travail ressemble à ceci:

function name_ent1()
{
while (1)
{
vec_set (temp,
my.x);
temp.z += 40;
trace_mode = ignore_me
+ ignore_models + ignore_passents;
if (trace (my.x,
camera.x) == 0)
{
vec_to_screen (temp, camera);
name1_txt.pos_x = temp.x;
name1_txt.pos_y = temp.y;
name1_txt.visible = on;
}
else
{
name1_txt.visible = off;
}
wait (1);
}
}

Ma démo utilise seulement deux fonctions qui sont comme celle ci-dessus; si votre jeu a 4 personnages différents, vous devez utiliser 4 textes et 4 fonctions pour eux. La fonction doit être appelée depuis l'intérieur de l'action attachée à votre entité, en dehors d'une boucle. Vous pouvez voir l'exemple utilisé pour ma démo dans le fichier office.wdl qui se trouve dans le repertoire \entstrings :

action player_prog
{
name_ent1();
..............
}

action patrol_prog
{
name_ent2();
.............
}

Comment cela fonctionne-t-il? Nous utilisons vec_to_screen pour convertir la position de l'entité dans un monde 3D en coordonnées 2D pour l'écran. Jetez un oeil sur l'image ci-dessous:

Vec_for_screen retourne l'origine de l'entité comme des coordonnées d'écran 2D en pixels; Comme nous voulons afficher le nom au dessus des têtes et non à l'origine, nous ajoutons 40 quants sur l'axe des z avant d'utiliser vec_for_screen. Jouez avec cette valeur, car vous risquez d'avoir besoin de valeurs différentes en fonction de vos entités.

Et voici le dernier truc : nous utilisons une instruction trace de l'entité à la caméra; si l'entité est visible à l'écran, nous mettonsles bonnes valeurs de pos_x et pos_y puis nous affichons le texte associé. Si l'entité n'est pas visible à l'écran (trace != 0) nous cachons le texte.

Assurez-vous de bien utiliserune camera à la 3ème personne pour le joueur. Pressez F7 pour basculer d'une caméra à la première personne vers une caméra à la 3ème personne si vous utilisez les templates sinon vous serez incapable de voir le joueur.


Untitled

Tireur d'élite

Cet article vous montre comment créer une simple lunette de visée que vous pourrez utiliser simultanément avec n'importe quelle arme.

L'idée principale consiste à décrémenter camera.arc pour faire un effet de zoom lorsque vous pressez "S"(S comme sniper bien entendu). Avant toute chose nous définissons 3 panneaux:

panel sniper640_pan
{
bmap = sniper640_map;
layer = 20;
pos_x = 0;
pos_y = 0;
flags = overlay, refresh, d3d;
}

panel
sniper800_pan
{
bmap = sniper800_map;
layer = 20;
pos_x = 0;
pos_y = 0;
flags = overlay, refresh, d3d;
}

panel
sniper1024_pan
{
bmap = sniper1024_map;
layer = 20;
pos_x = 0;
pos_y = 0;
flags = overlay, refresh, d3d;
}

J'ai besoin de 3 panneaux car je veux pouvoir réaliser mon effet pour toutes les résolutions (640x480, 800x600, 1024x768). Si vous voulez plus de résolutions dans votre jeu vous devez prévoir d'autres panneaux comme ceux ci-dessus. La fonction suivante doit être appelée à partir du main:

function init_sniper()
{
on_mouse_right
= null;
while (1)
{
if (key_s == 1)
{
if
(video_mode == 6)
{
sniper640_pan.visible = on;
}
if
(video_mode == 7)
{
sniper800_pan.visible = on;
}
if
(video_mode == 8)
{
sniper1024_pan.visible = on;
}
while
(camera.arc > 10)
{
camera.arc -= 5 * time; // vitesse
wait (1);
}
}
else
{
camera.arc
= 60;
sniper640_pan.visible
= off;
sniper800_pan.visible
= off;
sniper1024_pan.visible
= off;
}
wait (1);
}
}

On_mouse_right = null; nous voulons être sur que le curseur de la souris est non visible. La boucle "while" contrôle si la touche S est appuyée. Si c'est le cas le bon panneau est affiché dépendant de la résolution (video_mode = 6 signifie 640x480, video_mode = 7 signifie 800x600 etc). Puis zoome la caméra (camera.arc est décrémenté) à la vitesse donnée par 5 * time; jouez avec cette valeur pour avoir différentes vitesse de zoom. Si le joueur relache la touche "S" alors nous sautons à l'embranchement "else" : camera.arc est remis à sa valeur par défaut ( 60) et le panneau est rendu invisible. Je sais qu'il n'y a qu'un panneau de visible dépendant de la résolution mais cela ne gêne personne si je les rends invisbles tous les 3 en même temps.

Notez que les panneaux ne sont transparents que dans leur centre (RVB = 0, 0, 0)


Untitled

Vignette animée

Vous souvenz-vous de ces bons vieux jeux ou le joueur avait une petite image qui montrait son état de santé? L'image montrait le visage du joueur complètement détruit lorsque sa santé arrivait à 0 et un visage enjoué lorsque sa santé avoisinait le 100. C'est exactement ce que fait ce code.

panel player_panel
// affiche santé du joueur
{
pos_x = 0;
pos_y = 0;
digits = 160, 70,
3, health_font, 1, player._health;
flags = d3d, refresh, visible;
}

panel
health1_panel // affiche première image
{
bmap = health1_pcx;
pos_x = 0;
pos_y = 0;
flags = d3d, refresh;
}

panel
health2_panel // affiche deuxième image
{
bmap = health2_pcx;
pos_x = 0;
pos_y = 0;
flags = d3d, refresh;
}

panel
health3_panel // affiche
troisième image
{
bmap = health3_pcx;
pos_x = 0;
pos_y = 0;
flags = d3d, refresh;
}

panel
health4_panel // affiche quatrième image
{
bmap = health4_pcx;
pos_x = 0;
pos_y = 0;
flags = d3d, refresh;
}

panel
health5_panel // affiche
cinquième image
{
bmap = health5_pcx;
pos_x = 0;
pos_y = 0;
flags = d3d, refresh;
}

Vous pouvez voir que j'utuilise un simple panneausans bitmapt (bmap) pour afficher la santé du joueur; j'ai besoin d'un panneau parce que la santé est affichée comme un digit. Les 5 autres panneaux ne font rien d'autre que d'afficher une image sur le panneau.

Peut-être est-ce le bon moment pour vous rappeler que si vous utilisez les templates, la santé du joueur est stockée dans player._health - qui est le "digit" que nous affichons. J'ai besoin de quelque chose pour décrémenter la santé du joueur aussi j'ai créé cette action:

action player_killer
{
while (player == null) {wait (1);}
while (1)
{
if (vec_dist(my.x,
player.x) < 50 && player._health > 0)
{
player._health -= 0.5 * time;
}

          if (player._health
> 80)
{
health1_panel.visible = on;
}
else
{
if (player._health > 60)
{
health2_panel.visible = on;
}
else
{
if (player._health > 40)
{
health3_panel.visible = on;
}
else
{
if (player._health > 20)
{
health4_panel.visible = on;
}
else
{
health5_panel.visible = on;
}
}
}
}
wait (1);
}
}

Cette action est attachée au modèle guard.mdl qui est placée près du joueur dans notre niveau exemple. Bien entendu vous pouvez utiliser ce code pour un rayon laser etc. Nous ne pouvons pas prévoir si l'action player_killer sera exécutée avant l'action player_walk_fight qui est attachée à notre joueur. Si player_killer est exécuté en premier,le joueur n'est pas encore créé et player._health à la 3ème ligne n'a aucun sens. La première ligne de cette action règle le problème; player_killer démarrera uniquement après que player existe donc qu'il soit créé.

Si le joueur s'approche à moins de 50 quants et s'il est toujours vivant, sa santé est décrémentée avec la vitesse donnée par 0.5 * time. Voici ce qui se passes:
- Si player._health > 80 nous affichons le panneau health1;
- Si player._health = 60...80 nous affichons le panneau health2;
- Si player._health = 40...60 nous affichons le panneau health3;
- Si player._health = 20...40 nous affichons le panneau health4;
- Si player._health = 0...20 nous affichons le panneau health5;

L'action est un petit peu plus compliquée; chargez Aum12.zip et vous constaterz qu'il est nécessaire de rendre invisible les panneaux qui ne sont pas sensé&s apparaître à l'écran. Le code dans cet article est uniquement pédagogique. la version complète et fonctionnelle se trouvera dans le zip Aumxx.zip.


Untitled

Vision de nuit

Imaginez que vous marchez dans un niveau où il fait sombre - mais vous avez des jumelles de vision de nuit! Regardons comment c'est fait!

Nous créons des panneaux pour différentes résolutions:

panel nvision640_pan
{
bmap = nvision640_map;
layer = 20;
pos_x = 0;
pos_y = 0;
flags = overlay, refresh,
d3d;
}

panel
nvision800_pan
{
bmap = nvision800_map;
layer = 20;
pos_x = 0;
pos_y = 0;
flags = overlay, refresh,
d3d;
}

panel
nvision1024_pan
{
bmap = nvision1024_map;
layer = 20;
pos_x = 0;
pos_y = 0;
flags = overlay, refresh,
d3d;
}

Puis nous créons une entité sprite:

entity
fog_sprite
{
type = <fog+5.pcx>;
layer = 15;
x = 200;
y = 0;
z = 0;
flags = flare, bright;
}

La
fonction maitresse est appelée depuis la fonction principale (main) au
démarrage du jeu:

function init_nvision()
{
while (1)
{
if (key_n == 1)
// press and hold "N" for night vision
{
if (video_mode == 6) // 640x480
{
nvision640_pan.visible = on;
nvision800_pan.visible = off;
nvision1024_pan.visible = off;
}
if (video_mode == 7) // 800x600
{
nvision640_pan.visible = off;
nvision800_pan.visible = on;
nvision1024_pan.visible = off;
}

if (video_mode == 8) // 1024x768
{
nvision640_pan.visible = off;
nvision800_pan.visible = off;
nvision1024_pan.visible = on;
}
camera.ambient = 100;
fog_sprite.visible = on;
if (random(1) > 0.97) // every few seconds
{
fog_sprite.frame = random(5); // pick a random "fog" frame number
}
}
else
{
nvision640_pan.visible = off;
nvision800_pan.visible = off;
nvision1024_pan.visible = off;
fog_sprite.visible = off;
camera.ambient = 0;
}
wait (1);
}
}

Si nous pressons la touche "N", nous rendons le panneau correspondant à notre résolution visible et les deux autres non visibles. L'effet "vision de nuit" est obtenu en mettant camera.ambient à 100.

Vous vous souvenez de l'entité fog_sprite entity que nous avons défini quelques lignes plus haut? Son rôle est de donner l'aspect verdatre à l'objectif. Cest un sprite avec plusieurs frames qui est placé devant le joueur; de temps en temps sa frame change pour donner une impression de "bruit", etc. Vous pouvez ajouter un brouillard vert dans le niveau pour rendre la chose encore plus réaliste.

Si nous relachons la touche "N", tous les panneaux deviennent invisibles, le sprite disparrait et camera.ambient est mis à zéro de nouveau.


Untitled

Verre cassé

Lisez cet article et vous saurez comment créer une fenêtre dont le carreau se casse lorsqu'on tire dessus ou lorsque le joueur passe à travers. Le joueur perdra des points de santé s'il fait cela, normal, non?

action breakable_glass
{
my.transparent = on;
my.enable_impact = on;
my.enable_entity = on;
my.enable_shoot = on;
my.event = glass_pieces;
}

L'action ci-dessus est attachée à notre entité vitre et elle est rendue transparente et sensible aux impacts, tir ou autres entités. Lorsqu'un de ces évènements se produit, la fonction glass_pieces s'exécute:

function glass_pieces()
{
if (you == player) {player._health -= 20;} // décrémente la santé
du joueur s'il casse la fenêtre en passant à travers
my.passable = on;
snd_play (breakglass_snd, 50, 0);
my.skill10 = 0;
my.invisible = on;
while (my.skill10 < 15)
{
create (gibglass_mdl,
my.pos, gib_glass);
my.skill10 += 1;
wait (1);
}
ent_remove (me);
}

Si le joueur passe à travers la fenêtre sa santé est décrémentée de 20 points. La fenêtre est rendu passable car les morceaux de verre doivent pouvoir passer à travers, unson est joué et la fenêtre est rendue invisible. Vous pourriez vous demander pourquoi je n'aipas suppriméla fenêtre plitôt, la réponse est simple, j'utilise son skill10 comme compteur car nous devons créer 15 morceaux de verre dans la boucle while. Donc dés que tous les morceaux de verre sont créés nous enlevons la fenêtre.

Il est temps de regarder la fonction gib_glass():

function
gib_glass()
{
my.transparent = on;
my.skill1 = random(200) - 100;
my.skill2 = (random(100) + 25) * sign (my.y - player.y);
my.skill3 = -random(25) -
10;
my.skill1 *= time; // all
the values (skill1..skill3) are multiplied with time
my.pan = random(360);
my.tilt = random(360);
my.roll = random(360);
while(my.skill40 < 10)
{
move_mode = ignore_passable;
ent_move (nullvector,
my.skill1);
my.skill3 -= 20
* time;
my.skill40 += time;
wait(1);
}
my.passable = on;
while (my.alpha > 0)
{
my.alpha -= 0.3
* time;
wait (1);
}
ent_remove (me);
}

Les morceaux sont transparents; nous utilisons skill1..skill3 comme variables locales. Chaque morceau de verre se déplace dans une direction aléatoire en utilisant ent_move; vous pouvez voir le mouvement en x dans un intervalle de -100 à 100 (skill1), le mouvement en z dans un intervalle de -35 to -25 (skill3) parce que les morceaux de verre tombent. Quand est-il pour le mouvement en Y (skill2) et pourquoi dépend-il de la position du joueur?

Voyons cela en image, ça vaut tous les discours du monde:

Si le joueur casse la vitre, les morceaux doivent tomber de l'autre côté de la fenêtre. Skill2 (la vitesse en y) changera son signe en fonction de la position du joueur et de la fenêtre.

Les morceaux ont des valeurs aléatoires pour pan, tilt, roll et se déplacent à l'intérieur d'une boucle while. Skill3 (vitesse en z) décrémente encore et encore, rapprochant de plus en plus les morceaux vers le sol à chaque cycle. Lorsque le mouvement cesse, nous rendons les morceaux de verre passables, nous décrémentons leur valeur alpha puis nous les enlevons.


Untitled

Démonstration de voiture

Ce code est écrit pour tous ceux qui désirent créer un jeu de voiture et ont besoin d'une version autonome de l'action player_drive des templates.

action my_car
{
player = me;

my.enable_block = on;
my.enable_entity = on;
my.event = increase_damage;

    while (1)
{
if (damage > 100) {return;}
if (car_speed != 0)
{
my.pan -=
0.2 * key_force.x * car_speed * time;
}
car_speed = 3 * key_force.y
* time + max (1 - time * 0.1, 0) * car_speed;
if (key_shiftl + key_shiftr
== 0)
{
ent_move
(car_speed, nullvector);
move_carview();
}
if (damage > 20)
{
temp
= damage / 20;
emit
(temp, player.x, car_smoke);
}
wait (1);
}
}

La voiture se déplace aussi longtemps que ses dommages sont en-dessous de 100. La voiture peut tourner (changez son angle pan) aussi longtemps que car_speed != 0. La ligne qui calcule car_speed est semblable à celle dans le tutorial (pour mardi) du manuel A4/A5 : v - > v + dv. Changez 3 pour changer la vitesse et 0.2 pour changer la vitesse de rotation.

Si nous pressons une des touches MAJ., la voiture s'arrêtera (ent_move n'est plus exécutée) et nous pouvons tourner la voiture sans la déplacer en pressant deux touches de direction ( haut + droit, bas + à gauche, etc..) en même temps. C'est la seule manière de voir mon modèle dans toute sa splendeur aussi ne manquez pas cette occasion !

La voiture peut heurter les blocs et les entités de niveau ; quand ceci se produit, increase_damage est exécuté :

function
increase_damage()
{
if (event_type == event_block)
{
damage += 0.006 * abs(car_speed);
}
if (event_type == event_entity)
{
damage += 0.003 * abs(car_speed);
}
}

Vous pouvez voir que les dommages dépendent de car_speed. Si la voiture heurte des blocs, les dommages sont plus grands. C'est pourquoi j'utilise abs(car_speed) dans cette fonction parce que si vous frappiez un bloc avec le dos de la voiture, les dommages seraient négatifs :) en raison du car_speed négatif.

Si les dommages sont plus grands que 20 la voiture émettra de la fumée (plus les dommages sont grands et plus il y a de fumée) :

function
car_smoke()
{
if (my_age == 0)
{
my_map = carsmoke_map;
my_speed.x = random(1) - 0.5;
my_speed.y = random(1) - 0.5;
my_speed.z = random(1) + 1;
my_size = random(500) + 300;
my_flare = on;
}
my_alpha = 100 - my_age
* 4;
my_size = min (1000, my_size
+ 2 * time);
if (my_age > 25)
{
my_action = null;
}
}

Il n'y a rien spécial dans cette fonction : les particules deviennent transparentes à la fin de leur vie ; leur taille accroît jusqu'à 1000. Vous devriez émettre la fumée d'un des sommets du dos de la voiture. La dernière fonction est :

function
move_carview()
{
camera.x = player.x - camera_distance
* cos(player.pan);
camera.y = player.y - camera_distance * sin(player.pan);
camera.z = player.z + camera_height;
camera.pan += 0.2 * ang (player.pan - camera.pan);
}

Nous avons discuté de ce type de caméra dans une édition précédente d'AUM ; veuillez noter que la caméra suivra la voiture avec un léger retard (changez 0.2 à 0.05 ou 0.5)

J'ai presque oublié : l'action stand_still est attachée à chaque garde.mdl dans le niveau et arrête leurs animations.