そのうちポーカーのワールドを作るUdonSharpを何も知らないVRChatter

# Step.0 概要と前書き
## 概要
徐々にステップアップして、VRChat上で動くポーカーのワールドを作ることを目指すよ。

## 書いてる人のレベル
* Blenderと和解できない
* Unityと和解できない。けど、何とかVroidをエクスポートしたアバターをアップロードしたことはある。
* UdonSharp? VRChatの言語だってことは知ってる。
* C#ですら書いたことない。Cなら大学生時代にエイトクイーン書いた記憶があるけど…
* Pythonなら競技プログラミングで扱ってるから、それなりに(atcoder緑レベル)わかる。
* VRChatプレイ時間は600時間弱。

## 前書き
VRChatでよくポーカーで遊んでいます。
実はポーカーにはいろいろ種類があります。
* 多分一番多くの人が知っている5枚の手札で役を作る5ドローポーカー
* 2枚の手札と5枚の共通札で役作るホールデム
* 4枚の手札中2枚と5枚の共通札で役作るオマハ
* 7枚の手札で役を作るセブンカードスタッド
* etc...
 
いつも遊んでいるポーカーのワールドは、ホールデムとオマハは遊べるけど、それのみ。
あと、どういうプレイをしたかを後で振り返りたいのに履歴が出ない。
もっといろんなポーカーをフレンドとしたい! 振り返れように履歴機能欲しい!
なら、You、作っちゃいなよ!
と思って調べたけど、あちこちに情報が散らばってて、体系立ててまとまってるのがない…
ということで、

## 目標
* VRChat上で動くポーカーのワールドを作ることを目指しつつ、UdonSharpを学ぶ
* 学んだ結果をまとめて、自分レベルの人がゲームワールドを作れるような文章を作る

# Step.1 VCCによる環境構築
VRChatのワールドを作るのにCreator Companionとかいうのと、Unityが必要なので環境構築します。
下のページに従って(バージョン違いかちょっと画面違うところもあるけど)、CC落として画面の指示通りやればOKです。

https://note.com/rotemtable/n/nffb3b5596827

指示に従えばUnityも適したバージョンがインストールできるようになってるの便利。

# Step2. 床を作って、立つ
1. 下のページの「VCCを立ち上げてプロジェクトを作成する」の「VRCWorldプレハブと床を配置する」をします。

https://utanoblog.net/category5/vrcworldcreate.html

「VRCWorld」っていうのが、インスタンスに入った時の場所になります。
2. 出来たら上側にある「▶」ボタンを押すと、「VRChat Client Simulator」というのが立ち上がります。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/427576/3135c54f-9733-b10f-0183-cc7d6e6ac747.png)
「Close Menu」を押すと、動けるようになる。
マウスで視点変更、W/A/S/Dで前後左右移動できます。
抜けるときはEscキーを押して「Exit Playmode」をクリックします。

# Step3. キューブを置いてみる。
殺風景なので、何かモノを置いてみましょう。ひとまず立方体を置いてみます。
1. 画面右側の「Hierarchy」内で右クリック→「3D Object」→「Cube」とクリックします。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/427576/98aec999-db51-29a9-46e9-e82f869414d7.png)
真ん中に立方体が出現しました。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/427576/c8f17808-8a70-f35b-227f-e19f3ede5448.png)
「Cube」でなく「Sphere」を選べば球などを出すこともできます。
2. (床に埋まってしまっているときは、緑の矢印をドラッグして引き上げます。
赤・青の矢印も併用して、位置の調整が可能です。)
3. (右側の「Inspector」内の「Transform」下の「Scale」で大きさをいじれます。)

