Announcement

Collapse
No announcement yet.

Driving Gameplay with Data from Excel

Collapse
X
 
  • Filter
  • Time
  • Show
Clear All
new posts

    Driving Gameplay with Data from Excel

    Click image for larger version

Name:	DriveGameplay_Main-770x250-44824237.jpg
Views:	1
Size:	24.5 KB
ID:	1136145

    Ask any game programmer, and they’ll tell you that it’s best to adopt a data-driven approach that allows game designers to tweak as much about the gameplay as possible without the aid of a programmer. If you talk to the game designers themselves, many of them will tell you that for games that require a lot of tweaking and balancing, it’s best to be able to manipulate this data directly with the very capable set of tools provided in Excel (or other spreadsheet tools).

    So how can you get data defined by game designers out of Excel and into a format you can use in UE4? Well, I have some good news for you! This already works in UE4!

    Unreal Engine 4 supports importing data from comma separated variable (.csv) files, which is a format to which you can easily export your Excel files. You can even make macros in your excel documents to allow you to export to .csv with the click of a button inside the document itself. There’s an example of a document set up in this fashion here:

    *Note that you may have to press the “enable content” button to allow the macros to run. This is a security feature of Excel to keep arbitrary code from running on your machine without your permission.

    Click image for larger version

Name:	exportCSV-528x245-177573749.png
Views:	1
Size:	22.7 KB
ID:	1136144


    Types of Tables of Data

    We find the following two types of tables of data to be useful:
    • DataTables
    • CurveTables

    DataTables give you the freedom to define pretty much any type of data you want for import, whereas CurveTables are customized more for interpolating along a curve defined via floating point numbers.


    Data Tables

    With data tables you can define custom data structures and edit and maintain their values in Excel. You can define the format of the data by creating a struct that inherits from FTableRowBase, and defines various properties and default values.

    As an example, here is a definition for level up experience data and the CSV document that supplies the data to the game:

    Code:
    /** Structure that defines a level up table entry */
    
    USTRUCT(BlueprintType)
    
    struct FLevelUpData : public FTableRowBase
    
    {
    
    GENERATED_USTRUCT_BODY()
    
    public:
    
    FLevelUpData()
    
    : XPtoLvl(0)
    
    , AdditionalHP (0)
    
    {}
    
    /** XP to get to the given level from the previous level */
    
    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category=LevelUp)
    
    int32 XPtoLvl;
    
    /** Extra HitPoints gained at this level */
    
    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category=LevelUp)
    
    int32 AdditionalHP;
    
    /** Icon to use for Achivement */
    
    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category=LevelUp)
    
    TAssetPtr<UTexture> AchievementIcon;
    
    };
    The corresponding CSV Document would look like this, where the first column is a unique name to identify the row of data. Later when retrieving the data, you refer to the row of data you want by name.

    Code:
    Name,XPtoLvl,XP,Asset
    
    1,0,0,"Texture2d'/Game/Textures/AchievementIcon1'"
    
    2,1000,1000,"Texture2d'/Game/Textures/AchievementIcon2'"
    
    3,1000,2000," Texture2D'/Game/Textures/AchievementIcon3'"
    
    4,1500,3500," Texture2D'/Game/Textures/AchievementIcon4'"
    
    5,2000,5500," Texture2D'/Game/Textures/AchievementIcon5'"

    Curve Tables

    Curve tables are a little more specific in their use, as floats are the only data they contain. You can use various formulas and tools inside of excel to define curves for XP or some other aspect of your game, and then evaluate these curves in UE4.

    For example, you could define curves for how damage increases based on character level or some other factor like so:


    Example

    Here is an example table for damage progression curves (as you can see the column numbering need not be sequential integers, so you can include gaps):

    0 2 3 7
    Melee_Damage 15 20 25 30
    Melee_KnockBack 1 2 4 8
    Melee_KnockBackAngle 10 45 60 65
    Melee_StunTime 0 1 5 7
    Curves are particularly useful for balancing, item distribution, attributes for abilities and skills, etc. You can also evaluate any point on the curve you defined. For example if you had a monster whose health varied depending on the difficulty setting of the game, you could define a health curve that ranged from 100 to 10,000 in a linear, cubic, or some other fashion. Then you could sample the curve at say 75% of the way from minimum to maximum value to find the amount of health for “Hard” difficulty.


    Importing

    The importing process is pretty easy. You just export your Excel (or whatever software) spreadsheet to .csv format, and you can drag and drop that onto the Content Browser.

    Currently, if you have the .csv file open in Excel when you try to import it into UE4, it fails without a very clear error message, so remember to close the file in Excel before you import. We’re looking into this issue.

    You’ll be presented with a dialog to choose whether this is a DataTable (and thus also the Row Type defined earlier in C++) or CurveTable, in which case you have to choose a type of interpolation between the curve points, i.e. Linear or Cubic, etc:

    Click image for larger version

