【Godot Engine】任意の辞書を使って配列をカスタムソートする【GDScript】

Godot Engine Version: 3.5.1(lts version)

ユウです。

最近いろいろあって仕事がなくなったので、暇つぶしにGodot Engineでゲームを作ってます。

さて、ゲームにおいて、ソートの順序って重要ですよね。
アイテムのソートはもちろん、RPGだとスキル一覧とか、戦闘するときの敵の一覧とか、文字の羅列を並び替えるシチュエーションって多いわけです。

で、Godot EngineもといGDScriptにはそういったシチュエーションに対応するためのソートメソッドが用意されてます。

void sort_custom ( Callable func )

ですね。
通常のsort()メソッドでは昇順ソートのみ、かつアルファベット順と結構制約がありますが、sort_customを使えば降順にしたり数字順にしたり色々できます。

使い方は簡単。bool値を返すメソッドを持つクラスと、クラスが持つメソッド名をsort_customの引数として渡してあげれば、それを使ってヒープソートしてくれます(現行の最新バージョンであるGodot 4.xでは、直接メソッドを引数にできるようです)。

上述のドキュメントのexampleでは以下のようなコードが例示されています。

class MyCustomSorter:
    static func sort_ascending(a, b):
        if a[0] < b[0]:
            return true
        return false

var my_items = [[5, "Potato"], [9, "Rice"], [4, "Tomato"]]
my_items.sort_custom(MyCustomSorter, "sort_ascending")
print(my_items) # Prints [[4, Tomato], [5, Potato], [9, Rice]].

MyCustomSorter内部に定義されているsort_ascending(a, b)がboolを返すメソッドです。投げられる配列の0番目(数字)の大小を比較して昇順に並び替えてますね。

exmapleではmy_itemsが「先頭がintである配列の配列」なので、sort_ascending内部では配列の0番目の数字の大小を比較して並び替えているようです。

では、これを使って、適当に作ったスキル一覧の配列を並び替えてみましょう。

class SkillSorter:
	static func sort_ascending(a, b):
		if a[0] < b[0]:
			return true
		return false

func _ready():
	var skills = [[3, "キュア・ウーンズ"], [4, "ディテクト・マジック"], [5, "マジック・ウェポン"],
	 [1, "ソーマタージー"], [2, "ヘリッシュ・リビューク"], [0, "暗視"]]
	skills.sort_custom(SkillSorter, "sort_ascending")
	print(skills) # [[0, 暗視], [1, ソーマタージー], [2, ヘリッシュ・リビューク], [3, キュア・ウーンズ], [4, ディテクト・マジック], [5, マジック・ウェポン]]

うまくソートされましたね。

今回はダンジョンズ&ドラゴンズからスキル名を持ってきていますが、暗視やソーマタージーはティーフリング種族由来のスキルなので先頭寄りに、キュア・ウーンズなどはパラディンのクラス由来の末尾寄りに、かつ低レベルで習得するものは先頭寄りに…といった感じで重みを付けました。

ただ、これって見てわかる通り大変面倒ですよね。

exampleコードなので当たり前ですが、実際こんなことするぐらいだったら最初から配列を並び替えておけばよくね?みたいなところがあり。

なので、単なるStringの配列をイイ感じに並び替えたい!というのが今回のお話。前置き長いな。

ちなみにスキル名のみの配列を雑に昇順に並び替えるとこんな感じ。

class SkillSorter:
	static func sort_ascending(a, b):
		if a < b:
			return true
		return false

func _ready():
	var skills = ["キュア・ウーンズ", "ディテクト・マジック", "マジック・ウェポン",
	 "ソーマタージー", "ヘリッシュ・リビューク", "暗視"]
	skills.sort_custom(SkillSorter, "sort_ascending")
	print(skills) # [キュア・ウーンズ, ソーマタージー, ディテクト・マジック, ヘリッシュ・リビューク, マジック・ウェポン, 暗視]

やってることは通常のsort()メソッドと同じですね。

これはこれでソート自体は成立しているっぽく、どうやらあいうえお順でソートした後に漢字を持ってきているのかな?という感じ。

ただまぁ、最初に話した通りゲームのスキルということで、できれば開発側で指定した順番でソートしてほしいですね。

というわけで考えたのがこんなやり方。
1. ソート辞書となるenumを用意する
2. enumのメンバに割り振られる整数値を使ってソートする
3. バジリスクタイム!
という話。

要するに

class SkillSorter:
	
	enum sort_dic { 暗視, ソーマタージー, ヘリッシュ・リビューク, キュア・ウーンズ, ディテクト・マジック, マジック・ウェポン }
	
	static func sort_dic(a, b):
		if sort_dic[a] < sort_dic[b]:
			return true
		return false

func _ready():
	var skills = ["キュア・ウーンズ", "ディテクト・マジック", "マジック・ウェポン",
	 "ソーマタージー", "ヘリッシュ・リビューク", "暗視"]
	skills.sort_custom(SkillSorter, "sort_dic")
	print(skills)

というわけです。

じゃあこれで万事解決!最強卍卍!

