iT邦幫忙

2021 iThome 鐵人賽

DAY 24
0
Modern Web

這個網站也太嗨!30 個網頁動態提案系列 第 26

#23-用Canvas做Google恐龍遊戲(都市老妹生存記!能擊退經痛加班和渣男嗎?)

大家都知道Google斷線時會有小恐龍的離線遊戲,
以前第一次看到的時候超驚喜!(我到現在斷線的時候還是會玩一下XD)
品牌如果發揮一點創意,將品牌元素加進小遊戲裡面,
奔跑吧台北!就是完全以遊戲的方式,
將政見放在遊戲裡面~
或是將遊戲放在404頁面裡面,也許是個不錯的主意!?

今天就來用昨天Canvas做的小人物和背景,
結合昨天做的Spritesheet動畫傳送門
做個Google小恐龍遊戲的都市老妹生存記版本!
老樣子先看成品~

按住S或Shift鍵低頭,按住W或空白鍵往上跳
小心不要被經痛還是加班還是渣男給扣分了XD
在這邊玩

今天的內容主要是參照:這個影片教學 所做~
但加上了sprite動畫的code & 做了一些code的整理&和命名,往下看一起拆解吧!
(今天的篇幅史上無敵長XD)

主要分成

1.主角&敵人&分數的Class
2.遊戲功能的撰寫


1. 主角和敵人的Class 繪製

因為敵人和主角和分數都要重複繪製,所以做成class。
首先是遊戲主角的Class (放在constructor.js 裡面)

class Player {
  constructor (x, y, w, h) {
  //xy座標,寬高,
    this.x = x;
    this.y = y;
    this.w = w;
    this.h = h;
    
    this.dy = 0;
    this.originalHeight = h;
    this.grounded = false; //看是不是踏在地上
    //設定一個跳的動力!
    this.jumpForce = 15;
    this.jumpTimer = 0;

    //draw animate 昨天spritesheet動畫的部分~
    this.cols = 5;
    this.rows = 1;
    this.spriteSheet = new Image();
    this.spriteSheet.src="./test.png";  
    this.spriteWidth = 229.4; 
    this.spriteHeight = 557;
    this.srcX =0;
    this.srcY =0;

    //控制主角的動畫速度
    this.totalFrames = 5;
    this.currentFrame = 0;
    this.frameDrawn =0;
  }
  
  Animate () {
    // 跳
    if (keys['Space'] || keys['KeyW']) {
      this.Jump();
    } else {
      this.jumpTimer = 0;
    }

    // 蹲下
    if (keys['ShiftLeft'] || keys['KeyS']) {
      this.h = this.originalHeight / 3 * 2; //讓畫面變矮
    } else {
      this.h = this.originalHeight;
    }
    
    this.y += this.dy; //跳起來會變高,y會變小,所以dy是負的

    // 判斷他是不是在空中,
    //踩在地上的時候應該是this.y + this.h <= canvas.height
    if (this.y + this.h < canvas.height) {
      this.dy += gravity; //全域宣告
      this.grounded = false;
    } else {
      this.dy = 0;
      this.grounded = true;
      this.y = canvas.height - this.h;
    }
    
    //spritesheet畫畫的部分,不清楚的可以看昨天的文章![傳送門](https://ithelp.ithome.com.tw/articles/10279291)
    this.currentFrame = this.currentFrame % this.totalFrames;
    this.srcX = this.currentFrame * this.spriteWidth;

    this.frameDrawn++;
    if(this.frameDrawn>=10){
      this.currentFrame++;
      this.frameDrawn=0;
    }

    this.Draw();
  }

  Jump () {
    //在地上的時候
    if (this.grounded && this.jumpTimer == 0) {
      this.jumpTimer = 1;
      this.dy = -this.jumpForce; 
    } else if (this.jumpTimer > 0 && this.jumpTimer < 15) {
      //按壓越久,jumptimer越大, y的位置越高
      this.jumpTimer++;
      this.dy = - this.jumpForce - (this.jumpTimer / 50);
      //jumptimer除以的係數越小,可以跳越高
    }
  }
  
  Draw () {
    ctx.drawImage(this.spriteSheet, this.srcX, this.srcY,this.spriteWidth, this.spriteHeight, this.x, this.y,this.w, this.h);    
  }
}

再來是敵人的class

class Obstacle {
  constructor (x, y, w, h, t) {
    this.x = x;
    this.y = y;
    this.w = w;
    this.h = h;
    this.t = t; //type = 文字和顏色
  }
  
  Update () {
  //敵人的位置, gamespeed會在start時候宣告
  //其實老妹沒動,是敵人一直往左邊滑
    this.x -= gameSpeed; 
    this.Draw();
  }

  Draw () {
    ctx.beginPath();
    ctx.fillStyle = this.t.color;
    ctx.fillRect(this.x, this.y, this.w, this.h);
    ctx.fillStyle = '#ffffff';
    ctx.fillText(this.t.text, this.x + 40, this.y+ 20); //寫上渣男、加班、經痛的文字哈
    ctx.closePath();
  }
}

寫分數

class Text {
  constructor (t, x, y, a, c, s){
      this.t = t;
      this.x = x;
      this.y = y;
      this.a = a;
      this.c = c;
      this.s = s;    
    }

  Draw () {
      ctx.beginPath();
      ctx.fillStyle = this.c;
      ctx.font = this.s + "px sans-serif";
      ctx.textAlign = this.a;
      ctx.fillText(this.t, this.x, this.y);
      ctx.closePath();
  }
}

遊戲的功能

這邊功能主要就是

A.createCanvas() 呼叫—> B.Update () 然後會不停呼叫自己,重新繪製Canvas

