这是我用C ++编写的Snake游戏版本。如何进行改进,以及对将来的项目有什么一般建议?

#include <iostream>
#include <conio.h>

void run();
void printMap();
void initMap();
void move(int dx, int dy);
void update();
void changeDirection(char key);
void clearScreen();
void generateFood();

char getMapValue(int value);

// Map dimensions
const int mapwidth = 20;
const int mapheight = 20;

const int size = mapwidth * mapheight;

// The tile values for the map
int map[size];

// Snake head details
int headxpos;
int headypos;
int direction;

// Amount of food the snake has (How long the body is)
int food = 3;

// Determine if game is running
bool running;

int main()
{
    run();
    return 0;
}

// Main game function
void run()
{
    // Initialize the map
    initMap();
    running = true;
    while (running) {
        // If a key is pressed
        if (kbhit()) {
            // Change to direction determined by key pressed
            changeDirection(getch());
        }
        // Upate the map
        update();

        // Clear the screen
        clearScreen();

        // Print the map
        printMap();

        // wait 0.5 seconds
        _sleep(500);
    }

    // Print out game over text
    std::cout << "\t\t!!!Game over!" << std::endl << "\t\tYour score is: " << food;

    // Stop console from closing instantly
    std::cin.ignore();
}

// Changes snake direction from input
void changeDirection(char key) {
    /*
      W
    A + D
      S

      1
    4 + 2
      3
    */
    switch (key) {
    case 'w':
        if (direction != 2) direction = 0;
        break;
    case 'd':
        if (direction != 3) direction = 1;
        break;
    case 's':
        if (direction != 4) direction = 2;
        break;
    case 'a':
        if (direction != 5) direction = 3;
        break;
    }
}

// Moves snake head to new location
void move(int dx, int dy) {
    // determine new head position
    int newx = headxpos + dx;
    int newy = headypos + dy;

    // Check if there is food at location
    if (map[newx + newy * mapwidth] == -2) {
        // Increase food value (body length)
        food++;

        // Generate new food on map
        generateFood();
    }

    // Check location is free
    else if (map[newx + newy * mapwidth] != 0) {
        running = false;
    }

    // Move head to new location
    headxpos = newx;
    headypos = newy;
    map[headxpos + headypos * mapwidth] = food + 1;

}

// Clears screen
void clearScreen() {
    // Clear the screen
    system("cls");
}

// Generates new food on map
void generateFood() {
    int x = 0;
    int y = 0;
    do {
        // Generate random x and y values within the map
        x = rand() % (mapwidth - 2) + 1;
        y = rand() % (mapheight - 2) + 1;

        // If location is not free try again
    } while (map[x + y * mapwidth] != 0);

    // Place new food
    map[x + y * mapwidth] = -2;
}

// Updates the map
void update() {
    // Move in direction indicated
    switch (direction) {
    case 0: move(-1, 0);
        break;
    case 1: move(0, 1);
        break;
    case 2: move(1, 0);
        break;
    case 3: move(0, -1);
        break;
    }

    // Reduce snake values on map by 1
    for (int i = 0; i < size; i++) {
        if (map[i] > 0) map[i]--;
    }
}

// Initializes map
void initMap()
{
    // Places the initual head location in middle of map
    headxpos = mapwidth / 2;
    headypos = mapheight / 2;
    map[headxpos + headypos * mapwidth] = 1;

    // Places top and bottom walls 
    for (int x = 0; x < mapwidth; ++x) {
        map[x] = -1;
        map[x + (mapheight - 1) * mapwidth] = -1;
    }

    // Places left and right walls
    for (int y = 0; y < mapheight; y++) {
        map[0 + y * mapwidth] = -1;
        map[(mapwidth - 1) + y * mapwidth] = -1;
    }

    // Generates first food
    generateFood();
}

// Prints the map to console
void printMap()
{
    for (int x = 0; x < mapwidth; ++x) {
        for (int y = 0; y < mapheight; ++y) {
            // Prints the value at current x,y location
            std::cout << getMapValue(map[x + y * mapwidth]);
        }
        // Ends the line for next x value
        std::cout << std::endl;
    }
}

// Returns graphical character for display from map value
char getMapValue(int value)
{
    // Returns a part of snake body
    if (value > 0) return 'o';

    switch (value) {
        // Return wall
    case -1: return 'X';
        // Return food
    case -2: return 'O';
    }
}


评论

也许您也可以跨平台完成它
方向的枚举,而不是1,2,3,4,可能还有方向的查询表-> dx,dy

添加一个emscripten构建并将其托管在github页面或jsfiddle上进行播放。

仅供参考:关于堆栈溢出的有趣的相关答案

您的游戏很棒,我正在从中学习:)

#1 楼

除非您知道每台计算机上的游戏循环将持续多长时间,否则将sleep设置为常数通常是不好的做法。如果您知道想要2fps,则使其保持一致的一种好方法是在游戏循环开始时获取时间,然后在结束时找出差异,并以此来计算所需的睡眠时间保持相同的步骤。例如,如果循环需要0.1秒,而您想要2fps,则将睡眠时间设为0.4秒。

除此之外,我可能会说,您需要在food旁边添加另一个变量snakeLength或其他名称。我不知道您是否要在屏幕上打印分数,但是如果您跟踪分数,我想您希望它从0开始而不是3,而另外1个int当您获得更好的可读性时,没什么大不了的。

可能考虑将direction变成一个枚举,并带有UP,DOWN,LEFT和RIGHT,因为现在要跟随它有点棘手,而您不会不需要更改太多的逻辑,因为int枚举包含一些额外的内容,因此您可以轻松地以现在的方式进行比较。话虽如此,我不确定我是否遵循您的direction值,因为在任何地方都看不到direction设置为5,因此似乎不需要检查。

