【Godot 4.0】マルチプレイヤー実装に引っかかったら

導入

自分で色々試した上で、バグの原因となる場所を全てまとめました。

マルチプレイヤーの実装

そもそもマルチプレイヤーの実装の仕方が分からないという場合は、こちらの3分半の動画を見てみて下さい。コードを真似れば実装自体はできます。

基礎知識

公式ドキュメントで説明が不足している3つの概念について解説します。

MultiplayerSpawner:指定したPackedScene(AutoSpawnListに含まれるPackedScene)がサーバー側の指定した階層上(SpawnPath上)にあるとき、それを全てクライアント側に複製し、サーバー側で削除されたら、クライアント側でも削除する、というノードです。簡単にいえば、サーバー側の特定の階層をクライアント側にコピーするノードです。MultiplayerSynchronizerが変数をシンクロさせるなら、こっちはPackedSceneのインスタンスの存在をシンクロさせるノードと言えます。ただし、複製されるのはname(階層名)のみで、中身までコピーしているわけではありません。単に、サーバー側の動向に従い、クライアント側で自動でシーンをインスタンス化して階層に入れてくれるだけです。

MultiplayerSynchronizer:指定した変数のみ、他の全てのPeerの変数の値を、自分側の変数の値で上書きします。MultiplayerSynchronizerのauthorityが自分のPeerのidと一致している人だけが、他のPeerにある変数を上書きできます。つまり、MultiplayerSynchronizerのauthorityに設定されたPeerのみが複製権限を持ちます。

Authority:ノードが持つ変数。MultiplayerSynchronizerによって使用されます。set_multiplayer_authority()、get_multiplayer_authority()によって、設定、取得ができます。デフォルトは1です(Serverに割り振られるPeerのidと同じ)。authorityを変更すると、子ノードのauthorityも変更されます。上の動画でPlayerのauthorityを変更するのは、子ノードにあるMultiplayerSynchronizerのauthorityを変更する為です。

原因① 階層名が同じ

階層名が同じであるとバグがおきます。GodotはUnityと違い、階層名を一意にする必要があります。正確にいえば、通信機能を使わない場合は、階層名を一意にしなくても問題ないですが、Godotの通信機能は「同階層名が存在しない」という前提で設計されているので、同階層名が2つ以上あると、MultiplayerSpawnerが正しく機能せず、クライアント側でエラーが起きてしまいます。

例えば、nameが「Player」であるPackedSceneをnameを変更しないまま2つ作成して階層に入れると、ホスト側では、「on_spawn_receive: Method/function failed. Returning: ERR_INVALID_DATA」、クライアント側では、「Unable to auto-spawn node with reserved name. Make sure to add your replicated scenes via ‘add_child(node, true)’ to produce valid name.」などのエラーが発生します。

対処方法は、同じ名前の階層を2つ作らないことです。PackedSceneを追加する際には階層名(name)を変更してあげましょう。

また、「add_childの第二変数をtrueにして!」というエラーが表示されますが、それをしたところで変わらずエラーが起きます。

原因② Authorityの問題

MultiplayerSynchronizerのAuthorityが自分のPeerのidに設定されていない場合、値の複製権限が別のPeerにあるので、自分側で値を変更しても、何も変わりません。

対処方法は、複製されるインスタンスの_enter_tree()で、set_multiplayer_authority(<自分のpeerのid>)をすることです。別のPeerでも呼ばれる場所なら、どこに書いても問題ありません。

自分のPeer側でしかset_multiplayer_authority()が呼ばれない場合、原因③のように、別のPeerでは正しくauthorityが設定されません。

原因③ 相手のPeer側で値の設定が行われていない

「サーバー側でPackedSceneをインスタンス化し、様々な変数を設定して階層に入れる。」というコードがあるとします。これでは意味がありません。クライアント側では、MultiplayerSpawnerによって、変数の変更されていない新しいインスタンスが作られるだけです。

MultiplayerSpawnerは、指定したPackedSceneがサーバー側で作成されたときに、クライアント側で、指定したPackedSceneをインスタンス化して階層に追加するノードです。このとき引き継がれるのはname(階層名)だけなので、中身は別途追加してあげる必要があります。

対処方法は、①nameに必要な情報を書き込み、_enter_tree()の際にnameから必要な情報を読み取る。②MultiplayerSpawnerが複製を行う際に出されるspawned(node: Node)というシグナルと変数設定用の関数を繋げる。③全てのMultiplayerSynchronizerの権限をサーバー側に持たせ、変数がシンクロした後に権限をクライアント側に渡す。

【Godot 4.0】関数を次フレームに呼び出す方法

導入

