Apps Scriptでスプレッドシートのセルの値を画面に表示された文字列そのままの形で取得したい

Apps ScriptでSpreadsheet Serviceを使うと、Spreadsheet内の任意のセルの値を取得できる。

https://developers.google.com/apps-script/reference/spreadsheet/spreadsheet-app

処理の内容によって、画面に表示されている内容を文字列としてそのまま取得したい場合と、日付などその文字列が差している内容を適切な型で取得したい場合があるが、Spreadsheetにはそれぞれの目的に合わせて値を取得する関数が用意されている。

getValues

https://developers.google.com/apps-script/reference/spreadsheet/range#getValues()

上のような値が入った状態で、getValues()を使って値を取得すると、下記のような結果が得られる。

values = SpreadsheetApp.getActiveSpreadsheet().getSheetByName("TestSheet").getDataRange().getValues();

>> [[name, date], [Apple, Mon Jan 01 00:00:00 GMT+09:00 2024], [Orange, Wed Mar 13 00:00:00 GMT+09:00 2024]]

date欄に入力した値は 2024-01-01 という文字列だが、この文字列はSpreadsheet内では自動的に日付として認識されており、getValues()で取得した値は文字列ではなくdate型の値になっている。この挙動は時々厄介だ。ユーザが入力した文字列の値を想定していたら違う値が返ってきていることに気づかず、混乱の原因になったりする。

getDisplayValues

https://developers.google.com/apps-script/reference/spreadsheet/range#getdisplayvalues

getValues()の代わりにgetDisplayValues() を使うと、画面に表示されている内容をそのまま文字列として取得できる。

values = SpreadsheetApp.getActiveSpreadsheet().getSheetByName("TestSheet").getDataRange().getDisplayValues();

>> [[name, date], [Apple, 2024-01-01], [Orange, 2024-03-13]]

どちらを使うべきか

どちらを使うのが良いかはケースバイケースで判断が難しい。

例えば、date欄で日付を入力してもらうことを想定している場合、上記のようにユーザがもし特定の行の表示フォーマットを変更してしまったとしても、getValues() では表示フォーマットの影響を受けずに日時の値を取得できる。

[[name, date], [Apple, Mon Jan 01 00:00:00 GMT+09:00 2024], [Orange, Wed Mar 13 00:00:00 GMT+09:00 2024]]

一方で、getDisplayValues()では、画面に表示されている内容がそのまま返されるため、これを日時として処理したい場合はどちらのフォーマットで入力されても対応できるように実装する必要が出てくる。

[[name, date], [Apple, 2024/01/01], [Orange, 2024-03-13]]

従って一概にどちらを使うべきとは言えず、ユーザにどのような形式での入力をしてもらうか、運用と合わせて実装方法を考えるしかなさそうだ。

AndroidアプリのCI/CD – Google Play Developer APIs

Apple StoreにXcode Cloudが登場してから、iOS / macOSの世界ではアプリのCI/CDのハードルが一気に下がった。一度その快適さが当たり前になってしまうと、Androidアプリのリリースを手動で行うのが余計にストレスに感じられるようになってしまったので、CI/CD環境を作った。

残念ながらGoogle PlayにはまだXcode CloudにあたるようなマネージドなCI/CD環境はないが、公式APIが用意されているのでGitHub Actionsなどと組み合わせれば自力でCI/CD環境を作ることはできる。ただし、CI/CD環境を構築するにあたって押さえておかなければいけない使い方・仕組みが色々ある。一度に説明しようとすると多少ハードルが高いので、まずはAPIから。

“AndroidアプリのCI/CD – Google Play Developer APIs” の続きを読む

Xcode Cloudでビルドの前後に処理を行う

今更だが、Xcode Cloudが便利だ。Apple純正のCI/CD環境ということもあって、設定で複雑なことをする必要がなく、やれることも限られている分わかりやすい。

