Le moteur de jeu

Donc, même si le moteur de jeu est capable d'empiler des pièces pour la démo, il faudra qu'il soit plus fin que ça pour gérer une partie...

Ainsi, le moteur de jeu sera séparé du moteur de démo, donc pris en charge par un nouveau state. La logique de ce dernier est la suivante :

  • Descendre la pièce
  • Calculer sa nouvelle position
  • La nouvelle position est libre ?
    • Oui
      • Afficher la pièce à sa nouvelle position
      • Sauvegarder la position
    • Non
      • Récupérer l'ancienne position
      • "Geler" la pièce
      • Détecter les lignes remplies, et les supprimer
      • Placer une nouvelle piece
      • Il y'a de la place pour la nouvelle pièce ?
        • Non
          • Retour à la démo
        • Oui
          • Affiche la nouvelle pièce

Cette logique sera executée par :

  • Un timer, pour gérer la chute de la pièce;
  • Un intercepteur de contrôle au clavier, pour gérer déplacements de celle-ci par le joueur quand il appuie sur les flèches.

Au boulot !

Alors, en cuisine, il nous manque :

  • Un détecteur de lignes pleines, ou vides;
  • Quelque chose pour ranger les briques, lorsque les lignes pleines ont été retirées;
  • L'interception des touches pressées par le joueur pour faire bouger la pièce.

Pour le détecteur de ligne remplie, ça marche comme ceci :

// Renvoie TRUE si la ligne est pleine
integer IsFullRow(integer row)
{
    integer Idx = row * PLAYGROUNDWIDTH;
    return (llSubStringIndex(llGetSubString(gPlayGroundContent, Idx, Idx + PLAYGROUNDWIDTH - 1), "0") == -1);
}

En gros, vu que l'ensemble des cubes est stocké dans une chaine de 200 caractères, il suffit de chercher un "0" (zéro) dans les 10 caractères qui composent une ligne. S'il n'y en pas, la ligne n'est pas remplie.

Dans la foulée, on a aussi un détecteur de ligne vide qui marche selon le même principe : si une ligne est composée de dix "0", alors elle est vide.

// Renvoie TRUE si la ligne est vide
integer IsEmptyRow(integer row)
{
    integer Idx =  row * PLAYGROUNDWIDTH;
    return (llGetSubString(gPlayGroundContent, Idx, Idx + PLAYGROUNDWIDTH - 1) == gEmptyRow);
}

A présent, on va utiliser tout ça pour faire le bout de code qui "range" les briques, lorsque des lignes pleines ont été retirées :

// Supprime les lignes completes
integer ReArrangeBricks()
{
    // Redétermine la toute premiere ligne non vide
    gRowStart = 0;
    while (IsEmptyRow(gRowStart) && gRowStart < PLAYGROUNDHEIGHT)
        gRowStart += 1;

    // Vide les lignes pleines
    integer DownLines;

    integer Row;
    for (Row = gRowStart; Row < PLAYGROUNDHEIGHT; Row += 1)
        if (IsFullRow(Row))
        {
            // Suprime la ligne concernée
            integer Idx = Row * PLAYGROUNDWIDTH;
            gPlayGroundContent = gEmptyRow + llDeleteSubString(gPlayGroundContent, Idx, Idx + PLAYGROUNDWIDTH - 1);
            DownLines += 1;
        }

    // Il y'a eu des lignes en moins
    if (DownLines)
    {
        RefreshPlayGround(gRowStart);
        gRowStart += DownLines;
    }

    return DownLines;
}

Les contrôles

Pour que le joueur puisse lancer le jeu, il faut qu'il touche SLTris. A ce moment là, SLTRis demande la permission d'utiliser les contrôles.
Lorsque le joueur répond "Oui", l'évenement run_time_permissions est déclenché, et c'est à l'intérieur de celui-ci que nous allons prendre le contrôle sur les quatres touches fléchées d'une manière effective.
Tout ceci, pendant le déroulement de la démo, donc dans le state demo.
A noter que c'est le moment qu'on choisit pour passer au state playing qui contrôle le déroulement d'une partie.