関数を次フレームに呼び出す方法として、call_deferredがよく挙げられますが、誤った方法です。海外のコミュニティでも、「call_deferredを使えば良いよ!」という声しか無かったので、その誤解を解く為にこの記事を書きました。

方法

await get_tree().create_timer(0).timeout

を使う。このコードの意味は「シーンツリー上に0秒のタイマーを作成して、そのタイマーが切れるまで待つ」です。次フレームまで処理が中断されるので、このコードの後ろに実行したい関数をおけば良いです。

「0秒タイマー」によって丁度次フレームまで処理を中断させることができる理由は、タイマーの処理が各フレームの最初(自作スクリプトが実行される前)に行われるからです。

call_deferredのよくある誤解

call_deferredは「後で関数を呼ぶ」関数です。しかし、call_deferredによって指定された関数は次フレームに行く前に呼ばれてしまいます。例えば、あるフレームでcall_deferredを使って関数を呼んだ場合、同じフレームの_processの前に関数が呼ばれてしまうことがあります。

海外コミュニティにcall_deferredについての正確な動作を解説したポストがあります。

ここからかいつまんで説明すると、以下のようになります。

call_deferredはスタックに関数を入れますが、そのスタックの関数が実行されるタイミングは1フレームで複数回あります。コードの終わりなどにもそのタイミングがあるので、指定した関数が現フレームで呼ばれてしまいます。

Comment
byu/belzecue from discussion
ingodot

【python】pythonで画面キャプチャソフトを作ってみた

導入

画面の色を検知するソフトを作成してみたかったので、Pythonで画面キャプチャを作ってみました。

今回作ったもの

FPS=60で動かしたものです。

キャプチャした画面を更にキャプチャしているので、ミルフィーユのようになっています。

録画しながらのせいか右端に残像が残っていますが、普段は発生しません。

使用するライブラリ

  • tkinter (ウィンドウ作成の為)
  • Pillow (画像データの変換処理の為)
  • dxcam (高速でスクショをする為)

コード

import tkinter
import dxcam
from PIL import Image, ImageTk

SCREEN_WIDTH = 1920
SCREEN_HEIGHT = 1080
FPS = 60

root = tkinter.Tk()
root.geometry(f"{SCREEN_WIDTH}x{SCREEN_HEIGHT}")
root.title("画面キャプチャ")

canvas = tkinter.Canvas(root, width=SCREEN_WIDTH, height=SCREEN_HEIGHT, background="#FFF")
canvas.pack()

camera = dxcam.create()
camera.start(target_fps=FPS)

def update():
    global tk_image
    frame = camera.get_latest_frame()
    image = Image.fromarray(frame)
    tk_image = ImageTk.PhotoImage(image)
    canvas.create_image(SCREEN_WIDTH/2,SCREEN_HEIGHT/2,image=tk_image)
    root.after(int(1000/FPS), update)

update()
root.mainloop()

コード解説(オリジナル部分)

SCREEN_WIDTH、SCREEN_HEIGHT:スクリーンの幅と高さです。

FPS:一秒あたり何回描画するか。FPSを高くしてもcamera.get_latest_frame()でのスクショが追い付かないので、実際には指定した値よりも低くなります。

コード解説(tkinter)

tkinter.Tk():ウィンドウを作成します。

root.geometry(f”{SCREEN_WIDTH}x{SCREEN_HEIGHT}”):ウィンドウサイズを設定。このコードを書かない場合、ウィンドウの大きさはスクリーンの大きさになるので、今回は無意味です。

root.title(“画面キャプチャ”):ウィンドウのタイトル(名前)を「画面キャプチャ」にします。