在createCanvas裡面,就會利用主角和文字的Class去做內容。
Update() 裡面重要的功能:

1.createObstacle() —>製作障礙物
2.spawnObstacles:撒障礙物—>設置障礙物的x座標,會不斷更新=加快速度移動感覺
3.重置遊戲:當主角撞到障礙物就會重置速度分數等
4.setScore(); —>隨著主角越過越多障礙物,分數要一直更新
5.player.Animate(); —> 讓player動起來
6.requestAnimationFrame(Update);—>不斷呼叫自己,重複上述動作

大致上說明如上,詳細code:

const canvas = document.getElementById('game');
const ctx = canvas.getContext('2d'); 

// Variables
let score;
let scoreText;
let highscore;
let highscoreText;
let player;
let gravity;
let obstacles = [];
let gameSpeed;
let keys = {};
let obstacleType = [{text: '加班', color: '#4530ff'},{text: '渣男', color: '#009c27'},{text: '經痛', color: '#ff3e30'}];
let initialSpawnTimer = 200; //數字越小越快歸零就會越多敵人
let spawnTimer = initialSpawnTimer;

// Event Listeners
document.addEventListener('keydown', function (evt) {
  keys[evt.code] = true;
});
document.addEventListener('keyup', function (evt) {
  keys[evt.code] = false;
});


//重要Function
//A.遊戲最開始的繪製
function createCanvas () {
  canvas.width = 648;
  canvas.height = 572;
  ctx.font = "20px sans-serif";

  gameSpeed = 3;
  gravity = 1;  
  
  //做玩家
  player = new Player(25, 0, 80, 180);
  
  getScore();
  scoreText = new Text("Score: " + score, 25, 25, "left", "#ffffff", "20");
  highscoreText = new Text("Highscore: " + highscore, canvas.width - 25, 25, "right", "#ffffff", "20");

  //呼叫Update
  requestAnimationFrame(Update);
}

//A. 相關:取得以前的分數
function getScore(){
  score = 0;
  highscore = 0;

//看local storage有沒有之前玩的分數~
  if (localStorage.getItem('highscore')){
    highscore = localStorage.getItem('highscore');
  }
}

//重要Function
//B. 遊戲進行時Update會一直呼叫自己

function Update () {
  ctx.clearRect(0, 0, canvas.width, canvas.height);

  spawnTimer--;

  if (spawnTimer <= 0) {
    createObstacle(); //1.製作敵人
    spawnTimer = initialSpawnTimer - gameSpeed * 8; //數字越來越小=製作敵人速度越來越快
    if (spawnTimer < 60) {
      spawnTimer = 60; //但最快還是有60
    }
  }
  
  //2.Spawn enimies,撒敵人更新位置,並且看是不是撞到了
  spawnObstacles();
  
  //4.設置分數
  setScore();
  //5.讓主角動起來!
  player.Animate();

  
  gameSpeed += 0.003;
  requestAnimationFrame(Update); //6.重複呼叫自己
}


//1.做障礙物
function createObstacle () {
  //一次只做一個
  let size = RandomIntInRange(40, 70);
  let typeNum = RandomIntInRange(0, 2);
  let type = RandomIntInRange(0, 1);
  let obstacle = new Obstacle(canvas.width + size, canvas.height - size, size, size, obstacleType[typeNum]);

  //做高高的障礙物
  if (type == 1) {
    obstacle.y -= player.originalHeight - 10;
  }
  obstacles.push(obstacle);
}

//1.相關  做障礙物使用的函數
function RandomIntInRange (min, max) {
  return Math.round(Math.random() * (max - min) + min);
}

//2.撒敵人
function spawnObstacles(){
  for (let i = 0; i < obstacles.length; i++) {
    let o = obstacles[i];

    //移除滑出視窗的
    if (o.x + o.w < 0) {
      obstacles.splice(i, 1); 
    }

    //3.撞到障礙物的話,重置遊戲
    if (
      player.x < o.x + o.w &&
      player.x + player.w > o.x &&
      player.y < o.y + o.h &&
      player.y + player.h > o.y
    ) {
      obstacles = [];
      score = 0;
      spawnTimer = initialSpawnTimer;
      gameSpeed = 3;
      window.localStorage.setItem('highscore', highscore);
    }

    o.Update(); //放置敵人位置=敵人移動的速度
  }
}

4. 設定分數
function setScore(){
  score++;
  scoreText.t = "Score:" + score;
  scoreText.Draw();

  if (score > highscore) {
    highscore = score;
    highscoreText.t = "Highscore: " + highscore;
  }
  highscoreText.Draw();
}

//最開始呼叫
createCanvas();

以上!

今天的成果在這邊玩
原始碼放這裡:Class建構式這裡:遊戲的功能
(但上面的有整理,原始碼沒整理QQ)

這是我第一次寫遊戲!!
JS底還不夠扎實,也是第一次寫ES6以後才有的class功能。XD
這次鐵人賽參考很多人的寫法,也給自己不同的程式碼思維。

一樣有任何批評指教,請留言!!
祝福大家都能戰勝經痛/頭痛/各種痛 & 加班 & 和渣男/女
/images/emoticon/emoticon25.gif


上一篇
#22-掰惹Gif!用Sprite雪碧圖做動畫! (CSS & Canvas)
下一篇
#24-這個播放器也太潮!用Canvas放音樂!w/JS web audio API
系列文
這個網站也太嗨!30 個網頁動態提案33
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

1 則留言

0
金金
iT邦新手 1 級 ‧ 2021-10-11 22:39:33

這個好有趣!我之後也來玩玩看~

我要留言

立即登入留言