You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

基于最优缩放比例适配Div至屏幕的游戏实体位置偏移问题

云游戏固定帧适配窗口时实体位置偏移问题

背景

我开发的云游戏有固定尺寸的游戏帧:

// 关卡帧原始尺寸
const frameWidth = 1140;
const frameHeight = 518;

我试过多款2D渲染库,但都因Bug或功能不足放弃。由于游戏是云原生架构,客户端仅负责收发世界数据包并渲染场景,因此选择浏览器原生HTML渲染方案。

我希望将游戏帧适配至窗口,采用能完整放入屏幕的最小缩放比例并居中显示,为此编写了以下函数:

import ScreenSize from '@/app/util/ScreenSize';

export default function optimalFitRatio(originalSize: ScreenSize, fitToSize: ScreenSize): number {
    const horizontalRatio = fitToSize.width / originalSize.width;
    const verticalRatio = fitToSize.height / originalSize.height;
    return Math.min(horizontalRatio, verticalRatio);
}

问题

当前遇到的核心问题:游戏内世界实体的位置会随窗口分辨率变化而偏移,此前使用2D渲染库时无此问题。

最小复现代码

function optimalFitRatio(originalSize, fitToSize) {
    const horizontalRatio = fitToSize.width / originalSize.width;
    const verticalRatio = fitToSize.height / originalSize.height;
    return Math.min(horizontalRatio, verticalRatio);
}

// 关卡帧原始尺寸
const frameWidth = 1140;
const frameHeight = 518;

class LevelView {
    container1 = undefined;
    container2 = undefined;
    resizeListener = undefined;
    attached = false;

    scale = 0;
    width = 0;
    height = 0;

    attach() {
        this.attached = true;

        // 创建第一个容器
        this.newContainer();

        // 监听窗口 resize
        this.resizeListener = this.resizeFrame.bind(this);
        window.addEventListener('resize', this.resizeListener);
        this.resizeFrame();
    }

    detach() {
        window.removeEventListener('resize', this.resizeListener);
        this.container1?.remove();

        this.attached = false;
    }

    renderLevel(world) {
        this.container1?.remove();
        this.newContainer();
        this.orientContainer();

        // 按Z轴排序实体
        // sortEntitiesByZ(world.entities);

        for (const entity of world.entities) {
            if (entity.type == "Entity") {
                this.renderEntity(entity);
            }
        }
    }

    renderEntity(entity) {
        // 设置元素样式
        const orient = shape => {
            shape.style.position = "fixed";
            shape.style.left = `${entity.x}px`;
            shape.style.top = `${entity.y}px`;
            shape.style.transform = `rotate(${entity.rotationRadians}rad)`;
            shape.style.transformOrigin = "center";
            shape.style.userSelect = "none";
        };

        // 创建实体图片
        const bitmap = document.createElement("img");
        bitmap.src = "https://i.imgur.com/Dx200x9.png";
        orient(bitmap);
        this.container2.appendChild(bitmap);
    }

    resizeFrame() {
        if (!this.attached) {
            return;
        }

        // 计算适配比例
        const ratio = optimalFitRatio(
            { width: frameWidth, height: frameHeight },
            { width: window.innerWidth, height: window.innerHeight },
        );

        // 缓存尺寸和比例
        this.width = frameWidth * ratio;
        this.height = frameHeight * ratio;
        this.scale = ratio;

        // 更新容器样式
        this.orientContainer();
    }

    orientContainer() {
        // 设置外层容器尺寸
        const [w, h] = [this.width, this.height];
        this.container1.style.width = `${w}px`;
        this.container1.style.height = `${h}px`;

        // 缩放内层容器
        this.container2.style.transform = `scale(${this.scale})`;

        // 外层容器居中
        this.container1.style.left = `${window.innerWidth / 2 - w / 2}px`;
        this.container1.style.top = `${window.innerHeight / 2 - h / 2}px`;
    }

    newContainer() {
        const c0 = document.body;

        const c1 = document.createElement('div');
        c1.style.display = "inline-block";
        c1.style.background = "#000";
        c1.style.overflow = "hidden";
        c1.style.position = "fixed";
        c1.style.userSelect = "none";
        c0.appendChild(c1);
        this.container1 = c1;

        const c2 = document.createElement('div');
        c1.appendChild(c2);
        this.container2 = c2;
    }
}

const levelView = new LevelView();
levelView.attach();
levelView.renderLevel({
    entities: [{
        type: "Entity",
        x: 100,
        y: 100,
        rotationRadians: 0,
    }],
});

(更新说明:此前遗漏了{ type: "Entity", ...p },现在可复现问题)

修复方案

问题根源在于实体的定位逻辑:你给实体设置了position: fixed,使其相对于视口定位,而非缩放后的容器container2;同时container2未设置参考定位,导致缩放时实体坐标无法同步适配。

修改步骤

  1. 给container2添加相对定位,作为实体的定位参考,并设置其尺寸为游戏帧原始大小:
// 在newContainer方法中修改c2的创建逻辑
const c2 = document.createElement('div');
c2.style.position = "relative";
c2.style.width = `${frameWidth}px`;
c2.style.height = `${frameHeight}px`;
c1.appendChild(c2);
this.container2 = c2;
  1. 将实体的position从fixed改为absolute,使其相对于container2定位:
// 修改renderEntity中的orient函数
const orient = shape => {
    shape.style.position = "absolute";
    shape.style.left = `${entity.x}px`;
    shape.style.top = `${entity.y}px`;
    shape.style.transform = `rotate(${entity.rotationRadians}rad)`;
    shape.style.transformOrigin = "center";
    shape.style.userSelect = "none";
};

修改后,实体将基于原始游戏帧尺寸的container2定位,而container2会随外层容器整体缩放、居中,窗口resize时实体位置将自动同步适配,不会再出现偏移。

内容的提问来源于stack exchange,提问作者Hydroper

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.09 17:04:52