Skip to content

双人塔防游戏笔记(未完结)🎮️

目前尚未完结,文章思路会有些混乱,慎读

项目总览

游戏制作路线

技术难点

1.SDL中画面渲染的实现

SDL画面渲染流程

(1)资源丢给渲染器
(2)生成纹理
(3)纹理+Rect丢给渲染器渲染
(4)渲染器将渲染好的内容渲染到窗口上,即绘制画面

C++
// 创建窗口
SDL_Window* window = SDL_CreateWindow(u8"你好,世界", SDL_WINDOWPOS_CENTERED, SDL_WINDOWPOS_CENTERED, 1280, 720, SDL_WINDOW_SHOWN);
// 创建渲染器
SDL_Renderer* renderer = SDL_CreateRenderer(window, -1, SDL_RENDERER_ACCELERATED); 

// 加载图片
SDL_Surface* suf_img = IMG_Load("avatar.jpg");
// 渲染纹理
SDL_Texture* tex_img = SDL_CreateTextureFromSurface(renderer, suf_img); 

// 将纹理渲染到渲染器上
SDL_RenderCopy(renderer, tex_img, nullptr, &rect_img);

// 所有纹理都渲染到渲染器之后
SDL_RenderPresent(renderer); // 让渲染器的内容绘制到窗口上
游戏设计中画面渲染流程

srcrect : source_rect dstrect : destination_rect

2.动态延时功能的实现(帧率控制)

作用:节约CPU资源,将画面帧率控制在60帧

C++
// 动态延时
Uint64 last_counter = SDL_GetPerformanceCounter(); // 循环开始之前,当前计数器的计数
Uint64 counter_freq = SDL_GetPerformanceFrequency(); // 计数器的频率,计数 / 频率 = 用时 单位为秒(s)

while()
{
    Uint64 current_counter = SDL_GetPerformanceCounter();
    double delta = (double)(current_counter - last_counter) / counter_freq; // 上次循环用了delta秒
    last_counter = current_counter;

    if (delta * 1000 < 1000.0 / 60) // 如果一次循环所用的时间小于期望的帧数时间,则延迟
        SDL_Delay((Uint32)(1000.0 / 60 - delta * 1000)); // 延迟这一帧结束的时间间隔
}

3.JSON/CSV格式解析

为什么使用json/csv格式

1.json的格式具有通用性,严格性 => 可以使我们的游戏更好的拓展,便于游戏开发。
2.json有完整的生态如cjson,方便我们的读写。

解析的内容

JOSN:json就像C++中的结构体,可以保存我们的玩家属性、怪物属性等配置信息。

json
{
    "name" : "XiaoMing",
    "age" : 18,
    "pets" : ["dog", "cat", "bird"]
}

CSV:csv类似于表格,是一种逗号分隔文件,可以用来存储二维数据,所以用来保存我们的地图数据。

csv
1,2,3
4,5,6
7,8,9
解析json文件
用到的函数和类型

函数:

  • cJSON_Parse(char) 解析字符串为cJSON类型
  • cJSON_GetObjectItem(Cjson, string(key)) 找出Cjson中键为string的键值对

类型:

  • std::ifstream 用来判断文件是否读入成功
  • std::stringstream string流用来读取文件
  • cJSON* cJSON指针,指向json中的键值对
json解析流程和Cjson结构剖析示意图
小细节:如何解析数组形式的value

cJSON_ArrayForEach(element, array) 可以遍历整个array,赋值给element

代码实现
c++
#include<fstream>
#include<sstream>
#include<iostream>
#include<cJSON.h>

