Snake++
。我构建它是为了学习C ++。哪些功能和技术可以改进?我使用SDL进行一些基本的呈现,但我更关心的是语言的使用。
我非常关心的事情:
产生新食物意味着一次又一次地尝试随机位置,直到有空为止。这将很快成为一个问题。我在这里可以使用哪些数据结构?
我是否在使用它们的全部功能并避免不必要的复制?
Main.cpp
#include <iostream>
#include "Game.hpp"
using namespace std;
int main(int argc, char * argv[])
{
Game game = Game();
Game().Run();
cout << "Game has terminated successfully, score: " << game.GetScore()
<< ", size: " << game.GetSize() << endl;
return 0;
}
Game.hpp
#pragma once
#include <vector>
#include "SDL.h"
#include "SDL_image.h"
class Game
{
public:
Game();
void Run();
int GetScore();
int GetSize();
private:
bool running = false;
bool alive = false;
int fps = 0;
static const int FRAME_RATE = 1000 / 60;
static const int SCREEN_WIDTH = 640;
static const int SCREEN_HEIGHT = 640;
static const int GRID_WIDTH = 32;
static const int GRID_HEIGHT = 32;
SDL_Window * window = nullptr;
SDL_Renderer * renderer = nullptr;
enum class Block { head, body, food, empty };
enum class Move { up, down, left, right };
Move last_dir = Move::up;
Move dir = Move::up;
struct { float x = GRID_WIDTH / 2, y = GRID_HEIGHT / 2; } pos;
SDL_Point head = { static_cast<int>(pos.x), static_cast<int>(pos.y) };
SDL_Point food;
std::vector<SDL_Point> body;
Block grid[GRID_WIDTH][GRID_HEIGHT];
float speed = 0.5f;
int growing = 0;
int score = 0;
int size = 1;
void ReplaceFood();
void GrowBody(int quantity);
void UpdateWindowTitle();
void GameLoop();
void Render();
void Update();
void PollEvents();
void Close();
};
Game.cpp
#include <iostream>
#include <string>
#include <ctime>
#include "SDL.h"
#include "Game.hpp"
using namespace std;
Game::Game()
{
for (int i = 0; i < GRID_WIDTH; ++i)
for (int j = 0; j < GRID_HEIGHT; ++j)
{
grid[i][j] = Block::empty;
}
srand(static_cast<unsigned int>(time(0)));
}
void Game::Run()
{
// Initialize SDL
if (SDL_Init(SDL_INIT_VIDEO) < 0)
{
cerr << "SDL could not initialize! SDL_Error: " << SDL_GetError() << endl;
exit(EXIT_FAILURE);
}
// Create Window
window = SDL_CreateWindow("Snake Game", SDL_WINDOWPOS_CENTERED, SDL_WINDOWPOS_CENTERED,
SCREEN_WIDTH, SCREEN_HEIGHT, SDL_WINDOW_SHOWN);
if (window == NULL)
{
cout << "Window could not be created! SDL_Error: " << SDL_GetError() << endl;
exit(EXIT_FAILURE);
}
// Create renderer
renderer = SDL_CreateRenderer(window, -1, SDL_RENDERER_ACCELERATED);
if (renderer == NULL)
{
cout << "Renderer could not be created! SDL_Error: " << SDL_GetError() << endl;
exit(EXIT_FAILURE);
}
alive = true;
running = true;
ReplaceFood();
GameLoop();
}
void Game::ReplaceFood()
{
int x, y;
while (true)
{
x = rand() % GRID_WIDTH;
y = rand() % GRID_HEIGHT;
if (grid[x][y] == Block::empty)
{
grid[x][y] = Block::food;
food.x = x;
food.y = y;
break;
}
}
}
void Game::GameLoop()
{
Uint32 before, second = SDL_GetTicks(), after;
int frame_time, frames = 0;
while (running)
{
before = SDL_GetTicks();
PollEvents();
Update();
Render();
frames++;
after = SDL_GetTicks();
frame_time = after - before;
if (after - second >= 1000)
{
fps = frames;
frames = 0;
second = after;
UpdateWindowTitle();
}
if (FRAME_RATE > frame_time)
{
SDL_Delay(FRAME_RATE - frame_time);
}
}
}
void Game::PollEvents()
{
SDL_Event e;
while (SDL_PollEvent(&e))
{
if (e.type == SDL_QUIT)
{
running = false;
}
else if (e.type == SDL_KEYDOWN)
{
switch (e.key.keysym.sym)
{
case SDLK_UP:
if (last_dir != Move::down || size == 1)
dir = Move::up;
break;
case SDLK_DOWN:
if (last_dir != Move::up || size == 1)
dir = Move::down;
break;
case SDLK_LEFT:
if (last_dir != Move::right || size == 1)
dir = Move::left;
break;
case SDLK_RIGHT:
if (last_dir != Move::left || size == 1)
dir = Move::right;
break;
}
}
}
}
int Game::GetSize()
{
return size;
}
void Game::GrowBody(int quantity)
{
growing += quantity;
}
void Game::Update()
{
if (!alive)
return;
switch (dir)
{
case Move::up:
pos.y -= speed;
pos.x = floorf(pos.x);
break;
case Move::down:
pos.y += speed;
pos.x = floorf(pos.x);
break;
case Move::left:
pos.x -= speed;
pos.y = floorf(pos.y);
break;
case Move::right:
pos.x += speed;
pos.y = floorf(pos.y);
break;
}
// Wrap
if (pos.x < 0) pos.x = GRID_WIDTH - 1;
else if (pos.x > GRID_WIDTH - 1) pos.x = 0;
if (pos.y < 0) pos.y = GRID_HEIGHT - 1;
else if (pos.y > GRID_HEIGHT - 1) pos.y = 0;
int new_x = static_cast<int>(pos.x);
int new_y = static_cast<int>(pos.y);
// Check if head position has changed
if (new_x != head.x || new_y != head.y)
{
last_dir = dir;
// If we are growing, just make a new neck
if (growing > 0)
{
size++;
body.push_back(head);
growing--;
grid[head.x][head.y] = Block::body;
}
else
{
// We need to shift the body
SDL_Point free = head;
vector<SDL_Point>::reverse_iterator rit = body.rbegin();
for ( ; rit != body.rend(); ++rit)
{
grid[free.x][free.y] = Block::body;
swap(*rit, free);
}
grid[free.x][free.y] = Block::empty;
}
}
head.x = new_x;
head.y = new_y;
Block & next = grid[head.x][head.y];
// Check if there's food over here
if (next == Block::food)
{
score++;
ReplaceFood();
GrowBody(1);
}
// Check if we're dead
else if (next == Block::body)
{
alive = false;
}
next = Block::head;
}
int Game::GetScore()
{
return score;
}
void Game::UpdateWindowTitle()
{
string title = "Snakle++ Score: " + to_string(score) + " FPS: " + to_string(fps);
SDL_SetWindowTitle(window, title.c_str());
}
void Game::Render()
{
SDL_Rect block;
block.w = SCREEN_WIDTH / GRID_WIDTH;
block.h = SCREEN_WIDTH / GRID_HEIGHT;
// Clear screen
SDL_SetRenderDrawColor(renderer, 0x1E, 0x1E, 0x1E, 0xFF);
SDL_RenderClear(renderer);
// Render food
SDL_SetRenderDrawColor(renderer, 0xFF, 0xCC, 0x00, 0xFF);
block.x = food.x * block.w;
block.y = food.y * block.h;
SDL_RenderFillRect(renderer, &block);
// Render snake's body
SDL_SetRenderDrawColor(renderer, 0xFF, 0xFF, 0xFF, 0xFF);
for (SDL_Point & point : body)
{
block.x = point.x * block.w;
block.y = point.y * block.h;
SDL_RenderFillRect(renderer, &block);
}
// Render snake's head
block.x = head.x * block.w;
block.y = head.y * block.h;
if (alive) SDL_SetRenderDrawColor(renderer, 0x00, 0x7A, 0xCC, 0xFF);
else SDL_SetRenderDrawColor(renderer, 0xFF, 0x00, 0x00, 0xFF);
SDL_RenderFillRect(renderer, &block);
// Update Screen
SDL_RenderPresent(renderer);
}
void Game::Close()
{
SDL_DestroyWindow(window);
SDL_Quit();
}
#1 楼
对象用法此代码:
Game game = Game();
Game().Run();
cout << "Game has terminated successfully, score: " << game.GetScore()
<< ", size: " << game.GetSize() << endl;
...没有做任何事情,我敢肯定您认为是。这部分:
Game game = Game();
创建一个名为game
的对象,其类型为Game
。但是,我宁愿只使用Game game;
,它可以更轻松地完成相同的事情。然后您执行:
Game().Run();
。这将创建另一个(临时)Game
对象,并在该临时Run
对象上调用Game
成员函数(因此,您刚刚创建的名为Game
的game
对象处于空闲状态,什么也不做)。 > cout << "Game has terminated successfully, score: " << game.GetScore()
<< ", size: " << game.GetSize() << endl;
...,它尝试打印在名为
game
的对象中累积的分数-但game
尚未运行。只有临时对象运行(因此,按权利,您显示的分数应始终为0
)。如果这样做,我可能会做更多类似的事情:
Game game;
game.run();
cout << "Game has terminated successfully, score: " << game.GetScore()
<< ", size: " << game.GetSize() << endl;
using namespace std;
不仅在使用; (强烈建议)反对
using namespace std;
。对另一个名称空间使用using指令可能没问题,但是std::
包含大量内容,其中一些名称很常见,很可能与其他代码冲突。更糟糕的是,每一个新发布的C ++标准都为std
增加了更多的“东西”。通常最好在使用名称时对名称进行限定,因此(例如)上面显示的cout
更像:std::cout << "Game has terminated successfully, score: " << game.GetScore()
<< ", size: " << game.GetSize() << std::endl;
避免
std::endl
我建议避免一般。除了在流中写入换行符之外,它还会刷新流。您需要换行,但几乎从不希望刷新流,因此通常最好写一个
std::endl
。在极少数实际需要刷新的情况下,请明确执行它:\n
。避免使用C随机数生成例程
C的
std::cout << '\n' << std::flush;
/ srand()
有很多问题。我通常建议改用rand()
中的新例程。这有点痛苦(好好地看一下新生成器会特别痛苦),但是它们通常会产生更高的质量随机性,对多线程更友好,并且很好地使用它们将使C ++程序员保持冷静(现在有一个矛盾之词) 避免使用
<random>
编写C ++时,通常最好避免使用
exit()
。调用它通常会阻止堆栈上的对象的析构函数运行,因此您无法彻底关机。通常,我会在main以及您当前所在的位置添加一个
exit
/ try
块调用catch
,抛出从exit()
派生的对象。就您而言,std::exception
可能是有道理的。if (renderer == NULL)
{
throw std::runtime_error("Renderer could not be created!");
}
主要:在C ++中,要求
std::runtime_error
是值为nullptr
的整数常量(例如NULL
或NULL
)。 0
有点特殊-它可以转换为任何指针类型,但不能偶然转换为整数类型。因此,在可能考虑使用0
的任何地方,几乎可以肯定,使用0L
更好: >这样,如果您不小心使用了nullptr
的意思:错误的东西,尽管当前大多数编译器至少会对此发出警告。)#2 楼
using namespace std;
是一个坏习惯。很高兴看到您没有在标题中使用它。最好不要使用它。请参阅此帖子以获取更多信息。int main(int argc, char * argv[])
如果仍然不使用命令行参数,则使用空参数main:
int main()
return 0
在main的末尾是不必要的,它将由编译器提供。错误
int main(int argc, char * argv[])
{
Game game = Game();
Game().Run();
cout << "Game has terminated successfully, score: " << game.GetScore()
<< ", size: " << game.GetSize() << endl;
return 0;
}
Game().Run()
调用Game
构造函数并创建Game
的第二个实例,然后在该实例。您的赛后统计信息可能无法正常工作。不是吗?不要使用
Run()
。请改用'\ n'。 std::endl
刷新流,如果要执行此操作,则可以手动执行std::endl
,这将更加明确地说明您要完成的操作。对于在编译时已知的全局命名常量,<< '\n' << std::flush
更好。它们需要移到类之外,但仍可以在游戏头文件中。ALL_CAPS名称通常也用于宏。最好使用snake_case,camelCase或PascalCase。 (对于全局常量,我更喜欢使用snake_case,但仅是我。)
static const int FRAME_RATE = 1000 / 60;
static const int SCREEN_WIDTH = 640;
static const int SCREEN_HEIGHT = 640;
static const int GRID_WIDTH = 32;
static const int GRID_HEIGHT = 32;
这里定义的浮点数是两个整数相除的结果(不会返回浮点数)然后立即将结果转换为int。值得一提的是,您的值划分得很整洁(这就是为什么您没有注意到任何错误的原因。)我看到
constexpr
转换为pos
还有几次。只是将其设为int
struct { float x = GRID_WIDTH / 2, y = GRID_HEIGHT / 2; } pos;
SDL_Point head = { static_cast<int>(pos.x), static_cast<int>(pos.y) };
信誉不是很好的PRNG。了解
int
标头。只需将它们初始化为true即可。大括号初始化也比较惯用。srand(static_cast<unsigned int>(time(0)));
在C宏
<random>
上使用受C ++语言支持的nullptr
。 NULL
将在不希望的时间被静默转换为int。NULL
再次避免ReplaceFood()
。但是您是否考虑过维护rand
的std::vector<Point>
为空。然后,您可以从向量中随机索引以找到下一个食物的位置。您将不得不将尾巴的先前位置添加回向量中,并在每次移动中删除头部的位置。使用1:1行作为变量。尤其是在分配了一些而没有分配时。这会使阅读变得混乱。 Point
被分配了0吗?我知道答案了,但是仅仅看一下它就不太明显。不要在您的
frame_time
函数中使用参数。您只能将蛇长一。只需在内部增加大小并继续。如果有可能将不同的大小作为参数传递,则仅增加参数提供的大小。我将其分为两个或三个辅助函数。也许移动+包裹一个函数,然后检查另一个函数的头部。回到那个。我不确定我是否会改变您处理pos结构的方式,但是我会认真考虑一下。 解决方案可能是具有一个辅助函数,将演员表抽象到一个位置。像这样:
bool running = false;
bool alive = false;
评论
\ $ \ begingroup \ $
谢谢您的回答。 1)将常量移出类不会暴露它们吗?私有和封装的整个目的将被破坏。其他包含“ Game.hpp”的类的空间将受到污染。 2)std :: array是否会使代码非常冗长,因为我使用的是2d数组? 3)向游戏添加新机制时,GrowBody()可能会很有用。内联解决了这个问题吗? 4)我很清楚位置的浮动/整数问题。除了烦人的演员,我没有其他办法可以解决这个问题。
\ $ \ endgroup \ $
–阿方索·马托斯(Afonso Matos)
19年1月27日14:35
\ $ \ begingroup \ $
@AfonsoMatos这是一个常见的错误:在C ++(和其他语言中类似)中,类不是将接口与实现分开的唯一且不是主要的机制。在自己的编译单元(Game.cpp)中定义常量以使其私有。
\ $ \ endgroup \ $
–康拉德·鲁道夫(Konrad Rudolph)
19年1月28日,11:28
#3 楼
其他一些要点:避免使用“神类”。您的
Game
类绝对可以完成所有操作。这使得很难看到在哪个位置使用了哪些成员变量,并且距离使用全局变量仅一步之遥。程序越大,就越难理解。类应遵循单一职责原则(SRP),并且仅负责一件事。使用“资源获取即初始化”(RAII)方法来管理资源寿命。例如,可以将SDL上下文,窗口和渲染器封装在一个对象中,并在构造函数中进行初始化,并在析构函数中进行清理。
不要使用全局常量。
GRID_WIDTH
等适用于Game
类的所有实例,这是不必要的限制。将unsigned类型用于永远不能为负的变量(网格宽度/高度等)。
SDL_Point
使用错误的类型(int
),但是我们可以轻松定义自己的Point
类来代替。仅当需要使用SDL函数调用它时,我们才能转换为必需的SDL类型。不更改成员变量的成员函数(例如
GetScore()
,GetSize()
)应标记为const
(例如int GetScore() const;
)。 />选择新食物位置的更好策略可能是将所有空方格的位置添加到向量中,然后选择一个(通过选择小于向量大小的索引)。这是一个如何伪装
Game
类的(伪代码)示例。请注意,Game
类没有理由对SDL
一无所知。如果我们想更改为其他平台进行渲染/输入,则将其分开会更加整洁。不要害怕也使用自由函数。namespace Snake
{
template<class T>
struct Point
{
T x;
T y;
};
struct SDLContext
{
SDLContext(std::size_t window_width, std::size_t window_height);
~SDLContext();
SDL_Window * window = nullptr;
SDL_Renderer * renderer = nullptr;
};
SDLContext::SDLContext()
{
// ... SDL init
}
SDLContext::~SDLContext()
{
// ... SDL shutdown
}
struct Board
{
Board(std::size_t width, std::size_t height);
enum class Block { head, body, food, empty };
std::size_t width;
std::size_t height;
std::vector<std::vector<Block>> grid;
};
Board::Board()
{
// ... init grid to "empty"
}
struct Food
{
Point<std::size_t> position = Point{ 0, 0 };
};
struct Snake
{
void Grow(int amount);
void UpdatePosition(Board& board);
enum class Move { up, down, left, right };
Move last_dir = Move::up;
Move dir = Move::up;
Point<std::size_t> headPosition;
std::vector<Point<std::size_t>> body;
int size = 1;
float speed = 0.5f;
int growing = 0;
};
class Game
{
Game(std::size_t gridWidth, std::size_t gridHeight);
int GetScore() const;
int GetSize() const;
void Update();
private:
void ReplaceFood();
Board board;
Food food;
Snake snake;
int score = 0;
bool alive = true;
};
Game::Game(std::size_t gridWidth, std::size_t gridHeight):
Board(gridWidth, gridHeight)
{
ReplaceFood();
}
void PollEvents(SDLContext&, bool& quit)
{
// ...
}
void Render(SDLContext&, Game const& game)
{
// ...
}
void UpdateWindowTitle(SDLContext&, Game const& game)
{
// ...
}
void Run(SDLContext& context, Game& game, int frame_rate)
{
Uint32 before, second = SDL_GetTicks(), after;
int frame_time, frames = 0;
while (true)
{
before = SDL_GetTicks();
bool quit = false;
PollEvents(sdlContext, quit);
if (quit)
break;
game.Update();
Render(sdlContext, game);
frames++;
after = SDL_GetTicks();
frame_time = after - before;
if (after - second >= 1000)
{
UpdateWindowTitle(sdlContext, game.GetScore(), frames);
frames = 0;
second = after;
}
if (frame_rate > frame_time)
{
SDL_Delay(frame_rate - frame_time);
}
}
}
} // Snake
#include <SDL.h>
#include <iostream>
#include <cstddef>
int main(int argc, char * argv[])
{
using namespace Snake;
const std::size_t window_width = 640;
const std::size_t window_height = 640;
SDLContext sdlContext(window_width, window_height);
const std::size_t grid_width = 32;
const std::size_t grid_height = 32;
Game game(grid_width, grid_height);
const int frame_rate = 1000 / 60;
Run(sdlContext, game, frame_rate);
std::cout << "Game has terminated successfully, score: " << game.GetScore()
<< ", size: " << game.GetSize() << std::endl;
return 0;
}
带有
SDLContext&
的函数不一定要使用它,因为SDL通过全局函数工作。但是,这确实证明我们已经在调用该函数的位置正确初始化了SDL。评论
\ $ \ begingroup \ $
非常有见地的答案,代码很漂亮。 1)目前尚不清楚如何更改ReplaceFood()。你怎么知道什么块是空的?您要进行Row * Col搜索吗?那是非常低效的,不是吗?
\ $ \ endgroup \ $
–阿方索·马托斯(Afonso Matos)
19年1月27日在14:44
\ $ \ begingroup \ $
是的,我愿意(是的,它效率低下)。但这很简单,而且是固定的工作量(对于给定的网格大小),因此,如果它对于小蛇足够快,那么我们知道对大蛇也将是很好的。肯定有更快的方法(例如,维护移动蛇时我们更新的空块位置列表),但是可能不需要增加复杂性。
\ $ \ endgroup \ $
–user673679
19年1月27日在15:18
\ $ \ begingroup \ $
同样在结构板上,网格数组声明中不能包含变量。
\ $ \ endgroup \ $
–阿方索·马托斯(Afonso Matos)
19年1月27日在18:57
\ $ \ begingroup \ $
好点,固定的。 (顺便说一句,我可能会为该板分配一个平面向量,而不是vector
\ $ \ endgroup \ $
–user673679
19年1月27日在19:21
\ $ \ begingroup \ $
我同意您所说的“上帝的阶级”,但是您应该对“单一责任原则”做更多的研究。它并不是说一堂课只能做一件事,而是一个模块只对一个利益相关者负责。鲍勃·马丁(Bob Martin)在他的《清洁建筑》一书中简要讨论了这些差异。他还提到,SRP可能是SOLID原则中最不为人所知的,所以不要感到难过。
\ $ \ endgroup \ $
–迈克·博克兰
19年1月28日在12:33
评论
\ $ \ begingroup \ $
“您想要换行,但几乎从不希望冲洗流” –您能否详细说明?以我的经验,具有完全缓冲stdout的程序非常烦人,因为您只能在它们完成后读取它们的输出(即,如果它们完成而不会崩溃,则刷新stdout缓冲区)。
\ $ \ endgroup \ $
– Joker_vD
19年1月28日,下午3:31
\ $ \ begingroup \ $
等待为什么不使用命名空间std使用;如果您知道它与其他代码不冲突?例如,如果您知道您没有使用任何其他库; std名称空间中是否有重复的命名函数?
\ $ \ endgroup \ $
– Jeffmagma
19年1月28日在5:10
\ $ \ begingroup \ $
@Jeffmagma您将来可能会添加一个库,或者甚至只是想在某个时候使用一个名称。
\ $ \ endgroup \ $
– Omegastick
19年1月28日在5:11
\ $ \ begingroup \ $
@Jeffmagma:允许该实现向std名称空间添加额外的“东西”,因此,基本上不可能知道其中可能存在什么。即使现在检查,下一版本的编译器也可能会完全破坏您的代码。
\ $ \ endgroup \ $
–杰里·科芬(Jerry Coffin)
19年1月28日在5:46
\ $ \ begingroup \ $
真的为空吗?无论如何,为什么不渲染!
\ $ \ endgroup \ $
–重复数据删除器
19年1月28日在11:24