C++实现一笔画游戏

如果是C/C++项目需要源代码的,可以到GitHub下载(记得给个Star哦):WaWaJie/OneLineDrawingGame: 使用C++实现的一笔画游戏,但是你可以去设计关卡给你的好朋友玩,想想怎么折磨他吧!

(如果你不能打开GitHub,可以私信我给你发个网盘的下载链接)

视频演示:C++一笔画小游戏_哔哩哔哩_bilibili

前言:

这是我在在突发奇想之后花了两天时间去完成的内容,其特色是你可以通过编辑一个关卡来让你的朋友去玩。同时,你也可以去玩自己设计的关卡,并且每个关卡通过之后,都会随机生成一个新的关卡。

代码内容我不会讲很多(代码也不是很优雅就是了),只讲一下大致的思路,需要的话可以下载源码自行分析。

一、项目演示

1.菜单界面

菜单界面有两个部分,一个是"开始游戏",一个是"关卡编辑",左上角和左下角分别是人物动画和宝箱的动画。左下角点击可以进入GitHub仓库。

2.游戏界面

点击"开始游戏",进入对应的游戏界面,你可以拖动鼠标游标去联通所有的合法块到达终点,在宝箱开启动画结束后,会自动加载一个新的关卡。

此外,你也可以点击导入按钮,导入对应的关卡(csv文件)。

当你遇到一张随机生成的比较好的关卡,也可以通过导出按钮,将对应的关卡信息(csv文件存储)导出到指定路径。

3.关卡编辑界面

在编辑界面,你可以选定起点,随便画上一笔,得到一条路径,并点击导出按钮,将关卡导出到指定的文件路径。

4.关卡分享

你可以直接将导出的csv文件发给你的好友,让他去玩。

二、项目结构

该项目主要使用C++和第三方库SDL2去实现,整体主要通过场景管理器单例去控制场景的跳转。

因而需要设计一个Scene基类和SceneManager去管理场景界面。(类似于状态机的跳转)

//scene.h

cpp 复制代码
#pragma once

#include<SDL.h>


enum class SceneType
{
	Menu, Game, Editor
};

class Scene
{
public:
	Scene() = default;
	~Scene()= default;

	virtual void on_enter() {}
	virtual void on_exit() {}

	virtual void on_update(float delta) {}
	virtual void on_render(SDL_Renderer* renderer) {}
	virtual void on_input(const SDL_Event& event) {}

};

//scene_manager.h

cpp 复制代码
#pragma once

#include"scene.h"
#include"game_scene.h"
#include"menu_scene.h"
#include"singleton.h"
#include"editor_scene.h"


class SceneManager : public Singleton<SceneManager>
{
	friend class Singleton<SceneManager>;

private:
	SceneManager()
	{
		menu_scene = new MenuScene(); //这里可以替换为具体的场景类
		game_scene = new GameScene();
		editor_scene = new EditorScene();
		current_scene = menu_scene;
		current_scene->on_enter();
	}

public:
	

	void on_update(float delta)
	{
		if (current_scene)
			current_scene->on_update(delta);

		if (current_scene_type != ConfigManager::get_instance()->current_scene_type)
		{
			current_scene_type = ConfigManager::get_instance()->current_scene_type;
			switch_to(current_scene_type);
		}
	}
	void on_render(SDL_Renderer * renderer)
	{
		if (current_scene)
			current_scene->on_render(renderer);
	}
	void on_input(const SDL_Event& event)
	{
		if (current_scene)
			current_scene->on_input(event);
	}
	void switch_to(SceneType type)
	{
		current_scene->on_exit();
		switch (type)
		{
		case SceneType::Menu:
			current_scene = menu_scene;
			break;
		case SceneType::Game:
			current_scene = game_scene;
			break;
		case SceneType::Editor:
			current_scene = editor_scene;
			break;
		default:
			break;
		}
		current_scene->on_enter();
	}


private:
	Scene* current_scene = nullptr;
	Scene* menu_scene = nullptr;
	Scene* game_scene = nullptr;
	Scene* editor_scene = nullptr;

	SceneType current_scene_type = SceneType::Menu;
};

三、动画类实现

动画类的实现实际上是加载帧动画的每一帧,之后按照一定的时间间隔去循环展示图片,利用人体的视觉暂留效应展现出"动起来"的视觉效果。所以在实现动画之前,就要先去实现一个定时器Timer类,通过设置回调函数去完成在经过了指定时间就执行相应逻辑的功能。而在动画类当中,就是在加载了对应的图片资源到容器当中之后,内置一个定时器,指定其在计时完成后播放下一帧动画。

定时器Timer:

cpp 复制代码
#pragma once

#include<functional>

class Timer
{
public:
	Timer() = default;
	~Timer() = default;

	void pause()
	{
		paused = true;
	}

	void resume()
	{
		paused = true;
	}