touch_start(integer count)
    {
        llRequestPermissions(llDetectedKey(0), PERMISSION_TAKE_CONTROLS);
    }
   
    run_time_permissions(integer perms)
    {
        if (PERMISSION_TAKE_CONTROLS & perms)
        {
            llTakeControls(CONTROL_FWD | CONTROL_BACK | CONTROL_ROT_LEFT | CONTROL_ROT_RIGHT, TRUE, FALSE);
            state playing;
        }
    }

En plus de gérer le timer qui fait descendre la piece et applique la logique vue précédemment, il faut gérer les touches appuyées par le joueur. C'est ce que fait le bout de code suivant :

control(key id, integer level, integer edge)
    {
        integer start = level & edge;

        if (CONTROL_BACK & start)
        {
            gPieceY += 1;
            AfterPlayerMove();
        }
        else if (CONTROL_FWD & start)
        {
            if (++gPieceAngle > 3) gPieceAngle = 0;
            AfterPlayerMove();
        }
        else if (CONTROL_ROT_LEFT & start)
        {
            gPieceX -= 1;
            AfterPlayerMove();
        }
        else if (CONTROL_ROT_RIGHT & start)
        {
            gPieceX += 1;
            AfterPlayerMove();
        }
    }

Le script ! Le script !

Voilà ci-dessous le script complet pour rendre ce jeu de Tétris jouable. Il vous suffit de remplacer le précédent par celui-ci :

// Temporisation en seconde pour la chute d'une piece
float       INTERVALTIMERSTART           = 1.5;
float       INTERVALTIMERDEMO            = 0.5;

// Largeur et hauteur du terrain de jeu
integer     PLAYGROUNDWIDTH              = 10;
integer     PLAYGROUNDHEIGHT             = 20;

// Coordonnées relatives des carres occupes par les pieces par rapport au centre de rotation.
// Si 0,0 est specifie en premier, la piece ne doit pas tourner (le "O" par exemple")
list PIECES = [
<1.0, 0.0, 0.0>,  0, -1,  0,  0,  0,  1,  0,  2,    // "I" Rouge
<1.0, 1.0, 0.0>,  0, -1,  0,  0,  0,  1, -1,  1,    // "J" Jaune
<1.0, 0.0, 1.0>,  0, -1,  0,  0,  0,  1,  1,  1,    // "L" Magenta
<0.0, 0.0, 1.0>,  0,  0,  0,  1,  1,  0,  1,  1,    // "O" Bleu        <- Celle-ci ne tourne pas !
<0.0, 1.0, 0.0>, -1, -1, -1,  0,  0,  0,  0,  1,    // "S" Vert
<0.5, 0.5, 1.0>, -1,  0,  1,  0,  0,  0,  0,  1,    // "T" Brun
<0.5, 1.0, 1.0>, -1, -1,  0, -1,  0,  0,  1,  0     // "Z" Cyan
];

// Pieces
integer  gPieceType;
list     gPieceParams;
vector   gPieceColor;

// Caractéristiques de la piece en cours
integer  gPieceX;
integer  gPieceY;
integer  gPieceAngle;
list     gPieceCoordinates;

// Anciennes caractéristiques de la piece en cours
integer  gPieceOldX;
integer  gPieceOldY;
integer  gPieceOldAngle;
list     gPieceOldCoordinates;

// Grille
string   gPlayGroundContent;
string   gEmptyRow;

// Prisme plat
integer  FIRSTCUBELINKNUMBER             = 1;

DisplayCubes(list coordinates)
{
    integer i; integer Count = llGetListLength(coordinates);
    for (i = 0; i < Count; i += 2)
        DisplayCube(llList2Integer(coordinates, i), llList2Integer(coordinates, i + 1), gPieceColor);
}

// Efface les cubes qui composent une piece
EraseCubes(list coordinates)
{
    integer i; integer Count = llGetListLength(coordinates);
    for (i = 0; i < Count; i += 2)
        EraseCube(llList2Integer(coordinates, i), llList2Integer(coordinates, i + 1));
}

