Godot: read and output JSON file as Textbox according to the input - arrays

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?

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

How do I calculate the sum of the respective variables of a custom type array?

I am new to coding in Swift. My goal is to build a variable containing the sum of a number of custom class variables.
To speak in clear terms:
I have a custom class called "Entry" which has the Double variable "sum", e.g. 50.00.
I am using a ViewController to let the user create a new entry with text field inputs, inter alia a sum of the new entry. This new entry element is then appended to an array of type Entry when the relevant button is pressed.
#IBAction func addEntry(_ sender: UIButton){
// take user input
let name = nameTextField.text ?? ""
let sum = Double(sumTextField.text!) ?? 0.0
let category = selectedCategory
// save user input in new entry
let newEntry = Entry(name: name, sum: sum, category: category)
entries.append(newEntry)
saveEntries()
os_log("Saved new entry successfully.", log: OSLog.default, type: .debug)
In another ViewController, I want to access the array "entries" and build the sum of all sum variables of all Entry elements, e.g. sum of Entry 1 + sum of Entry 2 + sum of Entry 3
My current attempt in coding is as follows:
var entriesDatabase = [Entry]()
//extract sum of entries and build sumOfEntries
var sumArray = [Double]()
for entry in entriesDatabase {
sumArray.append(entry.sum)
}
sumOfEntries = sumArray.reduce(0, +)
The entries array from the first View Controller is saved by using the NSKeyedArchiver and loaded by NSKeyedUnarchiver.unarchiveObject(withFile:) in the second View Controller before calling the function above (which, I am aware, has been deprecated, but it works for my current purposes).
I used the print function to isolate the issue and as far as I can see, the sumOfEntries always remains 0.0, no matter how many Entry elements I create (which itself seems to work, though).
Has anyone a clue what I am doing wrong?
EDIT: The issue to me really seems to be that the calculation does not work rather than the passing of the data from one view to another. Somehow, the arrays always remain empty. The passage of the data works via saving it persistently on the drive and then loading it with the NSKeyedArchiver functions. For clarity see the following code:
/MARK: calculate and display balance
func calculate(){
//load data from user defaults
recurringCalculationValue = UserDefaults.standard.value(forKey: "recurringExpenses") ?? 0.0
monthlyIncomeCalculationValue = UserDefaults.standard.value(forKey: "monthlyIncome") ?? 0.0
//extract sum of entries and build sumOfEntries
var sumArray = [Double]()
for entry in entriesDatabase {
sumArray.append(entry.sum)
}
sumOfEntries = sumArray.reduce(0, +)
//cast the user defaults into Double
let mICV = monthlyIncomeCalculationValue as! Double
let rCV = recurringCalculationValue as! Double
//convert the Strings to Double! and calculate balance
balance = Double(mICV) - Double(rCV) - sumOfEntries
//display balance in sumLabel
sumLabel.text = "\(balance)"
//debugging
print("balance = \(balance)")
print("sumOfEntries = \(sumOfEntries)")
print("monthlyIncomeCalculationValue = \(monthlyIncomeCalculationValue)")
print("recurringCalculationValue = \(recurringCalculationValue)")
print("UserDefault monthlyIncome = \(String(describing: UserDefaults.standard.value(forKey: "monthlyIncome")))")
print("UserDefault recurringExpenses = \(String(describing: UserDefaults.standard.value(forKey: "recurringExpenses")))")
}
//this function is called when the ViewController is opened
#IBAction func unwindToThisViewController(segue: UIStoryboardSegue) {
//Load saved entries into entries array if entries were saved
let path = NSSearchPathForDirectoriesInDomains(.documentDirectory, .userDomainMask, true)[0] as String
let url = NSURL(fileURLWithPath: path)
if let pathComponent = url.appendingPathComponent("entries") {
let filePath = pathComponent.path
let fileManager = FileManager.default
if fileManager.fileExists(atPath: filePath) {
print("File available.")
entriesDatabase = loadEntries()!
print("Database loaded.")
} else {
print("File not available.")
}
} else {
print("File path not available.")
}
calculate()
}
private func loadEntries() -> [Entry]? {
return NSKeyedUnarchiver.unarchiveObject(withFile: Entry.ArchiveURL.path) as? [Entry]
}
I hope, that makes my problem clearer and thanks again!
The way I see it, in var entriesDatabase = [Entry]() you create a new array for objects of type Entry, which is (of course) initially empty. Therefore, the sum of the values will be 0.
What you want is to cache the values, e.g. in your saveEntries()-Function. You may want to take a look at UserDefaults which stores information in a map-like matter.

