文章目录
- 项目搭建
- 代码实现
- main.cpp
- object.h
- snake.h
- common.h
- 使用 demo
做到最后的话其实就只是验证了以前自己的一个想法,但是没有做成一个真正的游戏,可以算是一个 demo 而已吧,没做游戏的界面和关卡,不过完成了核心显式机制和功能之后,其实再搭建其他的玩法数值倒是很容易的,只不过现在时间紧张,只做了最关键的可玩部分,也没有很好的封装代码,不过可以明显感觉到的是,自己比以前本科的时候更具有设计的思想了,整个框架的设计会比较清晰一些,更会考虑封装和防御性编程方面的事情。
本来是想把这个画布分成均等的网格然后做普通的贪吃蛇的,做着做着想着做好玩一点于是改成跟着鼠标移动的轨迹了……
项目搭建
不想重新像本科大一大二的时候一样用字符界面做黑框框的程序,所以使用一个比较好的容易上手的 2D 图像库就是很重要的事情了,一般来说分为以下几种选择:
- EasyX、SFML
- Cocos2D、SDL2
- OpenGL、DirectX
为了快速实现简易的项目验证,所以这里使用 SFML2.1 框架,并且重新写一个 CMakeLists.txt 来完成项目的构建,而不是使用我之前那个非常死板的 CMake 模板,为此踩了不少坑,因为 CMake 更新的也有点快,但是 CMake 的官方文档除了函数的参数解释之外,没有使用示例。这里先记录一下 CMakeLists.txt 的几个坑:
- CMake 的函数名是不区分大小写的,但是 CMake 的很多选项是只能大写,坑
- CMake 的
include_directories
不会包含最顶层的目录!从给定的目录往下开始,也就是说如果头文件都是放在abc/
目录下,那么include_directories(abc/)
不能使得在代码中可以include<abc/xxx.h>
,因为顶层目录是不包含的,如果想在引入头文件的时候带上相对路径,则头文件的根目录本身需要被include_directories
之下,比如include_directories<include/abc>
,也就是还需要多建立一层额外的包装目录 - VScode 的 C++ intellisense 基于 CMake 在配置过程中生成的一条指令来解决头文件和库目录的解析问题,所以如果 CMake 不输出这个
compile_commands.json
的话,C++ 的 intellisense 就无法正常高亮代码工作 - CMake 无法把外部预编译好的第三方动态库视作内部的 target 类型变量,也就是说不能 install 这些外部的动态库,当然解决办法也是有的,不过要求 CMake 的版本比较新,要达到 3.21 才有这个功能,那就是
INSTALL(IMPORTED_RUNTIME_ARTIFACTS)
如果不查文档不知道还要琢磨到什么时候,反正就是一直报错ADD_LIBRARY(IMPORTED)
导入的外部库没有新建 target 类型变量,具体的原因和为什么 CMake 这么设计可以查看这篇 StackOverflow - 为什么需要
install
?因为一般使用动态库的话,最终生成的可执行文件必须和动态库在同一个目录下才能正常运行调试 FOREACH i RANGE n
不是常见编程语言那样理解的从 i 到 n-1 而真的是从 i 到 n ,问题 CMAKE 里面列表的下标又是从 0 开始的,所以如果通过这种取下标的方式同时遍历多个等长的序列的话会越界……只能傻傻地在循环中再添加一个IF
判断 i 是不是到了 n ……
临时起意做这个项目主要是最近找实习投了一些 C++ 的岗位,但是最近因为都在写 Golang 有段时间没写 C++ 了,很多特性以及八股也都有点忘了,所以回过头来捡起 C++ 用一下吧,复习复习。另外越来越觉得智能指针的好用了,用过 Python 或者 Golang 这种高级语言带有自动垃圾回收(比如标记清楚法之类的),C++ 就不想手动写释放内存了,还是智能指针好用。
CMake 两个自己写的觉得比较有用的小函数是找外部库的路径和名字以及头文件所在的路径,返回结果列表,然后再对这些列表中的每个元素进行 INCLUDE_DIRECTORIES
或者 ADD_LIBRARY
之类的操作,很方便,这样就不用自己手动添加每个头文件路径和外部预编译动态库了。
FUNCTION(FIND_LIBS lib_dir suffix return_lib_name_list return_lib_path_list)
UNSET(return_lib_name_list CACHE)
UNSET(return_lib_path_list CACHE)
FILE(
GLOB lib_path_list
${lib_dir}/*.${suffix}
)
FOREACH(_lib_path ${lib_path_list})
GET_FILENAME_COMPONENT(_lib_name ${_lib_path} NAME_WE)
# MESSAGE("_lib_name ${_lib_name}")
LIST(APPEND lib_name_list ${_lib_name})
ENDFOREACH(_lib_path ${lib_path_list})
# MESSAGE("lib_name_list ${lib_name_list}")
# MESSAGE("lib_path_list ${lib_path_list}")
SET(return_lib_name_list ${lib_name_list} PARENT_SCOPE)
SET(return_lib_path_list ${lib_path_list} PARENT_SCOPE)
ENDFUNCTION()
FUNCTION(FIND_HDRS hdr_dir return_hdr_dir_list)
UNSET(hdr_dir_list CACHE)
SET(hdr_dir_list)
FILE(
GLOB_RECURSE hdr_path_list
${hdr_dir}/*.h
${hdr_dir}/*.hpp
)
SET(hdr_dir_list ${hdr_dir})
FOREACH(_hdr_path ${hdr_path_list})
GET_FILENAME_COMPONENT(_hdr_dir ${_hdr_path} PATH)
LIST(APPEND hdr_dir_list ${_hdr_dir})
ENDFOREACH(_hdr_path ${hdr_path_list})
LIST(REMOVE_DUPLICATES hdr_dir_list)
SET(return_hdr_dir_list ${hdr_dir_list} PARENT_SCOPE)
ENDFUNCTION()
# print list item
FUNCTION(PRINT_LIST list_item title prefix)
IF(NOT list_item OR (list_item STREQUAL ""))
RETURN()
ENDIF()
MESSAGE("┌────────────────── ${title}")
FOREACH(item ${list_item})
MESSAGE("│ ${prefix} ${item}")
ENDFOREACH()
MESSAGE("└──────────────────]\n")
ENDFUNCTION()
把这些 CMake 函数或者宏单独放到一个文件里然后通过 INCLUDE
来在根目录的 CMakeLists.txt 包含这些子文件以使用函数或者宏。
目录结构如下所示:
C:\USERS\FREDOM\DESKTOP\TEMP
├─.vscode
├─app
├─build
│ ├─.cmake
│ │ └─api
│ │ └─v1
│ │ ├─query
│ │ │ └─client-vscode
│ │ └─reply
│ ├─app
│ │ └─CMakeFiles
│ │ └─main.dir
│ ├─CMakeFiles
│ │ ├─3.27.0-rc4
│ │ │ ├─CompilerIdC
│ │ │ │ └─tmp
│ │ │ └─CompilerIdCXX
│ │ │ └─tmp
│ │ └─pkgRedirects
│ └─install
│ └─Debug
│ └─bin
├─cmake
├─include
│ └─SFML
│ ├─Audio
│ ├─Graphics
│ ├─Network
│ ├─System
│ └─Window
├─lib
└─src
PS:Windows 下自带的这个 tree 树形目录结构显式可以使用的参数太少了,不支持指定展开到第几级?输出的都是目录名,但是输出带文件名的话根本看不完
项目根目录的 CMakeLists.txt 如下:
CMAKE_MINIMUM_REQUIRED(VERSION 3.21)
PROJECT(temp)
MESSAGE("build system: ${CMAKE_SYSTEM_NAME}")
MESSAGE("${CMAKE_CURRENT_BINARY_DIR}")
SET(CMAKE_CXX_STANDARD 17)
SET(CAMKE_C_STANDARD 17)
SET(CMAKE_INSTALL_PREFIX ${CMAKE_CURRENT_BINARY_DIR}/install/${CMAKE_BUILD_TYPE})
# set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}/bin) # binary executable
# set(CMAKE_LIBRARY_OUTPUT_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}/bin) # shared library
# set(CMAKE_ARCHIVE_OUTPUT_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}/lib) # static library
INCLUDE(cmake/macro.cmake)
# help vscode find compiler_command.json
ADD_DEFINITIONS(-DCMAKE_EXPORT_COMPILE_COMMANDS=ON)
# add_definitions(-DSFML_STATIC)
# 指定库目录
LINK_DIRECTORIES(lib)
# 包含头文件目录
FIND_HDRS(include return_hdr_dir_list)
PRINT_LIST("${return_hdr_dir_list}" "HDR_PATH" "")
FOREACH(_hdr_dir ${return_hdr_dir_list})
INCLUDE_DIRECTORIES(${_hdr_dir})
ENDFOREACH(_hdr_dir ${return_hdr_dir_list})
FIND_LIBS(lib "dll" return_lib_name_list return_lib_path_list)
PRINT_LIST("${return_lib_name_list}" "LIB_NAME" "")
PRINT_LIST("${return_lib_path_list}" "LIB_PATH" "")
LIST(LENGTH return_lib_name_list num_lib)
FOREACH(i RANGE 0 ${num_lib})
IF(${i} EQUAL ${num_lib})
BREAK()
ENDIF()
LIST(GET return_lib_name_list ${i} _lib_name)
LIST(GET return_lib_path_list ${i} _lib_path)
# MESSAGE("${i} ${_lib_name} ${_lib_path}")
ADD_LIBRARY(${_lib_name} SHARED IMPORTED)
SET_PROPERTY(
TARGET ${_lib_name}
PROPERTY IMPORTED_LOCATION ${_lib_path}
)
# only for windows dll, that's suck
SET_PROPERTY(
TARGET ${_lib_name}
PROPERTY IMPORTED_IMPLIB ${_lib_path}
)
INSTALL(
IMPORTED_RUNTIME_ARTIFACTS ${_lib_name}
)
ENDFOREACH()
# 移动动态库到 bin 文件同目录下
FOREACH(_lib ${sfmllibs})
INSTALL(
TARGETS ${sfmlibs}
RUNTIME DESTINATION bin
)
ENDFOREACH(_lib ${sfmllibs})
# 添加可执行文件
ADD_SUBDIRECTORY(app)
然后 app
目录下面的 CMakeList.txt 内容如下:
ADD_EXECUTABLE(main main.cpp)
TARGET_LINK_LIBRARIES(main ${return_lib_name_list})
INSTALL(TARGETS main RUNTIME DESTINATION bin)
# 链接库
# TARGET_LINK_LIBRARIES(
# main
# ${sfmllibs}
# )
不过后来发现其实 LINK_DIRECTORIES
不是很需要了,因为这次没有项目内部生成的库目标文件,都是外部动态库链接,不过为了模板具有一些普适性还是放着吧。在 VScode 里面使用快捷键 Ctrl+Shift+B
来执行构建,CMake 插件的配置过程输出如下:
[cmake] Not searching for unused variables given on the command line.
[cmake] build system: Windows
[cmake] C:/Users/fredom/Desktop/temp/build
[cmake] ┌────────────────── HDR_PATH
[cmake] │ include
[cmake] │ C:/Users/fredom/Desktop/temp/include/SFML
[cmake] │ C:/Users/fredom/Desktop/temp/include/SFML/Audio
[cmake] │ C:/Users/fredom/Desktop/temp/include/SFML/Graphics
[cmake] │ C:/Users/fredom/Desktop/temp/include/SFML/Network
[cmake] │ C:/Users/fredom/Desktop/temp/include/SFML/System
[cmake] │ C:/Users/fredom/Desktop/temp/include/SFML/Window
[cmake] │ C:/Users/fredom/Desktop/temp/include
[cmake] └──────────────────]
[cmake]
[cmake] ┌────────────────── LIB_NAME
[cmake] │ openal32
[cmake] │ sfml-audio-d-2
[cmake] │ sfml-graphics-d-2
[cmake] │ sfml-network-d-2
[cmake] │ sfml-system-d-2
[cmake] │ sfml-window-d-2
[cmake] └──────────────────]
[cmake]
[cmake] ┌────────────────── LIB_PATH
[cmake] │ C:/Users/fredom/Desktop/temp/lib/openal32.dll
[cmake] │ C:/Users/fredom/Desktop/temp/lib/sfml-audio-d-2.dll
[cmake] │ C:/Users/fredom/Desktop/temp/lib/sfml-graphics-d-2.dll
[cmake] │ C:/Users/fredom/Desktop/temp/lib/sfml-network-d-2.dll
[cmake] │ C:/Users/fredom/Desktop/temp/lib/sfml-system-d-2.dll
[cmake] │ C:/Users/fredom/Desktop/temp/lib/sfml-window-d-2.dll
[cmake] └──────────────────]
[cmake]
[cmake] -- Configuring done (0.0s)
[cmake] -- Generating done (0.0s)
[cmake] -- Build files have been written to: C:/Users/fredom/Desktop/temp/build
这样一来就把 SFML 库的动态库和头文件目录都引入项目了。
代码实现
主要构想是:
- 画布分成均等的网格
- 每个网格亮起之后渐弱变暗直到完全变黑(这样比较好看……)
- 网格的亮度变化和可移动对象分开,计算可移动对象的位置,可移动对象移动的路径上重置网格的亮度
根据 SFML 官网的入门教程,其实画面的渲染可以做成单独线程的离屏渲染,这样性能会好很多,不过时间关系这里就没有跟着写了,其实本身代码的封装性也不强,一般来说这种小游戏 demo 怎么来说也会有个 runloop、start、stop、eventloop、drawloop 之类的分离模块。
main.cpp
#include <iostream>
#include <vector>
#include <common.h>
#include <SFML/Graphics.hpp>
#include <object.h>
#include <snake.h>
const int WIN_WID = 1000;
const int WIN_HEI = 1000;
const int NUM_ROW = 100;
const int NUM_COL = 100;
int main()
{
// create the window
std::shared_ptr<sf::RenderWindow> window = std::make_shared<sf::RenderWindow>(sf::VideoMode(WIN_WID, WIN_HEI), "snake");
std::vector<ObjectDrawable> board;
for (int i = 0; i < NUM_ROW; ++i)
{
for (int j = 0; j < NUM_COL; ++j)
{
ObjectDrawable newObj(window);
newObj.setBoarder(WIN_WID/NUM_COL, WIN_HEI/NUM_ROW); // width, height
newObj.setPosition(WIN_WID/NUM_COL*j, WIN_HEI/NUM_ROW*i);
board.emplace_back(newObj);
}
}
Snake snake;
snake.extendBody();
// run the program as long as the window is open
sf::Clock clock;
float deltaTime = 0.0;
while (window->isOpen())
{
// calculate delta time elapsed
deltaTime = clock.getElapsedTime().asSeconds();
clock.restart();
// std::cout << deltaTime << std::endl;
// check all the window's events that were triggered since the last iteration of the loop
sf::Event event;
while (window->pollEvent(event))
{
// "close requested" event: we close the window
switch (event.type)
{
case sf::Event::Closed:
window->close();
break;
case sf::Event::KeyPressed:
switch (event.key.code)
{
case sf::Keyboard::Up:
snake.changeHeadSpd(sf::Vector2f(0.0f, -5.0f));
break;
case sf::Keyboard::Down:
snake.changeHeadSpd(sf::Vector2f(0.0f, +5.0f));
break;
case sf::Keyboard::Right:
snake.changeHeadSpd(sf::Vector2f(+5.0f, 0.0f));
break;
case sf::Keyboard::Left:
snake.changeHeadSpd(sf::Vector2f(-5.0f, 0.0f));
break;
}
break;
case sf::Event::MouseButtonPressed:
// board[event.mouseMove.y/(WIN_HEI/NUM_ROW) * NUM_COL + event.mouseMove.x/(WIN_WID/NUM_COL)].resetColor();
snake.extendBody();
break;
case sf::Event::MouseMoved:
const sf::Vector2f& headPos = snake.getHeadPos();
float xdspd = (event.mouseMove.x - headPos.x) * 2e-2;
float ydspd = (event.mouseMove.y - headPos.y) * 2e-2;
snake.changeHeadSpd(sf::Vector2f(xdspd, ydspd));
break;
}
}
// clear the window with black color
window->clear(sf::Color());
// draw everything here...
snake.update(deltaTime);
for (const auto& item : snake.getBody())
{
sf::Vector2f itemPos = item->getPos();
// std::cout << itemPos.x << ' ' << itemPos.y << std::endl;
int boardIdx = int(itemPos.y)/(WIN_HEI/NUM_ROW) * NUM_COL + int(itemPos.x)/(WIN_WID/NUM_COL);
if (boardIdx < 0) boardIdx = 0;
boardIdx = boardIdx % (NUM_ROW * NUM_COL);
board[boardIdx].resetColor();
}
for (ObjectDrawable& obj : board)
{
obj.update(deltaTime);
obj.draw();
}
// end the current frame
window->display();
sf::sleep(sf::milliseconds(16));
}
return 0;
}
这里还有一个关于 C++ 的 switch 块的注意点,就是 case 中不能声明跨作用域的变量,不过由于我这里每个 case 分支里面的变量都是当前 case 使用的,没有跨 case 使用,所以编译没有报错(是这样吗?也可能是因为 MinGW 的编译支持这么做而已)。
object.h
#ifndef OBJECT_H
#define OBJECT_H
#include <common.h>
#include <algorithm>
#include <SFML/Graphics.hpp>
class ObjectDrawable
{
private:
std::shared_ptr<sf::RenderWindow> renderWd;
std::shared_ptr<sf::Shape> shape;
sf::Color baseColor;
sf::Color currColor;
float xpos;
float ypos;
float brdx;
float brdy;
public:
ObjectDrawable(std::shared_ptr<sf::RenderWindow> _rdwp) : renderWd(_rdwp)
{
this->shape = std::make_shared<sf::RectangleShape>(sf::Vector2f(10.0f, 10.0f));
// this->baseColor = sf::Color(
// 150 + rand() % 100,
// 150 + rand() % 100,
// 150 + rand() % 100
// );
this->baseColor = sf::Color(50, 255, 50);
this->shape->setFillColor(this->baseColor);
}
void setPosition(const float& _xpos, const float& _ypos)
{
this->xpos = _xpos;
this->ypos = _ypos;
}
void setBoarder(const float& _brdx, const float& _brdy)
{
this->brdx = _brdx;
this->brdy = _brdy;
}
void resetColor()
{
this->currColor = this->baseColor;
}
void draw()
{
this->shape->setPosition(this->xpos, this->ypos);
this->shape->setFillColor(this->currColor);
std::dynamic_pointer_cast<sf::RectangleShape>(this->shape)->setSize(sf::Vector2f(this->brdx, this->brdy));
if (renderWd != nullptr)
this->renderWd->draw(*(this->shape.get()));
}
void update(const float& dt)
{
this->currColor.r = std::max(0, int(this->currColor.r - dt * 100));
this->currColor.g = std::max(0, int(this->currColor.g - dt * 100));
this->currColor.b = std::max(0, int(this->currColor.b - dt * 100));
}
};
class ObjectMovable
{
private:
float xpos;
float ypos;
float xspd;
float yspd;
public:
ObjectMovable() {}
ObjectMovable(
const float& _xpos,
const float& _ypos,
const float& _xspd = 0.0f,
const float& _yspd = 0.0f
) : xpos(_xpos), ypos(_ypos), xspd(_xspd), yspd(_yspd) {}
ObjectMovable(
const sf::Vector2f& _pos,
const sf::Vector2f& _spd = sf::Vector2f(0.0f, 0.0f)
) : xpos(_pos.x), ypos(_pos.y), xspd(_spd.x), yspd(_spd.y) {}
void update(const float& dt)
{
this->xpos += this->xspd * dt;
this->ypos += this->yspd * dt;
}
void changePos(const float& _xpos, const float& _ypos)
{
this->xpos = _xpos;
this->ypos = _ypos;
}
void changePos(const sf::Vector2f& _pos)
{
this->xpos = _pos.x;
this->ypos = _pos.y;
}
sf::Vector2f getPos() const
{
return sf::Vector2f(this->xpos, this->ypos);
}
void changeSpd(const float& _xspd, const float& _yspd)
{
this->xspd = _xspd;
this->yspd = _yspd;
}
void changeSpd(const sf::Vector2f& _spd)
{
this->xspd = _spd.x;
this->yspd = _spd.y;
}
sf::Vector2f getSpd() const{
return sf::Vector2f(this->xspd, this->yspd);
}
};
#endif
Object
头文件一开始的设想是有一个纯虚基类 Object
的,但是没时间实现了,只想快速验证 demo 于是分开了两个类来写,实际上 update
这个函数应该是 Object
基类的一个空方法才对。
snake.h
#ifndef SNAKE_H
#define SNAKE_H
#include <common.h>
#include <object.h>
class Snake
{
private:
std::vector<std::shared_ptr<ObjectMovable>> body;
int bodyLen;
public:
Snake()
{
this->body.emplace_back(
std::make_shared<ObjectMovable>(
0.0, 0.0, 0.0, 0.0
)
);
this->bodyLen = this->body.size();
}
void extendBody()
{
this->body.emplace_back(
std::make_shared<ObjectMovable>(
this->body.back()->getPos(),
this->body.back()->getSpd()
)
);
bodyLen++;
}
void update(const float& dt)
{
this->body[0]->update(dt);
for (int i = 1; i < this->bodyLen; ++i)
{
this->body[i]->changeSpd(
this->body[i-1]->getPos() - this->body[i]->getPos()
);
this->body[i]->update(dt);
}
}
void changeHeadSpd(const sf::Vector2f& dspd)
{
this->body[0]->changeSpd(
this->body[0]->getSpd() + dspd
);
}
const std::vector<std::shared_ptr<ObjectMovable>>& getBody() const {
return this->body;
}
sf::Vector2f getHeadPos() {
return this->body[0]->getPos();
}
};
#endif
common.h
#ifndef COMMON_H
#define COMMON_H
#include <memory>
#include <random>
#include <vector>
#endif
使用 demo
- 鼠标移动来吸引可移动对象
- 鼠标左键添加可移动对象