lundi 9 février 2015

Créer des jeux avec HTML5 et Javascript

Créer des jeux avec HTML5 et Javascript

Les navigateurs sont désormais suffisamment performants pour permettre de jouer à des jeux en 2D ou en 3D.

Voyons comment nous pouvons les développer nous-mêmes.

La balise <CANVAS>

Le navigateur peut afficher les éléments graphiques d'un jeu au sein d'une zone délimitée par la balise "<canvas>".

Des commandes Javascript permettent d'y dessiner en 2D de façon très simple.





Exemple :

<canvas id="canvas" width="200" height="100" style="border:1px solid #000000;"/>
<script>
    var canvas = document.getElementById( "canvas" );
    var context = canvas.getContext( "2d" );
  
    context.fillStyle = "#FF0000";
    context.fillRect( 0, 0, 200, 100 );
  
    context.moveTo( 0, 0 );
    context.lineTo( 200, 100 );
    context.stroke();
  
    context.beginPath();
    context.arc( 95, 50, 40, 0, 2 * Math.PI );
    context.stroke();
  
    context.font = "30px Arial";
    context.strokeText( "Hello World", 10, 50 );
</script>

 

Explications :

On réserve une zone de dessin de 200x100 pixels, avec la balise "<canvas>" :

    <canvas id="canvas" width="200" height="100" style="border:1px solid #000000;"/>


On récupère le contexte du canevas dans lequel on va dessiner :

    var canvas = document.getElementById( "canvas" );
    var context = canvas.getContext( "2d" );


On dessine un rectangle rempli de rouge :
   
    context.fillStyle = "#FF0000";
    context.fillRect( 0, 0, 200, 100 );

   
On trace une ligne oblique :

    context.moveTo( 0, 0 );
    context.lineTo( 200, 100 );
    context.stroke();

   
On dessine un cercle, par le biais d'un arc circulaire de 360° :

    context.beginPath();
    context.arc( 95, 50, 40, 0, 2 * Math.PI );
    context.stroke();

   
On dessine le contour du texte "Hello World" avec la police "Arial" :

    context.font = "30px Arial";
    context.strokeText( "Hello World", 10, 50 );


La technologie WEBGL

Elle permet de dessiner des objets vectoriels en 3D au sein d'une balise "<canvas>" .

Cette technologie est nettement plus compliquée, car les points et les textures des objets 3D doivent être envoyés directement à la carte graphique, avec des micro-programmes de rendu appelés "shaders".







Exemple :

<canvas id="canvas" style="border: none;" width="500" height="500"/>
<script id="shader-fs" type="x-shader/x-fragment">
    precision mediump float;
    varying vec4 vColor;
   
    void main( void )
    {
        gl_FragColor = vColor;
    }
</script>
<script id="shader-vs" type="x-shader/x-vertex">
    attribute vec3 positionAttr;
    attribute vec4 colorAttr;
    varying vec4 vColor;

    void main( void )
    {
        gl_Position = vec4( positionAttr, 1.0 );
        vColor = colorAttr;
    }
