Web-Dev-For-Beginners/blob/main/

2021-09-21  本文已影响0人  Mcq

microsoft/Web-Dev-For-Beginners: 24 Lessons, 12 Weeks, Get Started as a Web Developer (github.com)

微软开源的游戏入门教程,一个简易版的星际大战,学习其开发思想,如何拆分,如何组装

创建对象,什么对象,对象有那些属性,那些方法,如何绘制

一. Learn about Inheritance using both Classes and Composition and the Pub/Sub pattern, in preparation for building a game

思考一个游戏需要具备什么?会发现有一些游戏对象的一些共同属性

有坐标,用来表示绘制到屏幕的位置
能移动,
自毁,自我检测边界情况,判断是否dead,进而判断是否执行destroyed自毁
cool-down,冷却

表达行为
  1. 类的方式

//set up the class GameObject
class GameObject {
  constructor(x, y, type) {
    this.x = x;
    this.y = y;
    this.type = type;
  }
}

//this class will extend the GameObject's inherent class properties
class Movable extends GameObject {
  constructor(x,y, type) {
    super(x,y, type)
  }

//this movable object can be moved on the screen
  moveTo(x, y) {
    this.x = x;
    this.y = y;
  }
}

//this is a specific class that extends the Movable class, so it can take advantage of all the properties that it inherits
class Hero extends Movable {
  constructor(x,y) {
    super(x,y, 'Hero')
  }
}

//this class, on the other hand, only inherits the GameObject properties
class Tree extends GameObject {
  constructor(x,y) {
    super(x,y, 'Tree')
  }
}

//a hero can move...
const hero = new Hero();
hero.moveTo(5,5);

//but a tree cannot
const tree = new Tree();

  1. 组合
//create a constant gameObject
const gameObject = {
  x: 0,
  y: 0,
  type: ''
};

//...and a constant movable
const movable = {
  moveTo(x, y) {
    this.x = x;
    this.y = y;
  }
}
//then the constant movableObject is composed of the gameObject and movable constants
const movableObject = {...gameObject, ...movable};

//then create a function to create a new Hero who inherits the movableObject properties
function createHero(x, y) {
  return {
    ...movableObject,
    x,
    y,
    type: 'Hero'
  }
}
//...and a static object that inherits only the gameObject properties
function createStatic(x, y, type) {
  return {
    ...gameObject
    x,
    y,
    type
  }
}
//create the hero and move it
const hero = createHero(10,10);
hero.moveTo(5,5);
//and create a static tree which only stands around
const tree = createStatic(0,0, 'Tree'); 

发布订阅模式 Pub/sub pattern

这种模式的思想是,应用各部分相互隔离,类似请阅公众号的每个用户不用知道其他用户一样。
消息,
发布者,
订阅者,

//set up an EventEmitter class that contains listeners
class EventEmitter {
  constructor() {
    this.listeners = {};
  }
//when a message is received, let the listener to handle its payload
  on(message, listener) {
    if (!this.listeners[message]) {
      this.listeners[message] = [];
    }
    this.listeners[message].push(listener);
  }
//when a message is sent, send it to a listener with some payload
  emit(message, payload = null) {
    if (this.listeners[message]) {
      this.listeners[message].forEach(l => l(message, payload))
    }
  }
}

实现

//set up a message structure
const Messages = {
  HERO_MOVE_LEFT: 'HERO_MOVE_LEFT'
};
//invoke the eventEmitter you set up above
const eventEmitter = new EventEmitter();
//set up a hero
const hero = createHero(0,0);
//let the eventEmitter know to watch for messages pertaining to the hero moving left, and act on it
eventEmitter.on(Messages.HERO_MOVE_LEFT, () => {
  hero.move(5,0);
});

//set up the window to listen for the keyup event, specifically if the left arrow is hit, emit a message to move the hero left
window.addEventListener('keyup', (evt) => {
  if (evt.key === 'ArrowLeft') {
    eventEmitter.emit(Messages.HERO_MOVE_LEFT)
  }
});

2. Draw Hero and Monsters to Canvas

绘制canvas画布

<canvas id="myCanvas" width="200" height="100"></canvas>

在canvas绘制的步骤:

  1. 获取canvas元素引用
  2. 获取canvas的Context引用
  3. 执行绘制操作
// draws a red rectangle
//1. get the canvas reference
canvas = document.getElementById("myCanvas");

//2. set the context to 2D to draw basic shapes
ctx = canvas.getContext("2d");