void test_json()
{
	std::ifstream file("test.json");
	if (!file.good())
	{
		std::cout << "无法打开文件" << std::endl;
		return;
	}

	std::stringstream str_stream;
	str_stream << file.rdbuf(); // rdbuf()将该文件的全部内容读到str_stream中
	file.close();

	cJSON* json_root = cJSON_Parse(str_stream.str().c_str()); // json_root是整个json对象的指针,解析str_stream的字符串并且转换成char类型
	
	cJSON* json_name = cJSON_GetObjectItem(json_root, "name");
	cJSON* json_age = cJSON_GetObjectItem(json_root, "age");
	cJSON* json_pets = cJSON_GetObjectItem(json_root, "pets");

	std::cout << json_name->string << ": " << json_name->valuestring << std::endl;
	std::cout << json_age->string << ": " << json_age->valueint << std::endl;

	std::cout << json_pets->string << ": " << std::endl;

	cJSON* json_item = nullptr;
	cJSON_ArrayForEach(json_item, json_pets) // 实质是一个for循环使用json_pets数组的值给json_item赋值
	{
		std::cout << "\t" << json_item->valuestring << std::endl; // 将json_item打印出来
	}

}
解析csv文件
csv解析流程示意图
c++
void test_csv()
{
	std::ifstream file("test.csv");
	if (!file.good())
	{
		std::cout << "无法打开文件" << std::endl;
		return;
	}

	std::string str_line;
	while (std::getline(file, str_line))
	{
		std::string str_grid;
		std::stringstream str_stream(str_line);
		while (std::getline(str_stream, str_grid, ',')) // 从
		{
			std::cout << str_grid << " ";
		}
		std::cout << std::endl;
	}

	file.close();
}

4.可继承单例模板的实现

什么是单例模式

单例模式是指在内存中只会创建且仅创建一次对象的设计模式。在程序中多次使用同一个对象且作用相同时,为了防止频繁地创建对象使得内存飙升,单例模式可以让程序仅在内存中创建一个对象,让所有需要调用的地方都共享这一单例对象。 pA0YSm9.png

可继承单例模式示意图
可继承单例模式的最小代码实现
c++
// main.cpp
#include<iostream>

#include "game_manager.h"

int main(int argc, char** argv)
{
	return GameManager::instance()->run(argc, argv);
}

// manager.h
#ifndef _MANAGER_H_
#define _MANAGER_H_

template <typename T> 
class Manager // 单例模式
{
public:
	static T* instance()
	{
		if (!manager)
			manager = new T();

		return manager;
	} // 懒加载
	

private:
	static T* manager;

protected:
	~Manager() = default;
	Manager() = default;
	Manager(const Manager&) = delete;
	Manager& operator=(const Manager&) = delete;

};

template <typename T>
T* Manager<T>::manager = nullptr;

#endif // !_MANAGER_H_

// 继承manager的子类game_manager
#ifndef _GAME_MANAGER_H_
#define _GAME_MANAGER_H_

#include "manager.h"

class GameManager : public Manager<GameManager>
{
    friend class Manager<GameManager>

public:

protected:
    GameManager()
    {

    }

    ~GameManager()
    {

    }
}

#endif // !_GAME_MANAGER_H_
template <typename T> 的作用

pA0JdzD.png

5.洋流图

什么是洋流图?

洋流图是一个高效的寻路策略,广泛应用于许多实时策略(RTS)游戏和需要处理大量单位的游戏中。它通过在地图上预先设定的方向性箭头,帮助游戏中的单位快速确定移动路径,从而显著降低计算性能的需求。

具体而言,洋流图的原理类似于洋流的流向,每个单位在生成后,不必实时计算最佳路径,而是可以直接根据地图上预先定义的箭头信息,沿着指定方向行进。这种方式有效地将寻路算法的时间复杂度降至最低,只需对数组进行简单检索。

在塔防游戏中,采用洋流图的优势主要体现在两个方面:
1.性能优化:通过在设计地图时预先规划好移动路线,单位在行进过程中只需遵循这些方向指示,避免了复杂的实时路径计算,从而节省了计算资源。
2.路径控制:洋流图允许开发者指定单位的行进路径,而不是依赖动态生成的路径算法。这样可以确保敌人沿着预定路线移动,使得玩家能够在这些路径上合理布局防御塔,避免出现不可预见的行进模式。

综上所述,洋流图以其高效性和可控性,成为游戏设计中不可或缺的一部分,特别是在需要精确掌控敌人移动策略的场景中。

实现从地图缓存中解析洋流图路径

6.地图设计

地图设计为双层图,底层是瓦片地图纹理,顶层为行进方向、家、防御塔、装饰设计。

7.计时器设计

8.vector2二维向量

小知识点

如何理解enum class ...

游戏主体

GameManager的实现

注意:

  • 流程图中的instance()实例化函数是继承于manager中的,这就是manager单例模式的作用
  • GameManager()流程的是实例化运行的构造函数以及程序结束后运行的析构函数
  • GameManager->run() 就是运行整个游戏的方法