</script>
<script type="text/javascript">
    var gl = null;
    var viewportWidth = 0;
    var viewportHeight = 0;

    function initGL( canvas )
    {
        try
        {
            gl = canvas.getContext( "webgl" );
           
            if ( !gl )
            {
                gl = canvas.getContext( "experimental-webgl" );
            }
           
            if ( gl )
            {
                viewportWidth = canvas.width;
                viewportHeight = canvas.height;
            }
        }
        catch ( e )
        {
        }
       
        if ( !gl )
        {
            alert( "Could not initialise WebGL" );
        }
    }

    function getShader( gl, id )
    {
        var script = document.getElementById( id );
       
        if ( !script )
        {
            return null;
        }

        var shader;
       
        if ( script.type == "x-shader/x-fragment" )
        {
            shader = gl.createShader( gl.FRAGMENT_SHADER );
        }
        else if ( script.type == "x-shader/x-vertex" )
        {
            shader = gl.createShader( gl.VERTEX_SHADER );
        }
        else
        {
            return null;
        }

        gl.shaderSource( shader, script.text );
        gl.compileShader( shader );

        if ( !gl.getShaderParameter( shader, gl.COMPILE_STATUS ) )
        {
            alert( gl.getShaderInfoLog( shader ) );
           
            return null;
        }

        return shader;
    }

    var program;

    function initShaders()
    {
        var vertexShader = getShader( gl, "shader-vs" );
        var fragmentShader = getShader( gl, "shader-fs" );

        program = gl.createProgram();
        gl.attachShader( program, vertexShader );
        gl.attachShader( program, fragmentShader );
        gl.linkProgram( program );

        if ( !gl.getProgramParameter( program, gl.LINK_STATUS ) )
        {
            alert( "Could not initialise shaders" );
        }

        gl.useProgram( program );
        program.positionAttr = gl.getAttribLocation( program, "positionAttr" );
        gl.enableVertexAttribArray( program.positionAttr );
        program.colorAttr = gl.getAttribLocation( program, "colorAttr" );
        gl.enableVertexAttribArray( program.colorAttr );
    }

    var buffer;

    function initGeometry()
    {
        buffer = gl.createBuffer();
        gl.bindBuffer( gl.ARRAY_BUFFER, buffer );
       
        var vertexData =
        [
            // X    Y     Z     R     G     B     A
            0.0,   0.8,  0.0,  1.0,  0.0,  0.0,  1.0,
            -0.8, -0.8,  0.0,  0.0,  1.0,  0.0,  1.0,
            0.8,  -0.8,  0.0,  0.0,  0.0,  1.0,  1.0
        ];
       
        gl.bufferData( gl.ARRAY_BUFFER, new Float32Array( vertexData ), gl.STATIC_DRAW );
    }

    function drawScene()
    {
        gl.viewport( 0, 0, viewportWidth, viewportHeight );
        gl.clear( gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT );
        gl.bindBuffer( gl.ARRAY_BUFFER, buffer );
       
        var stride = 7 * Float32Array.BYTES_PER_ELEMENT;
       
        gl.vertexAttribPointer( program.positionAttr, 3, gl.FLOAT, false, stride, 0 );
        gl.vertexAttribPointer( program.colorAttr, 4, gl.FLOAT, false, stride, 3 * Float32Array.BYTES_PER_ELEMENT );
        gl.drawArrays( gl.TRIANGLES, 0, 3 );
    }

    function webGLStart()
    {
        var canvas = document.getElementById( "canvas" );
        initGL( canvas );
        initShaders()
        initGeometry();

        gl.clearColor( 0.0, 0.0, 0.0, 1.0 );
        gl.disable( gl.DEPTH_TEST );

        drawScene();
    }
</script>
<body onload="webGLStart();"/>


Explications :

On réserve une zone de dessin de 500x500 pixels, avec la balise "<canvas>" :

    <canvas id="canvas" style="border: none;" width="500" height="500"/>

On déclare un script de traitement des pixels (un "fragment shader") :

    <script id="shader-fs" type="x-shader/x-fragment">
        precision mediump float;
        varying vec4 vColor;
      
        void main( void )
        {
            gl_FragColor = vColor;
        }
    </script>


On déclare un script de traitement des points (un "vertex shader") :

    <script id="shader-vs" type="x-shader/x-vertex">
        attribute vec3 positionAttr;
        attribute vec4 colorAttr;
        varying vec4 vColor;
    
        void main( void )
        {
            gl_Position = vec4( positionAttr, 1.0 );
            vColor = colorAttr;
        }
    </script>

   
On obtient un contexte de dessin "webgl" :

    gl = canvas.getContext( "webgl" );
   
On compile les shaders :

    gl.shaderSource( shader, script.text );
    gl.compileShader( shader );
  
    ...
  
    var vertexShader = getShader( gl, "shader-vs" );
    var fragmentShader = getShader( gl, "shader-fs" );

    program = gl.createProgram();
    gl.attachShader( program, vertexShader );
    gl.attachShader( program, fragmentShader );
    gl.linkProgram( program );

    if ( !gl.getProgramParameter( program, gl.LINK_STATUS ) )
    {
        alert( "Could not initialise shaders" );
    }

    gl.useProgram( program );
    program.positionAttr = gl.getAttribLocation( program, "positionAttr" );
    gl.enableVertexAttribArray( program.positionAttr );
    program.colorAttr = gl.getAttribLocation( program, "colorAttr" );
    gl.enableVertexAttribArray( program.colorAttr );


