美文网首页
Web-Dev-For-Beginners/blob/main/

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

作者: Mcq | 来源:发表于2021-09-21 20:21 被阅读0次

    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();
    });
    

    相关文章

      网友评论

          本文标题:Web-Dev-For-Beginners/blob/main/

          本文链接:https://www.haomeiwen.com/subject/iwybgltx.html