tkinter.Canvas(root, width=SCREEN_WIDTH, height=SCREEN_HEIGHT, background=”#FFF”):ウィンドウに画像を描画する為のキャンバスを作成します。背景色は白です。

canvas.pack():キャンバスを配置します。このコードが無い場合、キャンバスは表示されません。

global tk_image:tk_imageがガベージコレクタによって廃棄されないようにする為にグローバル変数にする。このコードが無いと、tk_imageが即座に捨てられてしまい、キャンバスには何も表示されなくなります。

canvas.create_image(x,y,image=tk_image):x,yには画像の中心座標を入れます。

root.after(int(1000/FPS), update):int(1000/FPS)ミリ秒あとに、update関数を呼びます。第一引数は整数にする必要があります。

コード解説(Pillow)

image = Image.fromarray(frame):numpyのndarrayをPillowのImageオブジェクトに変換します。

コード解説(dxcam)

dxcam.create():DXCameraインスタンスを作成します。

camera.start(target_fps=FPS):指定したFPSでスクショを撮る作業をスタートします。

camera.get_latest_frame():新しいスクショが撮られるまで待ち、スクショが撮られたら値を返します。返り値の型は、numpyのndarrayです。

【python/深層学習】tensorflow-gpuが削除された話

導入

古い深層学習の情報源では「tensorflow-gpu」というライブラリが登場しますが、現在そのライブラリは削除され、pipを使ってインストールしようとするとエラーが表示されるようになっています。

pipでのエラー時に詳細が全て表示されるので、エラーを読む人にとっては何も問題ありませんが、なにより無用な手間を減らす為にこの記事を書きました。

概要

2022年12月に「tensorflow-gpu」というライブラリは削除され、代わりに「tensorflow」にその役割が引き継がれました。

また、すでに「tensorflow-gpu」をインストールしている人はそのまま使うことができます。

詳細はここに書いてあります。

https://pypi.org/project/tensorflow-gpu/

【群論】有限体の乗法群が巡回群になることの証明

問題

有限体Kの乗法群K*が巡回群になることを示せ。

証明の流れ

NをK*の元が取る最大の位数としたとき、K*の任意の元のN乗が1であることを示す。

N<#K*なら矛盾するので、N=#K*が示される。

証明

Kを有限体とし、K*をKの乗法群とする。

K*の元の内、最大の位数を取る元をx、その位数をNとおく。y∈K*に対して、xyの位数がmであるとする。mがNの約数なら、(xy)N=1であり、yN=1。mがNの約数でないなら、x-mの位数はNになるので、yの位数はNになる。yで生成される巡回群は、yで生成される巡回群の部分群なので、yの位数はNとなる。つまり、yN=1。

Kは体なので、方程式XN=1のK上の解の数はN以下であるが、K*の任意の元がXN=1の解となるので、#K*≦Nとなる。#K*≧Nは自明なので、N=#K*

以上より、K*はxで生成される巡回群となる。

補足

なぜN=#K*を示すのか?:N=#Kなら、xの巡回群の位数が#K*となり、(xの巡回群)=#Kとなるから。

mがNの約数でないなら、x-mの位数はNになる:x-mの位数がk(≠N)なら、x-mk=1となるが、-mkがNの倍数でないのでおかしい。

yで生成される巡回群は、yで生成される巡回群の部分群なので、yの位数はNとなる:ラグランジュの定理(「部分群の位数は、それを含む群の位数の約数になる」)より、yの位数はNの倍数となるが、K*の元が取る最大の位数はNなので、yの位数はNとなる。

#K*≧Nは自明:位数が、その集合の要素数を超えることはない。もし、N>#K*となるなら、xの巡回群の要素数は#K*を上回るので、おかしい。

【証明付き】素数に関する定理まとめ

導入

素数に関するまとめサイトが無かったので作りました。

証明されているものに限り記載しています。

目次

  • 弱いゴールドバッハ予想
  • ベルトラン・チェビシェフの定理
  • 素数定理
  • オイラー積
  • 素数の間隔に関する事実
  • 立方数に関する事実
  • ウィルソンの定理
  • グリーン・タオの定理
  • 算術級数定理

弱いゴールドバッハ予想

7 より大きい奇数は 3 個の素数の和で表せる。3 個の素数は同じ数であってもよい。

ハラルド・ヘルフゴットによる証明

ベルトラン・チェビシェフの定理

任意の自然数 n に対して、n < p ≤ 2n を満たす素数 p が存在する。

エルデシュによる初等的な証明

素数定理

π(x) ~ Li x

素数定理 – Wikipedia

ポール・エルデシュによる証明

ニューマンによる短い証明

ゴールドフェルドによる初等的な証明

アヴィガドと他3人による証明

素数定理の証明と歴史

オイラー積

ディリクレ級数を素数に関する総乗の形で表した無限積。

オイラー積 – Wikipedia

素数の間隔に関する事実

間隔が246以下の2つの素数の組は無限に存在する。

D.H.J. Polymath による証明

立方数に関する事実

n が十分大きければ n3 と (n + 1)3 の間には必ず素数が存在する。

アルバート・イングハムによる証明

ウィルソンの定理

p が素数ならば (p − 1)! ≡ −1 (mod p) が成り立つ。
逆に、整数 p > 1 に対し、(p − 1)! ≡ −1 (mod p) ならば、p は素数である。

ウィルソンの定理 – Wikipedia (証明付き)

グリーン・タオの定理

素数の列は、任意の長さの等差数列を含んでいる。

テレンス・タオと他2人による証明

算術級数定理

初項と公差が互いに素である等差数列には無限に素数が存在する。

算術級数定理 – Wikipedia (証明付き)