EraseCube(integer x, integer y)
{
    integer Idx = y * PLAYGROUNDWIDTH + FIRSTCUBELINKNUMBER + x;
    llSetLinkColor(Idx, <1, 1, 1>, ALL_SIDES);
    llSetLinkAlpha(Idx, .25, ALL_SIDES);
}

DisplayCube(integer x, integer y, vector color)
{
    integer Idx = y * PLAYGROUNDWIDTH + FIRSTCUBELINKNUMBER + x;
    llSetLinkColor(Idx, color, ALL_SIDES);
    llSetLinkAlpha(Idx, 1, ALL_SIDES);
}

// Initialise le jeu
InitGame()
{
    // Construit une ligne vide
    gEmptyRow = "";

    integer Idx;
    for (Idx = 0; Idx < PLAYGROUNDWIDTH; Idx += 1)
        gEmptyRow += "0";
}

// Initialise la grille
InitPlay()
{
    // Construit le playground
    gPlayGroundContent = "";

    integer Idx;
    for (Idx = 0; Idx < PLAYGROUNDHEIGHT; Idx += 1)
        gPlayGroundContent += gEmptyRow;

    RefreshPlayGround(0);
}

// Envoie un message sur le debug channel
Debug(list msg)
{
    llSay(DEBUG_CHANNEL, llList2CSV(msg));
}

// Affiche la piece en cours en effacant la position precedente
// On ne touche pas aux cellules qui "restent"
DisplayMovedPiece()
{
    list OldXYList = gPieceOldCoordinates;
    list NewXYList = gPieceCoordinates;

    integer Count = llGetListLength(OldXYList);

    integer i = 0;
    do
    {
        integer Idx = llListFindList(NewXYList, llList2List(OldXYList, i, i+1));
        if (Idx % 2)
        {
            i += 2;
        }
        else
        {
            OldXYList = llDeleteSubList(OldXYList, i, i+1);
            NewXYList = llDeleteSubList(NewXYList, Idx, Idx+1);
            Count -= 2;
        }
    }
    while(i < Count);

    // On efface seulement les cubes qui disparaissent...
    EraseCubes(OldXYList);
    // Pour n'afficher que les nouveaux
    DisplayCubes(NewXYList);
}

// Rotation
SetRotatedPieceParams()
{
    gPieceCoordinates = [];

    integer i;
    for (i = 0; i < 8; i += 2)
    {
        integer OffsetX = llList2Integer(gPieceParams, i);
        integer OffsetY = llList2Integer(gPieceParams, i + 1);

        // Neutralise la rotation pour les pieces symétriques comme "O" qui ne tournent pas.
        // Ce genre de pieces est repéré par les premieres coordonnees toujours à 0,0
        if (i == 0)
            if (OffsetX == 0 && OffsetY == 0)
                gPieceAngle = 0;

        if (gPieceAngle == 0)
            gPieceCoordinates += [gPieceX + OffsetX, gPieceY + OffsetY];
        else if (gPieceAngle == 1)
            gPieceCoordinates += [gPieceX - OffsetY, gPieceY + OffsetX];
        else if (gPieceAngle == 2)
            gPieceCoordinates += [gPieceX - OffsetX, gPieceY - OffsetY];
        else
            gPieceCoordinates += [gPieceX + OffsetY, gPieceY - OffsetX];
    }
}

// retourne TRUE si la la piece peut occuper la position suivante
integer IsPositionFree()
{
    integer i;
    for (i = 0; i < 8; i += 2)
    {
        integer X = llList2Integer(gPieceCoordinates, i);
        integer Y = llList2Integer(gPieceCoordinates, i + 1);

        if (X < 0  || Y < 0 || X >= PLAYGROUNDWIDTH || Y >= PLAYGROUNDHEIGHT)
            return FALSE;

        integer Idx = X + Y * PLAYGROUNDWIDTH;

        if (llGetSubString(gPlayGroundContent, Idx, Idx) != "0")
            return FALSE;
    }

    return TRUE;
}

// Valide la nouvelle position
StoreOldPosition()
{
    gPieceOldX = gPieceX;
    gPieceOldY = gPieceY;
    gPieceOldAngle = gPieceAngle;
    gPieceOldCoordinates = gPieceCoordinates;
}

