從零開始做遊戲!

canvas上的宇宙飛船

WEB前端三大語言

WEB前端三大語言

HTML

CSS

JavaScript

HTML

Hypertext Markup Language

超文本標記語言

告訴瀏覽器如何顯示網頁

CSS

Cascading Style Sheets

定義網頁元素的樣式

JS

JavaScript

回應使用者操作

控制HTML5網頁元素

在瀏覽器端做運算

初探JavaScript

JavaScript-變數宣告

JavaScript-變數宣告

//在JS中,型態不需要宣告,型態資訊儲存在物件上:

var num = 10;        //num是一個數字
var num = 'ten';     //現在num是字串

//這就是動態型別(Dynamic Typing)。

JavaScript-資料型態

var playerName = "Twilight"                  // String
var life = 100;                              // Number
var alive = true;                            // Boolean
var friends = ["Pinkie", "Apple", "Rarity"]; // Array
var Backpack = {owner:"Twilight", book:10};  // Object

遊戲中不管是玩家、敵人、子彈、背景...
經過宣告就可以在程式中拿來使用。

如果要量產同一種類的物件呢?

var Player1 = {name:"Twilight", HP:100, MP:100}; 
var Player2 = {name:"Sunset", HP:100, MP:100}; 
var Player3 = {name:"Rainbow", HP:150, MP:0}; 

如果要量產同一種類的物件呢?

var Player1 = {name:"Twilight", HP:100, MP:100}; 
var Player2 = {name:"Sunset", HP:100, MP:100}; 
var Player3 = {name:"Rainbow", HP:150, MP:0}; 

物件內容變多的話,宣告式會變得臃腫

在JS的世界裡,
函式負責執行各種動作。
而"物件"除了個別宣告,
也可以用函式產生。

JavaScript-函式宣告

JavaScript-函式宣告

function functionName() {
   //Write something...
}  
//函式宣告式

JavaScript-函式宣告

function Player(name) {
   this.name = name;
   this.HP = 100;
   this.MP = 100;
} 
var twilight = new Player("Twilight");
twilight.MP = 150;

Start! 繪製宇宙背景

打開你的Notepad++ 或者記事本...

<!DOCTYPE html>   <!-- 告訴瀏覽器文檔的類型 -->
<html>

<canvas></canvas>

</html>

儲存成"game.html"。

在canvas標籤上加點東西:

<canvas id="myCanvas" width="600" height="480" 
        style="background-color:Black"></canvas>
<!-- 讓畫布變成黑色 -->

Question 1:

canvas的長、寬寫在html標籤裡,

和寫在CSS(style)裡的差別?

在HTML中嵌入JS

兩種方法

在HTML中嵌入JS-方法A

<script>

//在這裡寫JavaScript...

</script>

在HTML中嵌入JS-方法B

新增一個檔案,儲存為script.js

把檔案連結添加在<script>標籤上:

<script src="script.js"></script>

開始寫JS吧!!

在script.js的開頭取得畫布元素:

var canvas = document.getElementById("myCanvas");
//getElementById():用id來叫出html中的元素

var ctx = canvas.getContext("2d");
//getContext():設定canvas的渲染環境
//"渲染環境"可以是"2d"、可以是"WebGL"(繪製3D用)。

2D繪圖-canvas的坐標系

2D繪圖-矩形

ctx.fillStyle = "white";
//填入白色。
ctx.fillRect(100, 100 , 150, 150);
//從(100,100)開始繪製寬150,高150的矩形。

在畫布上灑滿星星

Canvas繪圖-星星製造函式

function Star(){
    this.x = Math.random() * canvas.width;
    this.y = Math.random() * canvas.height;
    //Math.random():產生0~1的隨機數
}

Canvas繪圖-量產星星

var star_count = 150; //星星有150顆
var stars = [];       //宣告stars陣列
for(var i = 0; i < star_count; i++){
        stars[i] = new Star();
        //在陣列中生成新的星星
}

stars[]陣列中的每顆星星都有自己的(x,y)座標。

