Search code examples
javascriptjqueryanimationgraphics2d-games

How can I achieve a wraparound effect in an arcade-like game?


I am working on a clone of the game Spacewar!. In the game, ships can travel to the edge of the map and wrap around to the other side, at times split in half, one half on each side of the screen (when the ships go into the corners they are split in four). You can play the game here.

Right now I am using the modulus operator to wrap a div around the screen, but it doesn't split the block in half like I would it to. Is there any way this is possible in JavaScript or jQuery?


Solution

  • Wrapping sprites

    Interesting link you have provided. They have implemented a full CPU emulation and running the game written in assembly.

    Improving the modulo

    Anyway if you are using the canvas to render sprites (images) the easiest way is a simple modulo with a modification to handle negative values.

    The normal modulo breaks down when using negative values.

    x = 100 % 200; // 100
    x = 300 % 200; // 100 
    x = -100 % 200; // -100  the wrong sign should be 100
    x = -50 % 200;  // -50 wrong sign wrong direction should be 150
    

    You need the modulo to always return a positive value in the correct direction. To handle the negative number do the modulo twice, the first will get within the range you want but +/- the range. Then make the negative positive by adding the range. Then just do the modulo again.

    var range = 200;
    var x = 150;
    x = ((x % range) + range) % range; // result 150
    x = -50;
    x = ((x % range) + range) % range; // result 150 correct.
    

    Simple wrapping

    Using the above modulo algorithm it is then a simple matter to check boundaries and render the sprite as needed.

    // assuming ctx is canvas context 2D
    // canvas is the canvas.
    // img is a HTMLImageElement
    
    var canW = canvas.width;                               // the canvas size
    var canH = canvas.height;                              
    // draw a sprite that is wrapped around the edges of the canvas
    function drawWrappedSprite(img,x,y){
        x1 = ((x % canW) + canW) % canW;                   // wrap around correcting for negative values
        y1 = ((y % canH) + canH) % canH;
        x2 = x1 + img.width;                               // get right and bottom
        y2 = y1 + img.height;
        ctx.drawImage(img, x1, y1);                        // draw first copy
        if(x2 > canW){                                     // check if touching right side
            ctx.drawImage(img, x1 - canW, y1);             // draw left copy
            if(y2 > canH){                                 // check if touching bottom
                ctx.drawImage(img, x1 - canW, y1 - canH);  // draw top copy
            }
        }
        if(y2 > canH){
            ctx.drawImage(img, x1 , y1 - canH);            // draw top copy
        }
    }
    

    Wrapping rotated sprites

    As the game has sprites that are rotated the above function will not work as the rotation will change the size of the sprite. To handle the rotated sprite you need to check the max size it can be. This is the length of the diagonal line across the sprite which can be found with sqrt(width * width + height * height)

    Assuming that you want the sprite rotated about its center you can find the sprite max extent (top,bottom,left and right) by subtracting and adding half the diagonal to the x,y center position. The just as the first function, do the modulo and draw the sprite as needed.

    There will be some situations where the sprite is drawn on the opposite side even though it is not visible. If you are drawing lots of sprites (100+) you may want to get the exact extent rather than the max extent but then you will have to transform at least one corner of the sprite to get the horizontal and vertical extent. Then just use those values rather than diag as in the function.

    // assuming ctx is canvas context 2D
    // canvas is the canvas.
    // img is a HTMLImageElement
    
    var canW = canvas.width;                               // the canvas size
    var canH = canvas.height;  
    // draws sprite rotated about its center by r
    function drawWrappedRotatedSprite(img,x,y,r){  // x,y center pos, r rotation in radians
        var diag = Math.sqrt(img.width * img.width + img.height * img.height);  // get diagonal size
        diag /= 2;                                      // half the diagonal
        x1 = (((x - diag) % canW) + canW) % canW;       // get left extent position  and 
        y1 = (((y - diag) % canH) + canH) % canH;       // wrap around correcting for negative values
        var w = - img.width / 2;                        // get image width height
        var h = - img.height / 2                        // offset in rotated space
        x2 = x1 + diag * 2;                             // get right and bottom max extent
        y2 = y1 + diag * 2;
        ctx.setTransform(1,0,0,1,x1 + diag, y1 + diag); // set origin
        ctx.rotate(r);                                  // set rotation
        ctx.drawImage(img, w, h);                      // draw image rotated around its center    
        if(x2 > canW){                                  // check if touching right side
            ctx.setTransform(1,0,0,1,x1 + diag - canW, y1 + diag); // set origin
            ctx.rotate(r);                              // set rotation
            ctx.drawImage(img,w,h);                     // draw image rotated around its center    
            if(y2 > canH){                              // check if touching bottom
                ctx.setTransform(1,0,0,1,x1 + diag - canW, y1 + diag - canH); // set origin
                ctx.rotate(r);                          // set rotation
                ctx.drawImage(img,w,h);                 // draw image rotated around its center    
            }
        }
        if(y2 > canH){
            ctx.setTransform(1,0,0,1,x1 + diag, y1 + diag - canH); // set origin
            ctx.rotate(r);                              // set rotation
            ctx.drawImage(img,w,h);                     // draw top image rotated around its center    
        }
    
        ctx.setTransform(1,0,0,1,0,0); // reset the transform (should only do this after all sprites
                                       // using this function have been drawn 
    }