…と思いきや、残念ながらそうはいきませんでした。というのも、GDScriptのenumのメンバに日本語(というかマルチバイト文字)が使えないっぽいんですよね。
NightVision, Thomatage…といった具合で英語でメンバを入れればいいんですが、英語対応する気がないのにわざわざローカライズを実装するのはダルいですよね。
メソッド名とかならともかく、スキル名をいちいち翻訳したりするのもメンドーですし。

というわけで、何かいい方法はないかなーと思ったんですが、どうやらenumに後からメンバをぶち込むことでマルチバイト文字も使えるようです(正確に言うとマルチバイト文字を初期メンバとするenumをGDScriptがコンパイルできない?)。

てわけで、こう。

class SkillSorter:
	
	enum sort_dic {}
	
	static func sort_dic(a, b):
		if sort_dic[a] < sort_dic[b]:
			return true
		return false

func _ready():
	
	var sorter = SkillSorter.new()
	
	sorter.sort_dic.merge({"暗視": 0})
	sorter.sort_dic.merge({"ソーマタージー": 1})
	sorter.sort_dic.merge({"ヘリッシュ・リビューク": 2})
	sorter.sort_dic.merge({"キュア・ウーンズ": 3})
	sorter.sort_dic.merge({"ディテクト・マジック": 4})
	sorter.sort_dic.merge({"マジック・ウェポン": 5})
	print(sorter.sort_dic) # {キュア・ウーンズ:3, ソーマタージー:1, ディテクト・マジック:4, ヘリッシュ・リビューク:2, マジック・ウェポン:5, 暗視:0}
	
	var skills = ["キュア・ウーンズ", "ディテクト・マジック", "マジック・ウェポン",
	 "ソーマタージー", "ヘリッシュ・リビューク", "暗視"]
	skills.sort_custom(SkillSorter, "sort_dic")
	print(skills) # [暗視, ソーマタージー, ヘリッシュ・リビューク, キュア・ウーンズ, ディテクト・マジック, マジック・ウェポン]

うまくソートできたみたいです。

あとはjsonで辞書を用意します。

{
  "skills": [
    "暗視",
    "ソーマタージー",
    "ヘリッシュ・リビューク",
    "キュア・ウーンズ",
    "コマンド",
    "コンペルド・デュエル",
    "サンダラス・スマイト",
    "シアリング・スマイト",
    "シールド・オヴ・フェイス",
    "ディヴァイン・フェイヴァー",
    "ディテクト・イーヴル・アンド・グッド",
    "ディテクト・ポイズン・アンド・ディジーズ",
    "ディテクト・マジック",
    "ピュリファイ・フード・アンド・ドリンク",
    "ヒロイズム",
    "ブレス",
    "プロテクション・フロム・イーヴル・アンド・グッド",
    "ラスフル・スマイト",
    "エイド",
    "ゾーン・オヴ・トゥルース",
    "ファインド・スティード",
    "ブランディング・スマイト",
    "プロテクション・フロム・ポイズン",
    "マジック・ウェポン",
    "レッサー・レストレーション",
    "ロケート・オブジェクト"
  ]
}

スクリプト上で読み込んでsorterのenumにぶち込みます。

	var sorter = SkillSorter.new()
	
	var data_file = File.new()
	
	if data_file.open("res://Skill_Dictionary.json", File.READ) != OK:
		print(data_file.get_error())
		return
	
	var data_text = data_file.get_as_text()
	data_file.close()
	
	var data_parse = JSON.parse(data_text)
	
	if data_parse.error != OK:
		print(data_parse.error)
		return
	
	var data = data_parse.result
	
	for index in range(data["skills"].size()):
		sorter.sort_dic.merge({data["skills"][index]: index})

出来上がったenumを使ってソートして終わり!

class SkillSorter:
	
	enum sort_dic {}
	
	static func sort_dic(a, b):
		var sortA = sort_dic.get(a)
		if sortA == null:
			sortA = -1
		var sortB = sort_dic.get(b)
		if sortB == null:
			sortB = -1
		if sortA < sortB:
			return true
		return false

func _ready():
	var sorter = SkillSorter.new()
	
	var data_file = File.new()
	
	if data_file.open("res://Skill_Dictionary.json", File.READ) != OK:
		print(data_file.get_error())
		return
	
	var data_text = data_file.get_as_text()
	data_file.close()
	
	var data_parse = JSON.parse(data_text)
	
	if data_parse.error != OK:
		print(data_parse.error)
		return
	
	var data = data_parse.result
	
	for index in range(data["skills"].size()):
		sorter.sort_dic.merge({data["skills"][index]: index})
		
	var skills = ["キュア・ウーンズ", "ディテクト・マジック", "マジック・ウェポン",
	 "ソーマタージー", "ヘリッシュ・リビューク", "暗視"]
	skills.sort_custom(SkillSorter, "sort_dic")
	print(skills) # [暗視, ソーマタージー, ヘリッシュ・リビューク, キュア・ウーンズ, ディテクト・マジック, マジック・ウェポン]

あとはスキルが増えたり並び順を変えたくなったりすればjsonをパパッと弄ればいいわけですね。簡単ですし、使いまわせるようになったんじゃないかな。

今回はこんな感じ!もう書くの疲れた!終わり!

いいなと思ったら応援しよう!