	void on_update(double delta)
	{
		if (paused)return;

		pass_time += delta;

		if (pass_time >= wait_time)
		{
			bool can_shot = (one_shot && !shotted) || (!one_shot);
			if (can_shot && on_timeout)
				on_timeout();
			shotted = true;
			pass_time -= wait_time;
		}
	}

	void restart()
	{
		pass_time = 0;
		shotted = false;
	}

	void set_wait_time(double wait_time)
	{
		this->wait_time = wait_time;
	}

	void set_one_shot(bool flag)
	{
		one_shot = flag;
	}

	void set_on_timeout(std::function<void()>on_timeout)
	{
		this->on_timeout = on_timeout;
	}

private:
	std::function<void()>on_timeout = nullptr;
	double wait_time = 0;
	double pass_time = 0;
	bool one_shot = false;
	bool shotted = false;
	bool paused = false;
};

动画类animation.h

cpp 复制代码
#pragma once

#include"timer.h"
#include"map.h"

#include<vector>
#include<SDL.h>
#include<SDL_image.h>

class Animation
{
public:
	typedef std::function<void()>PlayBack;
	typedef std::vector<SDL_Texture*>TexList;

public:
	Animation()
	{
		timer.set_one_shot(false);
		timer.set_on_timeout([&]()
			{
				idx_frame++;
				if (idx_frame == rect_src_list.size())
				{
					idx_frame = is_loop ? 0 : rect_src_list.size() - 1;
					if (on_finished)
					{
						on_finished();
					}
				}
			});
		tex_list.clear();
	}
	~Animation() = default;

	void set_frame_seperate(bool flag)
	{
		is_frame_seperate = flag;
	}

	void add_texture(SDL_Texture* texture)
	{
		tex_list.push_back(texture);
		SDL_QueryTexture(texture, nullptr, nullptr, &width_frame, &height_frame);
		rect_src_list.emplace_back();
	}

	void set_animation(SDL_Texture* texture, int num_h, int num_v, const std::vector<int>& idx_list)//水平数量和垂直数量
	{
		int tex_width, tex_height;

		if (texture == nullptr)
		{
			std::cout << "texture is null" << std::endl;
			return;
		}

		this->texture = texture;
		SDL_QueryTexture(texture, nullptr, nullptr, &tex_width, &tex_height);
		width_frame = tex_width / num_h;
		height_frame = tex_height / num_v;

		rect_src_list.resize(idx_list.size());
		for (size_t i = 0; i < idx_list.size(); i++)
		{
			int idx = idx_list[i];
			SDL_Rect& rect_src = rect_src_list[i];

			rect_src.x = idx % num_h * width_frame;
			rect_src.y = idx / num_h * height_frame;
			rect_src.w = width_frame, rect_src.h = height_frame;
		}
	}

	void on_render(SDL_Renderer* renderer, const SDL_Point& pos_dst, double angle = 0, int ratio = 1)const
	{
		static SDL_Rect rect_dst;

		rect_dst.x = pos_dst.x, rect_dst.y = pos_dst.y;
		rect_dst.w = width_frame * ratio, rect_dst.h = height_frame * ratio;

		if (!is_frame_seperate)
			SDL_RenderCopyEx(renderer, texture, &rect_src_list[idx_frame], &rect_dst, angle, nullptr, SDL_FLIP_NONE);
		else
		{
			SDL_RenderCopyEx(renderer, tex_list[idx_frame], nullptr, &rect_dst, angle, nullptr, SDL_FLIP_NONE);
		}
	}
	void on_update(double delta)
	{
		timer.on_update(delta);
	}

	void set_interval(double interval)
	{
		timer.set_wait_time(interval);
	}

	void set_on_finished(PlayBack on_finished)
	{
		this->on_finished = on_finished;
	}

	void set_loop(bool flag)
	{
		is_loop = flag;
	}

	void reset()
	{
		idx_frame = 0;
		timer.restart();
	}

private:
	Timer timer;
	PlayBack on_finished;

	int idx_frame = 0;
	bool is_loop = true;
	bool is_frame_seperate = false;

	TexList tex_list;
	SDL_Texture* texture = nullptr;
	std::vector<SDL_Rect>rect_src_list;
	int width_frame = 0, height_frame = 0;
};

四、地图设计

游戏中的地图是一个8*8的地图(我给写死了的),在加载地图的时候也就是通过讲对应的文件内容加载到一个8*8的数组当中。在进行渲染的时候,就是通过地图的瓦片索引去计算对应的窗口坐标,并基于瓦片类型去加载对应的瓦片纹理进行渲染。

cpp 复制代码
#pragma once

#include<algorithm>
#include<iostream>
#include<fstream>
#include<string>
#include<vector>
#include<SDL.h>

#define TILE_SIZE 96

enum class TileType
{
	Failed=-1,Idle,Selected,Start,End
};


