Home > OS >  Godot: read and output JSON file as Textbook according to the input
Godot: read and output JSON file as Textbook according to the input

Time:12-20

I want to be able to call specific phrases by their assigned numbers in this JSON file, so when i call go(1) for example, it displays only the text that has 'Num' set as 1.

my JSON file:

[
    {"Num":0, "Name":"Afely", "Emotion":"Neutral", "Text":"TEST1"},
    {"Num":0, "Name":"Afely", "Emotion":"Neutral", "Text":"TEST2"},
    {"Num":0, "Name":"Afely", "Emotion":"Neutral", "Text":"TEST3"},

    {"Num":1, "Name":"Afely", "Emotion":"Neutral", "Text":"2TEST1"}
]

The Textbox code:

extends ColorRect

export var dialogPath = ""
export(float) var textSpeed = 0.005

var dialog

var phraseNum = 0
var finished = false

func go(phraseNum):
    $Timer.wait_time = textSpeed
    dialog = getDialog()
    assert(dialog, "Dialog not found")
    nextPhrase()
    var f = File.new()
    var img = dialog[phraseNum]["Emotion"]   ".png"
    $Portrait.texture = load(img)
    
func _unhandled_input(event):
    if event is InputEventKey:
        if event.pressed and event.scancode == KEY_Q:
            if finished:
                $NEXT.play()
                nextPhrase()
            else:
                $Text.visible_characters = len($Text.text)

func getDialog() -> Array:
    var f = File.new()
    assert(f.file_exists(dialogPath), "File path does not exist")
    
    f.open(dialogPath, File.READ)
    var json = f.get_as_text()
    
    var output = parse_json(json)
    
    if typeof(output) == TYPE_ARRAY:
        return output
    else:
        return []

func nextPhrase() -> void:
    if phraseNum >= len(dialog):
        queue_free()
        return
    
    finished = false
    
    $Name.bbcode_text = dialog[phraseNum]["Name"]
    $Text.bbcode_text = dialog[phraseNum]["Text"]
    
    $Text.visible_characters = 0
    

    
    while $Text.visible_characters < len($Text.text):
        $Text.visible_characters  = 1
        $TEXT_AUDIO.play()
        $Timer.start()
        yield($Timer, "timeout")
    
    finished = true
    phraseNum  = 1
    return

how I call it:

$TextBox.show()
$TextBox.go(1)

and lastly, the tutorial I followed for it: https://www.youtube.com/watch?v=GzPvN5wsp7Y

How would I approach to do this?

CodePudding user response:

What you asked

What you are asking requires quite some extra work. Let us start with the JSON file:

[
    {"Num":0, "Name":"Afely", "Emotion":"Neutral", "Text":"TEST1"},
    {"Num":0, "Name":"Afely", "Emotion":"Neutral", "Text":"TEST2"},
    {"Num":0, "Name":"Afely", "Emotion":"Neutral", "Text":"TEST3"},

    {"Num":1, "Name":"Afely", "Emotion":"Neutral", "Text":"2TEST1"}
]

This will parse as an Array (everything between [ and ]), where each element is a Dictionary (which are the ones between { and }). This means that you are going to need to iterate over the Array, check every Num.


Before we do that, we need to acknowledge that we would be using the name phraseNum to representing two things:

  • The index in the dialog Array
  • The desired value of Num

We are in this situation because of the source material you are using. They have phraseNum as a parameter (here: func go(phraseNum)) that hides a phraseNum field (here var phraseNum = 0 ).

This hinders communication. If I tell you do this with phraseNum, which one is it? That is bound to give us trouble.

I'll be rewriting go so it takes a Num instead of the index in the dialog Array, so I'll keep phraseNum for the index in the dialog Array, and have a different name for the parameter of go.


Let us begin rewriting go:

func go(num):
    pass

Now, let us get all the dialogues:

func go(num):
    dialog = getDialog()

We are going to iterate over them. Since I will want an index for phraseNum, I will iterate using index:

func go(num):
    dialog = getDialog()
    for index in dialog.size():
        pass

And we need to check if Num matches. If it does, we got our index:

func go(num):
    dialog = getDialog()
    for index in dialog.size():
        if num == dialog[index]["Num"]:
            phraseNum = index
            break

