HTML5移動遊戲開發高階程式設計 1:先飛後走,先難後易

CopperDong發表於2018-02-15

1.1 引言

   在HTML5上從頭開始構建一個一次性遊戲---一個名為Alien Invasion的縱向卷軸2D太空射擊類遊戲。

1.2 用500行程式碼構建一個完整遊戲

      Alien Invasion秉承了遊戲“1942”的精髓(但是在太空中),或可把它看成Galaga的一個簡化版本。玩家控制出現在螢幕底部的飛船,操作飛船垂直飛過無邊無際的太空領域,同時保衛地球,抵抗成群入侵的外星人。

      在移動裝置上玩遊戲時,使用者是通過顯示在螢幕左下角的左右箭頭進行控制的,發射按鈕在右側。在桌面上玩遊戲時,使用者可以使用鍵盤的箭頭鍵來控制飛行和使用空格鍵進行射擊。

      為彌補移動裝置螢幕大小各有不同這一不足,遊戲會調整遊戲區域,始終按照裝置大小來執行遊戲。在桌面上,遊戲會被放在瀏覽器頁面中間的一個矩形區域中執行。

      結構化遊戲:幾乎每個這種型別的遊戲都包含了幾塊相同的內容:一些資產的載入、一個標題畫面、一些精靈、使用者輸入、碰撞檢測以及一個把這幾塊內容整合在一起的遊戲迴圈。

      http://cykod.github.com/AlienInvasion/

1.3 新增HTML和CSS樣板程式碼

      <canvas>元素

<!DOCTYPE HTML>
<html lang="en">
<head>
   <meta charset="UTF-8"/>
   <title>Alien Invasion</title>
   <link rel="stylesheet" href="base.css" type="text/css" />
</head>
<body>
   <div id="container">
      <canvas id='game' width='320' height="480"></canvas>
   </div>
   <script src="game.js"></script>
</body>
</html>
base.css

#container {
	padding-top: 50px;
	margin: 0 auto;
	width: 480px;
}
canvas {
	background-color: black;
}

1.4 畫布入門

      drawImage有著幾種不同的呼叫形式,這取決於你是想繪製完整影像還是僅繪製部分影像。

      drawImage(image, sx, sy, sWidth, sHeight, dx, dy, dWidth, dHeight)

      這種形式使得你能使用引數sx、sy、sWidth和sHeight在影像中指定源矩形,以及使用引數dx、dy、dWidth和dHeight在畫布上指定目標矩形。要從精靈表的某個精靈中抽取單獨的幀,這就是你應用使用的格式

     game.js

var canvas = document.getElementById('game');
var ctx = canvas.getContext && canvas.getContext('2d');
if (!ctx) {
	// No 2d 
	alert('Please upgrade your browser');
} else {
	startGame();
}
function startGame() {
	ctx.fillStyle = "#FFFF00";
	ctx.fillRect(50, 100, 380, 400);
	ctx.fillStyle = "rgba(0,0,128,0.5);";
	ctx.fillRect(0, 50, 380, 400);
	//載入影像
	var img = new Image();
	img.onload = function() {
		// 非同步呼叫
		ctx.drawImage(img,18,0,18,25,100,100,18,25);
		console.log("onload");
	}
	img.src = 'images/sprites.png';
}

1.5 建立遊戲的結構

    利用鴨子型別:基於物件的外部介面(而非它們的型別)來使用物件的概念稱為鴨子型別(duck typing)

    Alien Invasion在遊戲介面和精靈這兩個地方用到了這一概念,該遊戲把任何響應step()和draw()方法呼叫的事物都當成遊戲介面物件或有效的精靈對待。把鴨子型別用於遊戲介面,這使得Alien Invasion能夠把標題畫面和遊戲中的介面當成同樣的物件型別看待,簡化了關卡和標題畫面之間的切換。同樣鴨子型別用於精靈意味著遊戲能夠靈活決定往遊戲皮膚中新增的內容,其中包括玩家、敵人、炮彈和HUD元素等。HUD是抬頭顯示裝置的簡稱,這個術語常指位於遊戲螢幕上方的元素,如剩餘的生命條數和玩家得分等。

    建立三個基本物件:Game物件(把所有東西捆綁在一起)、SpriteSheet物件(載入和繪製精靈)以及GameBoard(顯示、更新精靈元素和處理精靈元素碰撞)。該遊戲還需要一大群不同的精靈,如玩家、敵方飛船、導彈及諸如得分和剩餘生命條數一類的HUD物件等。