# Step4. キューブを回してみる
いよいよついに、UdonSharpを書きます。
1. 先ほど置いたキューブをクリックして、右側の「Inspector」内の「Add Component」ボタンをクリックします。
2. 入力欄が出てくるので「udon」と打って出てきた、「Udon Behavior」をクリックします。「Inspector」内に「Udon Behavior (Script)」というのが追加されます。
3. UdonSharpのScriptをまとめて置いておくフォルダを作ります。下の「Project」タブで「Assets」を右クリック > Create > Folder でフォルダができます。出来たフォルダを右クリック > Rename でわかりやすい名前に変更しておきます。
4. 3.で作ったフォルダを選択します。右側の「This folder is empty」と書かれているエリアで右クリックし、Create > U# Script とクリックします。
5. 「Save UdonSharp File」ウィンドウが開くので、ファイル名欄にスクリプトの名前を入れて、「保存」ボタンを押します。
6. 4.で右クリックした場所に、5.で付けたファイル名でファイルが二つ出来ています。大きく"#"が書かれている方をダブルクリックします。プログラムのエディタが立ち上がります。
7. 既になんか書いてありますが、丸ごと消して、下記のコードをコピペします。
```C#
using UdonSharp;
using UnityEngine;
using VRC.SDKBase;
using VRC.Udon;
// ↑いろいろ使うためのおまじない。
// Udon#では文頭に//をつけるとコメント行になります。

public class RotatingCubeBehaviour : UdonSharpBehaviour
//「public class 名前 : UdonSharpBehaviour」で基底クラス? を宣言? これもおまじない。
// 名前は5.で付けたファイル名と同じにする
{

    private void Update()
    // この下の{　}内に毎フレームごとに呼び出したい処理を書く
    {
        transform.Rotate(Vector3.up, 90f * Time.deltaTime);
    // transform. : 何かモノを動かすよ
    // Rotate(a,b): 回転させるよ。aで回転軸,bでどれくらいを指定するよ
    // Vector3.up : Step3の画像2枚目で示した緑色の矢印 (= 床面に垂直方向)
    // 90f * Time.deltaTime : 単位時間あたり90度
    }
}
```
Crtl+Sなどして保存することを忘れずにしてから、エディタを閉じます。
8. キューブをクリックして選択します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/427576/1047db9c-a3d8-122c-1ae8-1864de8244c2.png)
9. 2.で追加された「Udon Behavior (Script)」の「Program Source」右隣にある「None（Abstract ～」と書かれている所の◎マークをクリックします
10. 「Select AbstractUdonProgramS～」というウィンドウが出てくるので、入力欄に5.で付けたファイル名の一部を入れると、当該ファイルが出てくるのでクリックします。

これでキューブが回ります!
Step2-2と同様にSimulatorを立ち上げると…
![aaaaaa.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/427576/7c9275de-a25c-7e04-ab9a-d3c7357309dc.png)
確かに回っています! ...回ってるんですよ、動画じゃなくて画面キャプチャだからわからないけど。

参考URL:

https://github.com/MerlinVR/UdonSharp/wiki/examples#spinning-cube

https://qiita.com/codez_vrc/items/8347ac06d74fe0de8023

https://soft-rime.com/post-8858/

# Step5. キューブを止める/回すを切り替える
動といえば静、静といえば動。動いているものは止めたくなるし、止まっているものは動かしたくなりますよね?
ということで、インタラクトでキューブの回転/静止が切り替わるようにします。