在您的generateFood函数中,您可以直接访问map,并在其中创建了一个可以完全在getMapValue中完成此功能的函数,因此您可能要考虑使用它,因为将来某个时候您可能决定将其设置为Map类,然后您将在访问私有变量时会遇到错误(我希望如此!)。

除此之外,事情看起来还不错,所以我将开始精心挑选:P。我只是建议一些小事情,例如按字母顺序排列您的#include和功能原型。因为您有2个,所以这没什么大不了的,但是要记住一些事情。除此之外,您的clearScreen()printMap()感觉很像Draw(),因此您可以将它们都包装在该函数中,然后只需调用initupdatedrawcleanup(在进行对象加载并使用指针和因为您似乎几乎遵循了游戏循环模式(顺便说一句,如果您打算制作更多游戏,请阅读整本书,这是一件很美的事情),并且阅读该文章可以更好地解释我对sleep的观点。

#2 楼

这篇评论将主要围绕代码风格和常规代码质量改进进行。

OOP:

首先考虑的是,对于C ++程序,我们希望看到一些面向对象的编程-哎呀。您的程序基本上是结构化程序设计,看起来比C更像C,而不是C ++。

您应该首先将代码重构为几个类。
我会想到诸如SnakeGameBoard / MapFood之类的某些类。文件中的一些全局变量。在大多数情况下,应避免使用全局变量。在游戏中,您可以将这些变量作为参数传递给函数。

但是假设您要坚持使用全局变量。如果是这样,您仍应尝试最小化范围。如果不需要在声明变量的文件外部使用变量,则应将其包装在未命名的名称空间中,以使变量文件具有作用域。像这样:

namespace 
{
    // The tile values for the map
    int map[size];

    // Snake head details
    int headxpos;
    int headypos;
    int direction;

    ...
} // namespace


现在这些变量不能在声明它们的文件外部访问。
这减少了其他模块意外更改状态的机会。

此外,最好始终将全局变量初始化为某个默认安全值。这些变量:


int headxpos;
int headypos;
int direction;
bool running;



应初始化为某些内容。

命名花絮:

优选的常量命名约定,例如mapwidthmapheight,是ALL_UPPERCASE。通过清楚地将可变变量与编译时常量区分开来,这有助于提高可读性。

const int MAP_WIDTH      = 20;
const int MAP_HEIGHT     = 20;
const int TOTAL_MAP_SIZE = MAP_WIDTH * MAP_HEIGHT;
TOTAL_MAP_SIZE更具描述性。

其他:

通过将size放在文件末尾,可以避免使用函数原型。

通过将main()替换为交换机中的默认情况,可以简化getMapValue()

char getMapValue(int value)
{
    switch (value) {
    case -1 : return 'X'; // Return wall
    case -2 : return 'O'; // Return food
    default : return 'o'; // Returns a part of snake body
    }
}


可移植性:

if仅适用于Windows,因此此代码无法在其他操作系统上编译。
我不知道对conio.hkbhit()的任何简单替换,但是getch()
可以在C ++ 11中用_sleep()替换。

std::this_thread::sleep_for()也不属于C ++标准,因此不可移植。

评论


\ $ \ begingroup \ $
仅凭我的两分钱,我在工作场所中发现了很多不属于自己的OOP。坦白说,我认为人们应该先学习编程,然后再学习OOP,因此我认为该建议的优先级比您低。话虽这么说,您没有错,只是声称所有C ++都需要或期望拥有OOP。
\ $ \ endgroup \ $
– Poik
2014年10月13日15:12



\ $ \ begingroup \ $
@Poik,我绝不是说所有C ++需求都期望使用OOP,这就是为什么我使用这样的短语:“我们希望看到一些面向对象的编程”,因为它是语言所倡导的主要范例。在这里提供的代码中,OOP确实可以提供更好的解决方案。就像纯函数式编程一样。当OOP提供好处时,就应该使用OOP,而不仅仅是因为它是做事的“正确方法”。
\ $ \ endgroup \ $
–glampert
2014年10月13日下午16:31

\ $ \ begingroup \ $
我不理解anti-oop / pro-procedural论点。从体系结构的角度来看,类(或您的语言喜欢用的任何类)只是您已经编写的实例化版本。 (您文件的全局变量成为成员变量,而函数成为方法)。最大的区别是OOP更加灵活,因为可以在运行时定义和满足接口,从而实现干净的运行时实现交换或按接口组织的集合(而不是内存布局)之类的事情。
\ $ \ endgroup \ $
–weberc2
14-10-13在18:58

\ $ \ begingroup \ $
@Poik很抱歉使用C ++进行标记,我只是通过此帖子链接转到该页面,那里的最高答案是:“如果一个人用C风格编写代码并用C ++进行编译,那么这是一个C ++问题”。但是我确实看到OOP将使代码更易于阅读,并感谢您的提示:)。
\ $ \ endgroup \ $
–质朴
2014年10月13日在21:24



\ $ \ begingroup \ $
@ weberc2不幸的是,这不是这个论坛,但是我的大多数下意识来自于从不了解范例的人那里继承了不可重用的OOP代码,而这些人只是因为他们总是被告知他们应该。作为一名高性能计算研究人员,我倾向于使用很多过程编码,因为大多数事情至少在最初是临时的。一旦证明了有用性,便尝试将其抽象出来,或决定将要发生的事情。相反,对我来说,要做更多的工作,但与往常一样,您的里程[会]有所不同。
\ $ \ endgroup \ $
– Poik
14-10-14在14:11