Canvas繪圖-星空繪製函式

function drawStars(){
        ctx.fillStyle = "white";
        for(var i = 0; i < star_count; i++){      
            ctx.fillRect(stars[i].x,stars[i].y,2,2);
            //繪製從座標點(stars[i].x,stars[i].y)開始,
            //長寬皆為2的矩形
        }
}

Canvas繪圖-歡迎來到宇宙

drawStars();

像這樣呼叫這支函式看看...。

需要不同大小的星星?

ctx.fillRect(stars[i].x, stars[i].y, Math.random()*3, 
             Math.random()*3);
//繪製長與寬為0~3隨機數的矩形

修改drawStars()函式中fillRect的參數。

星空背景的程式碼

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

var star_count = 150;
var stars = []; 
for(var i = 0; i < star_count; i++){
        stars[i] = new Star();
}
function Star(){
        this.x = Math.random() * canvas.width;
        this.y = Math.random() * canvas.height;
}
function drawStars(){
        ctx.fillStyle = "white";
        for(var i = 0; i < star_count; i++){      
            ctx.fillRect(stars[i].x, stars[i].y, Math.random()*3, Math.random()*3);
        }
}
drawStars();

如何建造太空船

撿現成的太空船

OpenGameArt.org搜尋spaceship。

(或使用任何你喜歡的圖片)

將透明背景的png檔儲存下來,

和script.js放在同一個資料夾。

讀取圖檔

var ship = new Image;   //宣告ship是Image元素
ship.src = "ship1.png"; //ship的圖檔來源
ctx.drawImage(ship,300,240);
//在座標(300,240)的位置繪製出ship

讀取圖檔

var ship = new Image;   //宣告ship是Image元素
ship.src = "ship1.png"; //ship的圖檔來源
ctx.drawImage(ship,300,240);
//在座標(300,240)的位置繪製出ship

沒有出現...?! 莫慌

讀取圖檔

var ship = new Image();     //宣告ship是Image元素
ship.src = "ship1.png";     //ship的圖檔來源
ship.onload = function() {
        ctx.drawImage(ship,300,240);
        //在座標(300,240)的位置繪製出ship
};

這是因為圖片來不及載入。

onload: 等到圖片載入完畢再執行動作。

讀取圖檔

ctx.drawImage(ship,300,240,100,100);
//在座標(300,240)的位置繪製出ship
//將圖片的長寬縮放為100*100

drawImage()新增兩個參數,縮放圖片的大小。

把零件包裝好,

才能製造更多的太空船!

太空船製造函式

function Ship(){
    this.img = new Image;
    this.width = 100;
    this.height = 100;
    this.x = canvas.width/2 - this.width/2;   
    this.y = canvas.height - this.height*1.2;
    //太空船繪製時在畫布中下方
}

把這幾行:

var ship = new Image(); 
ship.src = "ship1.png";
ship.onload = function() {
        ctx.drawImage(ship,300,240,100,100);
};

改寫成:

var player = new Ship();       //宣告player是Ship物件
player.img.src = "ship1.png";  //player.img的圖檔來源
player.img.onload = function() {
    ctx.drawImage(player.img, player.x, player.y,
                  player.width, player.height);
}//將player繪製到畫布上

載入太空船的程式碼

var player = new Ship();
player.img.src = "ship1.png";
function Ship(){
    this.img = new Image;
    this.width = 100;
    this.height = 100;
    this.x = canvas.width/2 - this.width/2;   
    this.y = canvas.height- this.height*1.2;
}

player.img.onload = function(){
    ctx.drawImage(player.img, player.x, player.y,
              player.width, player.height);
}

下一個小節,我們將開始建立遊戲架構。

新手航行指南

遊戲基本架構

遊戲主迴圈-繪製畫面

function render() {
    //render stuff here...
}

setInterval(render, 30);
//每30/1000秒執行一次render()

遊戲主迴圈-繪製畫面

function render() {
    drawStars();
}

剪下剛剛的繪製星空函式,貼到render中間看看...