// Invalide la nouvelle position
RetrieveOldPosition()
{
    gPieceX = gPieceOldX;
    gPieceY = gPieceOldY;
    gPieceAngle = gPieceOldAngle;
    gPieceCoordinates = gPieceOldCoordinates;
}

// Fige la piece dans sa position actuelle : elle ne bougera plus
FreezePiece()
{
    string Piece = (string) (gPieceType + 1);
    integer i;
    for (i = 0; i < 8; i += 2)
    {
        integer Idx = llList2Integer(gPieceCoordinates, i) + llList2Integer(gPieceCoordinates, i + 1) * PLAYGROUNDWIDTH;
        gPlayGroundContent = llDeleteSubString(llInsertString(gPlayGroundContent, Idx, Piece), Idx + 1, Idx + 1);
    }
}

// Redessine la grille à partir de la ligne rowStart
RefreshPlayGround(integer rowStart)
{
    integer X; integer Y;
    for (X = 0; X < PLAYGROUNDWIDTH; X += 1)
        for (Y = rowStart; Y < PLAYGROUNDHEIGHT; Y += 1)
        {
            integer Idx = X + Y * PLAYGROUNDWIDTH;
            integer CodeCube = (integer) llGetSubString(gPlayGroundContent, Idx, Idx);

            if (CodeCube == 0)
                EraseCube(X,Y);
            else
                DisplayCube(X,Y, llList2Vector(PIECES, (CodeCube - 1) * 9));
        }
}

// Retourne un nombre aleatoire entre min et max
integer Random(integer min, integer max)
{
    return (integer) llFrand((float) max + 1.0 - (float) min) + min;
}

// Affiche une nouvelle piece
PlaceNewPiece()
{
    gPieceType = Random(0, llGetListLength(PIECES) / 9 - 1);
    gPieceX = Random(4, PLAYGROUNDWIDTH - 4);
    gPieceY = 2;
    gPieceAngle = Random(0, 3);

    gPieceParams = llList2List(PIECES, gPieceType * 9 + 1, gPieceType * 9 + 8);
    gPieceColor = llList2Vector(PIECES, gPieceType * 9);
    SetRotatedPieceParams();
}

// Apres le deplacement d'un joueur
AfterPlayerMove()
{
    SetRotatedPieceParams();

    if (IsPositionFree())
    {
        DisplayMovedPiece();
        StoreOldPosition();
    }
    else
    {
        RetrieveOldPosition();
    }
}

// Supprime les lignes completes
integer ReArrangeBricks()
{
    // Redétermine la toute premiere ligne non vide
    gRowStart = 0;
    while (IsEmptyRow(gRowStart) && gRowStart < PLAYGROUNDHEIGHT)
        gRowStart += 1;

    // Vide les lignes pleines
    integer DownLines;

    integer Row;
    for (Row = gRowStart; Row < PLAYGROUNDHEIGHT; Row += 1)
        if (IsFullRow(Row))
        {
            // Suprime la ligne concernée
            integer Idx = Row * PLAYGROUNDWIDTH;
            gPlayGroundContent = gEmptyRow + llDeleteSubString(gPlayGroundContent, Idx, Idx + PLAYGROUNDWIDTH - 1);
            DownLines += 1;
        }

    // Il y'a eu des lignes en moins
    if (DownLines)
    {
        RefreshPlayGround(gRowStart);
        gRowStart += DownLines;
    }

    return DownLines;
}

// Renvoie TRUE si la ligne est vide
integer IsEmptyRow(integer row)
{
    integer Idx =  row * PLAYGROUNDWIDTH;
    return (llGetSubString(gPlayGroundContent, Idx, Idx + PLAYGROUNDWIDTH - 1) == gEmptyRow);
}

// Renvoie TRUE si la ligne est pleine
integer IsFullRow(integer row)
{
    integer Idx = row * PLAYGROUNDWIDTH;
    return (llSubStringIndex(llGetSubString(gPlayGroundContent, Idx, Idx + PLAYGROUNDWIDTH - 1), "0") == -1);
}

integer gDemoMove;
float   gTimerEvent;
integer gRowStart;