//3. fill it with the color red
ctx.fillStyle = 'red';

//4. and draw a rectangle with these parameters, setting location and size
ctx.fillRect(0,0, 200, 200) // x,y,width, height

加载绘制图片

function loadAsset(path) {
  return new Promise((resolve) => {
    const img = new Image();
    img.src = path;
    img.onload = () => {
      // image loaded and ready to be used
      resolve(img);
    }
  })
}

// use like so


// To draw game assets to a screen
async function run() {
  const heroImg = await loadAsset('hero.png')
  const monsterImg = await loadAsset('monster.png')

  canvas = document.getElementById("myCanvas");
  ctx = canvas.getContext("2d");
  ctx.drawImage(heroImg, canvas.width/2,canvas.height/2);
  ctx.drawImage(monsterImg, 0,0);
}

接下来就是,

  1. 绘制canvas背景,
  2. 加载贴图
  3. 绘制hero
  4. 绘制 5* 5的monsters

定义一些常量

const MONSTER_TOTAL = 5;
const MONSTER_WIDTH = MONSTER_TOTAL * 98;
const START_X = (canvas.width - MONSTER_WIDTH) / 2;
const STOP_X = START_X + MONSTER_WIDTH;
 
// loop draw monsters
for (let x = START_X; x < STOP_X; x += 98) {
    for (let y = 0; y < 50 * 5; y += 50) {
      ctx.drawImage(enemyImg, x, y);
    }
  }

三 添加动作

键盘/ 鼠标,
游戏感应

屏幕上移动元素

1,改变坐标,2,清空屏幕,3,重新绘制

//set the hero's location
hero.x += 5;
// clear the rectangle that hosts the hero
ctx.clearRect(0, 0, canvas.width, canvas.height);
// redraw the game background and hero
ctx.fillRect(0, 0, canvas.width, canvas.height)
ctx.fillStyle = "black";
ctx.drawImage(heroImg, hero.x, hero.y);

处理键盘事件

window.addEventListener('keyup', (evt) => {
  // `evt.key` = string representation of the key
  if (evt.key === 'ArrowUp') {
    // do something
  }
})
// 有一些特殊的按键回影响window,比如方向键会移动屏幕等,需要取消它们的默认行为
let onKeyDown = function (e) {
  console.log(e.keyCode);
  switch (e.keyCode) {
    case 37:
    case 39:
    case 38:
    case 40: // Arrow keys
    case 32:
      e.preventDefault();
      break; // Space
    default:
      break; // do not block other keys
  }
};

window.addEventListener('keydown', onKeyDown);

游戏诱导动作
比如每次tick更新游戏对象位置等,通过setInterval,或 setTimeout

let id = setInterval(() => {
  //move the enemy on the y axis
  enemy.y += 10;
})


The game loop
游戏循环,是指一定的间隔,绘制游戏

let gameLoopId = setInterval(() =>
  function gameLoop() {
    ctx.clearRect(0, 0, canvas.width, canvas.height);
    ctx.fillStyle = "black";
    ctx.fillRect(0, 0, canvas.width, canvas.height);
    drawHero();
    drawEnemies();
    drawStaticObjects();
}, 200);

Add Code
添加对象

    
class GameObject {
  constructor(x, y) {
    this.x = x;
    this.y = y;
    this.dead = false;
    this.type = "";
    this.width = 0;
    this.height = 0;
    this.img = undefined;
  }

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

class Hero extends GameObject {
  constructor(x, y) {
    ...it needs an x, y, type, and speed
  }
}

class Enemy extends GameObject {
  constructor(x, y) {
    super(x, y);
    (this.width = 98), (this.height = 50);
    this.type = "Enemy";
    let id = setInterval(() => {
      if (this.y < canvas.height - this.height) {
        this.y += 5;
      } else {
        console.log('Stopped at', this.y)
        clearInterval(id);
      }
    }, 300)
  }
}

添加键盘事件

 let onKeyDown = function (e) {
       console.log(e.keyCode);
         ...add the code from the lesson above to stop default behavior
       }
 };

 window.addEventListener("keydown", onKeyDown);

实现发布订阅


 window.addEventListener("keyup", (evt) => {
   if (evt.key === "ArrowUp") {
     eventEmitter.emit(Messages.KEY_EVENT_UP);
   } else if (evt.key === "ArrowDown") {
     eventEmitter.emit(Messages.KEY_EVENT_DOWN);
   } else if (evt.key === "ArrowLeft") {
     eventEmitter.emit(Messages.KEY_EVENT_LEFT);
   } else if (evt.key === "ArrowRight") {
     eventEmitter.emit(Messages.KEY_EVENT_RIGHT);
   }
 });