1. Step4.(5)で作成されたスクリプト(大きく"#"が書かれている方)をダブルクリックして開きます。
2. ソースを下記通りに書き換えます。
(行冒頭の"+"はこの行が追加された、の印なのでペースト時に消してね)
```diff_c_sharp
using UdonSharp;
using UnityEngine;
using VRC.SDKBase;
using VRC.Udon;
// ↑いろいろ使うためのおまじない。
// Udon#では文頭に//をつけるとコメント行になります。

public class Spinning_Cube : UdonSharpBehaviour
//「public class 名前 : UdonSharpBehaviour」で基底クラス? を宣言? これもおまじない。
// 名前はStep4.(5)で付けたファイル名と同じにする
{
+   private bool isSpinning = false;
+   // isSpinningはtrue or false な変数(bool)だよ。初期値はfalseだよ。

    private void Update()
    // この下の{　}内に毎フレームごとに行いたい処理を書く
    {
+       if(isSpinning)
+       // isSpinningがTrueのときは下の{ }の中身を実行
        {
            transform.Rotate(Vector3.up, 90f * Time.deltaTime);
            // transform. : 何かモノを動かすよ
            // Rotate(a,b): 回転させるよ。aで回転軸,bでどれくらいを指定するよ
            // Vector3.up : Step3の画像2枚目で示した緑色の矢印 (= 床面に垂直方向)
            // 90f * Time.deltaTime : 単位時間あたりに90度
        }
    }

+    private override void Interact()
+    // この下の{ }内にインタラクトされたときに行いたい処理を書く
+    {
+        isSpinning = !isSpinning;
+        // isSppiningの値を反転させるよ
+    }
}
```
これでキューブを止めたり回せたりできます!
Step2-2と同様にSimulatorを立ち上げると…
![interact_and_two_mode.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/427576/05413ffd-5033-a0e4-c530-0e6d5f23634f.png)
確かにインタラクトできるようになっているし、インタラクト(Simurator上では右クリック)で回る/止まるが切り替えられます! ...切り替えられるんですよ、動画じゃなくて画面キャプチャだからわからないけど。

参考URL:

https://kurotorimkdocs.gitlab.io/kurotorimemo/010-VR/010_VRChat/UDON/UdonList/localObjectONOFF/

# Step6. 別のオブジェクトからキューブを止める/回すを切り替える
「回転部には手を触れるな」って工場勤めてた時に習いました。
なので、回ってるキューブを止めるのに、動いているキューブに触りたくないです。
ということで、別のオブジェクト(今回は球)にインタラクトして、その結果キューブの回転状態が変えられるようにします。

1. Step3と同様の手順でSphereを置く。
2. Step4と同様の手順で下記のスクリプトを作成してからSphereにアタッチする。
(行冒頭の"+"はこの行が追加された、"-"はこの行が消された、の印なのでペースト時に消してね)
```diff_c_sharp
using UdonSharp;
using UnityEngine;
using VRC.SDKBase;
using VRC.Udon;
// ↑いろいろ使うためのおまじない。
// Udon#では文頭に//をつけるとコメント行になります。

public class Spin_Operation : UdonSharpBehaviour
{
    public bool isSpinning = false;
    // isSpinningはtrue or false な変数(bool)だよ。初期値はfalseだよ。

+    public GameObject Spin_Cube;
+    // public GameObject ～と書くと、あとで割り当てができる。

    private void Update()
    // この下の{　}内に毎フレームごとに行いたい処理を書く
    {
        if(isSpinning)
        // isSpinningがTrueのときは下の{ }の中身を実行
        {
-            transform.Rotate(Vector3.up, 90f * Time.deltaTime);
+            Spin_Cube.transform.Rotate(Vector3.up, 90f * Time.deltaTime);
+           // Spin_Cube  : 動かす対象はSpin_Cubeだよ
            // transform. : 左記のものを動かすよ
            // Rotate(a,b): 回転させるよ。aで回転軸,bでどれくらいを指定するよ
            // Vector3.up : Step3の画像2枚目で示した緑色の矢印 (= 床面に垂直方向)
            // 90f * Time.deltaTime : 単位時間あたりに90度
        }
    }

    public override void Interact()
    // この下の{ }内にインタラクトされたときに行いたい処理を書く
    {
        isSpinning = !isSpinning;
        // isSppiningの値を反転させるよ
    }
}
```
3\. すると、[スクリプト名](U# Script)のところに、「Utilities」 >「None(Game Object)」が出来るので、そこの右端の◎マークをクリックします。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/427576/c35b6e36-50b2-5945-edca-9874c5dd0825.png)
4. 「Select GameObject」というウィンドウが出てくるので、動かしたいオブジェクトの名前の一部を入れて絞り込んでからダブルクリックして指定します。

これでキューブ自体に触らずに止めたり回せたりできます!
Step2-2と同様にSimulatorを立ち上げると…
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/427576/2af69228-7869-1936-38a6-faac7a1cec89.png)
確かに球体へのインタラクトで回る/止まるが切り替えられます!
...切り替えられるんですよ、動画じゃなくて画面キャプチャだからわからないけど。

