这是我在C语言中经典游戏“ 2048”的实现。
构建/运行项目的说明(GitHub可以在这里找到。
我从核心游戏开始,然后用它构建了一个GUI界面。
我希望对我的设计提供一些反馈。如何更好地编写相同的实现。习语,约定或您想到的任何内容。
在头文件中有一些有关功能的信息。希望能提高可读性和理解性。
我知道的改进:
我每次为文本2、4、8创建纹理。画瓷砖。这是在浪费资源,因为我只需创建一次16个磁贴就不必再担心了。
由于我的IDE不会自动执行此操作,因此Java和C样式之间可能存在一些括号不一致的问题。
我不知道有哪些改进:
我是否在任何地方泄漏内存?
样式/会议/性能/内存以及其他所有内容。
技术使我的代码更加通用/通用。如答案之一所示,使用矩阵结构将有助于创建矩形板。
编辑:回购协议会不断更新。请在发布此问题时使用上面的链接查看回购的版本。我也不介意对最新版本发表评论。
game.h
/**
* @file game.h
* @author Gnik Droy
* @brief File containing function declarations for the game gui.
*
*/
#pragma once
#include "../include/core.h"
#include "SDL2/SDL.h"
#include "SDL2/SDL_ttf.h"
#define SCREEN_WIDTH 500
#define SCREEN_HEIGHT 600
/**
* @brief Initializes the SDL window.
*
* When two pointers to the pointer of gWindow and gRenderer are provided,
* the function initializes both values with the values of created window
* and renderer.
*
* If initialization is failed it may display error to stderr but
* does not exit.
*
* @param gWindow The window of the game.
* @param gRenderer The renderer for the game
* @return If the initialization was successful.
*/
bool initSDL(SDL_Window **gWindow,SDL_Renderer** gRenderer);
/**
* @brief Closes the SDL window.
*
* Frees up resource by closing destroying the SDL window
*
* @param gWindow The window of the game.
*/
void SDLclose(SDL_Window* gWindow);
/**
* @brief Draws text centered inside a rect.
*
* When two pointers to the pointer of gWindow and gRenderer are provided,
* the function initializes both values with the values of created window
* and renderer.
*
* If initialization is failed it may display error to stderr but
* does not exit.
*
* @param gRenderer The renderer for the game
* @param font The font for the text
* @param text The text to write
* @param rect The SDL_Rect object inside which text is written
* @param color The color of the text
*/
void draw_text(SDL_Renderer* gRenderer,TTF_Font* font,const char* text, SDL_Rect rect, SDL_Color color);
/**
* @brief Draws white text centered inside a rect.
*
* Same as draw_text(..., SDL_Color White)
*
* @param gRenderer The renderer for the game
* @param font The font for the text
* @param text The text to write
* @param rect The SDL_Rect object inside which text is written
*/
void draw_text_white(SDL_Renderer* gRenderer,TTF_Font* font,const char* text, SDL_Rect rect);
/**
* @brief Clears the window
*
* Fills a color to entire screen.
*
* @param gRenderer The renderer for the game
*/
void SDLclear(SDL_Renderer* gRenderer);
/**
* @brief Draws black text centered inside the window.
*
* @param gRenderer The renderer for the game
* @param size The size for the text
* @param text The text to write
*/
void display_text(SDL_Renderer* gRenderer,const char* text,int size);
/**
* @brief Draws the game tiles.
*
* It draws the SIZE*SIZE game tiles to the window.
*
* @param gRenderer The renderer for the game
* @param font The font for the tiles
* @param matrix The game matrix.
*/
void draw_matrix(SDL_Renderer* gRenderer,unsigned char matrix[][SIZE], TTF_Font* font);
/**
* @brief Draws the new game button.
*
* It draws the new game button to the bottom corner.
*
* @param gRenderer The renderer for the game
* @param font The font for the button
* @param matrix The game matrix. Needed to reset game.
*/
void draw_button(SDL_Renderer* gRenderer,unsigned char matrix[][SIZE], TTF_Font* font);
/**
* @brief Handles the action of New Game button.
*
* Resets the game board for a new game, if the correct mouse event
* had occured.
* Function is run if left mouse button is released
*
* @param gRenderer The renderer for the game
* @param e The mouse event
* @param matrix The game matrix.
*/
void button_action(SDL_Event e,unsigned char matrix[][SIZE]);
/**
* @brief Draws the current game score
*
* It draws the current game score to the window
*
* @param gRenderer The renderer for the game
* @param font The font for the tiles
* @param matrix The game matrix.
*/
void draw_score(SDL_Renderer* gRenderer,unsigned char matrix[][SIZE], TTF_Font* font);
/**
* @brief Draws everything for the game and renders it to screen.
*
* It calls SDLclear(),draw_matrix(),draw_score() and draw_button()
* and also renders it to screem.
*
* @param gRenderer The renderer for the game
* @param font The font for the tiles
* @param matrix The game matrix.
*/
void render_game(SDL_Renderer* gRenderer,unsigned char matrix[][SIZE], TTF_Font* font);
/**
* @brief This is the main game loop that handles all events and drawing
*
* @param gRenderer The renderer for the game
* @param font The font for the tiles
* @param matrix The game matrix.
*/
void gameLoop(unsigned char matrix[][SIZE],SDL_Renderer* gRenderer);
/**
* @brief Handles keyboard presses that correspond with the arrowkeys.
*
* It transforms the game matrix according to the keypresses.
* It also checks if the game has been finished, draws game over screen
* and resets the board if game over.
*
* @param gRenderer The renderer for the game
* @param font The font for the tiles
* @param matrix The game matrix.
*/
void handle_move(SDL_Event e,unsigned char matrix[][SIZE], SDL_Renderer * gRenderer);
styles.h
/**
* @file styles.h
* @author Gnik Droy
* @brief File containing tile colors and related structs.
*
*/
#pragma once
/** @struct COLOR
* @brief This structure defines a RBGA color
* All values are stored in chars.
*
* @var COLOR::r
* The red value
* @var COLOR::g
* The green value
* @var COLOR::b
* The blue value
* @var COLOR::a
* The alpha value
*
*/
//Screen dimension constants
#define SCREEN_WIDTH 500
#define SCREEN_HEIGHT 600
#define SCREEN_PAD 10
//FONT settings
#define FONT_PATH "UbuntuMono-R.ttf"
#define TITLE_FONT_SIZE 200
#define GOVER_FONT_SIZE 100 //Game Over font size
#define CELL_FONT_SIZE 40
struct COLOR{
char r;
char g;
char b;
char a;
};
struct COLOR g_bg={211, 204, 201, 255};
struct COLOR g_fg={80, 80, 80, 255};
struct COLOR g_button_bg={255, 153, 102,255};
struct COLOR g_score_bg={143, 122, 102,255};
struct COLOR g_COLORS[]={
{230, 227, 232,255},
{255, 127, 89,255},
{224, 74, 69,255},
{237, 207, 114,255},
{65, 216, 127,255},
{54, 63, 135,255},
{78, 89, 178,255},
{109, 118, 191,255},
{84, 47, 132,255},
{125, 77, 188,255},
{163, 77, 188,255},
{176, 109, 196,255},
{0, 102, 204,255},
{0, 153, 255,255},
{51, 153, 255,255},
{153, 204, 255,255},
{102, 255, 102,255}
};
core.h
/**
* @file core.h
* @author Gnik Droy
* @brief File containing function declarations for the core game.
*
*/
#pragma once
#include <stdio.h>
#define SIZE 4
#define BASE 2
typedef char bool;
/**
* @brief Write the game matrix to the stream.
*
* The matrix is written as a comma seperated list of indices.
* Each row is seperated by a '\n' character.
* Each empty cell is represented by '-' character.
*
* The indices can be used to calculate the actual integers.
*
* You can use the constant stdout from <stdio.h> for printing to
* standard output
*
* @param matrix The game matrix that is to be printed.
* @param stream The file stream to use.
*/
void print_matrix(unsigned char matrix[][SIZE],FILE* stream);
/**
* @brief Checks if there are possible moves left on the game board.
*
* Checks for both movement and combinations of tiles.
*
* @param matrix The game matrix.
* @return Either 0 or 1
*/
bool is_game_over(unsigned char matrix[][SIZE]);
/**
* @brief This clears out the game matrix
*
* This zeros out the entire game matrix.
*
* @param matrix The game matrix.
*/
void clear_matrix(unsigned char matrix[][SIZE]);
/**
* @brief Adds a value of 1 to random place to the matrix.
*
* The function adds 1 to a random place in the matrix.
* The 1 is placed in empty tiles. i.e tiles containing 0.
* 1 is kept since you can use raise it with BASE to get required value.
* Also it keeps the size of matrix to a low value.
*
* NOTE: It has no checks if there are any empty places for keeping
* the random value.
* If no empty place is found a floating point exception will occur.
*/
void add_random(unsigned char matrix[][SIZE]);
/**
* @brief Calculates the score of a game matrix
*
* It score the matrix in a simple way.
* Each element in the matrix is used as exponents of the BASE. And the
* sum of all BASE^element is returned.
*
* @return An integer that represents the current score
*/
int calculate_score(unsigned char matrix[][SIZE]);
/**
* @brief Shifts the game matrix in X direction.
*
* It shifts all the elements of the game matrix in the X direction.
* If the direction is given as 0, it shifts the game matrix in the left
* direction. Any other non zero value shifts it to the right direction.
*
* @param matrix The game matrix.
* @param opp The direction of the shift.
*
* @return If the shift was successful
*/
bool shift_x(unsigned char matrix[][SIZE], bool opp);
/**
* @brief Merges the elements in X direction.
*
* It merges consecutive successive elements of the game matrix in the X direction.
* If the direction is given as 0, it merges the game matrix to the left
* direction. Any other non zero value merges it to the right direction.
*
* @param matrix The game matrix.
* @param opp The direction of the shift.
*
* @return If the merge was successful
*/
bool merge_x(unsigned char matrix[][SIZE],bool opp);
/**
* @brief Moves the elements in X direction.
*
* It simply performs shift_x() and merge_x().
* If either of them were successful, it also calls add_random()
*
* @param matrix The game matrix.
* @param opp The direction of the move.
*
*/
void move_x(unsigned char matrix[][SIZE], bool opp);
/**
* @brief Shifts the game matrix in Y direction.
*
* It shifts all the elements of the game matrix in the Y direction.
* If the direction is given as 0, it shifts the game matrix in the top
* direction. Any other non-zero value shifts it to the bottom.
*
* @param matrix The game matrix.
* @param opp The direction of the shift.
*
* @return If the shift was successful
*/
bool shift_y(unsigned char matrix[][SIZE], bool opp);
/**
* @brief Merges the elements in Y direction.
*
* It merges consecutive successive elements of the game matrix in the Y direction.
* If the direction is given as 0, it merges the game matrix to the top
* direction. Any other non zero value merges it to the bottom.
*
* @param matrix The game matrix.
* @param opp The direction of the shift.
*
* @return If the merge was successful
*/
bool merge_y(unsigned char matrix[][SIZE],bool opp);
/**
* @brief Moves the elements in Y direction.
*
* It simply performs shift_y() and merge_y().
* If either of them were successful, it also calls add_random()
*
* @param matrix The game matrix.
* @param opp The direction of the move.
*
*/
void move_y(unsigned char matrix[][SIZE],bool opp);
core.c
#include <stdlib.h>
#include <time.h>
#include <math.h>
#include "../include/core.h"
void clear_matrix(unsigned char matrix[][SIZE])
{
for (unsigned int x=0;x<SIZE;x++)
{
for(unsigned int y=0;y<SIZE;y++)
{
matrix[x][y]=0;
}
}
}
int calculate_score(unsigned char matrix[][SIZE])
{
int score=0;
for (unsigned int x=0;x<SIZE;x++)
{
for(unsigned int y=0;y<SIZE;y++)
{
if(matrix[x][y]!=0)
{
score+=pow(BASE,matrix[x][y]);
}
}
}
return score;
}
void print_matrix(unsigned char matrix[][SIZE],FILE* stream)
{
for (unsigned int x=0;x<SIZE;x++)
{
for(unsigned int y=0;y<SIZE;y++)
{
if (matrix[x][y])
{
fprintf(stream,"%d," ,matrix[x][y]);
} else{
fprintf(stream,"-,");
}
}
fprintf(stream,"\n");
}
fprintf(stream,"\n");
}
void add_random(unsigned char matrix[][SIZE])
{
unsigned int pos[SIZE*SIZE];
unsigned int len=0;
for(unsigned int x=0;x<SIZE;x++)
{
for (unsigned int y=0;y<SIZE;y++)
{
if (matrix[x][y]==0){
pos[len]=x*SIZE+y;
len++;
}
}
}
unsigned int index=rand() % len;
matrix[pos[index]/SIZE][pos[index]%SIZE] = 1;
}
bool is_game_over(unsigned char matrix[][SIZE])
{
for(unsigned int x=0;x<SIZE-1;x++)
{
for (unsigned int y=0;y<SIZE-1;y++)
{
if ( matrix[x][y]==matrix[x][y+1] ||
matrix[x][y]==matrix[x+1][y] ||
matrix[x][y]==0)
{return 0;}
}
if( matrix[x][SIZE-1]==matrix[x+1][SIZE-1] ||
matrix[x][SIZE-1]==0) return 0;
if( matrix[SIZE-1][x]==matrix[SIZE-1][x+1] ||
matrix[SIZE-1][x]==0) return 0;
}
return 1;
}
bool shift_x(unsigned char matrix[][SIZE], bool opp)
{
bool moved=0;
int start=0,end=SIZE,increment=1;
if (opp)
{
start=SIZE-1;
end=-1;
increment=-1;
}
for (int x=0;x<SIZE;x++)
{
int index=start;
for(int y=start;y!=end;y+=increment)
{
if (matrix[x][y]!=0)
{
matrix[x][index]=matrix[x][y];
if(index!=y) {
matrix[x][y]=0;
moved=1;
}
index+=increment;
}
}
}
return moved;
}
bool merge_x(unsigned char matrix[][SIZE],bool opp)
{
bool merged=0;
int start=0,end=SIZE-1,increment=1;
if (opp)
{
start=SIZE-1;
end=0;
increment=-1;
}
for (int x=0;x<SIZE;x++)
{
int index=start;
for(int y=start;y!=end;y+=increment)
{
if(matrix[x][y]!=0)
{
if(matrix[x][y]==matrix[x][y+increment])
{
matrix[x][index]=matrix[x][y]+1;
matrix[x][y+increment]=0;
if(index!=y) matrix[x][y]=0;
merged=1;
index+=increment;
}
else
{
matrix[x][index]=matrix[x][y];
if(index!=y) matrix[x][y]=0;
index+=increment;
}
}
}
if(matrix[x][end]!=0)
{
matrix[x][index]=matrix[x][end];
if(index!=end) matrix[x][end]=0;
}
}
return merged;
}
bool merge_y(unsigned char matrix[][SIZE],bool opp)
{
bool merged=0;
int start=0,end=SIZE-1,increment=1;
if (opp)
{
start=SIZE-1;
end=0;
increment=-1;
}
for (int y=0;y<SIZE;y++)
{
int index=start;
for(int x=start;x!=end;x+=increment)
{
if(matrix[x][y]!=0)
{
if(matrix[x][y]==matrix[x+increment][y])
{
matrix[index][y]=matrix[x][y]+1;
matrix[x+increment][y]=0;
if(index!=x) matrix[x][y]=0;
index+=increment;
merged=1;
}
else
{
matrix[index][y]=matrix[x][y];
if(index!=x) matrix[x][y]=0;
index+=increment;
}
}
}
if(matrix[end][y]!=0)
{
matrix[index][y]=matrix[end][y];
if(index!=end) matrix[end][y]=0;
}
}
return merged;
}
bool shift_y(unsigned char matrix[][SIZE],bool opp)
{
bool moved=0;
int start=0,end=SIZE,increment=1;
if (opp)
{
start=SIZE-1;
end=-1;
increment=-1;
}
for (int y=0;y<SIZE;y++)
{
int index=start;
for(int x=start;x!=end;x+=increment)
{
if (matrix[x][y]!=0)
{
matrix[index][y]=matrix[x][y];
if(index!=x)
{
matrix[x][y]=0;
moved=1;
}
index+=increment;
}
}
}
return moved;
}
inline void move_y(unsigned char matrix[][SIZE],bool opp)
{
//Assigning values insted of evaluating directly to force both operations
//Bypassing lazy 'OR' evaluation
bool a=shift_y(matrix,opp),b=merge_y(matrix,opp);
if( a||b) add_random(matrix);
}
inline void move_x(unsigned char matrix[][SIZE],bool opp)
{
//Assigning values insted of evaluating directly to force both operations
//Bypassing lazy 'OR' evaluation
bool a=shift_x(matrix,opp), b=merge_x(matrix,opp);
if(a||b)add_random(matrix);
}
game.c
#include "../include/styles.h"
#include "../include/game.h"
#include <time.h>
#include <stdlib.h>
bool initSDL(SDL_Window **gWindow,SDL_Renderer** gRenderer)
{
bool success = 1;
TTF_Init();
if( SDL_Init( SDL_INIT_VIDEO ) < 0 )
{
perror( "SDL could not initialize!" );
success = 0;
}
else
{
*gWindow = SDL_CreateWindow( "2048", SDL_WINDOWPOS_UNDEFINED, SDL_WINDOWPOS_UNDEFINED, SCREEN_WIDTH, SCREEN_HEIGHT, SDL_WINDOW_SHOWN );
if( gWindow == NULL )
{
perror( "Window could not be created!" );
success = 0;
}
else
{
*gRenderer = SDL_CreateRenderer( *gWindow, -1, SDL_RENDERER_ACCELERATED );
if( gRenderer == NULL )
{
perror( "Renderer could not be created!" );
success = 0;
}
else
{
SDL_SetRenderDrawColor( *gRenderer, g_bg.r,g_bg.g,g_bg.b,g_bg.a );
}
}
}
return success;
}
void draw_text(SDL_Renderer* gRenderer,TTF_Font* font,const char* text, SDL_Rect rect, SDL_Color color){
SDL_Surface* surfaceMessage = TTF_RenderText_Blended(font, text, color);
SDL_Texture* Message = SDL_CreateTextureFromSurface(gRenderer, surfaceMessage);
SDL_Rect message_rect;
TTF_SizeText(font, text, &message_rect.w, &message_rect.h);
message_rect.x = rect.x+rect.w/2-message_rect.w/2;
message_rect.y = rect.y+rect.h/2-message_rect.h/2;
SDL_RenderCopy(gRenderer, Message, NULL, &message_rect);
SDL_DestroyTexture(Message);
SDL_FreeSurface(surfaceMessage);
}
void draw_text_white(SDL_Renderer* gRenderer,TTF_Font* font,const char* text, SDL_Rect rect)
{
SDL_Color White = {255, 255, 255};
draw_text(gRenderer,font,text,rect,White);
}
void SDLclose(SDL_Window* gWindow)
{
SDL_DestroyWindow( gWindow );
gWindow = NULL;
TTF_Quit();
SDL_Quit();
}
void SDLclear(SDL_Renderer* gRenderer)
{
SDL_SetRenderDrawColor( gRenderer, g_bg.r,g_bg.g,g_bg.b,g_bg.a );
SDL_RenderClear( gRenderer );
}
void display_text(SDL_Renderer* gRenderer,const char* text,int size)
{
TTF_Font* font =NULL;
font= TTF_OpenFont(FONT_PATH, size);
if(font==NULL){
perror("The required font was not found");
exit(1);
}
SDL_Color black = {g_fg.r,g_fg.g, g_fg.b};
SDLclear(gRenderer);
SDL_Rect rect = {SCREEN_PAD ,SCREEN_HEIGHT/4 , SCREEN_WIDTH-2*SCREEN_PAD , SCREEN_HEIGHT/2 };
draw_text(gRenderer,font,text,rect,black);
SDL_RenderPresent( gRenderer );
SDL_Delay(1000);
TTF_CloseFont(font);
font=NULL;
}
void draw_matrix(SDL_Renderer* gRenderer,unsigned char matrix[][SIZE], TTF_Font* font)
{
int squareSize=(SCREEN_WIDTH - 2*SCREEN_PAD)/SIZE-SCREEN_PAD;
for(int x=0;x<SIZE;x++)
{
for(int y=0;y<SIZE;y++)
{
SDL_Rect fillRect = { SCREEN_PAD+x*(squareSize+SCREEN_PAD), SCREEN_PAD+y*(squareSize+SCREEN_PAD), squareSize , squareSize };
struct COLOR s=g_COLORS[matrix[y][x]];
SDL_SetRenderDrawColor( gRenderer, s.r, s.g, s.b, s.a );
SDL_RenderFillRect( gRenderer, &fillRect );
char str[15]; // 15 chars is enough for 2^16
sprintf(str, "%d", (int)pow(BASE,matrix[y][x]));
if(matrix[y][x]==0){
str[0]=' ';
str[1]='q4312078q';
}
draw_text_white(gRenderer,font,str,fillRect);
}
}
}
void handle_move(SDL_Event e,unsigned char matrix[][SIZE], SDL_Renderer * gRenderer)
{
if(is_game_over(matrix))
{
display_text(gRenderer,"Game Over",GOVER_FONT_SIZE);
clear_matrix(matrix);
add_random(matrix);
return;
}
switch(e.key.keysym.sym)
{
case SDLK_UP:
move_y(matrix,0);
break;
case SDLK_DOWN:
move_y(matrix,1);
break;
case SDLK_LEFT:
move_x(matrix,0);
break;
case SDLK_RIGHT:
move_x(matrix,1);
break;
default:;
}
}
void draw_button(SDL_Renderer* gRenderer,unsigned char matrix[][SIZE], TTF_Font* font)
{
char txt[]="New Game";
SDL_Rect fillRect = { SCREEN_PAD/2 ,
SCREEN_WIDTH+SCREEN_PAD ,
SCREEN_WIDTH/2-2*SCREEN_PAD ,
(SCREEN_HEIGHT-SCREEN_WIDTH)-2*SCREEN_PAD };
SDL_SetRenderDrawColor( gRenderer,g_button_bg.r, g_button_bg.g, g_button_bg.b,g_button_bg.a );
SDL_RenderFillRect( gRenderer, &fillRect );
draw_text_white(gRenderer,font,txt,fillRect);
}
void button_action(SDL_Event e,unsigned char matrix[][SIZE])
{
SDL_Rect draw_rect = { SCREEN_PAD/2 ,
SCREEN_WIDTH+SCREEN_PAD ,
SCREEN_WIDTH/2-2*SCREEN_PAD ,
SCREEN_HEIGHT-SCREEN_WIDTH-2*SCREEN_PAD };
if(e.button.button == SDL_BUTTON_LEFT &&
e.button.x >= draw_rect.x &&
e.button.x <= (draw_rect.x + draw_rect.w) &&
e.button.y >= draw_rect.y &&
e.button.y <= (draw_rect.y + draw_rect.h))
{
clear_matrix(matrix);
add_random(matrix);
}
}
void draw_score(SDL_Renderer* gRenderer,unsigned char matrix[][SIZE], TTF_Font* font)
{
char score[15]; //15 chars is enough for score.
sprintf(score, "%d", calculate_score(matrix));
char scoreText[30]="Score:";
strncat(scoreText,score,15);
SDL_Rect fillRect = { SCREEN_WIDTH/2+5,
SCREEN_WIDTH+SCREEN_PAD,
SCREEN_WIDTH/2-2*SCREEN_PAD,
SCREEN_HEIGHT-SCREEN_WIDTH-2*SCREEN_PAD };
SDL_SetRenderDrawColor( gRenderer,g_score_bg.r,g_score_bg.g,g_score_bg.b,g_score_bg.a );
SDL_RenderFillRect( gRenderer, &fillRect );
draw_text_white(gRenderer,font,scoreText,fillRect);
}
void render_game(SDL_Renderer* gRenderer,unsigned char matrix[][SIZE], TTF_Font* font)
{
SDLclear(gRenderer);
draw_matrix(gRenderer,matrix,font);
draw_score(gRenderer,matrix,font);
draw_button(gRenderer,matrix,font);
SDL_RenderPresent( gRenderer );
}
void gameLoop(unsigned char matrix[][SIZE],SDL_Renderer* gRenderer)
{
TTF_Font* font =NULL;
font= TTF_OpenFont(FONT_PATH, CELL_FONT_SIZE);
if(font==NULL){
perror("The required font was not found");
exit(1);
}
render_game(gRenderer,matrix,font);
bool quit=0;
SDL_Event e;
while (!quit)
{
while( SDL_PollEvent( &e ) != 0 )
{
//User requests quit
if( e.type == SDL_QUIT )
{
quit = 1;
}
else if(e.type==SDL_KEYUP)
{
handle_move(e,matrix,gRenderer);
//Redraw all portions of game
render_game(gRenderer,matrix,font);
}
else if(e.type==SDL_MOUSEBUTTONUP)
{
button_action(e,matrix);
render_game(gRenderer,matrix,font);
}
}
}
TTF_CloseFont(font);
//No need to null out font.
}
int main(int argc,char** argv)
{
//Set up the seed
srand(time(NULL));
//Set up the game matrix.
unsigned char matrix[SIZE][SIZE];
clear_matrix(matrix);
add_random(matrix);
//Init the SDL gui variables
SDL_Window* gWindow = NULL;
SDL_Renderer* gRenderer = NULL;
if(!initSDL(&gWindow,&gRenderer)){exit(0);};
display_text(gRenderer,"2048",TITLE_FONT_SIZE);
gameLoop(matrix,gRenderer);
//Releases all resource
SDLclose(gWindow);
return 0;
}
#1 楼
做得好。这不是一个完整的综述,而是一份(简短的)可能的改进清单,当我浏览您的代码时发现了这些改进。文档
首先:谢谢!有文档很棒。
请注意,是否将文档放在页眉或源中存在一些争论。我想指出的是,我只会在标题中添加一个
@brief
描述,并在源代码中添加一个完整的文档。这样一来,就可以快速浏览所有功能,并在找到正确的功能时对其进行详细调查。但是,这是个人喜好,在团队项目中,您将遵循已存在的准则。 doxygen生成的文档无论哪种方式都将保持不变。游戏现在停留在编译时选择的大小。如果要启用其他尺寸的电路板,则必须添加一些逻辑以使该电路板始终保持在矩阵中,因此必须使用变量arr[][SIZE]
。另外,像4x6这样有趣的板子目前尚无法实现。如果您确实想采用该方法,请使用类型别名:typedef unsigned char board_type[][SIZE];
但是,考虑到任意板尺寸,您可能希望在以下位置引入合适的矩阵类型一点:
struct board_type {
unsigned width;
unsigned height;
unsigned char * actual_board;
};
是否使用单个分配的
SIZE
或unsigned char matrix[][SIZE]
乘以malloc(sizeof(*actual_board)*SIZE*SIZE)
至少对于小尺寸而言并不重要。前者在内存方面更易于处理,后者在访问方面更容易。如果您想在两者之间进行交换,可以使用一组小的
SIZE
函数: br /> unsigned char board_get(struct board_type *board, unsigned row, unsigned col) {
assert(row < board->height);
assert(col < board->width);
return board->actual_board[row * board->width + col];
// or, if actual_board is a `unsigned char**`
return board->actual_board[row][col];
}
void board_set(struct board_type *board, unsigned row, unsigned col, unsigned char value) {
assert(row < board->height);
assert(col < board->width);
board->actual_board[row * board->width + col] = value;
// or, if actual_board is a `unsigned char**`
board->actual_board[row][col] = value;
}
malloc(sizeof(*actual_board)*SIZE)
不是整数inline
函数的两个参数都使用pow
,这很好。但是,对于简单的整数来说,这是一个完全的矫kill过正。在过去的回顾中,我分享了更多详细信息,但是对于您的游戏而言,简单的移位就足够了:unsigned long pow_integral(unsigned char base, unsigned char exponent) {
if(base == 2) {
return (1lu << exponent);
} else {
// exercise; use "double-and-add" method for logarithmic speed
}
}
没有神奇的数字
像文档一样,这是代码的重要功能。代码中没有魔术数字,每个数字都是正确的,以提供一些自我说明。但是,通常希望对
pow
进行一些注释,并且Doxygen应该发出一些警告。double
中只有一个魔术数字。请参阅下面的“失明”。C99具有
define
话虽如此,偶尔也有
#define
或main
。由于bool
,很明显它们的意思是bool success = 1
和0
。但是,您可以只使用bool
,而可以使用boolean定义的语言。这在StackExchange上的代码中并不明显,但是在GitHub上。您可能想解决此问题,因为几个编辑器使用8个空格作为制表符,而不是4个空格。提供的字符串,以及存储在true
上的错误代码的文字说明。据我所知,没有一个SDL函数设置false
,因此#include <stdbool.h>
不会报告正确的错误。相反,请使用perror
或perror
和errno
。早期返回
您的某些函数已经准备好返回代码,例如
errno
:bool initSDL(SDL_Window **gWindow,SDL_Renderer** gRenderer)
{
bool success = 1;
TTF_Init();
if( SDL_Init( SDL_INIT_VIDEO ) < 0 )
{
perror( "SDL could not initialize!" );
success = 0;
}
else
{
*gWindow = SDL_CreateWindow( "2048", SDL_WINDOWPOS_UNDEFINED, SDL_WINDOWPOS_UNDEFINED, SCREEN_WIDTH, SCREEN_HEIGHT, SDL_WINDOW_SHOWN );
if( gWindow == NULL )
{
perror( "Window could not be created!" );
success = 0;
}
else
{
*gRenderer = SDL_CreateRenderer( *gWindow, -1, SDL_RENDERER_ACCELERATED );
if( gRenderer == NULL )
{
perror( "Renderer could not be created!" );
success = 0;
}
else
{
SDL_SetRenderDrawColor( *gRenderer, g_bg.r,g_bg.g,g_bg.b,g_bg.a );
}
}
}
return success;
}
这段代码受厄运金字塔的影响。但是,在所有
perror
中,我们实际上都没有清理资源,因此我们可以编写以下内容:bool initSDL(SDL_Window **gWindow,SDL_Renderer** gRenderer)
{
TTF_Init();
if( SDL_Init( SDL_INIT_VIDEO ) < 0 )
{
fprintf(stderr, "SDL could not initialize: %s\n", SDL_GetError());
return false;
}
*gWindow = SDL_CreateWindow( "2048", SDL_WINDOWPOS_UNDEFINED, SDL_WINDOWPOS_UNDEFINED, SCREEN_WIDTH, SCREEN_HEIGHT, SDL_WINDOW_SHOWN );
if( gWindow == NULL )
{
fprintf(stderr, "Window could not be created: %s\n", SDL_GetError());
return false;
}
*gRenderer = SDL_CreateRenderer( *gWindow, -1, SDL_RENDERER_ACCELERATED );
if( gRenderer == NULL )
{
fprintf(stderr, "Renderer could not be created: %s\n", SDL_GetError());
// What about gWindow?
// We should probably do something about it
return false;
}
SDL_SetRenderDrawColor( *gRenderer, g_bg.r,g_bg.g,g_bg.b,g_bg.a );
}
资源管理和布尔盲目性
如上面的代码所示,当
printf
成功但fprintf
失败时,SDL_GetError
没有被正确销毁。此外,initSDL
的调用者无法找出初始化失败的原因。枚举通常是在这种情况下的解决方案,至少在我们不清理的情况下。也就是说,
if
中的CreateWindow
已关闭。零退出值表示游戏能够运行并退出。但是,如果CreateRenderer
失败,则游戏将无法运行,我们应该向操作系统报告该错误:对于
gWindow
s始终使用(适当)块但是,由于另一原因,该行很奇怪:它不遵循您通常的缩进方式。让我们解决一下:
if(!initSDL(&gWindow,&gRenderer)){exit(EXIT_FAILURE);};
有一个流浪的分号。虽然这不是错误,但它表示代码以前是
initSDL
,然后有些事情变了,然后又变回来了。有条件的代码,因此请确保使代码尽可能的干净。可读性
编译器不在乎是否编写
br />
但是人类会很难受。使您的代码也易于使用:
if(!initSDL(&gWindow,&gRenderer)){
exit(EXIT_FAILURE);
}; // <<--- ?
至少可以通过这种方式改进
exit(0)
和main
。命名
在计算机科学中,存在两个难题:命名,缓存和一次性错误。在这里,我们重点讨论第一个。
仅在前缀具有含义的情况下使用前缀
initSDL
和if
具有if(!initSDL(&gWindow,&gRenderer))exit(0);
前缀(未解释)。其他变量都没有前缀。如果
move_x
是SDL对象的通用前缀,那很好,但是,我想这是move_y
ame的意思。但是,很奇怪的是,板子本身就没有前缀。这样说...
按功能而不是形式命名
在整个游戏中,该板子称为
gRenderer
。但是,gWindow
是数学术语或电影标题的术语,但与“标板”功能并不完全匹配。另一方面,g
将是一个完美的名字。另外,如果您想实现一个变体,让玩家同时在两个板上玩游戏,那么复数会容易得多。不要惊讶其他人
g
和g
让我感到惊讶。这两个函数都没有遵循通常的matrix
方法,因为它们都不是matrix
的方法。 。或者,遵循board
方法并调用函数SDLclose
和SDLclear
。这些名称是完全明确的。创建与破坏之间的对称性
SDL_<name>
中存在某些问题:a=foo(), b=too(), c=quux()+a*b; if(a<b)c-=t;
鉴于我们作用于局部指针,因此从外部看不到
SDL
。这可能只是一个小错误。但是,如果要以对称方式使用initSDL
和clearSDL
,我们最终会得到a = foo();
b = too();
c = quux() + a * b;
if ( a < b ) {
c -= t;
}
在这里,
closeSDL
很有意义。但是,由于SDLclose
是少数没有随附文档的功能之一,因此尚不清楚预期的行为是什么,因此我坚持使用前者,例如void SDLclose(SDL_Window* gWindow)
{
SDL_DestroyWindow( gWindow );
gWindow = NULL;
TTF_Quit();
SDL_Quit();
}
评论
\ $ \ begingroup \ $
感谢您的好评!我没有考虑过移位或pow()的运行时。 SDL_GetError也是一个非常有用且具体的注释。我很确定我已经删除了所有魔术数字。我没有意识到exit(0/1)也可以删除。这真是令人惊喜。我选择了matrix [] [SIZE]格式,因为我永远不会摆弄非正方形板。但是,既然您提到它,实现起来似乎很有趣。再一次谢谢你!
\ $ \ endgroup \ $
– Gnik
18/12/27在15:15
\ $ \ begingroup \ $
不是OP,但是您可以解释一下:“无论是使用单个分配的malloc(sizeof(actual_board)* SIZESIZE)还是SIZE乘以malloc(sizeof(* actual_board)* SIZE),至少对于小尺寸而言,不重要,前者在内存方面更易于处理,后者在访问方面更容易。”,特别是为什么后者在访问方面更容易的原因。
\ $ \ endgroup \ $
–马里奥·伊沙克(Mario Ishac)
18/12/27在22:00
\ $ \ begingroup \ $
SDLclose函数肯定包含一个错误。我已在最新修订版中修复了该问题,但仍保留更改。也许我会在几周后对实现进行一些更改后发布更新后的问题。您对代码的理解程度也令人恐惧。非常感谢您抽出宝贵时间来参加本次评论。
\ $ \ endgroup \ $
– Gnik
18-12-28 at 2:19
\ $ \ begingroup \ $
@MarDev当然。第一个将导致TYPE *矩阵。现在,有效索引为\ $ 0,1,\ ldots,\ text {N} ^ 2-1 \ $。这对于重置非常方便,因为您只需要memset,但是现在您需要matrix [row + column * N]或类似的东西。后者导致TYPE **矩阵。我们现在有一个间接寻址,必须使用matrix [i] [j]。如果您想使用头寸,这会容易得多,因为我们不必转换\ $ \ {0,\ ldots \,N-1 \} \ times \ {0,\ ldots \,N-1 \}手动将\ to \ {0,\ ldots,N ^ 2-1 \} \ $与\ $ ind = xN + y \ $。但是,这种可能性不太可能会持续下去。我在答案中添加了示例。
\ $ \ endgroup \ $
– Zeta
18/12/28在9:10
#2 楼
由于其他评论已经达到了大多数要点,因此我仅提及一些尚未涵盖的内容。避免在
#include
s中使用相对路径通常最好从
#include
文件中省略相对路径名,而是将编译器指向适当的位置。因此,请使用以下代码:#include "../include/styles.h"
#include "../include/game.h"
编写以下代码:
#include "styles.h"
#include "game.h"
这样可以减少代码对实际文件结构,并将此类详细信息放在一个位置:
Makefile
或编译器配置文件。使用cmake
,我们可以使用include_directories
。由于您已经在顶级CMakeLists.txt
中获得了该名称,因此只需在该include
指令中附加CMake
目录。了解
#include
的工作方式在大多数平台上,
#include "math.h"
之间的区别#include <math.h>
是前者在当前目录中排名第一。因此,对于诸如SDL2/SDL.h
之类的系统文件,您应该真正使用#include <SDL2/SDL.h>
代替。有关更多详细信息,请参见此问题。在许多情况下,这两种方法都有可能奏效,但根据人类阅读者的惯例,项目中的文件使用
""
而系统包含(文件不在项目中)使用<>
。这是不精确的区分,但是是一种思考的有用方法。不要重复自己(DRY)
merge_x
和merge_y
函数几乎相同。我认为将它们组合为一个merge
函数是有意义的,它将一个方向作为附加参数。使用shift
和move
函数可以采用相同的方法。例如,这是一个组合的shift()
函数,它带有一个额外的参数,指示ydir
:bool shift(Board board, bool opp, bool ydir)
{
bool moved=false;
int start=0,end=SIZE,increment=1;
if (opp)
{
start=SIZE-1;
end=-1;
increment=-1;
}
for (int a=0;a<SIZE;a++)
{
int index=start;
for(int b=start;b!=end;b+=increment)
{
int x = ydir ? b : a;
int y = ydir ? a : b;
if (board[x][y]!=0)
{
if (ydir) {
board[index][y]=board[x][y];
} else {
board[x][index]=board[x][y];
}
if(index!=b) {
board[x][y]=0;
moved=true;
}
index+=increment;
}
}
}
return moved;
}
在可行的地方使用
const
Board
不是,也不应通过print_board
功能进行更改。因此,建议将函数的签名更改为:void print_board(const Board board, FILE* stream);
可以对
is_game_over
和calculate_score
做类似的更改>不要泄漏内存
在不泄漏内存的情况下,很难正确使用SDL接口,因为并不总是很容易看出分配了哪些功能以及分配了哪些功能。在此代码中,
initSDL
创建一个renderer
,但从不调用SDL_DestroyRenderer
。我建议添加指向渲染器的指针作为closeSDL
的参数,并在调用SDL_DestroyRenderer
之前确保其为非NULL。 :inline void move_x(Board board, bool opp)
{
//Assigning values insted of evaluating directly to force both operations
//Bypassing lazy 'OR' evaluation
bool a=shift_x(board,opp), b=merge_x(board,opp);
if(a||b)add_random(board);
}
它可以更清楚地写为:
inline void move_x(Board board, bool opp)
{
bool move_or_shift = shift_x(board,opp);
move_or_shift |= merge_x(board,opp);
if (move_or_shift) {
add_random(board);
}
}
用户的想法
有一些小改进可以使游戏更好。首先是允许用户看到并品尝高分,而不是立即启动新游戏。其次是检测是否可能进行任何移动,而不是在评估之前等待用户尝试移动。
评论
\ $ \ begingroup \ $
很棒的评论!我想澄清的一件事是DRY原则。我试图为shift制作一个函数,而不是shift_x和shift_y。你能告诉我是否可行?我考虑过为清楚起见而重复代码,而不是编写复杂的shift函数。您将如何处理?也适用于“简化代码”部分。我没明白你的意思。您只提供了代码,我认为该功能非常简单。我如何使它更简单?再次感谢您!
\ $ \ endgroup \ $
– Gnik
18-12-29在3:17
\ $ \ begingroup \ $
关于包含。我与样式不一致,但是用“”代替<>书写几乎不是总是更好吗?特别是由于#include“ SDL2 / SDL2.h”如果在源目录中未找到任何内容,则会回退到#include
\ $ \ endgroup \ $
– Gnik
18/12/29在3:39
\ $ \ begingroup \ $
我已经在答案中添加了答案,以尝试回答您的所有问题。如果仍然不清楚,请再次询问。
\ $ \ endgroup \ $
–爱德华
18/12/29在4:04
#3 楼
添加到@Zeta的出色答案。空白
您的空白策略非常不一致。有时,您会这样调用函数:
SDL_SetRenderDrawColor( *gRenderer, g_bg.r,g_bg.g,g_bg.b,g_bg.a );
在
()
内有空格,而其他时候,您会这样调用它们:TTF_SizeText(font, text, &message_rect.w, &message_rect.h);
里面没有空格。类似地,有时您会像上面一样在逗号后放置空格,而有时则不这样做,例如:此处:
display_text(gRenderer,"2048",TITLE_FONT_SIZE);
gameLoop(matrix,gRenderer);
有时您会在算术运算符周围放置空格,但通常不会。
struct COLOR g_COLORS[]={
{230, 227, 232,255},
{255, 127, 89,255},
// etc.
};
>有时,您还将指针与类型相关联,而有时将其与变量相关联:
int squareSize=(SCREEN_WIDTH - 2*SCREEN_PAD)/SIZE-SCREEN_PAD;
最后,在标头中,有时会有两行空行分隔docstring-forward-声明对,有时只有一对。
我鼓励您使用
clang-format
来保持样式一致。正确性
类似代码即使它们只是理论上的可移植性错误,也总是存在错误。即使在计算机上工作,也要摆脱编写它的习惯。
特别地,C标准允许
int
占用4个以上的空间。个字节。相反,您应该在运行时向snprintf
询问长度并分配自己,或者使用asprintf
(这是GNU扩展名)。要向snprintf
询问所需的缓冲区大小,请为其提供一个空指针和零长度,如下所示:bool initSDL(SDL_Window **gWindow,SDL_Renderer** gRenderer)
如果您愿意使用此模式,将使其更容易GNU扩展,例如:
char score[15]; //15 chars is enough for score.
sprintf(score, "%d", calculate_score(matrix));
char scoreText[30]="Score:";
strncat(scoreText,score,15);
当然,您总是可以将
snprintf
与固定大小的缓冲区一起使用,并在事情变得太大时才截断。评论
\ $ \ begingroup \ $
下次我写东西时会考虑clang格式。似乎大多数问题都可以通过使用更好的构建系统/ IDE来避免。同样,在运行时设置缓冲区大小绝对是解决问题的方法。谢谢!
\ $ \ endgroup \ $
– Gnik
18/12/28在11:09
评论
这是一个很好的问题,我很喜欢阅读它和评论。请注意:只有直接嵌入到问题中的代码才有资格审核。 (有兴趣查看它的任何人都可以使用GitHub链接,但该链接无效,无法复查。)如果您想查看更新的代码,则在实现更改后发布新问题是完全可以接受的。我们喜欢迭代式评论,尤其是在有趣的问题上。也许与您的需求无关,但是我可以使用javascript作为前端而使用python作为后端来制作2048吗?
@austingae 2048最初是用JavaScript编写的。这是一款单人游戏,因此没有使用后端。但是您可以轻松地使用python后端,并使用排行榜和其他模式/变体制作(多人游戏)2048。