0
\$\begingroup\$

I would like to know what the best way to implement the story functionality of an RPG would be. I am writing the game in C++ using the SFML library for graphics, and I have implemented drawing tiles, level loading and player movement, but I am not sure on how exactly to go about implementing the story. How would I implement when to do cutscenes, what to do in cutscenes, changes in dialogue, and have dialogue options affect the game? I am planning to have the dialogue in JSON files in a format similar to this

"dialogue" :[
        {"type" : "dialogue", "character" : "player", "text" : "dialogue here", "result" : "next"},
        {"type" : "dialogue", "character" : "character", "text" : "more dialogue", "result" : "next"},
        {"type" : "option", "options" : [{"text" : "option 1", "result" : "goto 4"}, {"text" : "option2", "result" : "goto 5"}]},
        {"type" : "dialogue", "character" : "character", "text" : "from option 1", "result" : "next"},
        {"type" : "dialogue", "character" : "character", "text" : "from option 2", "result" : "next"}
]

Is this a good way to go about this? Should I also consider embedding a scripting language like Lua for cutscenes and other high-level tasks?

\$\endgroup\$
  • \$\begingroup\$ You don't need Lua for this. Hardcode it in C++. Generally speaking there are many ways to go about what you're asking. \$\endgroup\$ – Ocelot Sep 14 at 12:33
  • \$\begingroup\$ This is probably not a situation where there's a universally/objectively "best" way to do it. How has your proposed approach been working for you so far? Is there one specific aspect you're unsatisfied with, that we could help you improve? Or one specific spot where you're stuck and need a method to overcome? Tell us about those specifics and we can help you find a working solution. \$\endgroup\$ – DMGregory Sep 14 at 13:05
  • \$\begingroup\$ Possible duplicate of RPG: store dialog \$\endgroup\$ – Philipp Sep 16 at 11:39
1
\$\begingroup\$

RPG elements in a home-brew game engine aren't difficult, but are hilariously complex to code. Look at the number of bugs in Skyrim, which still exist years after release.

Each npc entity has it's own dialogue when the player interacts with it, and that dialogue will very likely change when quests are completed, abandoned or failed. It may even change depending on some kind of reputation like mechanic.

First, map out your interaction tree on paper in a logical way:

  1. Each entity should have N dialogues (which may be cut scenes).
  2. Each dialogue should have M conditions which need to be met (M can be zero)

Functionally, each entity should have a collection of dialogues/scripted cut-scenes, and a script of some kind which will select the dialogue out of the collection depending on some data that the player entity contains. This will likely involve writing a scripting system. I recommend using LUA and luabridge. There are some really nice tutorials on this, so I won't cover it here. A very simplistic example of such a LUA script would follow:

function getDialogue(entity, player)
    local progress = player.getQuestProgress()
    local questProgress = progress.getProgress(entity.getQuestID())

    entity.setDialogue(questProgress)
end

In this example, each quest has a unique id, and each dialogue is unique within each npc.

The "progress" object is just a collection of integers, which store data on each quest in the game (0, for not default, 1 for in progress, 2 for complete).

If quest progress is 0, then the entity will offer the quest to the player when interacted with.

If 1, then the quest is in progress, and so a sort of "How are you progressing?" sort of dialogue should be used.

If 2, then a generic "Thanks for your help!" type dialogue.

In the case of cut scenes, this is more complex, but the basic idea is to shut off player controls (except maybe skip cut-scene), and then allow the script to control the camera and dialogue/animation.

To do this, you will have to tie up pretty much all your game engine and entity functions to your scripting engine, allowing C++ functions to be called from LUA. This will take some time, but is worth it in the end, as it allows you to test your scripts without recompiling your source.

Once you have an adequate amount of control of your engine from scripts, you will be surprised just how much control over your game you have.

\$\endgroup\$
1
\$\begingroup\$

There is best, however, I would argue for separating the concerns of deciding what text to show and getting the text itself.

You can hard code the decisions, it is a finite state machine. And, I would argue for reading the text from a file (which you can replace when the time comes for translations).

Or not. The text can be hard-coded too... read from a database, come from a web request, whatever. At this state it only matters that you separate it, so you can replace it later – and even replace how it is stored – without worrying of messing up the structure.


