【问题标题】:Move player on big canvas javascript game在大画布 JavaScript 游戏上移动播放器
【发布时间】:2020-06-06 00:28:39
【问题描述】:

我写了这段代码。这是一个简单的游戏引擎。 仍然缺少一些东西(碰撞、声音、文字描述),但这只是开始:-) 玩家可以使用箭头来控制。 但我有两个问题。也许有人会告诉我怎么做。

  1. 移动播放器 现在,当玩家移动时,屏幕会以不同的速度移动。 其效果是,播放器在短时间内被屏幕边缘遮挡。

我怀疑问题出在此处: ctx.translate(player.x, player.y); 但我不知道我在这段代码中写错了什么。

  1. 相机视图 默认情况下,画布将非常大,最大为 10,000 x 10,000 点。数百条路径和房间(绿色和沙色矩形)将被绘制在画布上。 我想现在整个画布都被渲染了,即使是屏幕外的部分。 这可能是对计算机资源的相当大的浪费。 但我不知道该怎么做。

当然,如果这里有什么需要改进的地方,我会很感激地接受任何建议。

window.addEventListener('load', function(event) {
  initCanvas();
});



let ctx;
let cW = 3000; // canvas width
let cH = 3000; // canvas height
let playerImgTop;
let playerImgBottom;
let playerImgLeft;
let playerImgRight;
let playerSpeed = 20;
let playerDir = 0;



function initCanvas() {
  ctx = document.getElementById('mycanvas').getContext('2d');
  ctx.canvas.width = cW;
  ctx.canvas.height = cH;
  let animateInterval = setInterval(render, 1000/30);

  playerImgTop = new Image();
  playerImgTop.src = "http://www.itbvega.pl/io/img/player-top.png";
  playerImgBottom = new Image();
  playerImgBottom.src = "http://www.itbvega.pl/io/img/player-bottom.png";
  playerImgLeft = new Image();
  playerImgLeft.src = "http://www.itbvega.pl/io/img/player-left.png";
  playerImgRight = new Image();
  playerImgRight.src = "http://www.itbvega.pl/io/img/player-right.png";

  let gameLocations = [
      {"id": "room0", "x": 180, "y": 180, "rw": 60, "rh": 60, "type": "room"},
      {"id": "room1", "x": 160, "y": 380, "rw": 100, "rh": 100, "type": "room"},
      {"id": "path0", "x": 200, "y": 240, "rw": 20, "rh": 140, "type": "path"}
  ];

  function renderGameLocations() {
    for (let i = 0; i < gameLocations.length; i++) {
      let loc = gameLocations[i];
      if (loc.type === "path") {
        ctx.fillStyle = "#62d299";
      } else if (loc.type === "room") {
        ctx.fillStyle = "#e4b65e";
      }
      ctx.fillRect(loc.x, loc.y, loc.rw, loc.rh);

    }
  }


  function render() {
    ctx.save();
    ctx.clearRect(0,0, cW, cH);

    renderGameLocations();

    player.render();

    ctx.translate(player.x, player.y);

    ctx.restore();
  }
}



function Player() {
  this.x = 200;
  this.y = 200;
  this.render = function() {
    if (playerDir === 0) {
      ctx.drawImage(playerImgTop, this.x, this.y);
    } else if (playerDir === 1) {
      ctx.drawImage(playerImgRight, this.x, this.y);
    } else if (playerDir === 2) {
      ctx.drawImage(playerImgBottom, this.x, this.y);
    } else if (playerDir === 3) {
      ctx.drawImage(playerImgLeft, this.x, this.y);
    }
  }
}

let player = new Player();


document.addEventListener('keydown', function(event) {
  let key_press = event.keyCode;
   //alert(event.keyCode + " | " + key_press);
  if (key_press === 38 ) { // top
    player.y -= playerSpeed;
    playerDir = 0;
  } else if (key_press === 40) { // bottom
    player.y += playerSpeed;
    playerDir = 2;
  } else if (key_press === 37) { // left
    player.x -= playerSpeed;
    playerDir = 3;
  } else if (key_press === 39) { // right
    player.x += playerSpeed;
    playerDir = 1;
  }
});
&lt;canvas id="mycanvas"&gt;&lt;/canvas&gt;