1.6 載入精靈表

    SpriteSheet類

1.7 建立Game物件

     主要目的是初始化遊戲引擎並執行遊戲迴圈,以及提供一種機制來改變所顯示的主場景。

1.8 新增滾動背景

    每幀繪製太多精靈會降低遊戲在移動裝置上的執行速度。一種解決方法是建立畫布的離屏緩衝區,在緩衝區中隨機繪製一堆星星,然後簡單地繪製慢慢向下移過畫布的星空。

     http://jsperf.com/prerendered-starfield測試HTML5效能

     StarField類需要完成的事情主要有三項,第一項是建立離屏畫布。

1.9 插入標題畫面

    一個文字標題和一個副標題

    Bangers字型賦予遊戲一種很好的復古風格

<link rel="stylesheet" href="http://fonts.googleapis.com/css?family=Bangers" type="text/css" />

1.10 新增主角

   第一步是新增一艘由玩家控制的飛船

   建立PlayerShip物件

index.html

<!DOCTYPE HTML>
<html lang="en">
<head>
   <meta charset="UTF-8"/>
   <title>Alien Invasion</title>
   <link rel="stylesheet" href="base.css" type="text/css" />
   <link rel="stylesheet" href="http://fonts.googleapis.com/css?family=Bangers" type="text/css" />
</head>
<body>
   <div id="container">
      <canvas id='game' width='480' height="600"></canvas>
   </div>
   <script src="engine.js"></script>
   <script src="game.js"></script>
</body>
</html>
base.css

#container {
	padding-top: 50px;
	margin: 0 auto;
	width: 480px;
}
canvas {
	background-color: black;
}
engine.js

var Game = new function() {
    this.initialize = function(canvasElementId,sprite_data,callback) {
    	this.canvas = document.getElementById(canvasElementId);
    	this.width = this.canvas.width;
    	this.height = this.canvas.height;
    	// Setu up the rendering context
    	this.ctx = this.canvas.getContext && this.canvas.getContext('2d');
    	if (!this.ctx) {
    		return alert("Please upgrade your browser to play");
    	}
    	this.setupInput();
    	this.loop();
    	SpriteSheet.load(sprite_data, callback);
    };
    // Handle Input
    var KEY_CODES = { 37:'left', 39:'right', 32:'fire' };
    this.keys = {};
    this.setupInput = function() {
    	window.addEventListener('keydown', function(e) {
    		if (KEY_CODES[event.keyCode]) {
    			Game.keys[KEY_CODES[event.keyCode]] = true;
    			e.preventDefault();
    		}
    	}, false);
    	window.addEventListener('keyup', function(e) {
    		if (KEY_CODES[event.keyCode]) {
    			Game.keys[KEY_CODES[event.keyCode]] = false;
    			e.preventDefault();
    		}
    	}, false);
    }
    // 存放的是已更新並已繪製到畫布上的遊戲的各塊內容,一個可能的皮膚例子是背景或標題畫面
    var boards = [];
    this.loop = function() {
    	var dt = 30/1000;
    	for (var i=0, len=boards.length; i<len; i++) {
    		if (boards[i]) {
    			boards[i].step(dt);
    			boards[i] && boards[i].draw(Game.ctx);
    		}
    	}
    	setTimeout(Game.loop, 30);
    };
    // Change an active game board
    this.setBoard = function(num, board) {
    	boards[num] = board;
    };
};