# Step7. キューブの回転速度を切り替える
あるオブジェクトを操作して、別のオブジェクトの動作を切り替える方法として、`SendCustomEvent`という「関数を実行できる関数」を使う方法もあるらしい。
SendCustomEventでStep6と同じことをしようとすると下記となった。
```c#:CubeにアタッチするU# Script
using UdonSharp;
using UnityEngine;
using VRC.SDKBase;
using VRC.Udon;

public class Spinning_Cube : UdonSharpBehaviour
{
    public bool isSpinning;
    
    private void Update()
    // この下の{　}内に毎フレームごとに行いたい処理を書く
    {
        if(isSpinning)
        // isSpinningがTrueのときは下の{ }の中身を実行
        {
            transform.Rotate(Vector3.up, 90f * Time.deltaTime);
            // transform. : 何かモノを動かすよ
            // Rotate(a,b): 回転させるよ。aで回転軸,bでどれくらいを指定よ
            // Vector3.up : Step3の画像2枚目で示した緑色の矢印 (= 床面に垂直方向)
            // SpinSpeed * Time.deltaTime : 単位時間あたりにSpinSpeed
        }
    }

    public void StartStop()
    // あとで別のスクリプトから実行させたい関数は頭につけるのはpublicにする
    {
        isSpinning = !isSpinning; // isSpinningの値を反転させる
    }

}
```
```c#:SphereにアタッチするU# Script
using UdonSharp;
using UnityEngine;
using VRC.SDKBase;
using VRC.Udon;

public class Spin_Operation : UdonSharpBehaviour
{
    public GameObject Spinning_Cube;
    UdonBehaviour udon;

    private void Start()
    {
        udon = (UdonBehaviour)Spinning_Cube.GetComponent(typeof(UdonBehaviour));
        // Spinning_CubeにアタッチされているUdonBehaviourを調べて代入
    }

    public override void Interact()
    // この下の{ }内にインタラクトされたときに行いたい処理を書く
    {
        udon.SendCustomEvent("StartStop");
        // Start時に調べて代入したudonの中の"StartStop"を実行する
    }
}
```
Step6の書き方では、キューブの回転状態を球体(にアタッチされたU#)が持っていて、球体がキューブに対して回転の有無や、速度を指示していたイメージ。
一方、SendCustomEventを用いる方法では、キューブの回転状態をキューブ本体(にアタッチされたU#)が持っていて、それに対して球体(にアタッチされたU#)から操作するようになっている。
【ここにStep6の動作とStep7での動作のイメージの違いの図を入れる】

SendCustomEventを用いる方法では操作元を増やせるので、止める/動かすだけでなく、もっといろいろなことができそう。
ということで、ある球体を触ったときは回転速度増加、ある球体を触ったときは回転速度減少するようにしてみる。
```diff_c_sharp:CubeにアタッチするU# Script
using UdonSharp;
using UnityEngine;
using VRC.SDKBase;
using VRC.Udon;

public class Spinning_Cube : UdonSharpBehaviour
{
    private bool isSpinning = true;
+   private float SpinSpeed = 200;
+   // SpinSpeedはfloatな変数だよ。初期値は200だよ。ほかのプログラムから直接読み書きはしないのでprivateにするよ

    private void Update()
    {
        if(isSpinning)
        {
            transform.Rotate(Vector3.up, SpinSpeed * Time.deltaTime);
        }
    }

    public void StartStop()
    {
        isSpinning = !isSpinning; // isSpinningの値を反転させる
    }
    
+    public void SpeedUp()
+    {
+        SpinSpeed = SpinSpeed + 100;
+    }

+    public void SpeedDown()
+    {
+        SpinSpeed = SpinSpeed - 100;
+        if(SpinSpeed < 0)
+            // 回転方向の反転を防ぐために、回転速度は負の値をとらないようにする
+            SpinSpeed = 0;
+    }

}
```
```diff_c_sharp:interactで回転速度増加するSphereアタッチするU# Script
using UdonSharp;
using UnityEngine;
using VRC.SDKBase;
using VRC.Udon;

public class Spin_SpeedUp : UdonSharpBehaviour
{
    public GameObject Spinning_Cube;
    UdonBehaviour udon;

    private void Start()
    {
        udon = (UdonBehaviour)Spinning_Cube.GetComponent(typeof(UdonBehaviour));
        // Spinning_CubeにアタッチされているUdonBehaviourを調べて代入
    }

    public override void Interact()
    // この下の{ }内にインタラクトされたときに行いたい処理を書く
    {
+        udon.SendCustomEvent("SpeedUp");
+        // 調べたUdonBehaviorの"SpeedUp"を実行してもらう
    }
}

```
```diff_c_sharp:interactで回転速度減少するSphereにアタッチするU# Script
using UdonSharp;
using UnityEngine;
using VRC.SDKBase;
using VRC.Udon;

public class Spin_SpeedDown : UdonSharpBehaviour
{
    public GameObject Spinning_Cube;
    UdonBehaviour udon;

    private void Start()
    {
        udon = (UdonBehaviour)Spinning_Cube.GetComponent(typeof(UdonBehaviour));
        // Spinning_CubeにアタッチされているUdonBehaviourを調べて代入
    }

    public override void Interact()
    // この下の{ }内にインタラクトされたときに行いたい処理を書く
    {
+        udon.SendCustomEvent("SpeedDown");
+        // 調べたUdonBehaviorの"SpeedDown"を実行してもらう
    }
}
```
これでキューブの回転速度を変更できます!
Step2-2と同様にSimulatorを立ち上げると…
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/427576/91a28c38-5dcb-f87f-01c8-62cdee127275.png)
確かに球体へのインタラクトでキューブの回転速度が変化しています!
...変化してるんですよ、動画じゃなくて画面キャプチャだからわからないけど。

参考URL:

https://kurotori4423.github.io/KurotoriMkDoc/VRChat/UDON%20%E3%81%A7%E3%82%AE%E3%83%9F%E3%83%83%E3%82%AF/

https://qiita.com/izmktr/items/2b8b978703a9956f2a0d

https://gengoshori.hatenablog.com/entry/2018/01/13/154625

# Step8. テキスト表示する
回転速度を変えられるようになったらその値を知りたくなりますよね。
ということで回転速度をテキストで表示できるようにしましょう。

1. Hierarchyで右クリック > UI > Canvasを選択します。するとHierarchyにCanvas,EventSystemができます。
2. Canvasの大きさとかの設定をします。
* Canvasのレイヤーをデフォルトにする。
Canvasを選択して、Inspector一番上の「Layer」を「UI」にする。
* Canvasの「Render Mode」を「World Space」にする。
Canvasを選択して、Inspectorの真ん中らへんから変える。
* CanvasにVRC_UiShapeコンポーネントをつける。
Canvasを選択して、Inspector一番下の「Add Component」を押して、「VRC Ui Shape」と入力して出てきたやつをクリックする。
* Canvasのサイズ・位置変更を行う。
Canvasを選択して、Inspector上の方のRect Transform内の値を変える。
ScaleをX,Yそれぞれ0.005にして、Width:300, Height:200とかにするとよいらしい。
Pos X/Y/Zもそれぞれ一度全部0にしてから、赤青緑矢印つかって調整するとよい。

3. HierarchyのCanvasを右クリックし > UI > Text とクリックします。Canvasの下部にTextができます。
4. Textの大きさとかの設定をします。
* Textの中のParagraph内にあるHorizontal OverflowをOverflowにします。
* Textの中のParagraph内にあるVertical OverflowをOverflowにします。
* Textの中のCharacter内にあるFont Sizeを必要に応じて変えます。

これで固定のテキストは表示できるようになりました。
プログラムを変えて回転速度の表示ができるようにします。

5. Cubeにアタッチしているプログラムを下記に書き換えます。
```diff_c_sharp:CubeにアタッチするU# Script
using UdonSharp;
using UnityEngine;
using VRC.SDKBase;
using VRC.Udon;
using UnityEngine.UI;  // Textを使うときにはこれも入れる!

public class SpinningCube : UdonSharpBehaviour

{
    private bool isSpinning = true;
    private float SpinSpeed = 200;
    
+    public GameObject SpeedIndicator;
+    public GameObject SpinCube;
// テキストのGameObjectを指定するために追記。複数のGameObjectを扱うので混乱防止にSpinCubeも追記。
    
    public void Update()
    {
+        Text SpeedIndicator_value = SpeedIndicator.GetComponent<Text>();
+        // GameObject「SpeedIndicator」の「Text」のコンポーネントを取得
+        SpeedIndicator_value.text = SpinSpeed.ToString("#,0");
+        // GameObject「SpeedIndicator」の「Text」を 変数「SpinSpeed」を3桁","区切りしたものにする

        if(isSpinning)
        {
-           transform.Rotate(Vector3.up, SpinSpeed * Time.deltaTime);
+           SpinCube.transform.Rotate(Vector3.up, SpinSpeed * Time.deltaTime);
// 複数のオブジェクトがかかわってくるようになったので明示した方がよいかなと思って変更した
        }
    }

    public void StartStop()
    {
        isSpinning = !isSpinning; // isSpinningの値を反転させる
    }
    
    public void SpeedUp()
    {
        SpinSpeed = SpinSpeed + 100;
    }

    public void SpeedDown()
    {
        SpinSpeed = SpinSpeed - 100;
        if(SpinSpeed < 0)
            SpinSpeed = 0;
    }
}
```
これでキューブの回転速度を表示できます!
Step2-2と同様にSimulatorを立ち上げると…
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/427576/6226bb1e-6145-dc9f-3b3b-7e082bd9462c.png)
確かに回転速度が表示されています!