大抵のアプリのビルドでは、Xcode Cloudの標準の機能だけで問題なくApp Storeへの申請/TestFlightでの配布ができているのだが、稀にビルドの前後に何らかの処理を行いたいケースがある。

https://developer.apple.com/documentation/xcode/writing-custom-build-scripts

Xcode Cloudには、Custom Build Scriptという仕組みが用意されており、このルールに従ってスクリプトを用意すると、下記のタイミングで任意の処理を実行できる。

  • レポジトリからのCloneが終わった直後(ci_post_clone.sh)
  • xcodebuildが実行される直前(ci_pre_xcodebuild.sh)
  • xcodebuildが終わった直後(ci_post_xcodebuild.sh) ※失敗時も呼ばれる

ビルド番号(CI_BUILD_NUMBER)や何をトリガーにワークフローが開始されたのか(CI_START_CONDITION)などの情報を環境変数として参照できるようになっている。

https://developer.apple.com/documentation/xcode/environment-variable-reference

デバッグ用の情報(COMMIT HASHなど)をアプリ内で参照できるようにする、など色々使える場面があって、覚えておくと何かと便利。

Godot 3系のstore_varで保存したファイルをGodot 4で読み込めない問題

GodotのFileクラスのstore_varというメソッドを使うと、容易にオブジェクトの状態を保存できる。

https://docs.godotengine.org/ja/stable/classes/class_file.html#class-file-method-store-var

store_varで保存したファイルをget_varで簡単に復元することができるので、Godotで色々なオブジェクトの状態を保存/復元する方法として紹介されることがある。便利なので自分のアプリでも使っている箇所があったのだが、今回そのアプリのGodot 4対応を進めようとして思わぬ問題にぶつかってしまった。

https://godotforums.org/d/31966-filestore-var-in-35-to-godot-4/16

フォーラムでも報告している人がいるように、Godot 3系のstore_varメソッドで作成したファイルをGodot 4のget_varで復元しようとすると正しく復元できないのだ。

https://github.com/godotengine/godot/issues/73530#issuecomment-1435792453

残念ながら、これは不具合ではなくGodot 4で想定済みの仕様のようで、今後バージョンアップなどで対応される予定もないそうだ。この問題に対する対策としては、下記の二つの方法が提案されている。

  1. Godot 4系にアップデートする前に、ファイルをJSONなど別のフォーマットに移行する機能を作る
  2. 自前でバイナリーローダーを書いて、3系で保存されたバイナリファイルを解読して4系に読み直す

後者は現実的に面倒で難易度も高そうなので、3系のstore_varで作成したデータを引き継ぐには前者の方法をとるしかなさそうだ。ただし、その場合は当面の間3系を使い続けることになってしまうので頭がいたい。

store_varは仕様上今後のアップデートでも同様の問題が起きる可能性があるので、オブジェクトの状態を保存したいときは面倒だがstore_stringとget_as_textを使って自前でシリアライズ/デシリアライズを実装した方が良いようだ。(現在公式ドキュメントのサンプルもそうなっている)

Godot 4でtresファイルのダイナミックロードがやりにくくなった問題

Godot 4で、フォルダ内にあるtresファイルを動的に読み込むプログラムを書いたところうまく動作しなかった。

for file in dir_contents(folder_path):
	if file.get_extension() == "tres":
		tres_files.append(file)

上のコードは、特定のフォルダにあるtresファイルを全てリストアップするためのコードだ。Godot Editor上でこのコードを実行すると、フォルダ内のtresファイルがリストアップされるが、iOS上ではファイルが一つもリストアップされない。調べてみると、.tresファイルは見つからないが代わりに.tres.remapというファイルが存在していることがわかった。

https://github.com/godotengine/godot/issues/66014

