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

SFML等距城镇游戏关闭时Vector下标越界问题排查

问题描述

我正在开发一款简单的等距视角城镇开发模拟游戏,核心类为Player类与World类。目前遇到的问题为:程序启动后运行流程符合预期,但当我准备点击窗口右上角“X”按钮关闭程序、鼠标移动到窗口右上角关闭按钮位置时,程序中断并抛出Vector subscript out of range错误,我在代码中并未手动创建vector容器。

错误截图

经测试,不创建Player对象时不会触发该错误,因此判断问题出在Player类相关逻辑中。


相关代码

Player.hpp

#ifndef PLAYER_HEADER
#define PLAYER_HEADER

#include "Main.hpp"
#include "ToScreenF.hpp"
#include <iostream>

class Player
{
public:
    sf::Vector2i MousePos;
    sf::Vector2i inCell;
    sf::Vector2i cellOffset;
    sf::Vector2i cellSelected;

    sf::Vector2f cellSelectedInWorldSpace;

    sf::Image selectedTileImage;
    sf::Texture selectedTileTexture;
    sf::Sprite selectedTileSprite;

    sf::Image CheatImage;

    sf::Color color;

    Player(std::string FILE);
    ~Player();

    void Update(sf::RenderWindow* Window, sf::Vector2f TileSize, sf::Vector2f vOrigin, int WorldWidth, int WorldHeight);
};

#endif PLAYER_HEADER

Player.cpp

#include "Player.hpp"

Player::Player(std::string FILE)
{
    selectedTileImage.loadFromFile(FILE);
    selectedTileImage.createMaskFromColor(sf::Color::White);
    selectedTileTexture.loadFromImage(selectedTileImage);
    selectedTileSprite.setTexture(selectedTileTexture);
    CheatImage.loadFromFile("tileCheat.png");
}
void Player::Update(sf::RenderWindow* Window, sf::Vector2f TileSize, sf::Vector2f vOrigin, int WorldWidth, int WorldHeight)
{    
    MousePos = {sf::Mouse::getPosition((*Window))};
    inCell = {(int)(MousePos.x / TileSize.x), (int)(MousePos.y / TileSize.y) };
    cellOffset = { MousePos.x % (int)TileSize.x, MousePos.y % (int)TileSize.y };
    cellSelected = { 
        (inCell.y - (int)vOrigin.y) + (inCell.x - (int)vOrigin.x),
        (inCell.y - (int)vOrigin.y) - (inCell.x - (int)vOrigin.x) 
    };
    color = CheatImage.getPixel(cellOffset.x, cellOffset.y);
    if (color == sf::Color::Red) { cellSelected.x += -1; cellSelected.y += 0; };
    if (color == sf::Color::Blue) { cellSelected.x += 0; cellSelected.y += -1; };
    if (color == sf::Color::Green) { cellSelected.x += 0; cellSelected.y += 1; };
    if (color == sf::Color::Yellow) { cellSelected.x += 1; cellSelected.y += 0; };
    if (cellSelected.x < 0) cellSelected.x = 0;
    if (cellSelected.x > (WorldWidth - 1)) cellSelected.x = 19;
    if (cellSelected.y < 0) cellSelected.y = 0;
    if (cellSelected.y > (WorldHeight - 1)) cellSelected.y = 19;
    cellSelectedInWorldSpace = ToScreen(cellSelected.x, cellSelected.y, TileSize, vOrigin);
    selectedTileSprite.setPosition(cellSelectedInWorldSpace);
    Window->draw(selectedTileSprite);
    std::cout << cellSelected.x << " " << cellSelected.y << std::endl;
}

ToScreenF.cpp

#include "ToScreenF.hpp"

sf::Vector2f ToScreen(int x, int y, sf::Vector2f TileSize, sf::Vector2f vOrigin)
{
    return sf::Vector2f
    {
        (vOrigin.x * TileSize.x) + (x - y) * (TileSize.x / 2),
        (vOrigin.y * TileSize.y) + (x + y) * (TileSize.y / 2)
    };
}

TApplication.hpp

#ifndef TAPPLICATION_HEADER
#define TAPPLICATION_HEADER

#include "Main.hpp"
#include "World.hpp"
#include "Player.hpp"

namespace Application
{
    class TApplication
    {
    protected:
        sf::RenderWindow *Window;
        World *GameWorld;
        Player* _Player;

    public:
        TApplication();
        ~TApplication();

        void Init();
        void Run();
        void End();
    };
}

TApplication.cpp

#include "TApplication.hpp"

namespace Application
{
    TApplication::TApplication() : Window(nullptr)
    {

    }

    TApplication:: ~TApplication()
    {

    }

    void TApplication::Init()
    {
        if (Window == nullptr)
            Window = new sf::RenderWindow(sf::VideoMode(1200, 800), "Town Builder Simulator");
            GameWorld = new World("BasicTile.png", 100, 100);
            _Player = new Player("selectedTile.png");
    }

    void TApplication::Run()
    {
        sf::Event event;
        while (Window->isOpen())
        {
            while(Window->pollEvent(event))
            {
                if (event.type == sf::Event::Closed)
                {
                    Window->close();
                }
            }

            Window->clear();
            GameWorld->Draw(Window);
            _Player->Update(Window, sf::Vector2f(40, 20), sf::Vector2f(10, 10), GameWorld->WorldWidth, GameWorld->WorldHeight);
            Window->display();
        }
    }

