双人塔防游戏笔记(未完结)🎮️
目前尚未完结,文章思路会有些混乱,慎读
项目总览
游戏制作路线
技术难点
1.SDL中画面渲染的实现
SDL画面渲染流程
(1)资源丢给渲染器
(2)生成纹理
(3)纹理+Rect丢给渲染器渲染
(4)渲染器将渲染好的内容渲染到窗口上,即绘制画面
// 创建窗口
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帧
// 动态延时
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++中的结构体,可以保存我们的玩家属性、怪物属性等配置信息。
{
"name" : "XiaoMing",
"age" : 18,
"pets" : ["dog", "cat", "bird"]
}
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
代码实现
#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解析流程示意图
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.可继承单例模板的实现
什么是单例模式
单例模式是指在内存中只会创建且仅创建一次对象的设计模式。在程序中多次使用同一个对象且作用相同时,为了防止频繁地创建对象使得内存飙升,单例模式可以让程序仅在内存中创建一个对象,让所有需要调用的地方都共享这一单例对象。
可继承单例模式示意图
可继承单例模式的最小代码实现
// 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> 的作用
5.洋流图
什么是洋流图?
洋流图是一个高效的寻路策略,广泛应用于许多实时策略(RTS)游戏和需要处理大量单位的游戏中。它通过在地图上预先设定的方向性箭头,帮助游戏中的单位快速确定移动路径,从而显著降低计算性能的需求。
具体而言,洋流图的原理类似于洋流的流向,每个单位在生成后,不必实时计算最佳路径,而是可以直接根据地图上预先定义的箭头信息,沿着指定方向行进。这种方式有效地将寻路算法的时间复杂度降至最低,只需对数组进行简单检索。
在塔防游戏中,采用洋流图的优势主要体现在两个方面:
1.性能优化:通过在设计地图时预先规划好移动路线,单位在行进过程中只需遵循这些方向指示,避免了复杂的实时路径计算,从而节省了计算资源。
2.路径控制:洋流图允许开发者指定单位的行进路径,而不是依赖动态生成的路径算法。这样可以确保敌人沿着预定路线移动,使得玩家能够在这些路径上合理布局防御塔,避免出现不可预见的行进模式。
综上所述,洋流图以其高效性和可控性,成为游戏设计中不可或缺的一部分,特别是在需要精确掌控敌人移动策略的场景中。
实现从地图缓存中解析洋流图路径
6.地图设计
地图设计为双层图,底层是瓦片地图纹理,顶层为行进方向、家、防御塔、装饰设计。
7.计时器设计
8.vector2二维向量
小知识点
如何理解enum class ...
游戏主体
GameManager的实现
注意:
- 流程图中的instance()实例化函数是继承于manager中的,这就是manager单例模式的作用
- GameManager()流程的是实例化运行的构造函数以及程序结束后运行的析构函数
- GameManager->run() 就是运行整个游戏的方法
GameManager的构造函数
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的析构函数
~GameManager()
{
SDL_DestroyRenderer(renderer);
SDL_DestroyWindow(window);
TTF_Quit();
Mix_Quit();
IMG_Quit();
SDL_Quit();
}
GameManager的run函数
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首尾的空格符去除
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属性
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)
: 加载地图
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;
}
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 });
}
}
}
}