Godot 3では動作していたのだが、どうやら仕様が変わりtresファイルもRemapされるようになったらしい。Remapされたファイルはload()やpreload()でres://プロトコルでファイルを指定して呼び出す際には内部的に自動でRemap後のデータが参照されるため、プログラム内で参照先をハードコーディングしているような場合は特にファイルの実態を意識する必要がないが、今回のように「フォルダからファイルを探して動的に読み込む」というような実装では「フォルダ上に見つかるファイル(remapファイル)」と「res://プロトコルで参照する際のURL」に差異が生まれてしまうのでそのままでは動かなくなってしまう。

for file in dir_contents(folder_path):
	if file.get_extension() == "tres":
		tres_files.append(file)
	if '.tres.remap' in file: # 追加
		image_files.append(file.trim_suffix(".remap"))

Godot 4でそのようなことをやりたい場合は、フォルダ内から.tres.remap ファイルを探し、読み込む際には.remapを取り除いたURLでload() するように実装する。ただし、Godot Editor上で作業している際にはRemapが行われていない元ファイルを参照しに行くことになるので、上のコードのように.tresファイルと.tres.remapファイルの両方に対応できるようなコードを書く必要がある。

https://github.com/godotengine/godot/issues/25672

最初にはったIssueと、上に貼ったもう一つのIssueでも指摘されているが、なかなかわかりにくい仕様だと思う。

M1 iMacへの買い換え

2020年末に購入したM1チップ搭載のMacBook Proが快適だ。Xcodeを使ったアプリ開発やPremiereでの動画編集はもちろん、最近少しずつ増えてきたニューラルネットワークが内部的に使われているようなAIアプリのパフォーマンスにおいて、Intel搭載のiMacとは顕著な差があることを実感している。

早くiMacもApple Silicon搭載の27インチモデルが発売されないかとずっと待ってきたが、待ち続けているうちに2年が経過してしまった。2023年中には発売されるのではないかという噂もあったが、昨今のApple製品の値段設定を見ていると発売されてもかなり高価になりそうなこと、それを待つ間の作業今のiMacの作業効率で失う生産性のバランスを考えて、24インチモデルの現行iMacに買い換えることにした。

Apple Siliconが出た当初はDockerや音源プラグインなどで色々問題があったが、最近ではほとんど解消され、さらにはプログラムの最適化も進んでおり環境構築もかなりスムーズに終わった。環境が綺麗な状態で、パフォーマンスをテストしておこうと思う。

“M1 iMacへの買い換え” の続きを読む

Golang用のGoogle Driveを扱うライブラリ

Google Driveにはプログラムから色々な操作を行うためのAPIが用意され、またそのAPIを簡単に呼び出せる各言語用の公式SDKが用意されている。公式SDKが使いにくいということはないが、実際に使いたい場面ではAPIの呼び出しのチェインやエラーハンドリング、パラメータの指定方法の都合などで少しばかり冗長なコードを書かざるを得ない部分がある。過去数年いくつかのシステムでGoogle Driveに関する機能の実装をしてきたが、振り返ってみると同じような冗長なコードをそれぞれのプロジェクトで書いてきていたので、このタイミングでコードを共通化して使いまわせるようにライブラリを作った。

https://github.com/yokoe/hakucho

v0.0.4現在、下記のような処理が実装されている。

  • フォルダの作成
  • ファイルのアップロード
  • アクセス権限の付与
  • ファイルの削除
  • 検索

例えば、ファイルの検索であればページングの処理なども含めて下記のように書くことができる。

files, err := hakucho.NewClient("/path/to/secret.json", "/path/to/token.json")
  .ListFiles(
    []string{"id", "createdTime", "name"}, // 作成日時、ファイル名などを結果に含める
    20, // 最大20件
    option.FullTextContains("Guitar"), // ファイル名や内容にGuitarが含まれるもの
    option.OrderBy("createdTime"), // 作成日時順に並び替える
)

GodotプロジェクトのiOSアプリ出力で、リソースをpckファイルにまとめるかフォルダ参照するかで挙動が変わる話