class Map
{
public:
    Map(const char* file_path)
    {
        for (int i = 0; i < 8; ++i)
            for (int j = 0; j < 8; ++j)
                m_mp[i][j] = -1;
        std::ifstream file(file_path);
        if (!file.is_open())
        {
            SDL_Log("Map file not found");
            return;
        }
        std::string line;
        int cur_line = 0;
        while (std::getline(file, line) && cur_line < 8)
        {
            // 以逗号为分隔拆分字符串
            std::vector<std::string> vec;
            size_t start = 0;
            size_t end = line.find(',');

            while (end != std::string::npos)
            {
                vec.push_back(line.substr(start, end - start));
                start = end + 1;
                end = line.find(',', start);
            }

            // 添加最后一个字段(最后一个逗号之后的部分)
            if (start < line.length()) {
                vec.push_back(line.substr(start));
            }

            // 处理空行的情况
            if (vec.empty()) {
                vec.push_back("-1"); // 默认值为禁用格
            }

            int limi = std::min<int>(8, vec.size());
            for (int i = 0; i < limi; i++)
            {
                // 去除可能的空格
                std::string cell = vec[i];
                cell.erase(0, cell.find_first_not_of(" \t\n\r")); // 去除前导空白
                cell.erase(cell.find_last_not_of(" \t\n\r") + 1); // 去除尾部空白

                if (!cell.empty())
                {
                    try
                    {
                        m_mp[cur_line][i] = std::stoi(cell);
                        if (m_mp[cur_line][i] < -1 || m_mp[cur_line][i] > 3)
                        {
                            m_mp[cur_line][i] = -1; // 超出范围设为禁用格
                        }
                    }
                    catch (const std::invalid_argument& e)
                    {
                        m_mp[cur_line][i] = -1; // 转换失败设为禁用格
                    }
                    catch (const std::out_of_range& e)
                    {
                        m_mp[cur_line][i] = -1; // 超出范围设为禁用格
                    }
                }
                else
                {
                    m_mp[cur_line][i] = -1; // 空单元格设为禁用格
                }
            }
            cur_line++;
        }
    }

    Map() {
        for (int i = 0; i < 8; ++i)
            for (int j = 0; j < 8; ++j)
                m_mp[i][j] = -1;
    }

	void check()
	{
		for (int i = 0; i < 8; i++)
		{
			for (int j = 0; j < 8; j++)
			{
				std::cout << m_mp[i][j] << "\t";
			}
			std::cout << std::endl;
		}
		std::cout << std::endl;
	}

public:
	int m_mp[8][8];


};
cpp 复制代码
void GameScene::render_map(SDL_Renderer*renderer,Map* mp)
{
    for (int i = 0; i < 8; ++i)
    {
        for (int j = 0; j < 8; ++j)
        {
            int tile_type = mp->m_mp[i][j];
            std::string tile_name = "tile_" + std::to_string(tile_type);
            SDL_Rect dst_rect = { j * 96 + st_j,i * 96,96,96 };
            SDL_SetRenderDrawColor(renderer, 0, 255, 0, 255);
            SDL_RenderCopy(renderer, ResourcesManager::get_instance()->find_texture(tile_name), NULL, &dst_rect);
            SDL_RenderDrawRect(renderer, &dst_rect);
            if (tile_type == (int)TileType::End && !is_game_over)
            {
                static SDL_Rect rect_src_target = { 0,0,48,48 };
                SDL_RenderCopy(renderer, ResourcesManager::get_instance()->find_texture("target"), &rect_src_target, &dst_rect);
            }
        }
    }
}

五、一笔画功能实现

获取鼠标游标的位置,计算其所处的瓦片索引位置,并判断当前鼠标按键是否按下。如果都满足的话,则判断当前所处位置是否合法(鼠标所处的瓦片索引位置需要与当前所处的瓦片位置的曼哈顿距离为1,且该瓦片必须是目标瓦片)

cpp 复制代码
void GameScene::update_tile()
{
    //越界检查
    if (ConfigManager::get_instance()->pos_cursor.x < st_j || cur_tile_idx_cursor.x < 0 || cur_tile_idx_cursor.y < 0 || cur_tile_idx_cursor.x >= 8 || cur_tile_idx_cursor.y >= 8)
        return;
	bool is_in_target_tile = (map_cache->m_mp[cur_tile_idx_cursor.x][cur_tile_idx_cursor.y] == (int)TileType::Idle)
		|| (map_cache->m_mp[cur_tile_idx_cursor.x][cur_tile_idx_cursor.y] == (int)TileType::End);

	if (is_in_target_tile && is_button_down)
	{
		//检查与当前游标所在的格子与当前角色所在的格子的曼哈顿距离是否为1
        int manhaton_distance = abs(idx_cur.x - cur_tile_idx_cursor.x) + abs(idx_cur.y - cur_tile_idx_cursor.y);
        if (manhaton_distance == 1)
        {
            idx_cur = cur_tile_idx_cursor;
            //更新缓存地图和角色位置
            if (map_cache->m_mp[idx_cur.x][idx_cur.y] != (int)TileType::End)
            {
                map_cache->m_mp[idx_cur.x][idx_cur.y] = (int)TileType::Selected;
                tile_num_owned++;
            }
        }
	}  
}