We need to handle the case where we didn't find it. Now, hmm… The source material only has an assert, I'll keep that approach. So we need a way to know the code didn't find it…

func go(num):
    var found := false
    dialog = getDialog()
    for index in dialog.size():
        if num == dialog[index]["Num"]:
            phraseNum = index
            found = true
            break

    assert(found, "Dialog not found")

Next the you call nextPhrase(), sure:

func go(num):
    var found := false
    dialog = getDialog()
    for index in dialog.size():
        if num == dialog[index]["Num"]:
            phraseNum = index
            found = true
            break

    assert(found, "Dialog not found")
    nextPhrase()

And then a not used var f = File.new(), I'll not add that.

And you set the portrait texture. Sure:

func go(num):
    var found := false
    dialog = getDialog()
    for index in dialog.size():
        if num == dialog[index]["Num"]:
            phraseNum = index
            found = true
            break

    assert(found, "Dialog not found")
    nextPhrase()
    var img = dialog[phraseNum]["Emotion"]   ".png"
    $Portrait.texture = load(img)

And I had skipped over the timer thing, I'll insert it now:

func go(num):
    var found := false
    dialog = getDialog()
    for index in dialog.size():
        if num == dialog[index]["Num"]:
            phraseNum = index
            found = true
            break

    assert(found, "Dialog not found")
    $Timer.wait_time = textSpeed
    nextPhrase()
    var img = dialog[phraseNum]["Emotion"]   ".png"
    $Portrait.texture = load(img)

Something else

Now, you said you want only the phrases with the given Num. This is open to interpretation.

To be clear, the code above will have the dialog start at the first instance of the Num you ask for. But it will not end - nor skip - when it finds a different Num. I don't know if you want that or not.

And we have a couple ways to do this. We could remember what was the num and check against it in nextPhrase. I really don't want to do that. So, I will give you an alternative approach: let us make a dialog array that only contains the elements we want.

It looks like this:

func go(num):
    var every_dialog = getDialog()
    dialog = []
    for candidate in every_dialog:
        if num == candidate["Num"]:
            dialog.append(candidate)

    assert(dialog, "Dialog not found")
    phraseNum = 0
    $Timer.wait_time = textSpeed
    nextPhrase()
    var img = dialog[phraseNum]["Emotion"]   ".png"
    $Portrait.texture = load(img)

Please notice that in this example we are not reading getDialog() to dialog. Instead we are building a dialog array that only contains the entries we want. And we do that by iterating the result of getDialog() (we add to the array with append).

This is a subtle change in the meaning of dialog, because it would no longer represent every entry form the JSON file. Instead it only represent the entries that will be displayed. Before those two things were the same, but they aren't anymore with this change.


What you didn't ask

  • The function getDialog reads from the JSON file. And you would be doing that every time you call go. You could instead do it once in _ready.

    It is important that you understand what each variable represents, and also where you read and write them. Above I mention that the there is a subtle meaning of dialog do to the alternative change. You need to consider that to make this change.

  • I strongly believe that nextPhrase should handle the timer, and the portrait. There should be no need to set those from go.

  • I want you to consider this alternative JSON file structure:

    [
        [
            {"Name":"Afely", "Emotion":"Neutral", "Text":"TEST1"},
            {"Name":"Afely", "Emotion":"Neutral", "Text":"TEST2"},
            {"Name":"Afely", "Emotion":"Neutral", "Text":"TEST3"},
        ],
        [
            {"Name":"Afely", "Emotion":"Neutral", "Text":"2TEST1"}
        ]
    ]
    

    Then you would get an Array of Arrays, where each element of the nested Array is a Dictionary. Then you can "simply" get the nested array by index, instead of having to iterate over every element.

    The structure you get resembles the structure of the JSON file. Which also means you will have to change how you use it. For example, instead of dialog[phraseNum]["Text"] it could be dialog[num][phraseNum]["Text"].

    Now consider the source material. In this case we have a node that has both the responsibility of parsing JSON and the responsibility of displaying character dialogues. It would be easier to tinker with this code if these were separated from each other. Presumably the intention of the author is that you would have different JSON files for the different dialogues, so you would switch JSON file when necessary (which also explain why they read the JSON file each time).

    But that is not what you asked.

  • Related