添加常量

const Messages = {
  KEY_EVENT_UP: "KEY_EVENT_UP",
  KEY_EVENT_DOWN: "KEY_EVENT_DOWN",
  KEY_EVENT_LEFT: "KEY_EVENT_LEFT",
  KEY_EVENT_RIGHT: "KEY_EVENT_RIGHT",
};

let heroImg, 
    enemyImg, 
    laserImg,
    canvas, ctx, 
    gameObjects = [], 
    hero, 
    eventEmitter = new EventEmitter();

初始化游戏

function initGame() {
  gameObjects = [];
  createEnemies();
  createHero();

  eventEmitter.on(Messages.KEY_EVENT_UP, () => {
    hero.y -=5 ;
  })

  eventEmitter.on(Messages.KEY_EVENT_DOWN, () => {
    hero.y += 5;
  });

  eventEmitter.on(Messages.KEY_EVENT_LEFT, () => {
    hero.x -= 5;
  });

  eventEmitter.on(Messages.KEY_EVENT_RIGHT, () => {
    hero.x += 5;
  });
}

设置游戏循环


window.onload = async () => {
  canvas = document.getElementById("canvas");
  ctx = canvas.getContext("2d");
  heroImg = await loadTexture("assets/player.png");
  enemyImg = await loadTexture("assets/enemyShip.png");
  laserImg = await loadTexture("assets/laserRed.png");

  initGame();
  let gameLoopId = setInterval(() => {
    ctx.clearRect(0, 0, canvas.width, canvas.height);
    ctx.fillStyle = "black";
    ctx.fillRect(0, 0, canvas.width, canvas.height);
    drawGameObjects(ctx);
  }, 100)
  
};

创建enemies


function createEnemies() {
  const MONSTER_TOTAL = 5;
  const MONSTER_WIDTH = MONSTER_TOTAL * 98;
  const START_X = (canvas.width - MONSTER_WIDTH) / 2;
  const STOP_X = START_X + MONSTER_WIDTH;

  for (let x = START_X; x < STOP_X; x += 98) {
    for (let y = 0; y < 50 * 5; y += 50) {
      const enemy = new Enemy(x, y);
      enemy.img = enemyImg;
      gameObjects.push(enemy);
    }
  }
}

创建hero

function createHero() {
  hero = new Hero(
    canvas.width / 2 - 45,
    canvas.height - canvas.height / 4
  );
  hero.img = heroImg;
  gameObjects.push(hero);
}

最后开始绘制

function drawGameObjects(ctx) {
 gameObjects.forEach(go => go.draw(ctx));
}

四、 碰撞检测

本节重点

发射子弹
碰撞检测

子弹击中enemy
子弹到达屏幕顶部
enemy到达屏幕底部
enemy击中hero

  1. 如何检测碰撞?
    其实是判断,两个对象有没有交叉,每个对象都有坐标(x,y),和width, height

获取对象的角坐标

rectFromGameObject() {
  return {
    top: this.y,
    left: this.x,
    bottom: this.y + this.height,
    right: this.x + this.width
  }
}

比较函数

// 这里的判断用排除法,相比判断交叉更简单
// 这也是一种思路,反向判断,排除法
function intersectRect(r1, r2) {
  return !(r2.left > r1.right ||
    r2.right < r1.left ||
    r2.top > r1.bottom ||
    r2.bottom < r1.top);
}
  1. 如何destroy
    只有下次不绘制就可以了。
// collision happened
enemy.dead = true
// filter the not dead object
gameObjects = gameObject.filter(go => !go.dead);

  1. 如何发射子弹
    创建子弹对象
    绑定键盘事件
    创建子弹的游戏对象

  2. 子弹冷却
    防止发射太多子弹

class Cooldown {
constructor(time) {
  this.cool = false;
  setTimeout(() => {
    this.cool = true;
  }, time)
}
}

class Weapon {
 constructor {
 }
fire() {
  if (!this.cooldown || this.cooldown.cool) {
    // produce a laser
     this.cooldown = new Cooldown(500);
   } else {
     // do nothing - it hasn't cooled down yet.
   }
 }
}

Add Code

// 表示游戏对象的矩形区域
rectFromGameObject() {
    return {
      top: this.y,
      left: this.x,
      bottom: this.y + this.height,
      right: this.x + this.width,
    };
  }
