如何为C ++游戏创建保存文件?


33

我正在为视频游戏编程课程的决赛编写代码,我想知道如何为我的游戏创建保存文件,以便用户可以玩,然后稍后再回来。任何想法如何做到的,我之前做过的每件事都是单运行程序。



2
您还可以使用SQLite
Nick Shvelidze 2012年

1
@Shvelo虽然可以做到,但似乎会增加很多不必要的复杂性。
Nate 2012年

Answers:


38

您需要使用序列化将变量存储在硬盘中。序列化有很多类型,.NET XML是一种通用格式,尽管有二进制和JSON序列化器可用。我不是C ++程序员,但是快速搜索找到了一个有关C ++序列化的示例:

有些库提供序列化功能。其他答案中提到了一些。

您将感兴趣的变量可能与游戏状态有关。例如,您可能想知道此类信息

  1. 玩家正在玩第3级
  2. 玩家处于X,Y世界坐标
  3. 玩家的背包中有三件物品
    1. 武器
    2. 盔甲
    3. 餐饮

您不会真正在乎使用什么纹理(除非您的播放器可以更改其外观,这是一种特殊情况),因为它们通常是相同的。您需要集中精力保存重要的游戏状态数据。

当您开始游戏时,您可以正常开始“新”游戏(这会加载纹理,模型等),但是在适当的时候,您将保存文件中的值重新加载到游戏状态对象中,从而替换了“默认”新游戏。游戏状态。然后,您允许播放器继续播放。

我在这里已经对其进行了极大的简化,但是您应该了解一般的想法。如果您有更具体的问题,请在此处提出一个新问题,我们可以尝试为您提供帮助。


我明白我需要保存,但我想知道什么是具体的方式是什么,你将它保存为.txt文件的项目,这些修改的变量,或者一些其他的方式
塔克摩根

是的,如果您的游戏很简单,那么一个文本文件可能就足够了;您需要记住,任何人都可以编辑文本文件并制作自己的保存游戏...
Nate

保存文本文件不仅仅用于简单的游戏。Paradox使用结构化的文本格式来保存他们使用与旗舰Europa Universalis引擎相同的引擎创建的所有游戏的文件。尤其是后期游戏,这些文件可能非常庞大。
Dan Neely 2012年

1
@DanNeely一个公平的说法,没有理由不能使用文本格式存储大量复杂数据,但是通常来说,当数据如此复杂时,使用另一种格式(二进制,xml等)的好处就更加明显。
Nate 2012年

1
@NateBross同意。Paradox游戏非常易于修改,并为场景数据使用了类似(相同?)的格式。将他们的大多数数据存储为文本意味着他们无需投资用于公共用途的方案编辑器工具。
Dan Neely 2012年

19

通常,这是特定于您的游戏的。我敢肯定,到目前为止,您已经了解了如何在类中读写文件。基本思想是:

  1. 退出游戏时,将要保存的值写入文件。
  2. 加载游戏时,请检查是否存在保存文件,如果存在,则将读取的值加载到程序中。如果该文件不存在,请像现在一样继续操作,并将值设置为其开始/默认值。

您写的内容取决于您,取决于您的游戏。一种写方法是将所需的变量按特定顺序写为字节流。然后在加载时,以相同的顺序将它们读入程序。

例如(使用快速伪代码):

SaveGame(FileInput file) {
    file.writeInt(playerLevel);
    file.writeInt(playerHealth);
    file.writeInt(gameProgress);
}

LoadGame(FileInput file) {
    if(file.exists()) {
        playerLevel= file.readInt();
        playerHealth = file.readInt();
        gameProgress = file.readInt();
    } else {
        playerLevel = 1;
        playerHealth = 100;
        gameProgress = 0;
    }
}

1
这种方法既好又小,尽管我建议为数据块放入一些简单的标签。这样,如果以后您需要更改通常在文件中间的内容,则可以这样做,并且您唯一需要做的“从旧版本转换”就在那个块内。对于一次完成任务而言,这并不重要,但是如果您在人们开始获取保存文件后继续工作,那么仅使用具有位置唯一标识符的纯字节就有点噩梦。
鲁宁2012年

1
是的,这不会生成可用于将来的保存文件。它也不适用于字节大小可变的数据(例如字符串)。后者很容易修复,方法是先写入将要写入的数据的大小,然后在加载时使用该大小读取正确的字节数。
MichaelHouse

6

可能有很多方法可以做到这一点,但是我一直发现并且个人和专业使用的最简单方法是创建一个包含所有要保存的值的结构。

struct SaveGameData
{
    int              characterLevel; // Any straight up values from the player
    int              inventoryCount; // Number of items the player has on them or stored or what not
    int[STAT_COUNT]  statistics;     // This is usually a constant size (I am tracking X number of stats)
    // etc
}