GodotではiOS用のエクスポートテンプレートを追加すると、ExportメニューからXcodeプロジェクトを書き出してiOSでアプリを動かすことができる。書き出し作業自体は手軽なのだが、毎回数十秒程度処理に時間がかかることやXcode側での設定(アプリ名のローカライズなど)がリセットされてしまうので、コードやリソースを変更するたびにExportを実行するのはなかなか面倒だ。

そこで、公式ドキュメントの「Active Development Considerations」の項では、XcodeプロジェクトからGodotのプロジェクトをフォルダ参照することで、変更のたびに毎回Exportし直さなくても常に最新の内容でiOSアプリをビルド・実行可能にする方法が説明されている。(※プラグインの追加時など、Xcodeプロジェクトの構造変更が必要な時はプロジェクトの出力し直しが必要になる)これは実に便利で、特にアプリ内課金の機能などiOSのネイティブ機能を使う部分の開発・デバッグには不可欠な手順になっている。

ただし、上の公式ドキュメントには特に記載がないがプロジェクトを毎回Exportした場合と、一度ExportしたXcodeプロジェクトからGodotフォルダ参照する場合ではResourceに関する挙動が異なるので注意する必要がある。

GodotからXcodeプロジェクトをExportすると、プロジェクト内で使用される画像やSceneなどのResourceを一つにまとめたリソースパック(pckファイル)が生成される。このpckファイルには、GodotでResourceとして扱われるファイルのみが含まれている。例えば2022年9月現在Godot 3.5ではJSONファイルはResourceファイルとしては扱われないため、プロジェクトフォルダ内に存在したとしてもpckファイルには含まれない。(2022年9月4日のissues #65295でJSONをResourceとして扱うための変更が提案されMergeされたため、将来はJSONもpckファイルに含まれるようになる予定)

一方でフォルダ参照をしている場合は、Godotのプロジェクトディレクトリ内にある全てのファイルをres://プロトコルのURLで参照することができる。Godotでは本来Resourceとして扱われないJSONファイル等もフォルダ参照の場合は参照することができてしまう。そのようなコードを書いてしまうと、フォルダ参照時は動作するのにpckファイル読み込み時は動作しない、という現象を引き起こしてしまうことになる。

一見全て参照できる分、基本的にはフォルダ参照にしておいた方が便利そうな気もするが、そうとも言えない。AppStoreへのサブミット時にリジェクト要因になってしまうファイルがバイナリに組み込まれてしまう場合があるためだ。例えばアプリ内課金を利用する場合、公式ドキュメントのやり方に従うとプロジェクトのres://ios/pluginsにiOSプラグインのファイルを配置することになるが、このプラグインのファイルをフォルダ参照で含めたままAppStoreにサブミットしようとするとInvalid Bundle Structureエラーが発生してしまう。Xcodeのビルド設定で特定のファイルを明示的に除外することでエラーを回避することも可能かもしれないが、開発をしているとプラグイン追加のタイミングなどXcodeプロジェクト自体を出力し直す機会が時々発生し、その度にこの辺りの細かい設定するのはできれば避けたい。

おそらく、現実的に無難な選択肢はpckファイルのみを更新するやり方だろう。Godotには、pckファイルのみをExportする機能が用意されている。

ワンステップ挟むことになるのでフォルダ参照に比べると多少不便ではあるが、この方法なら「フォルダ参照で想定外に動いてしまった」というような実装も防げるので、他プラットフォームへの展開なども考えると最も無難なのではないかと思う。

年末年始なのでGodotでゲームでも作ってみる 6日目

今日の目標

  • 音をつける
  • パーティクルシステムを使って軌跡を描く

音をつける

GodotではAudioStreamPlayerというNodeを使うと簡単にサウンドを再生させることができる。GameStageシーンにAudioStreamPlayerを追加する。

Nodeの名前はHitSoundにした。

FinderからFileSystem dockに適当なサウンドファイルを読み込む。

HitSoundのInspectorで、Streamプロパティのところに先ほど読み込んだサウンドファイルを指定する。

func _input(event):
	if event is InputEventMouseButton and event.button_index == BUTTON_LEFT and event.pressed:
		for enemy in get_tree().get_nodes_in_group("enemies"):
			var distance = event.position.distance_to(enemy.position)
			if distance <= ENEMY_RADIUS:
				enemy.position = Vector2(rng.randf_range(-STAGE_MARGIN, get_viewport().size.x + STAGE_MARGIN), -STAGE_MARGIN)
				score += 1
				get_node("HitSound").play(0)

サウンドを再生したいタイミングで、play(0)を呼ぶとそのサウンドが冒頭から再生される。今回のコードでは、標的が叩かれたタイミングでplay(0)を呼び出して効果音が鳴るようにした。

標的の軌跡を描く

パーティクルシステムを使って、標的の軌跡を描いてみる。

各標的から軌跡のパーティクルが放出されるようにしたいので、Hae.tscnにパーティクルシステムを導入する。Godotのパーティクルシステムには2種類のパーティクルシステムが用意されているが、Open GL ES 3系のプロジェクトでは、より高度でGPUで処理可能なParticles2Dノードを使うことができるので、今回はParticle2Dを追加。

Particles2Dノードが追加されたが、動作に必要なMaterialがセットされていないので警告マークが付いている。

ParticleのProcess Material(描画される一つ一つのパーティクルの表現を決めるもの)がemptyになっているので、New ParticlesMaterialを選択して新しいMaterialを作る。Inspectorの下の方にCanvas ItemのMaterialを設定する項目があるが、こちらではないので注意。

次にTexture(パーティクルの描画に使われる画像)を指定する。適当な画像をFileSystemに読み込んで、そこからドラッグで指定できる。

エディタ上でパーティクルシステムの動きが既に確認できるようになっている。現在は縦方向にパーティクルが放出されるだけであまりかっこよくないので、放射状に広がるようにパラメータを調整したい。

このパーティクルの動きは先ほど作ったMaterialの設定から行う。現在パーティクルが放出されると下に落ちていくのはGravityの影響なので、一旦Gravitiyを全て0にセットする。こうすることでパーティクルは生成されても下に落ちて行かなくなる。次にInitial Velocityに適当な値をセットする。この値はパーティクルが放出されるときの初速となり、大きくすれば大きくするほど勢いよくパーティクルが放出されるようになる。奇跡として描く場合はあまり大きくしすぎないほうが良いだろう。最後にDirectionで放出される方向を設定する。今回は特定の方向ではなく全方位に放出されて欲しいので、xyzはそれぞれ0をセットし、放出の範囲を決めるSpreadに180をセットした。

若干面倒ではあるが、Color Rampのところでグラデーションを指定することで、フェードインやフェードアウトも表現することができる。

設定の変更はすぐにエディタ上にも反映され動作を確認することができる。必要に応じて、その他のパラメータを調整して良い感じになるようにする。

最後に、Particles2DノードのDrawing / Local CoordsプロパティをOffにする。これをOnにしていると、放出されたパーティクルは親のNodeの移動に合わせて一緒に移動してしまう。今回は軌跡として表示をしたいので、標的(親ノード)が移動したとしても放出されたパーティクルは放出された場所にとどまって欲しいので、Offにする。

まとめ

一週間あるからある程度形になるだろうと思ってGodotを始めてみたが、前半油断してゆっくりやりすぎたこと、そしてそもそも実は休みが六日間しかなかったことなどもあって後半かなり駆け足になってしまった。とりあえずGodotを使ってゲームを作っていく上での基礎的なことは分かってきた感じがある。Godotはかなりサクサク動くし、Pythonライクな文法のGDScriptもコーディング量が少なく、気軽に遊びには良いゲームエンジンだなと思った。

Godotはクロスプラットフォームであるという点が魅力の一つなので、もうちょっとゲームとして形になったらiOS / Androidでそれぞれ動かしてみるというのを試してみたいと思う。