GameManager的构造函数
c++
GameManager()
	{
		// 库的初始化
		init_assert(!SDL_Init(SDL_INIT_EVERYTHING), u8"SDL2 初始化失败! "); // SDL_Init成功返回的是0
		init_assert(IMG_Init(IMG_INIT_JPG | IMG_INIT_PNG), u8"SDL_image 初始化失败! ");// IMG_Init成功返回的是一个非零值
		init_assert(Mix_Init(MIX_INIT_MP3), u8"SDL_mixer 初始化失败! ");
		init_assert(!TTF_Init(), u8"SDL_ttf 初始化失败!");

		// 打开声道
		Mix_OpenAudio(44100, MIX_DEFAULT_FORMAT, 2, 2048);

		SDL_SetHint(SDL_HINT_IME_SHOW_UI, "1"); // 启用多语言显示

		// 从configmanager中调用方法
		ConfigManager* config = ConfigManager::instance();

		init_assert(config->map.load("map.csv"), u8"加载游戏地图失败!");
		init_assert(config->load_level_config("level.json"), u8"加载关卡配置失败!");
		init_assert(config->load_game_config("config.json"), u8"加载配置失败!");

		window = SDL_CreateWindow(config->basic_template.window_title.c_str(), SDL_WINDOWPOS_CENTERED, SDL_WINDOWPOS_CENTERED,
			config->basic_template.window_width, config->basic_template.window_height, SDL_WINDOW_SHOWN);
		init_assert(window, u8"创建游戏窗口失败!");

		// 创建渲染器并开启硬件加速,垂直同步,渲染目标画到纹理上去的特性
		renderer = SDL_CreateRenderer(window, -1, SDL_RENDERER_ACCELERATED | SDL_RENDERER_PRESENTVSYNC | SDL_RENDERER_TARGETTEXTURE);
		init_assert(renderer, u8"创建渲染器失败");
		
		// 加载游戏资源
		init_assert(ResourcesManager::instance()->load_from_file(renderer), u8"加载游戏资源失败!");

		//
		init_assert(generate_tile_map_texture(), u8"生成瓦片地图失败!");
	}
GameManager的析构函数
c++
~GameManager()
	{
		SDL_DestroyRenderer(renderer);
		SDL_DestroyWindow(window);

		TTF_Quit();
		Mix_Quit();
		IMG_Quit();
		SDL_Quit();
	}
GameManager的run函数
c++
int run(int argc, char** argv)
	{
		Uint64 last_counter = SDL_GetPerformanceCounter();
		const Uint64 counter_freq = SDL_GetPerformanceFrequency();

		while (!is_quit)
		{
			// 1.处理输入
			while (SDL_PollEvent(&event))
				on_input();

			// 帧率控制
			Uint64 current_counter = SDL_GetPerformanceCounter();
			double delta = (double)(current_counter - last_counter) / counter_freq;
			last_counter = current_counter;
			if (delta * 1000 < 1000.0 / 60)
				SDL_Delay((Uint32)(1000.0 / 60 - delta * 1000));

			// 2.更新数据
			on_update(delta);

			// 3.绘制画面
			SDL_SetRenderDrawColor(renderer, 0, 0, 0, 255); // 渲染纯黑色背景
			SDL_RenderClear(renderer);

			on_render();

			SDL_RenderPresent(renderer);

		}

		return 0;
	}

地图解析

解析地图所需的类:
  • Tile : 用来存储瓦片数据
  • Map :
Map中需要的函数
  • std:string trim_str(const std::string& str) : 可以将str首尾的空格符去除
cpp
std::string trim_str(const std::string& str) // 对传入的字符串进行首尾空格去除操作并返回字符串
{
    // "     6\57\4\-1       "     =>    "6\57\4\-1"
    size_t begin_idx = str.find_first_not_of(" \t"); // 找出第一个不是空格的位置
    if (begin_idx == std::string::npos) // 没有找到begin_idx说明该字符串没有内容,直接return
        return "";
    size_t end_idx = str.find_last_not_of(" \t");
    size_t idx_range = end_idx - begin_idx + 1;

    return str.substr(begin_idx, idx_range); // 截取出来目标字符串
}
  • void load_tile_from_string(Tile& tile, const std::string& str) : 将单元格数据转化成Tile属性