On stocke la géométrie du triangle, càd les coordonnées et couleurs de ses points, dans un "buffer" :

    buffer = gl.createBuffer();
    gl.bindBuffer( gl.ARRAY_BUFFER, buffer );
 
    var vertexData =
    [
        // X    Y     Z     R     G     B     A
        0.0,   0.8,  0.0,  1.0,  0.0,  0.0,  1.0,
        -0.8, -0.8,  0.0,  0.0,  1.0,  0.0,  1.0,
        0.8,  -0.8,  0.0,  0.0,  0.0,  1.0,  1.0
    ];
 
    gl.bufferData( gl.ARRAY_BUFFER, new Float32Array( vertexData ), gl.STATIC_DRAW );

      
On lance le rendu graphique du "buffer" au moyen de la version compilée des "shaders" :

    gl.bindBuffer( gl.ARRAY_BUFFER, buffer );

    var stride = 7 * Float32Array.BYTES_PER_ELEMENT;

    gl.vertexAttribPointer( program.positionAttr, 3, gl.FLOAT, false, stride, 0 );
    gl.vertexAttribPointer( program.colorAttr, 4, gl.FLOAT, false, stride, 3 * Float32Array.BYTES_PER_ELEMENT );
    gl.drawArrays( gl.TRIANGLES, 0, 3 );


Le moteur de jeu CRAFTY.JS


Il s'agit d'une librairie Javascript facilitant le développement de jeux 2D au moyen de la balise "<canvas>".

Le jeu doit y être décomposé en scènes ("scene"), que l'on active avec la fonction "enterScene()".

Une scène se compose d'un arrière-plan ("background"), et d'entités graphiques interactives ("entity").

Une entité se définit en énumérant ses composantes.

Le programmeur doit donc simplement initialiser les scènes et définir dans des fonctions (via "bind()") le comportement des entités par rapport au différents événements qui les concernent (collisions, touches du clavier, clics de la souris, changement de scène, etc).

La librairie se charge du reste, càd :
- afficher le contenu de la scène courante
- animer les entités en gérant leurs collisions
- leur transmettre les événements qui leur sont adressés.




Exemple :

<script src="crafty.js"></script>
<script>
    window.onload = function() {
        "use strict";
       
        Crafty.init(600, 300);
        Crafty.background('rgb(127,127,127)');

        Crafty.scene("game", function() {
       
            // Paddles
           
            Crafty.e("Paddle, 2D, DOM, Color, Multiway")
                .color('rgb(255,0,0)')
                .attr({
                    x: 20,
                    y: 100,
                    w: 10,
                    h: 100
                })
                .multiway(4, {
                    Q: -90,
                    W: 90
                });
               
            Crafty.e("Paddle, 2D, DOM, Color, Multiway")
                .color('rgb(0,255,0)')
                .attr({
                    x: 580,
                    y: 100,
                    w: 10,
                    h: 100
                })
                .multiway(4, {
                    UP_ARROW: -90,
                    DOWN_ARROW: 90
                });

            // Ball
           
            Crafty.e("2D, DOM, Color, Collision")
                .color('rgb(0,0,255)')
                .attr({
                    x: 300,
                    y: 150,
                    w: 10,
                    h: 10,
                    dX: Crafty.math.randomInt(2, 5),
                    dY: Crafty.math.randomInt(2, 5)
                })
                .bind('EnterFrame', function() {
               
                    // Hit floor or roof
                   
                    if (this.y <= 0 || this.y >= 290)
                        this.dY *= -1;

                    if (this.x > 600) {
                        this.x = 300;
                        Crafty("LeftPoints").each(function() {
                            this.text(++this.points + " Points")
                        });
                    }
                   
                    if (this.x < 10) {
                        this.x = 300;
                        Crafty("RightPoints").each(function() {
                            this.text(++this.points + " Points")
                        });
                    }

                    this.x += this.dX;
                    this.y += this.dY;
                })
                .onHit('Paddle', function() {
                    this.dX *= -1;
                })

            // Score boards
           
            Crafty.e("LeftPoints, DOM, 2D, Text")
                .attr({
                    x: 20,
                    y: 20,
                    w: 100,
                    h: 20,
                    points: 0
                })
                .text("0 Points");
               
            Crafty.e("RightPoints, DOM, 2D, Text")
                .attr({
                    x: 515,
                    y: 20,
                    w: 100,
                    h: 20,
                    points: 0
                })
                .text("0 Points");
        })
       
        Crafty.e("2D, DOM, Text").attr({
            x: 250,
            y: 130,
            w: 300
        }).text("Click to play...");
       
        Crafty.e("2D, DOM, Mouse").attr({
            x: 0,
            y: 0,
            h: 300,
            w: 600
        }).bind("Click", function() {
            Crafty.scene("game");
        });
    }   