godot invalid get index '-1'(on base 'array') during A.P.I

I'm making an A.P.I. that displays dialogue, choices, sprites, audio etc. etc., i was able to do it after looking up this video on youtube but i keep getting this error invalid get '-1' (on base 'array') here's my code so you can see
extends Control
onready var bg_rect = get_node("Media_Sys/BG_Rect")
onready var char_sprite = get_node("Media_Sys/Sprite")
onready var text_box = get_node("Dialog_Sys/Dialog_Text")
onready var text_nam = get_node("Dialog_Sys/Label/Name")
onready var click = get_node("ClickManager")
var player = []
var lu = "Lu"
var ciel = "Ciel"
func _ready():
click.request_ready()
func write(char_name_str, text_str=null):
if text_str == null:
text_str = char_name_str
char_name_str = ""
player.push_front({"type": "dialogue","name": char_name_str, "text": text_str})
func write_component():
text_nam.clear()
text_box.clear()
text_nam.add_text(player[player.size() - 1]["name"])#the debugger says the problem is here
text_box.add_text(player[player.size() - 1]["text"])#as well as here if you remove the first line.
player.pop_back()
func _input(event):
if event is InputEventMouseButton:
write_component()
I have some people say that it's because i didn't define the size of the array, i have some people say that it's really weird that my code somehow set the player as an integer. I've check the array docs page for this and came up with nothing, I've changed some minor things in the code but it still churns out the same invalid error.
extends Node
onready var g = get_parent()
var lu = "Lu"
var ciel = "Ciel"
func _ready():
g.write(lu, "hello")
One thing to note is that it does display the dialogue and name, i just get the error immediantly afterwards.
You initialize the player variable to be an empty list. Then, in write_component you attempt to access this list with the calculated index [player.size() - 1]. The issue is that if player is empty, this ends up as [0-1] or [-1] as an index, which is not valid in GDScript. Your code assumes Write will be called first (and it may, some of the time) so that player is not empty by the time write_component is called.
The most immediate solution is to check if player.size() == 0 before attempting to access the last element.
func write_component():
text_nam.clear()
text_box.clear()
if player.size() > 0:
text_nam.add_text(player[player.size() - 1]["name"])
text_box.add_text(player[player.size() - 1]["text"])
player.pop_back()

Generate random Strings without a shuffle and repetition of previous ones?