var SpriteSheet = new function() {
	this.map = { };
	this.load = function(spriteData, callback) {
		this.map = spriteData;
		this.image = new Image();
		this.image.onload = callback;
		this.image.src = 'images/sprites.png';
	};
	this.draw = function(ctx,sprite,x,y,frame) {
		var s = this.map[sprite];
		if (!frame) {
			frame = 0;
		}
		//console.log(s);
		ctx.drawImage(this.image,
			s.sx + frame * s.w,
			s.sy,
			s.w, s.h,
			x, y,
			s.w, s.h);
	};
};

var TitleScreen = function TitleScreen(title, subtitle, callback) {
	this.step = function(dt) {
		if (Game.keys['fire'] && callback)
			callback();
	};
	this.draw = function(ctx) {
		ctx.fillStyle = "#FFFFFF";
		ctx.textAlign = "center";
		ctx.font = "bold 40px bangers";
		ctx.fillText(title, Game.width/2, Game.height/2);
		ctx.font = "bold 20px bangers";
		ctx.fillText(subtitle, Game.width/2, Game.height/2 + 40);
	}
};
game.js

var sprites = {
	ship: { sx: 0, sy: 0, w: 38, h: 42, frames: 1 }
};

function startGame() {
	//SpriteSheet.draw(Game.ctx, "ship", 100, 100, 1);
	Game.setBoard(0, new Starfield(20,0.4,100,true))
	Game.setBoard(1, new Starfield(50,0.6,100))
	Game.setBoard(2, new Starfield(100,1.0,50));
	Game.setBoard(3, new TitleScreen("Alien Invasion",
		"Press space to start playing",
		playGame));
}

window.addEventListener("load", function() {
	Game.initialize("game", sprites, startGame);
});

var Starfield = function(speed,opacity,numStars,clear){
	// Set up the offscreen canvas
	var stars = document.createElement("canvas");
	stars.width = Game.width;
	stars.height = Game.height;
	var starCtx = stars.getContext("2d");
	var offset = 0;
	// If the clear option is set,
	// make the background black instead of transparent
	if (clear) {
		starCtx.fillStyle = "#000";
		starCtx.fillRect(0,0,stars.width,stars.height);
	}
	// Now draw a bunch of random 2 pixel
	// rectangles onto the offscreen canvas
	starCtx.fillStyle = "#FFF";
	starCtx.globalAlpha = opacity;
	for (var i=0; i<numStars; i++) {
		starCtx.fillRect(Math.floor(Math.random()*stars.width),
			Math.floor(Math.random()*stars.height),
			2,
			2);
	}
	// This method is called every frame
	// to draw the starfield onto the canvas
	this.draw = function(ctx) {
		var intOffset = Math.floor(offset);
		var remaining = stars.height - intOffset;
		// Draw the top half of the starfield
		if (intOffset > 0) {
			ctx.drawImage(stars,
				0, remaining,
				stars.width, intOffset,
				0, 0,
				stars.width, intOffset);
		}
		// Draw the bottom half of the starfield
		if (remaining > 0) {
			ctx.drawImage(stars,
				0, 0,
				stars.width, remaining,
				0, intOffset,
				stars.width, remaining);
		}
	}
	// This method is called to update
	// the starfield
	this.step = function(dt) {
		offset += dt * speed;
		offset = offset % stars.height;
	}
}

var playGame = function() {
	Game.setBoard(3, new PlayerShip());
}

var PlayerShip = function() {
	this.w = SpriteSheet.map['ship'].w;
	this.h = SpriteSheet.map['ship'].h;
	this.x = Game.width/2 - this.w/2;
	this.y = Game.height - 10 - this.h;
	this.vx = 0;
	this.step = function(dt) {
		//
		this.maxVel = 200;
		this.step = function(dt) {
			if (Game.keys['left']) {
				this.vx = -this.maxVel;
			} else if (Game.keys['right']) {
				this.vx = this.maxVel;
			} else {
				this.vx = 0;
			}
			this.x += this.vx * dt;
			if (this.x < 0) {
				this.x = 0;
			} else if (this.x > Game.width - this.w) {
				this.x = Game.width - this.w;
			}
		}
	};
	this.draw = function(ctx) {
		SpriteSheet.draw(ctx, 'ship', this.x, this.y, 1);
	};
};




相關文章