</script>

       
Explications :

On charge Crafty.js :

    <script src="crafty.js"></script>
   
On initialise un canevas de 600x300 pixels :

    Crafty.init(600, 300);
   
On ajoute un arrière-plan gris :

    Crafty.background('rgb(127,127,127)');

On crée une scène "game" :

    Crafty.scene("game", function() {

On crée une entité pour la raquette de gauche :
      
    Crafty.e("Paddle, 2D, DOM, Color, Multiway")
       
On lui assigne une couleur, une position et une taille :

    .color('rgb(255,0,0)')
    .attr({
        x: 20,
        y: 100,
        w: 10,
        h: 100
    })
           

On lui assigne un déplacement vertical contrôlé par deux touches du clavier :

    .multiway(4, {
        Q: -90,
        W: 90
    });


On crée aussi une entité pour la raquette de droite :
           
    Crafty.e("Paddle, 2D, DOM, Color, Multiway")
        .color('rgb(0,255,0)')
        .attr({
            x: 580,
            y: 100,
            w: 10,
            h: 100
        })
        .multiway(4, {
            UP_ARROW: -90,
            DOWN_ARROW: 90
        });


On crée une entité pour la balle :
      
    Crafty.e("2D, DOM, Color, Collision")
       
On lui assigne une couleur, une position et une taille :

    .color('rgb(0,0,255)')
    .attr({
        x: 300,
        y: 150,
        w: 10,
        h: 10,
        dX: Crafty.math.randomInt(2, 5),
        dY: Crafty.math.randomInt(2, 5)
    })

           
On lui assigne une fonction exécutée au début de chaque affichage :

    .bind('EnterFrame', function() {
  
        // Hit floor or roof
      
        if (this.y <= 0 || this.y >= 290)
            this.dY *= -1;

        if (this.x > 600) {
            this.x = 300;
            Crafty("LeftPoints").each(function() {
                this.text(++this.points + " Points")
            });
        }
      
        if (this.x < 10) {
            this.x = 300;
            Crafty("RightPoints").each(function() {
                this.text(++this.points + " Points")
            });
        }

        this.x += this.dX;
        this.y += this.dY;
    })


On lui assigne une fonction exécutée quand elle touche l'entité de la raquette :

    .onHit('Paddle', function() {
        this.dX *= -1;
    })


On crée une entité pour le score du joueur de gauche :
      
    Crafty.e("LeftPoints, DOM, 2D, Text")

On lui assigne une couleur, une position, une taille et un nombre de points initial :

    .attr({
        x: 20,
        y: 20,
        w: 100,
        h: 20,
        points: 0
    })

           
On lui assigne un texte :

    .text("0 Points");

On crée aussi une entité pour le score du joueur de droite :
       
    Crafty.e("RightPoints, DOM, 2D, Text")
        .attr({
            x: 515,
            y: 20,
            w: 100,
            h: 20,
            points: 0
        })
        .text("0 Points");


On crée une entité pour le texte de démarrage :
       
    Crafty.e("2D, DOM, Text").attr({
        x: 250,
        y: 130,
        w: 300
    }).text("Click to play...");


On crée une entité couvrant toute la zone de jeu :   

    Crafty.e("2D, DOM, Mouse").attr({
        x: 0,
        y: 0,
        h: 300,
        w: 600


On lui assigne une fonction qui lance la scène quand on clique sur elle :

    }).bind("Click", function() {
        Crafty.scene("game");
    });


Références

http://www.w3schools.com/html/html5_canvas.asp
https://github.com/WebGLSamples/WebGLSamples.github.io/blob/master/hello-webgl/hello-webgl.html
https://github.com/craftyjs/craftyjs.github.com/blob/master/tutorial/games/pong/pong.html

Aucun commentaire:

Enregistrer un commentaire