struct Item
{
    int itemTypeId;
    int Durability; // also used as a 'uses' count for potions and the like
    int strength;   // damage of a weapon, protection of armor, effectiveness of a potion
    // etc
}

然后,我仅使用基本的文件IO值在文件中读写数据。ventoryCount是文件中主要SaveGameData结构之后保存的Item结构的数量,因此我知道在获取该数据后要读取多少个Item结构。这里的关键是,当我想保存一些新内容时,除非它包含项目列表或类似内容,否则我要做的就是在某个位置为结构添加一个值。如果它是一个项目列表,那么我将不得不添加一个读通道,就像我已经暗示了Item对象一样,在主标头中添加一个计数器,然后在条目中添加。

这样做的缺点是,在没有特殊处理的情况下,使不同版本的保存文件彼此不兼容(即使它只是主结构中每个条目的默认值)。但是总的来说,这仅需添加新的数据值并在需要时将值放入其中即可轻松扩展系统。

再次,有很多方法可以做到这一点,这可能比C ++引向C的更多机会,但是它已经完成了工作!


1
还值得注意的是,这不是独立于平台的,不适用于C ++字符串,不适用于通过引用或指针引用的对象或任何包含以上任何内容的对象!
Kylotan 2012年

为什么这个平台不独立?它在PC,PS *系统和360上都可以正常工作。fwrite(pToDataBuffer,sizeof(datatype),countOfElements,pToFile); 所有这些作品的对象假设你可以得到一个指向他们的数据和对象的大小,然后将它们的数量要写入..和..阅读比赛
詹姆斯

平台无关,不能保证保存在一个平台上的文件可以加载到另一个平台上。这与例如保存游戏数据无关。指向数据和大小的指针的东西显然可能有点尴尬,但它可以工作。
大约

3
实际上,不能保证它会永远为您服务-如果您发布使用新编译器甚至更改结构填充的新编译选项构建的新版本,会发生什么情况?仅出于这个原因,我会强烈建议不要使用raw-struct fwrite()(顺便说一句,我是从经验上讲)。
蓬松的2012年

1
这与“ 32位数据”无关。原始张贴者只是问“如何保存变量”。如果直接写变量,那么跨平台的信息将丢失。如果您必须在fwrite之前进行预处理,那么您就忽略了答案中最重要的部分,即。如何处理数据,以便正确保存数据,并且仅包含琐碎的位,即。调用fwrite将某些东西放在磁盘上。
Kylotan 2012年

3

首先,您需要确定需要保存哪些数据。例如,这可以是角色的位置,他的得分和硬币数量。当然,您的游戏可能会更加复杂,因此您需要保存其他数据,例如关卡号和敌人列表。

接下来,编写代码以将其保存到文件中(使用ofstream)。可以使用的相对简单的格式如下:

x y score coins

因此文件看起来像:

14 96 4200 100

这意味着他以4200分和100枚硬币处于第(14,96)位。

您还需要编写代码以加载该文件(使用ifstream)。


可以通过在文件中包含敌人的位置来保存敌人。我们可以使用以下格式:

number_of_enemies x1 y1 x2 y2 ...

首先number_of_enemies读取,然后通过一个简单的循环读取每个位置。


1

一种补充/建议是在序列化中添加加密级别,以便用户无法将其值文本编辑为“ 9999999999999999999”。这样做的一个很好的理由是防止整数溢出(例如)。



0

为了完整起见,我想提一个C ++序列化库,是我个人使用,并没有提到:谷物
它易于使用,并且具有很好的,干净的序列化语法。它还提供了您可以保存的多种类型的格式(XML,Json,Binary(包括考虑字节顺序的便携式版本))。它支持继承,并且仅标头,


0

您的游戏将危及您需要转换为字节(序列化)以便存储它们的数据结构(希望吗?)。将来,您可以将这些字节装回,然后将其转换回原始结构(反序列化)。在C ++中,它并不是那么棘手,因为反射非常有限。但是,有些图书馆仍然可以为您提供帮助。我写了一篇关于它的文章:https : //rubentorresbonet.wordpress.com/2014/08/25/an-overview-of-data-serialization-techniques-in-c/ 基本上,我建议您看看谷物库(如果您可能针对C ++ 11编译器)。无需像protobuf一样创建中间文件,因此,如果需要快速的结果,则可以在其中节省一些时间。您也可以在二进制和JSON之间进行选择,因此在这里调试很有帮助。

最重要的是,如果出于安全考虑,您可能希望对存储的数据进行加密/解密,尤其是在使用诸如JSON之类的人类可读格式时。AES之类的算法在这里很有帮助。


-5

您需要fstream用于输入/输出文件。语法很简单,例如EX:

#include <fstream>
// ...
std::ofstream flux ; // to open a file in ouput mode
flux.open("myfile.whatever") ; 

要么

#include <fstream>
// ...
std::ifstream flux ; // open a file in input mode
flux.open("myfile.whatever") ;

您的文件上还可以执行其他操作:appendbinarytrunc等。您将使用与上面相同的语法,例如,我们将std :: ios ::((flags)放到了:

  • ios::out 用于输出操作
  • ios::in 用于输入操作
  • ios::binary 用于二进制(原始字节)IO操作,而不是基于字符的
  • ios::app 在文件末尾开始写入
  • ios::trunc 如果文件已经存在,请删除旧内容并替换为新内容
  • ios::ate -将文件指针定位在“末尾”以进行输入/输出

例如:

#include <fstream>
// ...
std::ifstream flux ;
flux.open("myfile.whatever" , ios::binary) ;

这是一个更完整但简单的示例。

#include <iostream>
#include <fstream>

using namespace std ;

int input ;
int New_Apple ;
int Apple_Instock ;
int Eat_Apple ;
int Apple ;

int  main()
{
  bool shouldQuit = false;
  New_Apple = 0 ;
  Apple_Instock = 0 ;
  Eat_Apple = 0 ;

  while( !shouldQuit )
  {
    cout << "------------------------------------- /n";
    cout << "1) add some apple " << endl ;
    cout << "2) check apple in stock " << endl ;
    cout << "3) eat some apple " << endl ;
    cout << "4) quit " << endl ;
    cout << "------------------------------------- /n";
    cin >> input ;

    switch (input)
    {
      case 1 :
      {
        system("cls") ;

        cout << "------------------------------------ /n";
        cout << " how much apple do you want to add /n";
        cout << "------------------------------------ /n";      
        cin >> New_Apple ;

        ifstream apple_checker ;
        apple_checker.open("apple.apl") ;
        apple_checker >> Apple_Instock ;
        apple_checker.close() ; 

        Apple = New_Apple + Apple_Instock ;

        ofstream apple_adder ;
        apple_adder.open("apple.apl") ;
        apple_adder << Apple ;
        apple_adder.close() ;

        cout << "------------------------------------ /n";
        cout << New_Apple << " Apple has been added ! /n";
        cout << "------------------------------------ /n";
        break;
      }

      case 2 :  
      {
        system("cls") ;

        ifstream apple_checker ;
        apple_checker.open("apple.apl") ;
        apple_checker >> Apple_Instock ;
        apple_checker.close() ;

        cout << "------------------------------------ /n";
        cout << " there is " << Apple_Instock ;
        cout << "apple in stock /n" ;
        cout << "------------------------------------ /n";
        break;
      }

      case 3 :
      {
        system("cls") ;

        cout << "------------------------------------ /n";
        cout << "How many apple do you want to eat /n" ;
        cout << "------------------------------------ /n";
        cin >> Eat_Apple ;

        ifstream apple_checker ;
        apple_checker.open("apple.apl") ;
        apple_checker >> Apple_Instock ;
        apple_checker.close() ;

        Apple = Apple_Instock - Eat_Apple ; 

        ofstream apple_eater ;
        apple_eater.open("apple.apl") ;
        apple_eater << Apple ;
        apple_eater.close() ;

        cout << "----------------------------------- /n";
        cout << Eat_Apple ;
        cout << " Apple has been eated! /n";
        cout << "----------------------------------- /n";
        cout << Apple << " Apple left in stock /n";
        cout << "----------------------------------- /n";
        break;
      }

      case 4 :
      {
        shouldQuit = true;
        break;
      }

      default :
      {
        system("cls") ;

        cout << "------------------------------------ /n";
        cout << " invalide choice ! /n";
        cout << "------------------------------------ /n"; 
        break;
      }
    }
  }
  return 0;
}

4
-1这是一个非常糟糕的答案。您应该正确设置格式并显示代码,并解释自己在做什么,没有人愿意破译一大堆代码。
Vaillancourt

谢谢katu的评论,您是对的,我应该更好地解释我的代码,您能告诉我我如何格式化网站的源代码,因为我是这种事物的
新手

这个网站,或本网站?有关格式化帖子的帮助,您可以访问格式化帮助页面。您还可以在文本区域的标题旁边添加一个感叹号,以帮助您。
Vaillancourt

尝试记录询问的内容;您无需评论所有内容。在我的评论中,通过不解释自己的行为,我的意思是通常情况下,您至少会在短时间内介绍您建议的策略。(例如“一种技术是使用带有流运算符的二进制文件格式。您必须小心以相同的顺序进行读写,等等)”。
Vaillancourt

2
通过使用gotos,您将在公共场所被私刑。不要使用gotos。
Vaillancourt
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.