参考URL:

https://qiita.com/gatosyocora/items/cc13445bfb18d55077c4

https://note.com/42space/n/n93f7a762e563#drUU1

https://qiita.com/NT1123/items/cbd114854b39facbd5fb

https://www.midnightunity.net/csharp-tostring-format/

# Step.9 ダウンロードした3Dモデルを使ってみる
キューブを回すのも飽きてきました。
のでここは方向転換して、ハートを拡大・縮小してドキドキしているようにみせます。
が、冒頭の記載通り、筆者にはハートの3Dモデルですら作ることができません。Blenderは敵。
なので、ぼくはboothに頼る!
「ハート 3Dモデル」で検索するとちょうどよいものが。

https://booth.pm/ja/items/1224897

1. 無料ダウンロード押して落としたzipファイルを解凍します。
2. 中身の~.unitypackageをUnityのProjectのAssetsにドラッグアンドドロップします。
3. Import Unity Packageウィンドウが出てくるので、右下の「import」ボタンをクリック
4. Assets内と上のScene画面にimportしたハートが出てきます。

これをドキドキさせるようにします。
1. まずいらなくなったキューブを選択 > Delete キーでサヨナラします。
2. Step.4と同様にして、Hierarchy下のheartとSphereに下記のU# Scriptを作成・アタッチします。
    ```c#:HeartにアタッチするU# Script
    using UdonSharp;
    using UnityEngine;
    using VRC.SDKBase;
    using VRC.Udon;
    using UnityEngine.UI;
    
    public class beatingHeart : UdonSharpBehaviour
    {
        private bool IsBeating = true;
        // isBeatingはtrue or false な変数(bool)だよ。初期値はTrueだよ。ほかのプログラムからも読んだり書いたりできないよ(private)
        private float BeatingRate = (float)10.0;
        // isSpinningは小数点を取れる数だよ。初期値は10だよ。ほかのプログラムからも読んだり書いたりできないよ(private)
    
        private float deg;  // 角度。鼓動に用いる
        private float amp;  // ハートをどれくらい大きくするか
    
        public GameObject BeatRateIndicator;
        public GameObject Heart;
    
        public void Update()
        {
            Text BeatRateIndicator_value = BeatRateIndicator.GetComponent<Text>();
            BeatRateIndicator_value.text = BeatingRate.ToString("F");   // BeatingRateをテキストボックスに反映
    
            if(IsBeating)
            {
                amp = 20 * (float)Mathf.Sin(deg * BeatingRate); // (float)を付けて型変換をする
                if(amp < 0)
                {
                    // 鼓動っぽく見せるためにsinが負の値のところは動かさない
                    amp = 0;
                }
                Heart.transform.localScale = new Vector3(100 + amp, 100 + amp, 100 + amp); // 100+ampに拡大・縮小
                // transform. : 何かモノを動かすよ
                // localScale : 大きさを変えるよ
                // new Vector3(a,b,c) : x,y,z方向にa,b,c増減させるよ
    
                deg = deg + Time.deltaTime;
            }
    
        }
        
        public void StartStop()
        {
            IsBeating = !IsBeating; // isBeatingの値を反転させる
        }
        
        public void RateUp()
        {
            BeatingRate = BeatingRate + (float)1.0;
        }
    
        public void RateDown()
        {
            BeatingRate = BeatingRate - (float)1.0;
            if(BeatingRate < 0)
                BeatingRate = 0;
        }
    }
    ```
    
    ```c#:Sphere(インタラクトで一時停止)にアタッチするU# Script
    using UdonSharp;
    using UnityEngine;
    using VRC.SDKBase;
    using VRC.Udon;
    
    public class Spin_Operation : UdonSharpBehaviour
    {
        public GameObject Heart;
        UdonBehaviour udon;
    
        private void Start()
        {
            udon = (UdonBehaviour)Heart.GetComponent(typeof(UdonBehaviour));
        }
    
        public override void Interact()
        // この下の{ }内にインタラクトされたときに行いたい処理を書く
        {
            udon.SendCustomEvent("StartStop");
            // "StartStop"を実行してね、という
        }
    }
    ```
    ```c#:Sphere(インタラクトで鼓動が遅くなる)にアタッチするU# Script
    using UdonSharp;
    using UnityEngine;
    using VRC.SDKBase;
    using VRC.Udon;
    
    public class Spin_SpeedDown : UdonSharpBehaviour
    {
        public GameObject Heart;
        UdonBehaviour udon;
    
        private void Start()
        {
            udon = (UdonBehaviour)Heart.GetComponent(typeof(UdonBehaviour));
        }
    
        public override void Interact()
        {
            udon.SendCustomEvent("RateDown");
        }
    }
    ```
    ```c#:Sphere(インタラクトで鼓動が速くなる)にアタッチするU# Script
    using UdonSharp;
    using UnityEngine;
    using VRC.SDKBase;
    using VRC.Udon;
    
    public class Spin_SpeedUp : UdonSharpBehaviour
    {
        public GameObject Heart;
        UdonBehaviour udon;
    
        private void Start()
        {
            udon = (UdonBehaviour)Heart.GetComponent(typeof(UdonBehaviour));
        }
    
        public override void Interact()
        {
            udon.SendCustomEvent("RateUp");
        }
    }
    ```
