• Step by Step 使用HTML5开发一个星际大战游戏(2)


    HTML5 Canvas Game: 玩家飞船

    Languages: HTML5, JavaScript
    Code: https://github.com/straker/galaxian-canvas-game/tree/master/part2

    .玩家飞船

    上节,我们讲述了背景滚动的实现,本节我们来实现一下玩家飞船的移动和子弹的发射。

    首先让我们看一下本次教程的效果,如果界面没有反应,确认您鼠标点击了下面的界面。

    控制: 移动 – 上下左右箭头
    射击 – 空格键

    再次,我们先看html页面代码:

    <!DOCTYPE html>
    <html>
        <head>
            <title>Space Shooter Demo</title>
            <style>
                canvas {
                    position: absolute;
                    top: 0px;
                    left: 0px;
                    background: transparent;
                }
                #background {
                    z-index: -2;
                }
                #main {
                    z-index: -1;
                }
                #ship {
                    z-index: 0;
                }
            </style>
        </head>
        <body>
            <!-- The canvas for the panning background -->
            <canvas id="background" width="600" height="360">
                Your browser does not support canvas. Please try again with a different browser.
            </canvas>
            <!-- The canvas for all enemy ships and bullets -->
            <canvas id="main" width="600" height="360">
            </canvas>
            <!-- The canvas the ship uses (can only move up
             one forth the screen.) -->
            <canvas id="ship" width="600" height="360">
            </canvas>
            <script src="space_shooter_part_two.js"></script>
        </body>
    </html>

    与上一节的html文件相比,一些地方放生了变化,这里我们使用了3个画布(canvas),一个用来画背景,一个用来画飞船,一个用来画子弹和敌机,这样做的好处是我们不用每次都重绘游戏的每一帧。通过css定义了这三个图层的z序。

    首先,Drawable对象的init方法需要改变,代码如下:

    function Drawable() {
        this.init = function(x, y, width, height) {
            // Defualt variables
            this.x = x;
            this.y = y;
            this.width = width;
            this.height = height;
        }
    }

    因为我们的图像不再充满整个画布,所以我们补充了width, height 2个属性。

    图像仓库对象,也有改动,代码如下:

    /**
     * Define an object to hold all our images for the game so images
     * are only ever created once. This type of object is known as a
     * singleton.
     */
    var imageRepository = new function() {
        // Define images
        this.background = new Image();
        this.spaceship = new Image();
        this.bullet = new Image();
        // Ensure all images have loaded before starting the game
        var numImages = 3;
        var numLoaded = 0;
        function imageLoaded() {
            numLoaded++;
            if (numLoaded === numImages) {
                window.init();
            }
        }
        this.background.onload = function() {
            imageLoaded();
        }
        this.spaceship.onload = function() {
            imageLoaded();
        }
        this.bullet.onload = function() {
            imageLoaded();
        }
        // Set images src
        this.background.src = "imgs/bg.png";
        this.spaceship.src = "imgs/ship.png";
        this.bullet.src = "imgs/bullet.png";
    }
    加载三张图片,背景,飞船,子弹。三张图片都加载完成后,调用window.init();开始游戏。

    飞船

    接下来我们将讨论飞船的移动和子弹的发射。

    首先,我们先了解一个新的数据结构,对象池。

    对于一些可能需要被频繁创建和销毁的对象,我们可以将其缓存起来,重复使用,以减少系统的开销,提高游戏运行的速度,这里我们的子弹就属于这种对象。

    /**
     * Custom Pool object. Holds Bullet objects to be managed to prevent
     * garbage collection.
     */
    function Pool(maxSize) {
        var size = maxSize; // Max bullets allowed in the pool
        var pool = [];
        /*
         * Populates the pool array with Bullet objects
         */
        this.init = function() {
            for (var i = 0; i < size; i++) {
                // Initalize the bullet object
                var bullet = new Bullet();
                bullet.init(0,0, imageRepository.bullet.width,
                            imageRepository.bullet.height);
                pool[i] = bullet;
            }
        };
        /*
         * Grabs the last item in the list and initializes it and
         * pushes it to the front of the array.
         */
        this.get = function(x, y, speed) {
            if(!pool[size - 1].alive) {
                pool[size - 1].spawn(x, y, speed);
                pool.unshift(pool.pop());
            }
        };
        /*
         * Used for the ship to be able to get two bullets at once. If
         * only the get() function is used twice, the ship is able to
         * fire and only have 1 bullet spawn instead of 2.
         */
        this.getTwo = function(x1, y1, speed1, x2, y2, speed2) {
            if(!pool[size - 1].alive &&
               !pool[size - 2].alive) {
                    this.get(x1, y1, speed1);
                    this.get(x2, y2, speed2);
                 }
        };
        /*
         * Draws any in use Bullets. If a bullet goes off the screen,
         * clears it and pushes it to the front of the array.
         */
        this.animate = function() {
            for (var i = 0; i < size; i++) {
                // Only draw until we find a bullet that is not alive
                if (pool[i].alive) {
                    if (pool[i].draw()) {
                        pool[i].clear();
                        pool.push((pool.splice(i,1))[0]);
                    }
                }
                else
                    break;
            }
        };
    }

     当对象池被初始化时,建立了一个给定个数的子弹对象数组,每当需要一个新的子弹的时候,对象池查看数组的最后一个元素,看该元素是否当前被占用。如果被占用,说明当前对象池已经满了,无法产生新子弹;如果没被占用,

    取出最后一个子弹元素,放到数组的最前面。这样使得对象池中总是后面部分存放着待使用的子弹,而前面存放着正在使用的子弹(屏幕上看到的)。

    当对象池animates子弹的时候,判断当前子弹是否还在屏幕中(draw方法返回false),如果draw方法返回true,说明该子弹已经脱离屏幕区域,可以被回收供再利用)。

    现在对象池对象已经准备好,我们开始创建子弹对象,如下面代码:

    /**
     * Creates the Bullet object which the ship fires. The bullets are
     * drawn on the "main" canvas.
     */
    function Bullet() {
        this.alive = false; // Is true if the bullet is currently in use
        /*
         * Sets the bullet values
         */
        this.spawn = function(x, y, speed) {
            this.x = x;
            this.y = y;
            this.speed = speed;
            this.alive = true;
        };
        /*
         * Uses a "drity rectangle" to erase the bullet and moves it.
         * Returns true if the bullet moved off the screen, indicating that
         * the bullet is ready to be cleared by the pool, otherwise draws
         * the bullet.
         */
        this.draw = function() {
            this.context.clearRect(this.x, this.y, this.width, this.height);
            this.y -= this.speed;
            if (this.y <= 0 - this.height) {
                return true;
            }
            else {
                this.context.drawImage(imageRepository.bullet, this.x, this.y);
            }
        };
        /*
         * Resets the bullet values
         */
        this.clear = function() {
            this.x = 0;
            this.y = 0;
            this.speed = 0;
            this.alive = false;
        };
    }
    Bullet.prototype = new Drawable();
    子弹对象初始化状态设置alive为false,子弹对象包含三个方法spawn,draw和clear,仔细看draw方法可以看到if (this.y <= 0 - this.height),代表子弹已经在屏幕中消失,这里返回的true。

    矩形

    在刚才的draw方法中,我们使用了一个被称作脏矩形的技术,我们仅仅清除(clearRect)子弹前一瞬间的矩形区域,而不是整个屏幕。如果子弹离开了屏幕,draw方法返回true,指示该子弹对象可以被再回收;否则我们重绘它。

    脏矩形的使用是我们开始创建3个画布的原因。如果我们只有一个画布,我们不得不在每一帧中,重绘所有的对象,屏幕背景,子弹,飞船。如果我们使用2个画布,背景一个画布,子弹和飞船共用一个,那么子弹每一帧将被重绘,而飞船只有移动的时候才被重绘,如果子弹和飞船发生重叠,子弹的区域的清除将使得飞船不再完整,直到我们移动飞船,这将大大地影响可视效果。所以这里我们采用3个画布。

    对象

    我们最后要创建的是飞船对象,代码如下:

    /**
     * Create the Ship object that the player controls. The ship is
     * drawn on the "ship" canvas and uses dirty rectangles to move
     * around the screen.
     */
    function Ship() {
        this.speed = 3;
        this.bulletPool = new Pool(30);
        this.bulletPool.init();
        var fireRate = 15;
        var counter = 0;
        this.draw = function() {
            this.context.drawImage(imageRepository.spaceship, this.x, this.y);
        };
        this.move = function() {
            counter++;
            // Determine if the action is move action
            if (KEY_STATUS.left || KEY_STATUS.right ||
                KEY_STATUS.down || KEY_STATUS.up) {
                // The ship moved, so erase it's current image so it can
                // be redrawn in it's new location
                this.context.clearRect(this.x, this.y, this.width, this.height);
                // Update x and y according to the direction to move and
                // redraw the ship. Change the else if's to if statements
                // to have diagonal movement.
                if (KEY_STATUS.left) {
                    this.x -= this.speed
                    if (this.x <= 0) // Keep player within the screen
                        this.x = 0;
                } else if (KEY_STATUS.right) {
                    this.x += this.speed
                    if (this.x >= this.canvasWidth - this.width)
                        this.x = this.canvasWidth - this.width;
                } else if (KEY_STATUS.up) {
                    this.y -= this.speed
                    if (this.y <= this.canvasHeight/4*3)
                        this.y = this.canvasHeight/4*3;
                } else if (KEY_STATUS.down) {
                    this.y += this.speed
                    if (this.y >= this.canvasHeight - this.height)
                        this.y = this.canvasHeight - this.height;
                }
                // Finish by redrawing the ship
                this.draw();
            }
            if (KEY_STATUS.space && counter >= fireRate) {
                this.fire();
                counter = 0;
            }
        };
        /*
         * Fires two bullets
         */
        this.fire = function() {
            this.bulletPool.getTwo(this.x+6, this.y, 3,
                                   this.x+33, this.y, 3);
        };
    }
    Ship.prototype = new Drawable();

    飞船对象设置自己的移动速度为3, 子弹对象池大小为30,开火帧率为15。

    KEY_STATUS对象的代码如下:

    // The keycodes that will be mapped when a user presses a button.
    // Original code by Doug McInnes
    KEY_CODES = {
      32: 'space',
      37: 'left',
      38: 'up',
      39: 'right',
      40: 'down',
    }
    // Creates the array to hold the KEY_CODES and sets all their values
    // to false. Checking true/flase is the quickest way to check status
    // of a key press and which one was pressed when determining
    // when to move and which direction.
    KEY_STATUS = {};
    for (code in KEY_CODES) {
      KEY_STATUS[ KEY_CODES[ code ]] = false;
    }
    /**
     * Sets up the document to listen to onkeydown events (fired when
     * any key on the keyboard is pressed down). When a key is pressed,
     * it sets the appropriate direction to true to let us know which
     * key it was.
     */
    document.onkeydown = function(e) {
      // Firefox and opera use charCode instead of keyCode to
      // return which key was pressed.
      var keyCode = (e.keyCode) ? e.keyCode : e.charCode;
      if (KEY_CODES[keyCode]) {
        e.preventDefault();
        KEY_STATUS[KEY_CODES[keyCode]] = true;
      }
    }
    /**
     * Sets up the document to listen to ownkeyup events (fired when
     * any key on the keyboard is released). When a key is released,
     * it sets teh appropriate direction to false to let us know which
     * key it was.
     */
    document.onkeyup = function(e) {
      var keyCode = (e.keyCode) ? e.keyCode : e.charCode;
      if (KEY_CODES[keyCode]) {
        e.preventDefault();
        KEY_STATUS[KEY_CODES[keyCode]] = false;
      }
    }
    的步骤

    最后我们需要更新游戏对象以及动画功能,代码如下:

     /**
     * Creates the Game object which will hold all objects and data for
     * the game.
     */
    function Game() {
        /*
         * Gets canvas information and context and sets up all game
         * objects.
         * Returns true if the canvas is supported and false if it
         * is not. This is to stop the animation script from constantly
         * running on browsers that do not support the canvas.
         */
        this.init = function() {
            // Get the canvas elements
            this.bgCanvas = document.getElementById('background');
            this.shipCanvas = document.getElementById('ship');
            this.mainCanvas = document.getElementById('main');
            // Test to see if canvas is supported. Only need to
            // check one canvas
            if (this.bgCanvas.getContext) {
                this.bgContext = this.bgCanvas.getContext('2d');
                this.shipContext = this.shipCanvas.getContext('2d');
                this.mainContext = this.mainCanvas.getContext('2d');
                // Initialize objects to contain their context and canvas
                // information
                Background.prototype.context = this.bgContext;
                Background.prototype.canvasWidth = this.bgCanvas.width;
                Background.prototype.canvasHeight = this.bgCanvas.height;
                Ship.prototype.context = this.shipContext;
                Ship.prototype.canvasWidth = this.shipCanvas.width;
                Ship.prototype.canvasHeight = this.shipCanvas.height;
                Bullet.prototype.context = this.mainContext;
                Bullet.prototype.canvasWidth = this.mainCanvas.width;
                Bullet.prototype.canvasHeight = this.mainCanvas.height;
                // Initialize the background object
                this.background = new Background();
                this.background.init(0,0); // Set draw point to 0,0
                // Initialize the ship object
                this.ship = new Ship();
                // Set the ship to start near the bottom middle of the canvas
                var shipStartX = this.shipCanvas.width/2 - imageRepository.spaceship.width;
                var shipStartY = this.shipCanvas.height/4*3 + imageRepository.spaceship.height*2;
                this.ship.init(shipStartX, shipStartY, imageRepository.spaceship.width,
                               imageRepository.spaceship.height);
                return true;
            } else {
                return false;
            }
        };
        // Start the animation loop
        this.start = function() {
            this.ship.draw();
            animate();
        };
    }
    /**
     * The animation loop. Calls the requestAnimationFrame shim to
     * optimize the game loop and draws all game objects. This
     * function must be a gobal function and cannot be within an
     * object.
     */
    function animate() {
        requestAnimFrame( animate );
        game.background.draw();
        game.ship.move();
        game.ship.bulletPool.animate();
    }
     

     

  • 相关阅读:
    阿里Canal中间件的初步搭建和使用
    十大经典排序算法动画与解析,看我就够了!(配代码完全版)
    单点登录系统简介
    业务系统日志追踪
    kafka单机环境搭建及其基本使用
    Kafka 错误信息 java.io.IOException: Can't resolve address: VM_0_15_centos:9092
    CAS(乐观锁)以及ABA问题
    kafka_2.11-0.10.1.1集群搭建安装配置
    contos7 mongodb安装教程
    mongodb基础操作
  • 原文地址:https://www.cnblogs.com/ice-river/p/4155490.html
Copyright © 2020-2023  润新知