Name:	importingDataTable-482x148-1803892003.png
Views:	1
Size:	19.9 KB
ID:	1136146

    From there you’ll see a Data Table or Curve Table asset in the Content Browser, and you can double click it and open it to inspect the values.


    Using the Data

    In order to make the Data as easy to set as possible, I recommend making a Blueprint-exposed variable of Type FDataTableRowHandle or FCurveTableRowHandle. Then in a Blueprint a designer would specify the Table and the Row Name.

    Once these references are set, you can call FCurveTableRowHandle::GetCurve() to get a FRichCurve, or FDataTableRowHandle::FindRow() to get a reference to an instance of the struct you defined earlier. You shouldn’t hold on to these references any longer than the scope of a function though, because if the data is re-imported, you want the changes to take effect instantly, and you don’t want to access any invalid pointers.


    Further Information about DataTables and Curve Tables

    See the Data Driven Gameplay Elements documentation.

    I hope this post helps you use Excel to improve your design process, but as always feel free to post further questions on AnswerHub or here in the Forums!

    #2
    Nice article, thanks for posting. Though I have one question, that bothers me since I discovered data tables.

    How you would go about creating data for ability (spell ?) which:
    1. Damages Health and Mana at the same time (for example).
    2. Health damage = if(Target.Constitution < Causer.Constitution) then Intelligence * 2.5 else Intelligence * 1.
    3. Mana Damage = Health Damage * Wisdom * 0.5 and result * 1.5 is added back to causer.

    I honestly couldn't figure it out nice way to put it in single file, and after that thought it might be better to just put it blueprints.
    But I honestly would love to be proved wrong here
    https://github.com/iniside/ActionRPGGame - Action RPG Starter kit. Work in Progress. You can use it in whatever way you wish.

    Comment


      #3
      Quick Question:

      Is there anything that prevents us from using this type of data to default Class properties?

      Like here instead of ArmorAmount or AbsorptionPct, can we use data driven value?

      Code:
      AUTArmor::AUTArmor(const FPostConstructInitializeProperties& PCIP)
      : Super(PCIP)
      {
      	ArmorAmount = 50;
      	AbsorptionPct = 0.333f;
      	bCallModifyDamageTaken = true;
      }

      Comment


        #4
        Hi,

        Thanks for the useful intro into using data. Managed to import a data table correctly but having a little trouble with the next part. Any chance of an example?

        Cheers

        Using the Data

        In order to make the Data as easy to set as possible, I recommend making a Blueprint-exposed variable of Type FDataTableRowHandle or FCurveTableRowHandle. Then in a Blueprint a designer would specify the Table and the Row Name.

        Once these references are set, you can call FCurveTableRowHandle::GetCurve() to get a FRichCurve, or FDataTableRowHandle::FindRow() to get a reference to an instance of the struct you defined earlier. You shouldn’t hold on to these references any longer than the scope of a function though, because if the data is re-imported, you want the changes to take effect instantly, and you don’t want to access any invalid pointers.

        Comment


          #5
          Originally posted by iniside View Post
          Nice article, thanks for posting. Though I have one question, that bothers me since I discovered data tables.

          How you would go about creating data for ability (spell ?) which:
          1. Damages Health and Mana at the same time (for example).
          2. Health damage = if(Target.Constitution < Causer.Constitution) then Intelligence * 2.5 else Intelligence * 1.
          3. Mana Damage = Health Damage * Wisdom * 0.5 and result * 1.5 is added back to causer.

          I honestly couldn't figure it out nice way to put it in single file, and after that thought it might be better to just put it blueprints.
          But I honestly would love to be proved wrong here
          Health, Mana, Wisdom, etc are all just variables. Pull from the data table to set and get them.
          Map Generator 1.0
          Map Generator 2.0
          Map Generator 3.0

          Comment


            #6
            Hey, im having the same problems as Morphues. Importing the data is fine but getting a correct reference to it in code has got me stuck.
            If anyone has an example of how they set up the construction of their objects in c++ could you please post, it would be greatly appreciated. I found a page of code in the unreal engine wiki but it does not seem to work.

            Comment


              #7
              Originally posted by iniside View Post
              Nice article, thanks for posting. Though I have one question, that bothers me since I discovered data tables.

              How you would go about creating data for ability (spell ?) which:
              1. Damages Health and Mana at the same time (for example).
              2. Health damage = if(Target.Constitution < Causer.Constitution) then Intelligence * 2.5 else Intelligence * 1.
              3. Mana Damage = Health Damage * Wisdom * 0.5 and result * 1.5 is added back to causer.

              I honestly couldn't figure it out nice way to put it in single file, and after that thought it might be better to just put it blueprints.
              But I honestly would love to be proved wrong here
              If you think it's easier to store the gameplay data for your particular game in Blueprints, then that's fine too. It's all about finding the workflow best for you.

              You can define however many variables of whatever type you want for a DataTable, so if you wanted to put all of it in one file it would just be a matter of adding more variables of the right type.

              As for having these values be the result of some calculation, you can use formulas and macros in excel to calculate these values, then export them, but if you're more familiar with blueprint logic than excel logic, it might just be easier to go with what you know.

              The case where importing data from excel is really handy is when you're balancing gameplay for a relatively complex game. If you for example had a fighting game with 20 characters and you needed to be able to look at all the data for their attacks, etc in one place and tweak them in relation to each other, then Data and Curve tables could be really handy. Or if you had a lot of varieties of enemies and you needed to balance them to ensure some weren't too strong or two weak, etc. If you just have 3 variables to define, trying to do it in excel and import it might be more overhead than it's worth, but it's just a matter of trying it and seeing what works best for you.

              Comment


                #8
                Originally posted by ajvnd
                Now please let me do that in blueprints
                I added support for at least querying the tables in blueprints, that should release with 4.3:

                https://twitter.com/noevilpeople/sta...77648342609920

                Comment


                  #9
                  Originally posted by Elvince View Post
                  Quick Question:

                  Is there anything that prevents us from using this type of data to default Class properties?

                  Like here instead of ArmorAmount or AbsorptionPct, can we use data driven value?

                  Code:
                  AUTArmor::AUTArmor(const FPostConstructInitializeProperties& PCIP)
                  : Super(PCIP)
                  {
                  	ArmorAmount = 50;
                  	AbsorptionPct = 0.333f;
                  	bCallModifyDamageTaken = true;
                  }
                  I don't see any reason that shouldn't work, but if you run into any problems, please let us know!

                  Comment


                    #10
                    Originally posted by Morpheus View Post
                    Hi,

                    Thanks for the useful intro into using data. Managed to import a data table correctly but having a little trouble with the next part. Any chance of an example?

                    Cheers
                    Say you made a row struct like the following:

                    Code:
                    /** Structure to store the stats of a melee weapon for use in a UDataTable */
                    USTRUCT()
                    struct FMeleeWeaponStats : public FTableRowBase
                    {
                    	GENERATED_USTRUCT_BODY()
                    
                    public:
                    
                    	/** Max distance melee can hit an enemy. */
                    	UPROPERTY(EditAnywhere, BlueprintReadWrite, Category=WeaponMeleeStats)
                    	float RangeVSEnemies;
                    
                    }
                    I would recommend making a Blueprintable class with a UProperty FDataTableRowHandle so you can set the DataTable reference and RowName in a blueprint. The UProperty definition would look like this:

                    Code:
                    /** Information about melee attack */
                    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = Damage)
                    FDataTableRowHandle MeleeAttackStatData;
                    Then when you wanted to use the data, just use the GetRow method (I might have called this "FindRow" in my blog post, whoops) of FDataTableRowHandle like so:

                    Code:
                    const FMeleeWeaponStats* MeleeWeaponStats = MeleeAttackStatData.GetRow<FMeleeWeaponStats>();
                    if (MeleeWeaponStats != NULL)
                    {
                    	float AttackRange = MeleeWeaponStats->RangeVSEnemies;
                    	// Do something with this information...
                    }
                    Hope that helps!

                    Comment


                      #11
                      how do the matching is done between the CSV column and the Struct?
                      In the documentation it said that the column name should be a variable name, but in your sample they are not the same?

                      So what's are the rules?

                      Thanks,

                      Comment


                        #12
                        Originally posted by Joe Conley View Post
                        You can define however many variables of whatever type you want for a DataTable, so if you wanted to put all of it in one file it would just be a matter of adding more variables of the right type.
                        Heh there was my particular problem. I don't really know how much data I want store. Because each potential ability can have very different mechanics from each other. I could probably just define maximum amount of properties and left fields empty, but that just doesn't feel like proper design.

                        It would cool though if we could use JSON data instead.
                        JSON structures can be nested. So I could define each ability as single entity in json add only properties that are needed in nested entity in JSON.
                        Of course in that case we would need to still either calculcate the final results inside blueprint from data provided in json, or create file similar to Curve Table or Data Table, just no flat, but with nested tree structure.


                        The case where importing data from excel is really handy is when you're balancing gameplay for a relatively complex game. If you for example had a fighting game with 20 characters and you needed to be able to look at all the data for their attacks, etc in one place and tweak them in relation to each other, then Data and Curve tables could be really handy.
                        I'm not sure about it. At least to me it looks pretty similar, at least in case of abilities. You still define formulas that calculate something. Whether it is done in excel per ability and X levels is calculated and then you just poll that data or in blueprint, where the result is calculated based on input data from other source (for example owner attributes and target attributes), the result in case of change is still the same. You must go back and forth and adjust your formula.

                        But I can definitely see merit to in case of balancing NPC, especially when if you a lot of them and they all have different attributes. Tweaking them in one place is definitely more handy than tweaking them in 7182 different files .
                        https://github.com/iniside/ActionRPGGame - Action RPG Starter kit. Work in Progress. You can use it in whatever way you wish.

                        Comment


                          #13
                          Originally posted by Elvince View Post
                          how do the matching is done between the CSV column and the Struct?
                          In the documentation it said that the column name should be a variable name, but in your sample they are not the same?

                          So what's are the rules?

                          Thanks,
                          I second that question. Naively, looking at the API (https://docs.unrealengine.com/latest...dle/index.html), I would assume that a pointer coming from the blueprint data (with the help of an FObjectFinder ?) should be stored in the .DataTable variable while we also store a search key in the .RowName variable.

                          Code:
                          LevelUpDataHandle.DataTable = (from the Blueprint)
                          LevelUpDataHandle.RowName = "2";
                          EDIT:

                          After reading this post - https://answers.unrealengine.com/que...able-in-c.html - I think I found the solution.

                          Code:
                          // IMPORTANT : Do this part of the code in a constructor.
                          ConstructorHelpers::FObjectFinder<UDataTable> ExcelTable_BP(TEXT("DataTable'/Game/Data/levelup.levelup'")); // A reference to my Blueprint data table.
                          
                          if (ExcelTable_BP.Object)
                          {
                               LevelUpDataHandle.DataTable = ExcelTable_BP.Object;
                          }
                          Later on ...

                          Code:
                          if (LevelUpDataHandle.DataTable)
                          {
                          	LevelUpDataHandle.RowName = "3";
                          	const FLevelUpData* LevelUpData = LevelUpDataHandle.GetRow<FLevelUpData>();
                          
                          	if (LevelUpData)
                          	{
                                       (...)
                          	}
                          }
                          It worked. There must be a more elegant method to query the desired row. Tell me what you think ...
                          Last edited by DrHobo; 06-30-2014, 01:03 PM. Reason: Found a solution for this....

                          Comment


                            #14
                            @Jon Thanks, it does help, for some reason I thought id be doing all the work in Blueprint, no wonder I could find the GetRow function It would be great if that functionality were there of course

                            Comment


                              #15
                              I'm running into problems trying to implement this. I keep getting "Too few rows". Has someone else run into this?

                              This is the code (pretty much straight out of the example):

                              /** Structure that defines a level up table entry */
                              USTRUCT(BlueprintType)
                              struct FCharacterData : public FTableRowBase
                              {
                              GENERATED_USTRUCT_BODY()

                              public:

                              FCharacterData()
                              : XPtoLvl(0)
                              , XP(0)
                              {}

                              /** The 'Name' column is the same as the XP Level */

                              /** XP to get to the given level from the previous level */
                              UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = LevelUp)
                              int32 XPtoLvl;

                              /** This was the old property name (represented total XP) */
                              UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = LevelUp)
                              int32 XP;

                              UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = LevelUp)
                              TAssetPtr<UTexture> Asset;
                              };

                              And the contents of the .csv file:

                              Name,XPtoLvl,XP,Asset
                              1,0,0,"Texture2d'/Game/UI/HUD/Actions/Barrel'"
                              2,1000,1000,"Texture2d'/Game/UI/HUD/Actions/Barrel'"
                              3,1000,2000,"Texture2d'/Game/UI/HUD/Actions/Barrel'"
                              4,1500,3500,"Texture2d'/Game/UI/HUD/Actions/Barrel'"
                              5,2000,5500,"Texture2d'/Game/UI/HUD/Actions/Barrel'"
                              6,2500,8000,"Texture2d'/Game/UI/HUD/Actions/Barrel'"
                              7,3000,11000,"Texture2d'/Game/UI/HUD/Actions/Barrel'"
                              8,3500,14500,"Texture2d'/Game/UI/HUD/Actions/Barrel'"
                              9,4000,18500,"Texture2d'/Game/UI/HUD/Actions/Barrel'"
                              10,4500,23000,"Texture2d'/Game/UI/HUD/Actions/Barrel'"
                              11,5000,28000,"Texture2d'/Game/UI/HUD/Actions/Barrel'"
                              12,5500,33500,"Texture2d'/Game/UI/HUD/Actions/Barrel'"
                              13,6000,39500,"Texture2d'/Game/UI/HUD/Actions/Barrel'"
                              14,6500,46000,"Texture2d'/Game/UI/HUD/Actions/Barrel'"
                              15,7000,53000,"Texture2d'/Game/UI/HUD/Actions/Barrel'"
                              16,7500,60500,"Texture2d'/Game/UI/HUD/Actions/Barrel'"
                              17,8000,68500,"Texture2d'/Game/UI/HUD/Actions/Barrel'"
                              18,8500,77000,"Texture2d'/Game/UI/HUD/Actions/Barrel'"
                              19,9000,86000,"Texture2d'/Game/UI/HUD/Actions/Barrel'"
                              20,9500,95500,"Texture2d'/Game/UI/HUD/Actions/Barrel'

                              Any help would be appreciated!

                              Comment

                              Working...
                              X