C++
void load_tile_from_string(Tile& tile, const std::string& str) // 从字符串解析到tile对象中的函数
{
    std::string str_tidy = trim_str(str);
    
    std::string str_value;
    std::vector<int> values;
    std::stringstream str_stream(str_tidy);

    while (std::getline(str_stream, str_value, '\\'))
    {
        int value;
        try
        {
            value = std::stoi(str_value);
        }
        catch (const std::invalid_argument) // 读取的数据有异常
        {
            value = -1;
        }
        values.push_back(value);
    }

    tile.terrian = (values.size() < 1 || values[0] < 0) ? 0 : values[0];
    tile.decoration = (values.size() < 2) ? -1 : values[1];
    tile.direction = (Tile::Direction)((values.size() < 3 || values[2] < 0) ? 0 : values[2]);
    tile.special_flag = (values.size() <= 3) ? -1 : values[3];
}
  • bool load(const std::string& path) : 加载地图
cpp
bool load(const std::string& path) // 加载函数
	{
		std::ifstream file(path); // 打开path
		if (!file.good()) return false;

		TileMap tile_map_temp; // 临时的tilemap

		int idx_x = -1, idx_y = -1; // 当前读取的索引

		std::string str_line;
		while (std::getline(file, str_line)) // 从file中读取到str_line中
		{
			str_line = trim_str(str_line);
			if (str_line.empty()) // 跳过这一行
				continue;

			// 一行一行的读入
			idx_x = -1, idx_y++; 
			// 新增一行瓦片
			tile_map_temp.emplace_back();

			std::string str_tile;
			std::stringstream str_stream(str_line);

			// 从str_line中读,以','为分隔,每次循环赋值到str_tile中
			while (std::getline(str_stream, str_tile, ','))
			{
				idx_x++;
				// 新增一个瓦片
				tile_map_temp[idx_y].emplace_back();
				// 将这个瓦片引用给tile
				Tile& tile = tile_map_temp[idx_y].back();
				// 使用load_tile_from_string解析字符串
				load_tile_from_string(tile, str_tile);
			}
		}
		file.close();

		// 如果整个地图是空的
		if (tile_map_temp.empty() || tile_map_temp[0].empty())
			return false;

		tile_map = tile_map_temp; // 将临时地图复制到tilemap中

		generate_map_cache();

		return true;
	}
C++
void generate_map_cache() // 生成地图的home
{
    for (int y = 0; y < get_height(); y++)
    {
        for (int x = 0; x < get_width(); x++)
        {
            const Tile& tile = tile_map[y][x];
            if (tile.special_flag < 0)
                continue;

            if (tile.special_flag == 0)
            {
                idx_home.x = x;
                idx_home.y = y;
            }
            else
            {
                spawner_route_pool[tile.special_flag] = Route(tile_map, { x,y }); 
            }
        }
    }
}

游戏资源图鉴

tileset

pADIpf1.png

player

pA06KKI.png

coin

pArB0Dx.png

home

pArBfKI.png

tower

tower_axeman

pA06DaT.png

tower_gunner

pA06rIU.png

tower_archer

pA0WTv4.png

enemy

goblin

pA0yTvn.png

skeleton

pA0yHuq.png

goblin_priest

pA0yqbV.png

king_slime

pA0yOET.png

slime

pA06pvR.png

bullet

bullet_arrow

pA0WXUx.png

bullet_shell

pA0WxPK.png

bullet_axe

pA0Wz8O.png

ui

ui_heart

pA0fCKH.png

ui_coin

pA0fPrd.png

ui_home

pAB4bm4.png

ui_player_avatar

pAB4L79.png

ui_upgrade_hovered_left

pAB4XkR.png

ui_upgrade_hovered_right

pAB5F7d.png

ui_upgrade_hovered_top

pAB5AAA.png

ui_upgrade_idle

pAB5EtI.png

ui_place_hovered_left

pAB5e9P.png

ui_place_hovered_right

pAB5m1f.png

ui_place_hovered_top

pAB7aGR.png

ui_place_idle

pAB7dR1.png

ui_select_cursor

pAB7BM6.png

ui_loss_text

pAB7DsK.png

ui_win_text

pAB7yZD.png

ui_game_over_bar

pAB72id.png

effect

effect_impact_left

pAB75sf.png

effect_impact_down

pAB7TeS.png

effect_impact_right

pAD5hwQ.png

effect_impact_up

pAD5HS0.png

effect_flash_left

pAD5IFs.png

effect_flash_down

pAD5oYn.png

effect_flash_right

pAD5oYn.png

effect_flash_up

pADISYR.png

effect_explode

pAD5jw4.png

Last updated:

本站总访问量