遊戲主迴圈-繪製畫面

function render() {
    drawStars();
}

星星似乎逐漸變亮,這是因為每過30/1000秒

都會繪製一遍星空,重疊了很多層。

遊戲主迴圈-繪製畫面

function render() {
    ctx.clearRect(0, 0, canvas.width, canvas.height);
    //清空從(0,0)開始,長寬與畫布相同的矩形
    drawStars();
}

為了避免這種情況,每次RENDER前先將畫布清乾淨。

遊戲主迴圈-繪製畫面

滾動的星空

遊戲主迴圈-更新狀態

function update(){
    for (var i = 0; i < star_count; i++) {
        stars[i].y += 5;
        //每次執行update,星星的y座標都會+5
    }
}

遊戲主迴圈-更新狀態

function update(){
    for (var i = 0; i < star_count; i++) {
        stars[i].y += 5;
        //每次執行update,星星的y座標都會+5
        if(stars[i].y > canvas.height){
            stars[i].y -= canvas.height;
        } //如果超過畫布範圍,移動位置回到最上面
    }
}

遊戲主迴圈-更新狀態

function render() {
    update(); //每次render前先更新狀態
    ctx.clearRect(0, 0, canvas.width, canvas.height);
    drawStars();
}

在render函式裡呼叫update

遊戲主迴圈-玩家操作

遊戲主迴圈-玩家操作

document.onkeydown = function(event){
    //偵測鍵盤輸入
}

遊戲主迴圈-玩家操作

player.speed = 10; //添加新的屬性值:速度
document.onkeydown = function(event){
    if(event.keyCode == 37) {
        player.x -= player.speed;
    } // <-鍵
    if(event.keyCode == 39) {
        player.x += player.speed;
    } // ->鍵
}

遊戲主迴圈-玩家操作

document.onkeydown = function(event){
    if(event.keyCode == 37) {
        if(player.x > 0) 
            player.x -= player.speed;
    }
    if(event.keyCode == 39) {
        if(player.x+player.width < canvas.width)  
            player.x += player.speed;
    }
}

限制玩家移動範圍

遊戲主迴圈-玩家操作

把這幾行:

player.img.onload = function() {
    ctx.drawImage(player.img, player.x, player.y,
              player.width, player.height);
}

改寫成:

function draw(ship) {
    ctx.drawImage(ship.img, ship.x, ship.y,
                  ship.width, ship.height);
}

遊戲主迴圈-玩家操作

function render() {
    update(); 
    ctx.clearRect(0, 0, canvas.width, canvas.height);
    drawStars();
    draw(player); //把太空船畫在最上層
}

\現在可以操縱太空船了/

遊戲主迴圈-操作、更新、繪製的程式碼

document.onkeydown = function(event){
    if(event.keyCode == 37) {
        if(player.x > 0) 
            player.x -= player.speed;
    }
    if(event.keyCode == 39) {
        if(player.x+player.width < canvas.width)  
            player.x += player.speed;
    }
}
function update(){
    for(var i = 0; i < star_count; i++) {
        stars[i].y += 5;
        if(stars[i].y > canvas.height){
            stars[i].y -= canvas.height;
        }
    }
}
function render() {
    update(); 
    ctx.clearRect(0, 0, canvas.width, canvas.height);
    drawStars();
    draw(player);
}
setInterval(render, 30);

進入小行星帶

召喚敵軍

var enemy_count = 5; //5個敵人
var enemy = []; //很多個敵人放在一個陣列
for(var i = 0; i < enemy_count; i++){
        enemy[i] = new Ship();
        //每個敵人都是一艘船(?)
}

有敵人才不會孤單

召喚敵軍

for(var i = 0; i < enemy_count; i++) {
    enemy[i] = new Ship();
    enemy[i].img.src = "asteroid.png";
    randEnemy(enemy[i]);
}

function randEnemy(obj) {
        obj.width = Math.random()*30+20;
        obj.height = obj.width;
        obj.x = Math.random()*canvas.width;
        obj.y = -canvas.height/2;
        obj.speed = (Math.random()+1)*3;
}