六、随机地图的生成

1.整体生成策略

核心逻辑是「多次尝试随机生成→失败则使用固定保底地图」,确保地图 100% 有解:

(1)最多尝试 10 次生成随机单路径地图;

(2)若所有尝试失败,直接使用预设的固定路径地图(保底方案);

(3)最后通过 BFS 校验地图的可达性,确保从起点能走到所有有效格子和终点。

2.随机单路径地图生成(核心函数 generate_single_path_map

这是随机地图的核心逻辑,目标是生成一条从起点到终点的连续路径:

(1)起点 / 终点固定化:不随机选起点终点,而是预设 4 种对角 / 近对角模式(如 (0,0)→(7,7)、(0,7)→(7,0)),避免起点终点过近导致路径过短;

(2)网格初始化:先把 8x8 网格全部设为「禁用格(-1)」,仅标记起点(Start)和终点(End);

(3)路径生成规则

  • 从起点出发,每一步只向「上下左右」4 个方向移动;
  • 仅能移动到「未访问、在网格内、禁用格 / 终点格」的位置;
  • 移动偏好:70% 概率朝终点方向走(减少无效绕路),30% 随机走(增加路径随机性);

(4)回溯机制:若当前位置无可行移动方向,最多回溯 3 步(删除最后几步路径、恢复格子为禁用态),重新寻找可行方向;

(5)有效性校验:生成后必须满足两个条件才算成功:

  • 路径最终到达终点;
  • 路径上的「正常格(Idle)」数量≥8(避免路径过短)。

3.保底地图机制(兜底方案)

当随机生成多次失败时,用预设的固定路径兜底,保证游戏有解:

(1)复杂保底(generate_fallback_map):预设一条蛇形的长路径,覆盖网格大部分区域,路径复杂但 100% 连通;

(2)简单保底(generate_simple_fallback_map):备用的 L 形简单路径(从 (0,0) 向右走满第一行,再向下走到 (7,7)),逻辑更简单,适合兜底。

4.可达性校验(is_map_reachable)

生成地图后,用「广度优先搜索(BFS)」验证地图的有效性

(1)从起点出发,遍历所有可达的格子;

(2)校验「所有正常格(Idle)+ 终点格(End)」是否都能从起点到达;

(3)确保生成的地图没有 "孤立的有效格子",避免玩家无法完成游戏。

5.最终整合(generate_random_map)

封装整个生成流程:

(1)调用随机生成函数,最多尝试 10 次;

(2)失败则切换到保底地图;

(3)复制生成的地图数据到游戏的地图对象中;

(4)记录起点位置和需要走过的正常格数量(用于游戏通关判定)。

cpp 复制代码
  bool is_map_reachable()
  {
      // 找到起点
      SDL_Point start;
      bool found_start = false;

      for (int i = 0; i < 8; ++i) {
          for (int j = 0; j < 8; ++j) {
              if (map_ini->m_mp[i][j] == (int)TileType::Start) {
                  start.x = i;
                  start.y = j;
                  found_start = true;
                  break;
              }
          }
          if (found_start) break;
      }

      if (!found_start) return false;

      // 使用BFS检查从起点是否可以到达所有正常格、已选格和终点
      bool visited[8][8] = { false };
      std::queue<SDL_Point> q;
      q.push(start);
      visited[start.x][start.y] = true;

      int reachable_count = 0;
      int total_reachable = 0;

      // 计算需要到达的格子总数
      for (int i = 0; i < 8; ++i) {
          for (int j = 0; j < 8; ++j) {
              int tile = map_ini->m_mp[i][j];
              if (tile == (int)TileType::Idle || tile == (int)TileType::End) {
                  total_reachable++;
              }
          }
      }

      // BFS
      while (!q.empty()) {
          SDL_Point current = q.front();
          q.pop();

          // 检查当前格子
          int tile = map_ini->m_mp[current.x][current.y];
          if (tile == (int)TileType::Idle || tile == (int)TileType::End) {
              reachable_count++;
          }

          // 检查四个方向
          SDL_Point directions[4] = { {-1, 0}, {1, 0}, {0, -1}, {0, 1} };
          for (const auto& dir : directions) {
              SDL_Point next = { current.x + dir.x, current.y + dir.y };

              // 检查边界
              if (next.x < 0 || next.x >= 8 || next.y < 0 || next.y >= 8) continue;

              // 检查是否已访问
              if (visited[next.x][next.y]) continue;

              // 检查是否可通行(不是禁用格)
              int next_tile = map_ini->m_mp[next.x][next.y];
              if (next_tile != (int)TileType::Failed) {
                  visited[next.x][next.y] = true;
                  q.push(next);
              }
          }
      }

      // 如果所有需要到达的格子都可达,返回true
      return reachable_count == total_reachable;
  }
cpp 复制代码
// 尝试生成一条单一路径的地图
bool generate_single_path_map(int maze[8][8], std::mt19937& gen)
{
    std::uniform_int_distribution<> dis(0, 7);
    std::uniform_int_distribution<> percent_dis(0, 99);

    // 1. 随机选择起点和终点(确保它们在不同位置)
    SDL_Point start, end;

    // 使用4种固定模式,确保距离足够
    int pattern = percent_dis(gen) % 4;
    switch (pattern) {
    case 0: start = { 0, 0 }; end = { 7, 7 }; break;
    case 1: start = { 0, 7 }; end = { 7, 0 }; break;
    case 2: start = { 1, 1 }; end = { 6, 6 }; break;
    case 3: start = { 1, 6 }; end = { 6, 1 }; break;
    }

    // 2. 初始化地图
    for (int i = 0; i < 8; ++i) {
        for (int j = 0; j < 8; ++j) {
            maze[i][j] = -1; // 全部设为禁用格
        }
    }

    maze[start.x][start.y] = (int)TileType::Start;
    maze[end.x][end.y] = (int)TileType::End;

    // 3. 生成路径
    std::vector<SDL_Point> path;
    path.push_back(start);

    SDL_Point current = start;
    bool visited[8][8] = { false };
    visited[start.x][start.y] = true;

    int max_steps = 30; // 最大步数限制
    int step = 0;

    while ((current.x != end.x || current.y != end.y) && step < max_steps) {
        step++;

        // 获取所有可能的移动方向
        std::vector<SDL_Point> possible_moves;
        SDL_Point moves[4] = { {-1, 0}, {1, 0}, {0, -1}, {0, 1} };

        for (const auto& move : moves) {
            SDL_Point next = { current.x + move.x, current.y + move.y };

            // 检查边界
            if (next.x < 0 || next.x >= 8 || next.y < 0 || next.y >= 8) {
                continue;
            }

            // 检查是否已访问
            if (visited[next.x][next.y]) {
                continue;
            }

            // 检查格子类型:只能移动到禁用格或终点
            if (maze[next.x][next.y] == -1 || maze[next.x][next.y] == (int)TileType::End) {
                possible_moves.push_back(next);
            }
        }

        // 如果没有可能的移动,路径生成失败
        if (possible_moves.empty()) {
            // 尝试回溯一步(最多回溯3步)
            bool backtrack_success = false;
            for (int backtrack = 0; backtrack < 3 && path.size() > 1; ++backtrack) {
                // 移除最后一步
                path.pop_back();
                SDL_Point prev = path.back();

                // 将当前位置恢复为禁用格
                if (!(current.x == start.x && current.y == start.y) &&
                    !(current.x == end.x && current.y == end.y)) {
                    maze[current.x][current.y] = -1;
                    visited[current.x][current.y] = false;
                }

                current = prev;

                // 重新检查可能的移动
                possible_moves.clear();
                for (const auto& move : moves) {
                    SDL_Point next = { current.x + move.x, current.y + move.y };

                    if (next.x < 0 || next.x >= 8 || next.y < 0 || next.y >= 8) continue;
                    if (visited[next.x][next.y]) continue;
                    if (maze[next.x][next.y] == -1 || maze[next.x][next.y] == (int)TileType::End) {
                        possible_moves.push_back(next);
                    }
                }

                if (!possible_moves.empty()) {
                    backtrack_success = true;
                    break;
                }
            }

            if (!backtrack_success) {
                return false; // 生成失败
            }
        }

        // 选择下一个移动
        SDL_Point next;

        // 计算到终点的方向
        int dx = end.x - current.x;
        int dy = end.y - current.y;

        // 创建一个偏好列表
        std::vector<SDL_Point> preferred_moves;
        std::vector<SDL_Point> other_moves;

        for (const auto& move : possible_moves) {
            // 判断是否朝向终点
            bool toward_end = false;
            if (dx > 0 && move.x > current.x) toward_end = true;
            if (dx < 0 && move.x < current.x) toward_end = true;
            if (dy > 0 && move.y > current.y) toward_end = true;
            if (dy < 0 && move.y < current.y) toward_end = true;

            if (toward_end) {
                preferred_moves.push_back(move);
            }
            else {
                other_moves.push_back(move);
            }
        }

        // 70%概率选择朝向终点的方向,30%概率选择其他方向
        if (!preferred_moves.empty() && percent_dis(gen) % 100 < 70) {
            std::uniform_int_distribution<> pref_dis(0, preferred_moves.size() - 1);
            next = preferred_moves[pref_dis(gen)];
        }
        else if (!other_moves.empty()) {
            std::uniform_int_distribution<> other_dis(0, other_moves.size() - 1);
            next = other_moves[other_dis(gen)];
        }
        else {
            std::uniform_int_distribution<> move_dis(0, possible_moves.size() - 1);
            next = possible_moves[move_dis(gen)];
        }

        // 移动到下一个位置
        current = next;
        visited[current.x][current.y] = true;
        path.push_back(current);

        // 将路径上的格子设为正常格(除了起点和终点)
        if (!(current.x == start.x && current.y == start.y) &&
            !(current.x == end.x && current.y == end.y)) {
            maze[current.x][current.y] = (int)TileType::Idle;
        }
    }

    // 检查是否到达终点
    if (current.x != end.x || current.y != end.y) {
        return false; // 没有到达终点
    }

    // 检查路径长度是否合理(至少10个Idle格子)
    int idle_count = 0;
    for (int i = 0; i < 8; ++i) {
        for (int j = 0; j < 8; ++j) {
            if (maze[i][j] == (int)TileType::Idle) {
                idle_count++;
            }
        }
    }

    if (idle_count < 8) {
        return false; // 路径太短
    }

    return true; // 生成成功
}

// 生成保底地图(永远不会失败)
void generate_fallback_map(int maze[8][8])
{
    // 使用一个固定的、永远不会失败的蛇形路径
    for (int i = 0; i < 8; ++i) {
        for (int j = 0; j < 8; ++j) {
            maze[i][j] = -1; // 全部设为禁用格
        }
    }

    // 起点在(0,0),终点在(7,7)
    maze[0][0] = (int)TileType::Start;
    maze[7][7] = (int)TileType::End;

    // 生成一个复杂的蛇形路径
    // 第一行:从(0,0)到(0,6)
    for (int j = 1; j <= 6; ++j) {
        maze[0][j] = (int)TileType::Idle;
    }

    // 向下:(0,6)到(6,6)
    for (int i = 1; i <= 6; ++i) {
        maze[i][6] = (int)TileType::Idle;
    }

    // 向左:(6,6)到(6,1)
    for (int j = 5; j >= 1; --j) {
        maze[6][j] = (int)TileType::Idle;
    }

    // 向上:(6,1)到(1,1)
    for (int i = 5; i >= 1; --i) {
        maze[i][1] = (int)TileType::Idle;
    }

    // 向右:(1,1)到(1,5)
    for (int j = 2; j <= 5; ++j) {
        maze[1][j] = (int)TileType::Idle;
    }

    // 向下:(1,5)到(5,5)
    for (int i = 2; i <= 5; ++i) {
        maze[i][5] = (int)TileType::Idle;
    }

    // 向左:(5,5)到(5,2)
    for (int j = 4; j >= 2; --j) {
        maze[5][j] = (int)TileType::Idle;
    }

    // 向上:(5,2)到(2,2)
    for (int i = 4; i >= 2; --i) {
        maze[i][2] = (int)TileType::Idle;
    }

    // 向右:(2,2)到(2,4)
    for (int j = 3; j <= 4; ++j) {
        maze[2][j] = (int)TileType::Idle;
    }

    // 向下:(2,4)到(4,4)
    for (int i = 3; i <= 4; ++i) {
        maze[i][4] = (int)TileType::Idle;
    }

    // 向左:(4,4)到(4,3)
    maze[4][3] = (int)TileType::Idle;

    // 向下:(4,3)到(7,3)
    for (int i = 5; i <= 7; ++i) {
        maze[i][3] = (int)TileType::Idle;
    }

    // 向右:(7,3)到(7,7)
    for (int j = 4; j <= 7; ++j) {
        if (maze[7][j] != (int)TileType::End) {
            maze[7][j] = (int)TileType::Idle;
        }
    }
}

// 更简单的保底地图(如果上面的太复杂)
void generate_simple_fallback_map(int maze[8][8])
{
    // 最简单的L形路径
    for (int i = 0; i < 8; ++i) {
        for (int j = 0; j < 8; ++j) {
            maze[i][j] = -1;
        }
    }

    maze[0][0] = (int)TileType::Start;
    maze[7][7] = (int)TileType::End;

    // 向右走7步
    for (int j = 1; j <= 7; ++j) {
        maze[0][j] = (int)TileType::Idle;
    }

    // 向下走6步(跳过已经设置为Idle的(0,7))
    for (int i = 1; i <= 6; ++i) {
        maze[i][7] = (int)TileType::Idle;
    }
}

void GameScene::generate_random_map()
{
    // 创建随机数生成器
    std::random_device rd;
    std::mt19937 gen(rd());
    std::uniform_int_distribution<> dis(0, 7);
    std::uniform_int_distribution<> percent_dis(0, 99);

    // 1. 先预设一个简单的地图作为保底
    int temp_map[8][8];

    // 2. 使用多次尝试的方法,如果失败则使用保底方案
    bool success = false;
    int max_attempts = 10; // 最多尝试10次

    for (int attempt = 0; attempt < max_attempts && !success; ++attempt) {
        success = generate_single_path_map(temp_map, gen);
        if (success) {
            SDL_Log(u8"第%d次尝试生成地图成功", attempt + 1);
        }
    }

    // 3. 如果所有尝试都失败,使用保底方案
    if (!success) {
        SDL_Log(u8"多次尝试失败,使用保底地图");
        generate_fallback_map(temp_map);
    }

    // 4. 更新地图对象
    delete map_ini;
    delete map_cache;

    map_ini = new Map();
    map_cache = new Map();

    // 复制地图数据
    for (int i = 0; i < 8; ++i) {
        for (int j = 0; j < 8; ++j) {
            map_ini->m_mp[i][j] = temp_map[i][j];
            map_cache->m_mp[i][j] = temp_map[i][j];
        }
    }

    // 5. 重新计算需要走过的格子数和起点位置
    tile_num_needed = 0;
    for (int i = 0; i < 8; ++i) {
        for (int j = 0; j < 8; ++j) {
            if (map_ini->m_mp[i][j] == (int)TileType::Start) {
                idx_cur.x = i;
                idx_cur.y = j;
                SDL_Log(u8"地图起点: (%d, %d)", idx_cur.x, idx_cur.y);
            }
            else if (map_ini->m_mp[i][j] == (int)TileType::Idle) {
                tile_num_needed++;
            }
        }
    }

    SDL_Log(u8"地图生成成功!需要走过的格子数: %d", tile_num_needed);
}

七、文件导入和导出

这个直接交给AI去写就好了,知道你自己能实现什么功能,然后在下次想要实现相同的功能的时候,就不要过多地去重复工作了。

cpp 复制代码
void GameScene::import_map()
{
    // 1. 初始化Windows打开文件对话框
    OPENFILENAME ofn = { 0 };
    wchar_t szOpenPath[MAX_PATH] = L""; // 存储用户选择的路径(宽字符)

    ofn.lStructSize = sizeof(OPENFILENAME);
    ofn.hwndOwner = NULL; // 若有SDL窗口句柄,可填 SDL_GetWindowFromRenderer(renderer) 关联窗口
    ofn.lpstrFilter = L"CSV文件 (*.csv)\0*.csv\0所有文件 (*.*)\0*.*\0"; // 文件筛选器
    ofn.lpstrFile = szOpenPath;
    ofn.nMaxFile = MAX_PATH;
    ofn.Flags = OFN_EXPLORER | OFN_FILEMUSTEXIST | OFN_HIDEREADONLY; // 文件必须存在
    ofn.lpstrTitle = L"选择地图文件"; // 对话框标题

    // 2. 弹出打开对话框,用户选择文件
    if (GetOpenFileName(&ofn))
    {
        // 宽字符转多字节(适配C++文件操作)
        char openPath[MAX_PATH] = { 0 };
        WideCharToMultiByte(CP_ACP, 0, szOpenPath, -1, openPath, MAX_PATH, NULL, NULL);

        SDL_Log(u8"导入地图文件:%s", openPath);

        try
        {
            // 3. 创建新的地图对象
            Map* newMap = new Map(openPath);

            // 4. 检查地图有效性(8x8)
            bool validMap = true;
            int startCount = 0;
            int endCount = 0;

            // 验证地图数据
            for (int i = 0; i < 8; ++i)
            {
                for (int j = 0; j < 8; ++j)
                {
                    int tileValue = newMap->m_mp[i][j];

                    // 检查值范围
                    if (tileValue < -1 || tileValue > 3)
                    {
                        validMap = false;
                        SDL_Log(u8"错误:地图数据超出范围 (%d, %d): %d", i, j, tileValue);
                    }

                    // 统计起点和终点
                    if (tileValue == (int)TileType::Start)
                        startCount++;
                    else if (tileValue == (int)TileType::End)
                        endCount++;
                }
            }

            // 检查起点和终点数量
            if (startCount != 1 || endCount != 1)
            {
                validMap = false;
                SDL_Log(u8"错误:地图必须有且只有一个起点和终点。起点数:%d,终点数:%d", startCount, endCount);
            }

            if (validMap)
            {
                // 5. 删除旧地图,使用新地图
                delete map_ini;
                delete map_cache;

                map_ini = new Map(openPath);
                map_cache = new Map(openPath);

                // 6. 重置游戏状态
                tile_num_needed = 0;
                tile_num_owned = 0;

                // 重新计算需要走过的格子数和起点位置
                for (int i = 0; i < 8; ++i)
                {
                    for (int j = 0; j < 8; ++j)
                    {
                        if (map_ini->m_mp[i][j] == (int)TileType::Start)
                        {
                            idx_cur.x = i;
                            idx_cur.y = j;
                            SDL_Log(u8"新起点位置: %d, %d", idx_cur.x, idx_cur.y);
                        }
                        else if (map_ini->m_mp[i][j] == (int)TileType::Idle)
                        {
                            tile_num_needed++;
                        }
                    }
                }

                // 7. 重置游戏
                game_restart();

                SDL_Log(u8"地图导入成功!需要走过的格子数:%d", tile_num_needed);
            }
            else
            {
                // 无效地图,删除临时对象
                delete newMap;
                SDL_Log(u8"导入失败:地图文件格式无效");

                // 可选:显示错误消息框
                MessageBox(NULL,
                    L"地图文件格式无效!\n请确保:\n1. 地图为8x8格\n2. 数值范围:-1到3\n3. 有且只有一个起点(2)和终点(3)",
                    L"导入错误",
                    MB_OK | MB_ICONERROR);
            }
        }
        catch (const std::exception& e)
        {
            SDL_Log(u8"导入失败:读取文件时发生错误 - %s", e.what());
             
            // 可选:显示错误消息框
            MessageBox(NULL,
                L"读取文件时发生错误!\n请确保文件格式正确且未被占用。",
                L"导入错误",
                MB_OK | MB_ICONERROR);
        }
    }
    else
    {
        SDL_Log(u8"用户取消了导入操作");
    }
}

void GameScene::export_map()
{
    // 1. 初始化Windows保存文件对话框
    OPENFILENAME ofn = { 0 };
    wchar_t szSavePath[MAX_PATH] = L""; // 存储用户选择的路径(宽字符)

    ofn.lStructSize = sizeof(OPENFILENAME);
    ofn.hwndOwner = NULL; // 若有SDL窗口句柄,可填 SDL_GetWindowFromRenderer(renderer) 关联窗口
    ofn.lpstrFilter = L"CSV文件 (*.csv)\0*.csv\0所有文件 (*.*)\0*.*\0"; // 文件筛选器
    ofn.lpstrFile = szSavePath;
    ofn.nMaxFile = MAX_PATH;
    ofn.lpstrDefExt = L"csv"; // 默认扩展名
    ofn.Flags = OFN_EXPLORER | OFN_HIDEREADONLY | OFN_OVERWRITEPROMPT; // 覆盖提示、隐藏只读
    ofn.lpstrTitle = L"导出地图为CSV"; // 对话框标题

    // 2. 弹出保存对话框,用户选择路径后写入文件
    if (GetSaveFileName(&ofn))
    {
        // 宽字符转多字节(适配C++文件操作)
        char savePath[MAX_PATH] = { 0 };
        WideCharToMultiByte(CP_ACP, 0, szSavePath, -1, savePath, MAX_PATH, NULL, NULL);

        // 3. 打开文件并写入8x8地图数据
        std::ofstream csvFile(savePath);
        if (!csvFile.is_open())
        {
            SDL_Log(u8"导出失败:无法打开文件 %s", savePath);
            return;
        }

        // 遍历map_cache的8x8数组,按行写入CSV
        for (int i = 0; i < 8; ++i)
        {
            for (int j = 0; j < 8; ++j)
            {
                if(map_cache->m_mp[i][j] == (int)TileType::Selected)
                    csvFile << (int)TileType::Idle; // 将选中格转换为空闲格
                else csvFile << map_cache->m_mp[i][j]; // 写入当前格子值
                if (j != 7) csvFile << ",";       // 最后一列不加逗号
            }
            csvFile << std::endl; // 每行结束换行
        }

        csvFile.close();
        SDL_Log(u8"地图导出成功!路径:%s", savePath);
    }
    else
    {
        SDL_Log(u8"用户取消了导出操作");
    }
}
相关推荐
程序员-King.2 小时前
day140—前后指针—删除排序链表中的重复元素Ⅱ(LeetCode-82)
数据结构·算法·leetcode·链表
小尧嵌入式2 小时前
【Linux开发一】类间相互使用|继承类和构造写法|虚函数实现多态|五子棋游戏|整数相除混合小数|括号使用|最长问题
开发语言·c++·算法·游戏
你的冰西瓜2 小时前
C++中的map容器详解
开发语言·c++·stl
BHXDML2 小时前
第三章:聚类算法
算法·机器学习·聚类
向前V2 小时前
Flutter for OpenHarmony数独游戏App实战:胜利弹窗
java·flutter·游戏
仙俊红2 小时前
二分查找边界模板:第一个 > target / 第一个 < target(找不到就返回边界)
算法
苦藤新鸡2 小时前
16.求数组除了当前元素的所有乘积
算法·leetcode·动态规划
Benny_Tang2 小时前
题解:P14841 [THUPC 2026 初赛] 哈姆星与古地球学术行为影响星球文明的考古学分析
c++·算法
WilliamHu.2 小时前
A2A协议
java·数据结构·算法