    void TApplication::End()
    {
        if (Window != nullptr)
        {
            delete Window;
            delete GameWorld;
            Window = nullptr;
        }
    }
}

Main.hpp

#ifndef MAIN_HEADER
#define MAIN_HEADER

#include <SFML/Graphics.hpp>
#include <SFML/Window.hpp>
#include <SFML/Audio.hpp>
#include <SFML/Network.hpp>
#include <SFML/System.hpp>

#endif

问题原因

报错核心原因是鼠标移出窗口客户区时坐标越界,触发SFML内部vector容器的下标访问错误:

  1. 当鼠标移动到窗口右上角关闭按钮位置时,已经脱离窗口客户区范围,此时sf::Mouse::getPosition返回的坐标会出现负值,或者大于窗口宽高值
  2. 代码中没有对鼠标坐标、计算得到的cellOffset做范围校验,直接传入CheatImage.getPixel()访问像素
  3. SFML的sf::Image内部用vector存储像素数据,你的CheatImage尺寸和瓦片一致为40*20,合法坐标范围x为039、y为019,传入超出范围的坐标时就会抛出vector下标越界错误,和你观察到的现象完全吻合。

代码另外存在三个隐藏问题:

  • 边界钳制逻辑硬编码:cellSelected超出世界范围时直接赋值为19,当世界尺寸大于20时逻辑错误
  • Init函数的if判断未加花括号,GameWorld和_Player的初始化不受Window == nullptr条件约束,存在逻辑隐患
  • End函数未释放_Player对象内存,存在内存泄漏

修复方案
  1. 修改Player::Update函数,增加坐标范围校验,修正边界钳制逻辑:
// 记得在文件头引入<algorithm>头文件以使用std::clamp,C++17以下版本可手动写范围判断
void Player::Update(sf::RenderWindow* Window, sf::Vector2f TileSize, sf::Vector2f vOrigin, int WorldWidth, int WorldHeight)
{    
    MousePos = sf::Mouse::getPosition(*Window);
    // 鼠标不在窗口客户区内时直接返回,不做后续计算
    sf::Vector2u winSize = Window->getSize();
    if (MousePos.x < 0 || MousePos.x >= (int)winSize.x || MousePos.y < 0 || MousePos.y >= (int)winSize.y)
    {
        return;
    }

    inCell = {(int)(MousePos.x / TileSize.x), (int)(MousePos.y / TileSize.y) };
    cellOffset = { MousePos.x % (int)TileSize.x, MousePos.y % (int)TileSize.y };
    // 双重校验偏移量在CheatImage尺寸范围内,避免越界
    sf::Vector2u cheatSize = CheatImage.getSize();
    if (cellOffset.x < 0 || cellOffset.x >= (int)cheatSize.x || cellOffset.y <0 || cellOffset.y >= (int)cheatSize.y)
    {
        return;
    }

    cellSelected = { 
        (inCell.y - (int)vOrigin.y) + (inCell.x - (int)vOrigin.x),
        (inCell.y - (int)vOrigin.y) - (inCell.x - (int)vOrigin.x) 
    };
    color = CheatImage.getPixel(cellOffset.x, cellOffset.y);
    if (color == sf::Color::Red) cellSelected.x -= 1;
    if (color == sf::Color::Blue) cellSelected.y -= 1;
    if (color == sf::Color::Green) cellSelected.y += 1;
    if (color == sf::Color::Yellow) cellSelected.x += 1;

    // 修正边界钳制逻辑,不要硬编码19
    cellSelected.x = std::clamp(cellSelected.x, 0, WorldWidth - 1);
    cellSelected.y = std::clamp(cellSelected.y, 0, WorldHeight - 1);

    cellSelectedInWorldSpace = ToScreen(cellSelected.x, cellSelected.y, TileSize, vOrigin);
    selectedTileSprite.setPosition(cellSelectedInWorldSpace);
    Window->draw(selectedTileSprite);
    std::cout << cellSelected.x << " " << cellSelected.y << std::endl;
}

如果使用的C版本低于C17,可替换std::clamp为手动判断:

if (cellSelected.x < 0) cellSelected.x = 0;
if (cellSelected.x >= WorldWidth) cellSelected.x = WorldWidth - 1;
if (cellSelected.y < 0) cellSelected.y = 0;
if (cellSelected.y >= WorldHeight) cellSelected.y = WorldHeight - 1;
  1. 修正TApplication中的逻辑错误与内存泄漏:
void TApplication::Init()
{
    if (Window == nullptr)
    {
        Window = new sf::RenderWindow(sf::VideoMode(1200, 800), "Town Builder Simulator");
        GameWorld = new World("BasicTile.png", 100, 100);
        _Player = new Player("selectedTile.png");
    }
}

void TApplication::End()
{
    if (Window != nullptr)
    {
        delete Window;
        delete GameWorld;
        delete _Player; // 补充Player对象的内存释放
        Window = nullptr;
        GameWorld = nullptr;
        _Player = nullptr;
    }
}

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.30 10:33:25