召喚敵軍-在Update()函式中加入

for(var i = 0; i < enemy_count; i++) {
    enemy[i].y += enemy[i].speed;
    //敵人每次都會往下移動speed的量
}

召喚敵軍-在Update()函式中加入

for(var i = 0; i < enemy_count; i++) {
    enemy[i].y += enemy[i].speed;
    //敵人每次都會往下移動speed的量
    if(enemy[i].y > canvas.height) {
        randEnemy(enemy[i]); 
        //隨機重置超出畫面的敵人
    }
}

召喚敵軍-在Render()函式中加入

for(var i = 0; i < enemy_count; i++) {
    draw(enemy[i]);
    //用draw函式繪製敵人
}

碰撞!

血量與分數

player.score = 0;  //玩家的分數:0
player.HP = 100;   //玩家的血量:100

注意要放在var player(宣告過)之後

血量與分數

function drawText() {
    ctx.fillStyle = "white"; //字體顏色
    ctx.font = "26px Arial"; //字體大小和字形
    ctx.fillText("HP:" + player.HP, 450, 40);
    //在(450,40)的位置印出生命值
    ctx.fillText("SCORE:" + player.score, 300, 40);
    //在(300,40)的位置印出分數
    ctx.fill();
}

血量與分數-在Render()函式中加入

function render() {
    //略...
    drawText(); //畫在最上層,也就是最後才畫
}

碰撞偵測函式

function collisionDetect(obj1,obj2) {
    var dx = Math.abs(obj1.x - obj2.x);
    var dy = Math.abs(obj1.y - obj2.y);
    // Math.abs(): 取絕對值
}

碰撞偵測函式

function collisionDetect(obj1,obj2) {
    var dx = Math.abs(obj1.x - obj2.x);
    var dy = Math.abs(obj1.y - obj2.y);
    if(dx < player.width/2 && dy < obj2.height) {
        if(obj1 == player && player.HP > 0) {
            player.HP -= 10; //玩家被撞少10滴血
            randEnemy(obj2); //撞到玩家的obj2重置
        }
    }
}

碰撞偵測-在Update()函式中加入

function update(){
    //更新星星狀態的迴圈
    //略...
    
    //更新敵人狀態的迴圈
    for(var i = 0; i < enemy_count; i++) {
        collisionDetect(player,enemy[i]);
        //執行碰撞測試!
        enemy[i].y += enemy[i].speed;
        if(enemy[i].y > canvas.height) {
            randEnemy(enemy[i]); 
        }
    }
}

記分板-以時間計分

function addScore() {
    if(player.HP > 0) player.score ++;
        //在玩家血量歸零之前,每過一秒加一分
}
setInterval(addScore, 1000);

Game Over的情況...

function drawText() {
    if(player.HP > 0) {
            //印出血量與分數
    }else {
            //印出gameover與計分
        ctx.fillStyle = "red";
        ctx.font = "70px Arial";
        ctx.fillText("Game Over", 120, 240);
        ctx.font = "30px Arial";
        ctx.fillText("Your Score: " + player.score, 
                      220, 288);
        ctx.fill();
    }
}

Game Over-更改Render函式

if(player.HP > 0) draw(player);
//玩家生命值大於零,才畫出玩家

進入可遊玩測試的階段了

目前為止的JS程式碼

歡迎各位加入我們!

FB粉絲頁 FB社團 課程網頁
https://www.facebook.com/NTNUCIC/ https://www.facebook.com/groups/ntnucic/ http://www.ntnucic.club/104/
Selector( '[data-markdown]' ); } }, { src: 'plugin/highlight/highlight.js', async: true, condition: function() { return !!document.querySelector( 'pre code' ); }, callback: function() { hljs.initHighlightingOnLoad(); } }, { src: 'plugin/zoom-js/zoom.js', async: true }, { src: 'plugin/notes/notes.js', async: true } ] });