【问题讨论】:

    标签: javascript canvas html5-canvas game-engine


    【解决方案1】:

    您的问题涉及的内容太多,无法详细回答。我将其分为两个主题,运动场和视图以及四边形树,它们将简要介绍您的两个问题。

    我添加了一个示例,该示例在最基本的级别上实现了这两个主题。使用示例来探索更精细的细节。如果您有任何问题,请在 cmets 中提问或作为新的 SO 问题提问。

    运动场和景观

    游戏场是整个地图。 (参见示例playfield) 它不包含像素,它只包含要显示和交互的项目。 (见例子playfield.quadMapplayfield.visibleItemsmapItems

    视图就是画布。

    它不大于显示器,只包含像素。

    视图独立于运动场,可以定位、旋转和缩放。该示例仅定位视图。

    视图是相对于运动场中感兴趣的事物设置的。以播放器为例。 (参见示例playfield.setView

    您通过获取玩家位置并减去视图宽度和高度的一半来设置视图 (topleft)。

    为确保视图不会超出运动场,请检查左上角不小于 0 且左上角加上视图宽度和高度不大于运动场宽度和高度。 (参见示例playfield.setView

    要使用您设置画布变换的视图,以便原点为-top-left,然后在正常位置绘制项目。

    四叉树

    当您有一个包含许多项目的大型游戏场时,设备绘制所有项目可能需要做很多工作。即使是视图之外的项目也会占用一点 CPU 时间,这会增加并让游戏变得非常缓慢。

    要提高性能,您只需要绘制(和更新)可见的项目。然而,查找可见项目的过程会增加与调用绘图函数一样多的负载。您需要使用一种可以快速找到可见项目的方法,而无需测试操场中的每个项目。

    在 2D 中,这可以使用一种称为四叉树的特殊类型的链表来完成。

    • 四叉树由正方形组成。每个正方形都有一个 x,y 位置和一个宽度和高度。例如Quad
    • 第一个四边形与运动场大小相同。例如,它是 16000 像素的正方形。
    • 每个四边形也可以有 4 个子四边形,表示四边形被 4 分。参见示例 Quad.prototype.divide
    • 您对每个 subQuad 重复,每增加 4 个 subquad。您设置了最大深度,即您将子四边形划分为更多子四边形的次数。 (在示例中,maxDepth 为 5)
    • 因此,示例中最小的四边形是运动场size / (2 ** maxDepth) === 16000 / 32 === 500,这意味着它覆盖了运动场的 500 x 500 像素。
    • 在四叉树的底部存储与该四叉树重叠的项目。一个项目可能与多个四边形重叠,该项目重叠的每个四边形必须包含该项目。 (例如,一个项目只是一个带有xywhcolor 的矩形)
    • 您可以测试是否有任何四边形与当前视图重叠。请参阅示例 (Quad.prototype.isInView) 如果没有,则它及其所有子四边形及其包含的项目不可见。这很快就无需检查多达 3/4 的项目。

    playfield 对象包含最上面的Quad。当您设置视图playfield.setView 时,它会构建视图变换并创建所有可见四边形中所有项目的映射。当setView 返回地图时,playfield.visibleItem 包含与视图重叠的四边形中的所有项目。

    由于地图项目可以同时在多个四边形中,您需要快速构建项目列表而不重复相同的项目。您可以使用 MapSet(这些是内置在 JavaScript 对象中)来执行此操作,并且无需检查项目是否已在列表中。

    示例

    使用箭头键移动。您必须单击画布才能启动,因为 sn-ps 不会自动聚焦键盘。

    地图playfield 非常大,16,000 x 16,000 像素,地图中有 10,000 个项目。

    使用四叉树查找可见项目可以以 60FPS 实时动画视图。

    const keys = { // keys to listen to
        ArrowUp: false,
        ArrowLeft: false,
        ArrowRight: false,
        ArrowDown: false,
    };
    document.addEventListener('keydown', keyEvent);
    document.addEventListener('keyup', keyEvent);  
    document.addEventListener("click",()=>requestAnimationFrame(mainLoop),{once:true});
    var startTime;
    var globalTime;
    const mapItemCount = 10000;
    const maxItemSize = 120; // in pixels
    const minItemSize = 20; // in pixels
    const maxQuadDepth = 5;
    const playfieldSize = 16000; // in pixels
    var id = 1;  // unique ids for map items
    const mapItems = new Map();  // unique map items
    const directions = {
        NONE: {idx: 0, vec: {x: 0, y: 0}},
        UP: {idx: 3, vec: {x: 0, y: -1}},
        RIGHT: {idx: 0, vec: {x: 1, y: 0}},
        DOWN: {idx: 1, vec: {x: 0, y: 1}},
        LEFT: {idx: 2, vec: {x: -1, y: 0}},
    };
    const ctx = canvas.getContext("2d");
    
    function mainLoop(time) {
        if(!startTime) { startTime = time }
        globalTime = time - startTime;
        playfield.sizeCanvas();
        ctx.setTransform(1,0,0,1,0,0);
        ctx.clearRect(0,0,canvas.width,canvas.height);
        
        player.update();
        playfield.setView(player);  // current transform set to view
        
        playfield.drawVisible();
        player.draw();
        
        info.textContent = `Player: X:${player.x|0} Y${player.y|0}: , View Left:${playfield.left | 0} Top:${playfield.top | 0} , visibleItems: ${playfield.visibleItems.size} of ${mapItems.size}`;
        
        requestAnimationFrame(mainLoop);
    }
    
    function Quad(x, y, w, h, depth = maxQuadDepth) {
        this.x = x;
        this.y = y;
        this.w = w;
        this.h = h;
        if (depth > 0) { this.divide(depth) }
        else { this.items = [] }
    }
    Quad.prototype = {
        divide(depth) {
            this.subQuads = [];
            this.subQuads.push(new Quad(this.x, this.y, this.w / 2, this.h / 2, depth - 1));
            this.subQuads.push(new Quad(this.x + this.w / 2, this.y, this.w / 2, this.h / 2, depth - 1));
            this.subQuads.push(new Quad(this.x + this.w / 2, this.y + this.h / 2, this.w / 2, this.h / 2, depth - 1));
            this.subQuads.push(new Quad(this.x, this.y + this.h / 2, this.w / 2, this.h / 2, depth - 1));       
        },
        isInView(pf) {  // pf is playfield
            return !(this.x > pf.left + pf.cWidth || this.x + this.w < pf.left || this.y > pf.top + pf.cHeight || this.y + this.h < pf.top);
        },
        addItem(item) {
            if (!(item.x > this.x + this.w || item.x + item.w < this.x || item.y > this.y + this.h || item.y + item.h < this.y)) {
                if (this.subQuads) {
                    for (const quad of this.subQuads) { quad.addItem(item) }
                } else { this.items.push(item.id) }
            }
        },
        getVisibleItems(pf, itemMap, items = new Map()) {
            if (this.subQuads) {
                for (const quad of this.subQuads) {
                    if (quad.isInView(pf)) { quad.getVisibleItems(pf, itemMap, items) }
                }
            } else {
                for (const id of this.items) { items.set(id, itemMap.get(id)) }
            }
            return items
        }   
    }
    // only one instance then define as object
    const playfield = {
        width: playfieldSize,
        height: playfieldSize,
        view: [1,0,0,1,0,0],  // view as transformation matrix
        cWidth: 0,  // canvas size
        cHeight: 0, // canvas size
        top: 0,
        left: 0,
        sizeCanvas() {
            if(canvas.width !== innerWidth || canvas.height !== innerHeight) {
                this.cWidth = canvas.width = innerWidth;
                this.cHeight = canvas.height = innerHeight;         
            }
        },
        setView(player) {
            var left = player.x - this.cWidth / 2;
            var top = player.y - this.cHeight / 2;
            left = left < 0 ? 0 : left > this.width - this.cWidth ? this.width - this.cWidth : left;
            top = top < 0 ? 0 : top > this.height - this.cHeight ? this.height - this.cHeight : top;
            this.view[4] = -(this.left = left);
            this.view[5] = -(this.top = top);
            ctx.setTransform(...this.view);
            this.visibleItems.clear();
            this.quadMap.getVisibleItems(this, mapItems, this.visibleItems);
        },
        drawVisible() {
            for(const item of this.visibleItems.values()) { item.draw() }  
        },
        quadMap: new Quad(0, 0, playfieldSize, playfieldSize),
        visibleItems: new Map(),
    }
    
    function MapItem(x, y, w, h, col = "#ABC") {
        this.x = x;
        this.y = y;
        this.w = w;
        this.h = h;
        this.col = "#ABC";
        this.id = id++;
        mapItems.set(this.id,this);
        playfield.quadMap.addItem(this);
    }
    MapItem.prototype = {
        draw() {
            ctx.fillStyle = this.col;
            ctx.fillRect(this.x, this.y, this.w, this.h);
        }
    }
    addMapItems(mapItemCount)
    function addMapItems(count) {
        while (count-- > 0) {
            const x = Math.random() * playfield.width;
            const y = Math.random() * playfield.height;
            const w = Math.random() * (maxItemSize - minItemSize) + minItemSize;
            const h = Math.random() * (maxItemSize - minItemSize) + minItemSize;
            const item = new MapItem(x,y,w,h);
        }
    }
    
    // only one instance then define as object
    const player = {
        x: 1200,
        y: 1200,
        speed: 10,
        image: undefined,
        direction: undefined,
        draw() {
            ctx.fillStyle = "#F00";
            ctx.save();  // need to save and restore as I use rotate to change the current transform that 
                         // holds the current playfield view.
            const x = this.x;
            const y = this.y;
            ctx.transform(1,0,0,1,x,y);
            ctx.rotate(this.direction.idx / 2 * Math.PI);
            ctx.beginPath();
            ctx.lineTo(20, 0);
            ctx.lineTo(-10, 14);
            ctx.lineTo(-10, -14);
            ctx.fill();
            ctx.restore();
            
        },
        update() {
            var dir = directions.NONE;
            if (keys.ArrowUp) { dir = directions.UP }
            if (keys.ArrowDown) { dir = directions.DOWN }
            if (keys.ArrowLeft) { dir = directions.LEFT }
            if (keys.ArrowRight) { dir = directions.RIGHT }
            this.x += dir.vec.x * this.speed;
            this.y += dir.vec.y * this.speed;
            this.x = this.x < 0 ? 0 : this.x > playfield.width ? playfield.width : this.x;
            this.y = this.y < 0 ? 0 : this.y > playfield.height ? playfield.height : this.y;
            this.direction = dir;
            
        }
    };
    
    
    function keyEvent(e) {
        if (keys[e.code] !== undefined) {
            keys[e.code] = e.type === "keydown";
            e.preventDefault();
        }
    }
    canvas {
        position: absolute;
        left: 0px;
        top: 0px;
    }
    #info {
        font-family: arial;
        position: absolute;
        left: 0px;
        top: 0px;
        font-size: small;
    }
    <canvas id="canvas"></canvas>
    <div id="info">Click to start</div>

    【讨论】:

    • 15 分钟,我从地板上抬起下巴。可能是因为看到代码后我的大脑被冻结了;-) 你的代码太棒了!不幸的是,我是编程的初学者,我做的每一件事都非常简单。即使有人在半夜叫醒我,我也能快速解释我的代码 :) 非常感谢您提供如此详尽的代码,并提供了很好的解释!为了了解这些机制如何工作以及如何使用它们,我将不得不解除这段代码很多天。再次感谢!
    猜你喜欢
    • 2021-05-29
    • 1970-01-01
    • 1970-01-01
    • 2016-03-11
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多