default
{
    state_entry()
    {
        InitGame();
        state demo;
    }
}

state demo
{
    state_entry()
    {
        InitPlay();

        PlaceNewPiece();
        DisplayCubes(gPieceCoordinates);
        StoreOldPosition();

        llSetTimerEvent(INTERVALTIMERDEMO);
    }

    timer()
    {
        gDemoMove = !gDemoMove;

        if (gDemoMove)
        {
            gPieceY += 1;
            SetRotatedPieceParams();

            if (IsPositionFree())
            {
                DisplayMovedPiece();
                StoreOldPosition();
            }
            else
            {
                RetrieveOldPosition();
                FreezePiece();

                PlaceNewPiece();

                if (!IsPositionFree())
                {
                    llSetTimerEvent(0);
                    InitPlay();
                    PlaceNewPiece();
                    llSetTimerEvent(INTERVALTIMERDEMO);
                }

                DisplayCubes(gPieceCoordinates);
                StoreOldPosition();
            }
        }
        else
        {
            integer Move = Random(0, 4);

            if (Move == 0)
                gPieceX += 1;
            else if (Move == 1)
                gPieceX -= 1;
            else if (Move == 2)
                gPieceY += 1;
            else if (++gPieceAngle > 3)
                gPieceAngle = 0;

            AfterPlayerMove();
        }
    }
   
    touch_start(integer count)
    {
        llRequestPermissions(llDetectedKey(0), PERMISSION_TAKE_CONTROLS);
    }
   
    run_time_permissions(integer perms)
    {
        if (PERMISSION_TAKE_CONTROLS & perms)
        {
            llTakeControls(CONTROL_FWD | CONTROL_BACK | CONTROL_ROT_LEFT | CONTROL_ROT_RIGHT, TRUE, FALSE);
            state playing;
        }
    }

    state_exit()
    {
        llSetTimerEvent(0);
    }
}

state playing
{
    state_entry()
    {
        InitPlay();

        PlaceNewPiece();
        DisplayCubes(gPieceCoordinates);
        StoreOldPosition();

        gTimerEvent = INTERVALTIMERSTART;
        llSetTimerEvent(gTimerEvent);
    }

    timer()
    {
        gPieceY += 1;

        SetRotatedPieceParams();

        if (IsPositionFree())
        {
            DisplayMovedPiece();
            StoreOldPosition();
        }
        else
        {
            RetrieveOldPosition();
            FreezePiece();

            ReArrangeBricks();
            PlaceNewPiece();

            if (!IsPositionFree())
            {
                state demo;
            }
            else
            {
                DisplayCubes(gPieceCoordinates);
                StoreOldPosition();
            }
        }
    }
   
    control(key id, integer level, integer edge)
    {
        integer start = level & edge;

        if (CONTROL_BACK & start)
        {
            gPieceY += 1;
            AfterPlayerMove();
        }
        else if (CONTROL_FWD & start)
        {
            if (++gPieceAngle > 3) gPieceAngle = 0;
            AfterPlayerMove();
        }
        else if (CONTROL_ROT_LEFT & start)
        {
            gPieceX -= 1;
            AfterPlayerMove();
        }
        else if (CONTROL_ROT_RIGHT & start)
        {
            gPieceX += 1;
            AfterPlayerMove();
        }
    }
   
    state_exit()
    {
        llSetTimerEvent(0);
    }
}

Bon amusement !


Si vous êtes pressés, vous pouvez acheter SLTRis 3D en version complète sur SLExchange pour 250 malheureux L$. Attention: il utilise quand même 201 prims. En revanche, cette version affiche les 5 premiers scores, et distribue des cadeaux aux meilleurs !

Si 201 prims vous font peur, il existe une version plate : SLTRis 2D, qui ne prend que 41 prims, multilangue, avec le code source entièrement disponible !
Si cette version spéciale vous interesse, vous la trouverez ici, sur SLExchange, pour 300 pauvres L$. Hormis l'affichage un peu spécial (200 cellules pour 41 prims, c'est un petit miracle technique...), le moteur de jeu est rigoureusement identique à celui de SLTRis 3D.