My code already generates a random String of an array when I press a button, but sometimes a String gets repeated. What do I have to do so that the String "Mango" only gets called again when all the other Strings where already called without using a shuffle, I want to call one String at a time?
Example: "Mango", "Kiwi", "Banana", "Pineapple", "Melon", "Mango", "Kiwi",.....
Here is my code:
var array = ["Mango", "Banana", "Apple","Kiwi", "Melon", "Pineapple"]
let fruits = Int(arc4random_uniform(UInt32(array.count)))
print(array[fruits])
In order to avoid repetitions you need to keep track of which fruits have previously been seen. There are several ways to do that an all of the proposed solutions do it in one way or another.
For your specific use case, you will need this tracking to be retained outside of the code executed by the button (in your view controller for example).
Here is a generalized structure that could help with this:
(you can define it inside the view controller if this is a one-time thing or outside of it if you intend to reuse the mechanism elsewhere)
struct RandomItems
{
var items : [String]
var seen = 0
init(_ items:[String])
{ self.items = items }
mutating func next() -> String
{
let index = Int(arc4random_uniform(UInt32(items.count - seen)))
let item = items.remove(at:index)
items.append(item)
seen = (seen + 1) % items.count
return item
}
}
To use it, you would declare a variable (in your VC) that will keep track of your fruits:
var fruits = RandomItems(["Mango", "Banana", "Apple","Kiwi", "Melon", "Pineapple"])
And in the button's code use that variable (fruits) to print a non-repeating fruit name at each execution
func buttonPressed() // <- use your function here
{
print( fruits.next() )
}
You need to implement some logic. It's quite easy if you think harder. Run this in your Playground, or if you fully understand this block of code, you can do this in your project already.
var array = ["Mango", "Banana", "Apple","Kiwi", "Melon", "Pineapple"]
var selectedIndices = [Int]()
for _ in 1...20 {
let randomFruitIndex = Int(arc4random_uniform(UInt32(array.count)))
// Print only if not yet printed once
if !selectedIndices.contains(randomFruitIndex) {
print(array[randomFruitIndex])
selectedIndices.append(randomFruitIndex)
}
// Reset
if selectedIndices.count == array.count {
print("----- CLEARING SELECTED INDICES----")
selectedIndices.removeAll()
}
}
So as you can see, we are adding each generated random number (in your case, it's the fruits variable.) into an array of Int. Then if the number of selectedIndices is equal to the count of the array of fruits, clear all the stored selectedIndices.
OUTPUT:
Pineapple
Melon
Mango
Kiwi
Banana
Apple
----- CLEARING SELECTED INDICES----
Mango
Melon
This is an adaption from the accepted answer of the linked topic in my comment:
var source = ["Mango", "Banana", "Apple","Kiwi", "Melon", "Pineapple"]
var usedElements = [String]()
func choosePseudoRandomElement() -> String {
if source.count == 0 {
source = usedElements
usedElements = []
}
let randomIndex = Int(arc4random_uniform(UInt32(source.count)))
let randomItem = source[randomIndex]
usedElements.append(randomItem)
source.remove(at: randomIndex)
return randomItem
}
for _ in 1...18 {
print("Item: \(choosePseudoRandomElement())")
}
One potential issue with this solution is that it may happen, that the last element of one complete iteration also occurs as the first element of the second iteration. You can handle that case by comparing the randomly chosen item with the item which was chosen before (use a while loop until the items doesn't match anymore).
Also, this does remove elements from the source array. If you do not want that, create a copy of the source array.

circular reference using weak variables causing the unwrapping of nils

let me start off by saying I searched to find a topic that would help me but I could not anything that helped. Here is my situation...
I created a class sign where I created the variable thing1. I then created an array called arrayX that uses sign. Then I used a NSURL session to pull values from a domain. I then created thingX that will hold that value and finally I appended my arrayX with that value of thingX. This works GREAT except that it appears i have strong circular references and the memory cannot be deallocated. I tried fixing it with weak references but then the occasional empty thingX causes my app to crash because it is trying to unwrap a nil. Please help!!!
class sign {
var thing1 = ""
init(thing1: String) {
self.thing1 = thing1
}
}
var arrayX : [sign] = [sign]()
index1 = 0
while index1 < count {
//NSURL will grab info info will grab a value from my domain and thingX will be given this value (occasionally the value will be empty)
let thingX = ‘NSURL value’ as! String
let data1 = sign(thing1: thingX)
self.arrayX.append(data1)
index1++
}

Is it possible to access a String inside an Array, inside an Array in swift?

Is it possible to access a string inside an array that is inside another array? - Swift
for instance:
var a = 1
var b = 2
var maleDogs = ["Fido","Thor"]
var femaleDogs = ["Linn","Eva"]
var dogs = [maleDogs,femaleDogs]
And then do something like
dogs[a][b]
In this instance, I wanted to get "Thor" as an output, but it calls an error. (Inside playground)
Your code is completely correct, but note that Swift array indices are zero-based, so accessing index 2 causes an "Array index out of range" exception.
You can see the error message if you open the "Assistant Editor" for the Playground file
(View -> Assistant Editor -> Show Assistant Editor).
What you probably wanted is
var a = 0
var b = 1
var maleDogs = ["Fido","Thor"]
var femaleDogs = ["Linn","Eva"]
var dogs = [maleDogs,femaleDogs]
dogs[a][b] // Thor

Resources