Now, since you mention cut-scenes, and not just dialog, I want to mention that there are cut-scenes systems. They take more data than just the text and options. They take the location, characters and animations to use. Even lighting and camera movement.

You can even have a separate tool to create them – something that is more practical to writers, artists and game designers – and it would output a file that can be embedded or otherwise loaded by the game... and of course there would be code to play it.

These are – of course – not for talking with some random NPC, but – you know – cut-scenes.


Or you can use a script, sure.

\$\endgroup\$
0
\$\begingroup\$

As others have noted, how you store the story files is not that important: When you feel comfortable with Lua, it's an amazing tool to "remote control" your game engine to

  • block user input
  • move character models/sprites around
  • display dialogue

aka implement cut-scenes.

You can also encode all this in JSON files, as an array of action steps. Your game will have to read and interpret the instructions on its own, as opposed to Lua directly controlling your engine. Either way, then you need triggers to load the script at specific player positions, or when the player interacts with objects or NPCs.

\$\endgroup\$
0
\$\begingroup\$

My answer will be based on how dialogue and cutscene system worked in my top-down and platformer games.

Long story short:

Ini files for simple actions and dialogues, Lua scripts for more advanced logic.

Simple scenes

My .ini files looked like that:

[dialog]
1 0=ifi val m3p3event 1 10 15 1
1 1=ifi val m3p3event 3 11 12 13
1 10=setb 100
1 11=setb 200
1 12=setb 300
1 13=setb 400
1 15=setb 0
100 0=scene
100 1=w8 1
100 5=str Obi-Wan: Hello there.
99=str Grievous: General Kenobi.
98=str Obi-Wan: What's up?
97=str Grievous: An unit vector pointing in Z direction, what about it?
... 
83=str Obi-Wan: And that's why communism is bad. See you later!
82 0=save val m3p3event 1
82 1=unscene
82 2=setb 0

Each NPC had information which .ini file it should launch when talked to.

First number (due to historical reasons called "B") is the step in dialogue - player presses Enter -> number decreases by 1. Second number ("i" cause, well, it's the loop index) is used for handling multiple actions in single dialogue step. "i" goes from 0 up to 100. At each step of this internal loop I read string for key "[B] [i]" and parse it. First word is the action, rest are the arguments.

Advanced cutscenes and logic:

In the above example is the line:

82 0=save val m3p3event 1

In the background there is a lua script (to be specific, there was a background one and one for each NPC - this one was for the NPC). If the value "m3p3event" is set to 1 it assumes the cutscene is started. Code for the cutscene:

function update()
    if(mode==0) then
        local dbv=getDBValue("m3p3event")

        if(dbv==1) then
            mode=1
            end
    elseif(mode==1) then -- Event started
        x, y=getPosition() -- Get this NPC's position

        if(testCollision(x+48, y+96, 4, 4)) then
            jump(108)
        elseif(testCollision(x+24, y+16, 4, 4)) then
            jump(48)
            end

        if(x<=1670) then
            moveRight()
        else
            timer=timer+dt

            if(timer>=1) then
                timer=0
                mode=2

                setNpcBounceLeftByID("m3", 384, 400)
                end
            end
... and so on.

Additionally I could define Lua mini-scripts in the .ini files, eg.:

10=str Jump if you're happy!
9=lua playerJump(64)
9 1=str Player: Yay!

Lua is better at handling mathematical operations than my ini-script, so whenever i needed to calculate something (spawn NPC relative to the player, detect player's position and start cutscene...) i used Lua. For most of the dialogues i only used ini-script.

To sum it up:

Pros:

  • Ini files are fairly easy to parse,
  • Ini-script is more than easy to parse,
  • No tools needed to edit the ini-script (format is human-readable),
  • No recompilation needed to see the changes in the script during development (as files are stored outside of the code),
  • For the most of the dialogues using Lua would be overkill,
  • Writing dialogues is really fast (in your JSON example you have to repeatedly write a lot of stuff).

Cons:

  • Adding something in-between existing command requires reordering of later actions (that's why i always left space for around 100 entries between different scenes/dialogue options),
  • Localization is difficult, as the cutscene code is mixed with the texts,
  • Tricky communication between Lua and ini-script.

Hopefully looking at other answers and my example you'll find a way to implement your cutscene system.

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.