// 碰撞检测
function intersectRect(r1, r2) {
  return !(
    r2.left > r1.right ||
    r2.right < r1.left ||
    r2.top > r1.bottom ||
    r2.bottom < r1.top
  );
}
 // 添加常量信息
 KEY_EVENT_SPACE: "KEY_EVENT_SPACE",
 COLLISION_ENEMY_LASER: "COLLISION_ENEMY_LASER",
 COLLISION_ENEMY_HERO: "COLLISION_ENEMY_HERO",

// 处理空格
  } else if(evt.keyCode === 32) {
    eventEmitter.emit(Messages.KEY_EVENT_SPACE);
  }

// 添加监听
 eventEmitter.on(Messages.KEY_EVENT_SPACE, () => {
 if (hero.canFire()) {
   hero.fire();
 }
eventEmitter.on(Messages.COLLISION_ENEMY_LASER, (_, { first, second }) => {
  first.dead = true;
  second.dead = true;
})

使子弹逐渐向上移动

  class Laser extends GameObject {
  constructor(x, y) {
    super(x,y);
    (this.width = 9), (this.height = 33);
    this.type = 'Laser';
    this.img = laserImg;
    let id = setInterval(() => {
      if (this.y > 0) {
        this.y -= 15;
      } else {
        this.dead = true;
        clearInterval(id);
      }
    }, 100)
  }
}

处理碰撞


function updateGameObjects() {
  const enemies = gameObjects.filter(go => go.type === 'Enemy');
  const lasers = gameObjects.filter((go) => go.type === "Laser");
// laser hit something
  lasers.forEach((l) => {
    enemies.forEach((m) => {
      if (intersectRect(l.rectFromGameObject(), m.rectFromGameObject())) {
      eventEmitter.emit(Messages.COLLISION_ENEMY_LASER, {
        first: l,
        second: m,
      });
    }
   });
});

  gameObjects = gameObjects.filter(go => !go.dead);
}  

实现冷却


class Hero extends GameObject {
 constructor(x, y) {
   super(x, y);
   (this.width = 99), (this.height = 75);
   this.type = "Hero";
   this.speed = { x: 0, y: 0 };
   this.cooldown = 0;
 }
 fire() {
   gameObjects.push(new Laser(this.x + 45, this.y - 10));
   this.cooldown = 500;

   let id = setInterval(() => {
     if (this.cooldown > 0) {
       this.cooldown -= 100;
     } else {
       clearInterval(id);
     }
   }, 200);
 }
 canFire() {
   return this.cooldown === 0;
 }
}

五、 计分和计命

ctx.font = "30px Arial";
ctx.fillStyle = "red";
ctx.textAlign = "right";
ctx.fillText("show this on the screen", 0, 0);

处理enemy, hero 碰撞

enemies.forEach(enemy => {
    const heroRect = hero.rectFromGameObject();
    if (intersectRect(heroRect, enemy.rectFromGameObject())) {
      eventEmitter.emit(Messages.COLLISION_ENEMY_HERO, { enemy });
    }
  })

在hero中添加

this.life = 3;
this.points = 0;

绘制得分

function drawLife() {
  // TODO, 35, 27
  const START_POS = canvas.width - 180;
  for(let i=0; i < hero.life; i++ ) {
    ctx.drawImage(
      lifeImg, 
      START_POS + (45 * (i+1) ), 
      canvas.height - 37);
  }
}

function drawPoints() {
  ctx.font = "30px Arial";
  ctx.fillStyle = "red";
  ctx.textAlign = "left";
  drawText("Points: " + hero.points, 10, canvas.height-20);
}

function drawText(message, x, y) {
  ctx.fillText(message, x, y);
}

将下列方法添加进游戏循环

drawPoints();
drawLife();

每次hero和enemy碰撞,减去生命值1,击中enemy加100

decrementLife() {
  this.life--;
  if (this.life === 0) {
    this.dead = true;
  }
}
  incrementPoints() {
    this.points += 100;
  }

添加事件订阅

eventEmitter.on(Messages.COLLISION_ENEMY_LASER, (_, { first, second }) => {
   first.dead = true;
   second.dead = true;
   hero.incrementPoints();
})

eventEmitter.on(Messages.COLLISION_ENEMY_HERO, (_, { enemy }) => {
   enemy.dead = true;
   hero.decrementLife();
});
上一篇下一篇

猜你喜欢

热点阅读