3. Step.6-3と同様にGemaObjectをあてはめる。

ついでに位置を整えてっと。

https://twitter.com/tonomotohide/status/1697636381999395302

(ここにきてようやく動画のキャプチャの取り方を知った)

参考URL:

https://qiita.com/Nekomasu/items/f526b9392fd16f2bd243

https://tech.pjin.jp/blog/2021/12/23/unity-script-transform/

# Step10. 複数人でインスタンスに入った時の挙動を確認する
それなりに形になってきたので、複数人でインスタンスに入った時の挙動を確認したくなりますよね。
アップロードして、フレンドさんと一緒にjoinしましょう!

...それができれば苦労はしない。
コミュ力がないので、もっとちゃんとワールドらしきものが出来た後ならともかく、この状態で「見てー!」って気軽にお願いできねーんですよ。
そんな人でも簡単に複数人で入った時の挙動が確認できるんだ。そう、VRChatならね。

1. メニューバーの「VRChat SDK」> Show Control Panel とクリック
2. VRChat SDKウィンドウが立ち上がる。初回だとID/PWを聞かれるので、VRChatで使っているものを入力
3. SettingsタブのDeveloperのShow Extra Options on build page and account pageにチェックを入れる
4. Builderタブに移って、Local Testingの「Build ＆Test」ボタンを押す
5. (パソコンの性能にってはちょっと時間かかるので待つと)VRChatの画面が出てきて、インスタンスに入ったときの画面になるので「OK」を押す
6. 1人目がインスタンスに入れます。
マウスで視点変更、W/A/S/Dで前後左右移動、スペースでジャンプ、Escでメニューが開きます。
7. 2,3人目を追加します。そのままでは入力　をVRChatの画面に取られてしまうので、Alt + TabなどでUnity画面に戻ります。
8. 4.と同画面で追加したい人数をNumber of Clientsに入れてから、「Last Build」ボタンを押します。
9. 5.と同様になって、インスタンスに入れます。

どんどん追加してみた結果! まめひなたちゃんがいっぱい!!
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/427576/fbcc8c84-6366-7d2e-a0d7-b7319cbb7d06.png)
9人目を入れようとしたらエラーになったので、多分8人が限界っぽいです。

# Step11. イベントを同期する
ということでコミュ力不足なのでStep10の方法で複数人で入った時の挙動を確認しました。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/427576/05164b11-3941-3eff-9025-cf9bd0e8cad8.png)
同じインスタンスなのに鼓動の速さが違う!? どうして!?

調べてみると、["VRChatのワールドは「何もしなければ」すべてローカルで動作"](https://note.com/noritama_vrc/n/nada045c1b053)するらしいです。
なるほど、ローカルのキューブを持ち上げて座標が変わっても他の人にはそのままの位置にあるように見えるのと同じか。
どうすればグローバルにできるかというと同期というのをとらなければいけないらしい。
ということで、参考URLに従って`SendCustomEvent`を`SendCustomNetworkEvent`に書き換えます。
具体的には下記。
```diff_c_sharp:Sphere(インタラクトで鼓動が遅くなる)にアタッチするU# Script
(記載省略)

public class Spin_SpeedDown : UdonSharpBehaviour
{
    (記載省略)

    public override void Interact()
    {
-        udon.SendCustomEvent("RateDown");
+        udon.SendCustomNetworkEvent(VRC.Udon.Common.Interfaces.NetworkEventTarget.All, "RateDown");
    }
}
```
残りの二つの球体にアタッチしているスクリプトも同様に書き換えます。
その結果。

https://twitter.com/tonomotohide/status/1697669767761744292


参考URL:

https://note.com/noritama_vrc/n/nada045c1b053

# Step12. 値を同期する

【To be continue】

# 番外編 エラーが出た時の対処
## error CS1002: ; expected
Udon#はif文とかpublic class～みたいな下に{ }が付く文以外では末尾に; が必要だけど不足しているよ、なエラー。
必要なところに ; がついていない or { } が抜けている etcなので確認して、付ける。
(Pythonだと逆にif文の末尾に : 付けなきゃいけないからややこしい…)

## CS0246: The type or namespace name 'Text' could not be found (are you missing a using directive or an assembly reference?)

`using UnityEngine.UI;`を冒頭に入れ忘れてる可能性が高い。

https://monaski.hatenablog.com/entry/2015/11/03/125957

## error CS0266: Cannot implicitly convert type 'double' to 'float'. An explicit conversion exists (are you missing a cast?)

型変換が必要。
```diff_c_sharp:CubeにアタッチするU# Script
private float BeatingRate = 5;
- BeatingRate = BeatingRate + 0.1;
+ BeatingRate = BeatingRate + (float)0.1;
```

https://learn.microsoft.com/ja-jp/dotnet/csharp/language-reference/compiler-messages/cs0266