9
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

お題は不問!Qiita Engineer Festa 2023で記事投稿!

Unityで音ゲーを作ろう(ノーツ・譜面(プログラミング)編)

Last updated at Posted at 2023-06-29

初めに

今回はノーツを動かすプログラムや、譜面を作成(変換)するプログラムを作ろうと思います。
プログラムをたくさん小分けにしてしまい、少しめんどくさくなってしまっていますが、そこは許してください...

記事一覧

ボタンを押したときの動作の作成

まずはボタンを押したときにステージが光る(?)動作を作っていきたいと思います。今回はdfjkキーを使用しようと思います。
1ライン目から4ライン目までほぼ同じプログラムなので1つにまとめても良かったのですが、動作が重くなると嫌&多少楽になるのでラインごとに分けます。(多分こっちの方が重くなりそう...)

まずAssetsフォルダーの中に新しくScriptsフォルダーを作成してください。また、その中にClickというフォルダーを作成してください。

1ライン目

Clickフォルダー内で新しくC#スクリプトを作成してください。名前はCL1にします。
作成出来たらスクリプトを開き、下のプログラムを入力してください。

CL1.cs
using UnityEngine;

public class CL1 : MonoBehaviour
{
    // Start is called before the first frame update
    void Start()
    {
        
    }
    // Update is called once per frame
    void Update()
    {
        if (Input.GetKey("d"))
        {
            this.transform.localScale = new Vector3(0.9625f, 0.000001f, 5.67f);
            this.transform.localScale = this.transform.localScale;
        }
        else
        {
            transform.localScale = Vector3.zero;
        }
    }
}

軽く解説をしていきます。
1行目ではusingでUnityEngineを使用できるようにしています。
13行目から21行目ではキーの入力を検知しています。dキーを押していたら前パートで作ったタップ時のエフェクトのオブジェクトが15行目に書いてある大きさになります。押していないときはVector3.zeroとなり、見えなくなります。
これを4つ作成します。

作成し、保存し終わったらProjectタブ内にあるこのスクリプト(CL1.cs)をHierarchyタブのCL1にドラッグアンドドロップしてアタッチしてください。

2ライン目

CL2.cs
using UnityEngine;

public class CL2 : MonoBehaviour
{
    // Start is called before the first frame update
    void Start()
    {
        
    }
    // Update is called once per frame
    void Update()
    {
        if (Input.GetKey("f"))
        {
            this.transform.localScale = new Vector3(0.9625f, 0.000001f, 5.67f);
            this.transform.localScale = this.transform.localScale;
        }
        else
        {
            transform.localScale = Vector3.zero;
        }
    }
}

これも同様にCL2にドラッグアンドドロップしてください。

3ライン目

CL3.cs
using UnityEngine;

public class CL3 : MonoBehaviour
{
    // Start is called before the first frame update
    void Start()
    {
        
    }
    // Update is called once per frame
    void Update()
    {
        if (Input.GetKey("j"))
        {
            this.transform.localScale = new Vector3(0.9625f, 0.000001f, 5.67f);
            this.transform.localScale = this.transform.localScale;
        }
        else
        {
            transform.localScale = Vector3.zero;
        }
    }
}

4ライン目

CL4.cs
using UnityEngine;

public class CL4 : MonoBehaviour
{
    // Start is called before the first frame update
    void Start()
    {
        
    }
    // Update is called once per frame
    void Update()
    {
        if (Input.GetKey("k"))
        {
            this.transform.localScale = new Vector3(0.9625f, 0.000001f, 5.67f);
            this.transform.localScale = this.transform.localScale;
        }
        else
        {
            transform.localScale = Vector3.zero;
        }
    }
}

ノーツを動かそう

ついにノーツを動かすところまでやってきました。
ノーツの動かし方としては、ノーツの複製をしてからそれぞれのノーツに動いてもらう感じにします。
そのため、複製(duplication)のプログラムと動かすプログラムの2種類を作成します。
使用するJsonファイル(譜面データ)は1パート目にあるTera-IO.jsonです。これを/Assets/Resources/Chartsに保存してください。

jsonファイルを読み込む準備

Jsonファイルを読み込むためにはパッケージが必要なのでインストールします。
画面上部のメニューからWindow>Package Managerの順にクリックしてください。
パッケージマネージャーが開いたら+ボタンを押し、Add package form git URL...をクリックし、com.unity.nuget.newtonsoft-jsonを入力してください。これでAddを押すと追加されているはずです。

コンフィグ

多分気が付いていると思いますが(?)それぞれのプログラムにjsonファイル名を割り当てていたら大変なので一括で管理できるコードを書きます。Scriptsフォルダー直下に置いておきましょう。

adata.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class adata : MonoBehaviour
{
    public static string chart = "Tera-IO";
    public static float speed = 4.0f;
}

複製

複製はjsonファイルを読み込み、for分で回して複製します。
Scriptsフォルダー内にDuplicationというフォルダーを新しく作成してください。複製のプログラムはここに書きます。

1ライン目

DL1.cs
using System.Collections.Generic;
using UnityEngine;
using Newtonsoft.Json.Linq;

public class DL1 : MonoBehaviour
{
    public GameObject prefab;
    public GameObject prefab_long;

    // Start is called before the first frame update
    void Start()
    {
        string chart = adata.chart;
        string loadjson = Resources.Load<TextAsset>("Charts/" + chart).ToString();

        JObject jsonObj = JObject.Parse(loadjson);
        int notecnt = int.Parse(jsonObj["chartdata"]["L1"]["L1data"]["notes"] + "");
        var objs = new List<GameObject>();
        for (int i = 1; i <= notecnt; i++)
        {
            if (jsonObj["chartdata"]["L1"][i + ""]["type"].ToString() == "tap")
            {
                objs.Add((GameObject)Instantiate(prefab));
                objs[i - 1].name = "L1_" + i;
            }
            else if (jsonObj["chartdata"]["L1"][i + ""]["type"].ToString() == "long")
            {
                objs.Add((GameObject)Instantiate(prefab_long));
                objs[i - 1].name = "L1-long_" + i;
            }
        }
    }

    // Update is called once per frame
    void Update()
    {
        // None
    }
}

解説

1行目から3行目で使用するライブラリなどを宣言しています。
7,8行目ではタップノーツ・ロングノーツのプレハブを代入するための変数を宣言しています。代入方法は後程説明します。
13行目でコンフィグファイルからチャートファイル名を読み込んでいます。
14行目でjsonファイルからデータを読み込みます。
16,17行目で必要な情報だけjsonファイルから読みだしています。
18行目ではこれから作成するGameObjectのリストを新しく作成しています。
19~31行目でノーツの作成をしています。
for分内部のif文で作るノーツの種類を選択し、作成し、リストに追加し、名前の設定をしています。この時の名前は、L1_+chartdata.L1.(数値)の数値の部分です。

2ライン目

DL2.cs
using System.Collections.Generic;
using UnityEngine;
using Newtonsoft.Json.Linq;

public class DL2 : MonoBehaviour
{
    public GameObject prefab;
    public GameObject prefab_long;

    // Start is called before the first frame update
    void Start()
    {
        string chart = adata.chart;
        string loadjson = Resources.Load<TextAsset>("Charts/" + chart).ToString();

        JObject jsonObj = JObject.Parse(loadjson);
        int notecnt = int.Parse(jsonObj["chartdata"]["L2"]["L2data"]["notes"] + "");
        var objs = new List<GameObject>();
        for (int i = 1; i <= notecnt; i++)
        {
            if (jsonObj["chartdata"]["L2"][i + ""]["type"].ToString() == "tap")
            {
                objs.Add((GameObject)Instantiate(prefab));
                objs[i - 1].name = "L2_" + i;
            }
            else if (jsonObj["chartdata"]["L2"][i + ""]["type"].ToString() == "long")
            {
                objs.Add((GameObject)Instantiate(prefab_long));
                objs[i - 1].name = "L2-long_" + i;
            }
        }
    }

    // Update is called once per frame
    void Update()
    {
        // None
    }
}

3ライン目

DL3.cs
using System.Collections.Generic;
using UnityEngine;
using Newtonsoft.Json.Linq;

public class DL3 : MonoBehaviour
{
    public GameObject prefab;
    public GameObject prefab_long;

    // Start is called before the first frame update
    void Start()
    {
        string chart = adata.chart;
        string loadjson = Resources.Load<TextAsset>("Charts/" + chart).ToString();

        JObject jsonObj = JObject.Parse(loadjson);
        int notecnt = int.Parse(jsonObj["chartdata"]["L3"]["L3data"]["notes"] + "");
        var objs = new List<GameObject>();
        for (int i = 1; i <= notecnt; i++)
        {
            if (jsonObj["chartdata"]["L3"][i + ""]["type"].ToString() == "tap")
            {
                objs.Add((GameObject)Instantiate(prefab));
                objs[i - 1].name = "L3_" + i;
            }
            else if (jsonObj["chartdata"]["L3"][i + ""]["type"].ToString() == "long")
            {
                objs.Add((GameObject)Instantiate(prefab_long));
                objs[i - 1].name = "L3-long_" + i;
            }
        }
    }

    // Update is called once per frame
    void Update()
    {
        // None
    }
}

4ライン目

DL2.cs
using System.Collections.Generic;
using UnityEngine;
using Newtonsoft.Json.Linq;

public class DL4 : MonoBehaviour
{
    public GameObject prefab;
    public GameObject prefab_long;

    // Start is called before the first frame update
    void Start()
    {
        string chart = adata.chart;
        string loadjson = Resources.Load<TextAsset>("Charts/" + chart).ToString();

        JObject jsonObj = JObject.Parse(loadjson);
        int notecnt = int.Parse(jsonObj["chartdata"]["L4"]["L4data"]["notes"] + "");
        var objs = new List<GameObject>();
        for (int i = 1; i <= notecnt; i++)
        {
            if (jsonObj["chartdata"]["L4"][i + ""]["type"].ToString() == "tap")
            {
                objs.Add((GameObject)Instantiate(prefab));
                objs[i - 1].name = "L4_" + i;
            }
            else if (jsonObj["chartdata"]["L4"][i + ""]["type"].ToString() == "long")
            {
                objs.Add((GameObject)Instantiate(prefab_long));
                objs[i - 1].name = "L4-long_" + i;
            }
        }
    }

    // Update is called once per frame
    void Update()
    {
        // None
    }
}

アタッチ方法

スクリプトをアタッチする方法は前までに紹介しましたが、プレハブなどをアタッチ(代入)する方法は紹介していなかったのでここで紹介します。
言葉で説明すると長ったらしくなるので、gifで説明します...(汗
2023-06-29-08-53-53.gif
このような感じでアタッチすればOKです。

ノーツ移動

ついにノーツの移動に入っていきます。その前に、確認として現在のファイル構成を確認しておきましょう。

tree /F プロジェクトのパス\Assets

E:\KEICH\UNITY\RHYTHMGAME\ASSETS
│  Click.mat
│  Click.mat.meta
│  Long L1.prefab
│  Long L1.prefab.meta
│  Long L2.prefab
│  Long L2.prefab.meta
│  Long L3.prefab
│  Long L3.prefab.meta
│  Long L4.prefab
│  Long L4.prefab.meta
│  Long.mat
│  Long.mat.meta
│  Resources.meta
│  Scenes.meta
│  Scripts.meta
│  Stage.mat
│  Stage.mat.meta
│  Tap L1.prefab
│  Tap L1.prefab.meta
│  Tap L2.prefab
│  Tap L2.prefab.meta
│  Tap L3.prefab
│  Tap L3.prefab.meta
│  Tap L4.prefab
│  Tap L4.prefab.meta
│  Tap.mat
│  Tap.mat.meta
│
├─Resources
│  │  Charts.meta
│  │
│  └─Charts
│          Tera-IO.json
│          Tera-IO.json.meta
│
├─Scenes
│      SampleScene.unity
│      SampleScene.unity.meta
│
└─Scripts
    │  adata.cs
    │  adata.cs.meta
    │  Click.meta
    │  Duplication.meta
    │
    ├─Click
    │      CL1.cs
    │      CL1.cs.meta
    │      CL2.cs
    │      CL2.cs.meta
    │      CL3.cs
    │      CL3.cs.meta
    │      CL4.cs
    │      CL4.cs.meta
    │
    └─Duplication
            DL1.cs
            DL1.cs.meta
            DL2.cs
            DL2.cs.meta
            DL3.cs
            DL3.cs.meta
            DL4.cs
            DL4.cs.meta

のようになっていたらOKです。
確認し終えたら、Scripts内に新しくMoveというフォルダーを作成しておいてください。ここに移動させるスクリプトを保存します。
それでは、ノーツの移動処理を作っていく前に、ノーツをどうやって移動させるかについて軽く説明したいと思います。

移動方法

移動方法は、処理毎に現在自分がいる(いなければならない)座標を取得して移動させるような感じです。
この方法になる前は、FPSを固定してなんやかんややる、という方法でしたが、FPSは変動があり、ラグが起きてしまうため没になりました。(今ではなんでこんな方法にしようとしたのか自分でも分からないです...)
それでは作っていきましょう。

1ライン目

L1.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using System;
using System.IO;
using Newtonsoft.Json.Linq;
using System.Diagnostics;
using Debug = UnityEngine.Debug;
using UnityEngine.UIElements;
using System.Threading.Tasks;

public class L1 : MonoBehaviour
{
    public string[] data;
    public float arrsec;
    public float speed;
    public int id;
    public string json;
    public JObject jsonObj;
    public float game_time;
    public JObject cd;
    public float now;
    private float endsec;
    private float length;
    private float tmptime;
    private float offset;
    private float time_remaining;

    private Vector3 startPoint;
    private float sPoint;
    private Vector3 endPoint;
    private float ePoint;
    private bool isLongNoteActive = false;

    float whereiam()
    {
        time_remaining = (game_time - arrsec) * -1.0f;
        now = (time_remaining * speed) - 9.51f;
        return now;
    }

    // Start is called before the first frame update
    void Start()
    {
        string chart = adata.chart;
        game_time = -3.0f;
        data = name.Split('_');
        id = int.Parse(data[1]);
        string loadjson = Resources.Load<TextAsset>("Charts/" + chart).ToString();
        JObject jsonObj = JObject.Parse(loadjson);
        cd = JObject.Parse(jsonObj["chartdata"]["L1"][id.ToString()] + "");
        arrsec = float.Parse(cd["time"] + "");
        offset = float.Parse(jsonObj["maindata"]["offset"] + "") / 1000;
        if (cd["type"] + "" == "long")
        {
            endsec = float.Parse(cd["endtime"] + "");
            length = adata.speed * 10.0f * float.Parse(cd["speed"] + "") * (endsec - arrsec);
            this.transform.localScale = new Vector3(0.7f, 0.01f, length);
            startPoint = transform.position;
            startPoint.z = -length / 2;
            endPoint = startPoint + new Vector3(0, 0, length);
            isLongNoteActive = true;
            Transform initial_Transform = this.transform;
            Vector3 initial_pos = initial_Transform.position;
            initial_pos.z = whereiam() + (length / 2.0f);
            initial_Transform.position = initial_pos;
        }
        else if (cd["type"] + "" == "tap")
        {
            Transform initial_Transform = this.transform;
            Vector3 initial_pos = initial_Transform.position;
            initial_pos.z = whereiam();
            initial_Transform.position = initial_pos;
        }
        speed = float.Parse(cd["speed"] + "") * (adata.speed * 10.0f);

        Transform myTransform = this.transform;
        Vector3 pos = myTransform.position;
        pos.z = -4;
        myTransform.position = pos;
    }

    // Update is called once per frame
    void Update()
    {
        if (tmptime >= 5 - offset)
        {
            game_time += Time.deltaTime;
            if (!isLongNoteActive)
            {
                Transform myTransform = this.transform;
                Vector3 pos = myTransform.position;
                pos.z = whereiam();
                myTransform.position = pos;
            }
            if (isLongNoteActive)
            {
                Transform myTransform = this.transform;
                Vector3 pos = myTransform.position;
                pos.z = whereiam() + (length / 2.0f);
                myTransform.position = pos;
            }
        }
        else
        {
            tmptime += Time.deltaTime;
            Transform myTransform = this.transform;
            Vector3 pos = myTransform.position;
            float timelag = game_time - arrsec;
            pos.z = whereiam();
            myTransform.position = pos;
        }
    }
}

解説

1~10行目はライブラリの使用の宣言
14~33行目はプログラム内で使用する変数の初期化。
35~40行目はノーツがいる(べき)場所を取得するクラス(?)
43行目のstart関数内は到着時間の取得、ロングノーツは長さの設定、初期位置の設定を行っています。
84行目のupdate関数でノーツの移動処理を実装しています。

2ライン目

L2.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using System;
using System.IO;
using Newtonsoft.Json.Linq;
using System.Diagnostics;
using Debug = UnityEngine.Debug;
using UnityEngine.UIElements;
using System.Threading.Tasks;

public class L2 : MonoBehaviour
{
    public string[] data;
    public float arrsec;
    public float speed;
    public int id;
    public string json;
    public JObject jsonObj;
    public float game_time;
    public JObject cd;
    public float now;
    private float endsec;
    private float length;
    private float tmptime;
    private float offset;
    private float time_remaining;

    private Vector3 startPoint;
    private float sPoint;
    private Vector3 endPoint;
    private float ePoint;
    private bool isLongNoteActive = false;

    float whereiam()
    {
        time_remaining = (game_time - arrsec) * -1.0f;
        now = (time_remaining * speed) - 9.51f;
        return now;
    }

    // Start is called before the first frame update
    void Start()
    {
        string chart = adata.chart;
        game_time = -3.0f;
        data = name.Split('_');
        id = int.Parse(data[1]);
        string loadjson = Resources.Load<TextAsset>("Charts/" + chart).ToString();
        JObject jsonObj = JObject.Parse(loadjson);
        cd = JObject.Parse(jsonObj["chartdata"]["L2"][id.ToString()] + "");
        arrsec = float.Parse(cd["time"] + "");
        offset = float.Parse(jsonObj["maindata"]["offset"] + "") / 1000;
        if (cd["type"] + "" == "long")
        {
            endsec = float.Parse(cd["endtime"] + "");
            length = adata.speed * 10.0f * float.Parse(cd["speed"] + "") * (endsec - arrsec);
            this.transform.localScale = new Vector3(0.7f, 0.01f, length);
            startPoint = transform.position;
            startPoint.z = -length / 2;
            endPoint = startPoint + new Vector3(0, 0, length);
            isLongNoteActive = true;
            Transform initial_Transform = this.transform;
            Vector3 initial_pos = initial_Transform.position;
            initial_pos.z = whereiam() + (length / 2.0f);
            initial_Transform.position = initial_pos;
        }
        else if (cd["type"] + "" == "tap")
        {
            Transform initial_Transform = this.transform;
            Vector3 initial_pos = initial_Transform.position;
            initial_pos.z = whereiam();
            initial_Transform.position = initial_pos;
        }
        speed = float.Parse(cd["speed"] + "") * (adata.speed * 10.0f);

        Transform myTransform = this.transform;
        Vector3 pos = myTransform.position;
        pos.z = -4;
        myTransform.position = pos;
    }

    // Update is called once per frame
    void Update()
    {
        if (tmptime >= 5 - offset)
        {
            game_time += Time.deltaTime;
            if (!isLongNoteActive)
            {
                Transform myTransform = this.transform;
                Vector3 pos = myTransform.position;
                pos.z = whereiam();
                myTransform.position = pos;
            }
            if (isLongNoteActive)
            {
                Transform myTransform = this.transform;
                Vector3 pos = myTransform.position;
                pos.z = whereiam() + (length / 2.0f);
                myTransform.position = pos;
            }
        }
        else
        {
            tmptime += Time.deltaTime;
            Transform myTransform = this.transform;
            Vector3 pos = myTransform.position;
            float timelag = game_time - arrsec;
            pos.z = whereiam();
            myTransform.position = pos;
        }
    }
}

3ライン目

L3.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using System;
using System.IO;
using Newtonsoft.Json.Linq;
using System.Diagnostics;
using Debug = UnityEngine.Debug;
using UnityEngine.UIElements;
using System.Threading.Tasks;

public class L3 : MonoBehaviour
{
    public string[] data;
    public float arrsec;
    public float speed;
    public int id;
    public string json;
    public JObject jsonObj;
    public float game_time;
    public JObject cd;
    public float now;
    private float endsec;
    private float length;
    private float tmptime;
    private float offset;
    private float time_remaining;

    private Vector3 startPoint;
    private float sPoint;
    private Vector3 endPoint;
    private float ePoint;
    private bool isLongNoteActive = false;

    float whereiam()
    {
        time_remaining = (game_time - arrsec) * -1.0f;
        now = (time_remaining * speed) - 9.51f;
        return now;
    }

    // Start is called before the first frame update
    void Start()
    {
        string chart = adata.chart;
        game_time = -3.0f;
        data = name.Split('_');
        id = int.Parse(data[1]);
        string loadjson = Resources.Load<TextAsset>("Charts/" + chart).ToString();
        JObject jsonObj = JObject.Parse(loadjson);
        cd = JObject.Parse(jsonObj["chartdata"]["L3"][id.ToString()] + "");
        arrsec = float.Parse(cd["time"] + "");
        offset = float.Parse(jsonObj["maindata"]["offset"] + "") / 1000;
        if (cd["type"] + "" == "long")
        {
            endsec = float.Parse(cd["endtime"] + "");
            length = adata.speed * 10.0f * float.Parse(cd["speed"] + "") * (endsec - arrsec);
            this.transform.localScale = new Vector3(0.7f, 0.01f, length);
            startPoint = transform.position;
            startPoint.z = -length / 2;
            endPoint = startPoint + new Vector3(0, 0, length);
            isLongNoteActive = true;
            Transform initial_Transform = this.transform;
            Vector3 initial_pos = initial_Transform.position;
            initial_pos.z = whereiam() + (length / 2.0f);
            initial_Transform.position = initial_pos;
        }
        else if (cd["type"] + "" == "tap")
        {
            Transform initial_Transform = this.transform;
            Vector3 initial_pos = initial_Transform.position;
            initial_pos.z = whereiam();
            initial_Transform.position = initial_pos;
        }
        speed = float.Parse(cd["speed"] + "") * (adata.speed * 10.0f);

        Transform myTransform = this.transform;
        Vector3 pos = myTransform.position;
        pos.z = -4;
        myTransform.position = pos;
    }

    // Update is called once per frame
    void Update()
    {
        if (tmptime >= 5 - offset)
        {
            game_time += Time.deltaTime;
            if (!isLongNoteActive)
            {
                Transform myTransform = this.transform;
                Vector3 pos = myTransform.position;
                pos.z = whereiam();
                myTransform.position = pos;
            }
            if (isLongNoteActive)
            {
                Transform myTransform = this.transform;
                Vector3 pos = myTransform.position;
                pos.z = whereiam() + (length / 2.0f);
                myTransform.position = pos;
            }
        }
        else
        {
            tmptime += Time.deltaTime;
            Transform myTransform = this.transform;
            Vector3 pos = myTransform.position;
            float timelag = game_time - arrsec;
            pos.z = whereiam();
            myTransform.position = pos;
        }
    }
}

4ライン目

L4.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using System;
using System.IO;
using Newtonsoft.Json.Linq;
using System.Diagnostics;
using Debug = UnityEngine.Debug;
using UnityEngine.UIElements;
using System.Threading.Tasks;

public class L4 : MonoBehaviour
{
    public string[] data;
    public float arrsec;
    public float speed;
    public int id;
    public string json;
    public JObject jsonObj;
    public float game_time;
    public JObject cd;
    public float now;
    private float endsec;
    private float length;
    private float tmptime;
    private float offset;
    private float time_remaining;

    private Vector3 startPoint;
    private float sPoint;
    private Vector3 endPoint;
    private float ePoint;
    private bool isLongNoteActive = false;

    float whereiam()
    {
        time_remaining = (game_time - arrsec) * -1.0f;
        now = (time_remaining * speed) - 9.51f;
        return now;
    }

    // Start is called before the first frame update
    void Start()
    {
        string chart = adata.chart;
        game_time = -3.0f;
        data = name.Split('_');
        id = int.Parse(data[1]);
        string loadjson = Resources.Load<TextAsset>("Charts/" + chart).ToString();
        JObject jsonObj = JObject.Parse(loadjson);
        cd = JObject.Parse(jsonObj["chartdata"]["L4"][id.ToString()] + "");
        arrsec = float.Parse(cd["time"] + "");
        offset = float.Parse(jsonObj["maindata"]["offset"] + "") / 1000;
        if (cd["type"] + "" == "long")
        {
            endsec = float.Parse(cd["endtime"] + "");
            length = adata.speed * 10.0f * float.Parse(cd["speed"] + "") * (endsec - arrsec);
            this.transform.localScale = new Vector3(0.7f, 0.01f, length);
            startPoint = transform.position;
            startPoint.z = -length / 2;
            endPoint = startPoint + new Vector3(0, 0, length);
            isLongNoteActive = true;
            Transform initial_Transform = this.transform;
            Vector3 initial_pos = initial_Transform.position;
            initial_pos.z = whereiam() + (length / 2.0f);
            initial_Transform.position = initial_pos;
        }
        else if (cd["type"] + "" == "tap")
        {
            Transform initial_Transform = this.transform;
            Vector3 initial_pos = initial_Transform.position;
            initial_pos.z = whereiam();
            initial_Transform.position = initial_pos;
        }
        speed = float.Parse(cd["speed"] + "") * (adata.speed * 10.0f);

        Transform myTransform = this.transform;
        Vector3 pos = myTransform.position;
        pos.z = -4;
        myTransform.position = pos;
    }

    // Update is called once per frame
    void Update()
    {
        if (tmptime >= 5 - offset)
        {
            game_time += Time.deltaTime;
            if (!isLongNoteActive)
            {
                Transform myTransform = this.transform;
                Vector3 pos = myTransform.position;
                pos.z = whereiam();
                myTransform.position = pos;
            }
            if (isLongNoteActive)
            {
                Transform myTransform = this.transform;
                Vector3 pos = myTransform.position;
                pos.z = whereiam() + (length / 2.0f);
                myTransform.position = pos;
            }
        }
        else
        {
            tmptime += Time.deltaTime;
            Transform myTransform = this.transform;
            Vector3 pos = myTransform.position;
            float timelag = game_time - arrsec;
            pos.z = whereiam();
            myTransform.position = pos;
        }
    }
}

アタッチ方法

このプログラムを普通にアタッチするには少しコツのような何かが必要なので、またgifを載せておきます。
2023-06-29-12-52-45.gif

理由はわかりませんが、タップノーツのプレハブにアタッチしたらHierarchyタブにあるタップノーツにも適用されてしまうので、Hierarchyタブのタップノーツをクリックしスクリプトを無効化する必要があります。

動かしてみよう

さて、ついに動かします。Playボタンを押しプログラムを走らせてください。
下のようになったら成功です!
2023-06-29-13-04-16.gif

譜面作成

譜面の作成はsetchiさんが作成したNoteEditorを使用します。
使用方法は省きます。

注意
このツールではロングノーツを斜めにも伸ばせますが、対応していないため、斜めには伸ばさないようにしてください。(つまり直線のみ)

変換ツール

変換ツールはPythonで書きます。コピペで保存してください。

convert.py
import json
import sys
from time import sleep
import os

def convert_json(json_data):
    # 変換後のデータを格納する辞書
    converted_data = {
        "maindata": {
            "music": str(json_data["name"]),
            "bpm": json_data["BPM"],
            "offset": json_data["offset"]
        },
        "chartdata": {}
    }
    
    # ノーツの情報を変換して格納
    for note in json_data["notes"]:
        # blockの値を文字列化
        block = str(note["block"])
        
        # ブロックごとのデータがなければ初期化
        if block not in converted_data["chartdata"]:
            converted_data["chartdata"][block] = {
                block + "data": {
                    "notes": 0
                }
            }
        
        # ブロックのノーツ数を更新
        converted_data["chartdata"][block][block + "data"]["notes"] += 1
        # ノーツの種類に応じてデータを追加
        if note["type"] == 1:  # タップノーツ
            converted_data["chartdata"][block][str(converted_data["chartdata"][block][block + "data"]["notes"])] = {
                "speed": 0.2,
                "time": 60 / json_data["BPM"] / note["LPB"] * note["num"],
                "type": "tap"
            }
        elif note["type"] == 2:  # ロングノーツ
            converted_data["chartdata"][block][str(converted_data["chartdata"][block][block + "data"]["notes"])] = {
                "speed": 0.2,
                "time": 60 / json_data["BPM"] / note["LPB"] * note["num"],
                "endtime": 60 / json_data["BPM"] / note["notes"][0]["LPB"] * note["notes"][0]["num"],
                "type": "long"
            }
    
    return json.dumps(converted_data, indent=2)

def convert_json_2(json_data):
    chartdata = json_data["chartdata"]
    converted_chartdata = {}
    index = 1

    for key, value in chartdata.items():
        new_key = "L" + str(index)
        index += 1
        new_value = value.copy()
        new_value[new_key + "data"] = new_value.pop(key + "data")
        converted_chartdata[new_key] = new_value

    json_data["chartdata"] = converted_chartdata
    return json_data

if len(sys.argv) == 1:
    jsonpath = input("Path of the json file before conversion: ")
    with open(jsonpath, "r", encoding="utf-8-sig") as f:
        json_data = f.read()
        f.close()
else:
    jsonpath = sys.argv[1]
    with open(jsonpath, "r", encoding="utf-8-sig") as f:
        json_data = f.read()
        f.close()

converted_json = json.dumps(convert_json_2(json.loads(str(convert_json(json.loads(str(json_data)))))), indent=2, ensure_ascii=False)
with open("./"+os.path.basename(jsonpath), "w", encoding="utf-8") as g:
    g.write(str(converted_json))
    g.close()

GUIバージョンもお作っているので載せておきます。

convert-gui.py
import json
import sys
import os
from time import sleep
import random
import tkinter as tk
from tkinter import messagebox
from tkinter import filedialog
from tkinter import ttk
import shutil

filename = ""

def set_chart():
    global filename
    global fn
    global convert_btn
    filename = filedialog.askopenfilename(initialdir="/", title="Select file", filetypes=(("json files", "*.json"), ("all files", "*.*")))
    fn.config(text=f"{filename}が選択されています。")
    convert_btn.config(state=tk.NORMAL)

def process_data():
    global converted_data
    global l1_notes
    global l2_notes
    global l3_notes
    global l4_notes
    global filename
    global fn
    global convert_btn

    try:
        try:
            print("Load in UTF-8-sig...")
            with open(filename, "r", encoding="utf-8-sig") as f:
                data = json.load(f)
                print("Success for load in UTF-8-sig.")
        except UnicodeDecodeError:
            print("Error.")
            try:
                print("Load in UTF-8...")
                with open(filename, "r", encoding="utf-8") as f:
                    data = json.load(f)
                    print("Success for load in UTF-8.")
            except UnicodeDecodeError:
                print("Error.")
                try:
                    print("Load in Shift_JIS...")
                    with open(filename, "r", encoding="Shift_JIS") as f:
                        data = json.load(f)
                        print("Success for load in Shift_JIS.")
                except UnicodeDecodeError:
                    print("Error: UnicodeDecodeError")
                    sleep(5)
                    sys.exit(1)
    except Exception as e:
        messagebox.showerror(title="エラー", message="ファイル読み込み時にエラーが発生しました。", detail=f"{e}")
        fn.config(text="ファイルを選択してください。")
        convert_btn.config(state=tk.DISABLED)

    l1_notes = 0
    l2_notes = 0
    l3_notes = 0
    l4_notes = 0

    converted_data = ""

    composer = composer_entry.get()
    charter = charter_entry.get()
    level = level_combo.get()

    bpm = data["BPM"]

    converted_data += "{\"maindata\": {\"music\": \"" + data["name"] + "\", \"bpm\": " + str(bpm) + ", \"offset\": " + str(data["offset"]) + ", \"composer\": \"" + composer + "\", \"charter\": \"" + charter + "\", \"level\": \""+str(level)+"\"},\"chartdata\": {"

    try:
        for i in range(4):
            converted_data += "\"L"+str(i+1)+"\": {"
            for x in range(len(data["notes"])):
                #print(i, x)
                #print(data["notes"][x])
                spd = "0.2"
                #spd = random.uniform(-0.35, 0.35)
                if data["notes"][x]["block"] == 0 and i == 0:
                    if data["notes"][x]["type"] == 1:
                        l1_notes += 1
                        converted_data += "\""+str(l1_notes)+"\": { \"speed\": "+str(spd)+", \"time\": "+str(60 / bpm / data["notes"][x]["LPB"] * data["notes"][x]["num"])+", \"type\": \"tap\"},"
                    elif data["notes"][x]["type"] == 2:
                        l1_notes += 1
                        converted_data += "\""+str(l1_notes)+"\": { \"speed\": "+str(spd)+", \"time\": "+str(60 / bpm / data["notes"][x]["LPB"] * data["notes"][x]["num"])+", \"endtime\": "+str(60 / bpm / data["notes"][x]["notes"][0]["LPB"] * data["notes"][x]["notes"][0]["num"])+", \"type\": \"long\"},"
                elif data["notes"][x]["block"] == 1 and i == 1:
                    if data["notes"][x]["type"] == 1:
                        l2_notes += 1
                        converted_data += "\""+str(l2_notes)+"\": { \"speed\": "+str(spd)+", \"time\": "+str(60 / bpm / data["notes"][x]["LPB"] * data["notes"][x]["num"])+", \"type\": \"tap\"},"
                    elif data["notes"][x]["type"] == 2:
                        l2_notes += 1
                        converted_data += "\""+str(l2_notes)+"\": { \"speed\": "+str(spd)+", \"time\": "+str(60 / bpm / data["notes"][x]["LPB"] * data["notes"][x]["num"])+", \"endtime\": "+str(60 / bpm / data["notes"][x]["notes"][0]["LPB"] * data["notes"][x]["notes"][0]["num"])+", \"type\": \"long\"},"
                elif data["notes"][x]["block"] == 2 and i == 2:
                    if data["notes"][x]["type"] == 1:
                        l3_notes += 1
                        converted_data += "\""+str(l3_notes)+"\": { \"speed\": "+str(spd)+", \"time\": "+str(60 / bpm / data["notes"][x]["LPB"] * data["notes"][x]["num"])+", \"type\": \"tap\"},"
                    elif data["notes"][x]["type"] == 2:
                        l3_notes += 1
                        converted_data += "\""+str(l3_notes)+"\": { \"speed\": "+str(spd)+", \"time\": "+str(60 / bpm / data["notes"][x]["LPB"] * data["notes"][x]["num"])+", \"endtime\": "+str(60 / bpm / data["notes"][x]["notes"][0]["LPB"] * data["notes"][x]["notes"][0]["num"])+", \"type\": \"long\"},"
                elif data["notes"][x]["block"] == 3 and i == 3:
                    if data["notes"][x]["type"] == 1:
                        l4_notes += 1
                        converted_data += "\""+str(l4_notes)+"\": { \"speed\": "+str(spd)+", \"time\": "+str(60 / bpm / data["notes"][x]["LPB"] * data["notes"][x]["num"])+", \"type\": \"tap\"},"
                    elif data["notes"][x]["type"] == 2:
                        l4_notes += 1
                        converted_data += "\""+str(l4_notes)+"\": { \"speed\": "+str(spd)+", \"time\": "+str(60 / bpm / data["notes"][x]["LPB"] * data["notes"][x]["num"])+", \"endtime\": "+str(60 / bpm / data["notes"][x]["notes"][0]["LPB"] * data["notes"][x]["notes"][0]["num"])+", \"type\": \"long\"},"
            if i == 3:
                converted_data += "\"L"+str(i+1)+"data\": {\"notes\": "+str(eval("l"+str(i+1)+"_notes"))+"}}"
            else:
                converted_data += "\"L"+str(i+1)+"data\": {\"notes\": "+str(eval("l"+str(i+1)+"_notes"))+"}},"
    except IndexError:
        print("次の箇所でエラーが発生しました。")
        print(f"i={i},x={x},data={data['notes'][x]}")
        fn.config(text="ファイルを選択してください。")
        convert_btn.config(state=tk.DISABLED)
        messagebox.showerror(title="エラー", message="次の個所でエラーが発生しました。", detail=f"i={i},x={x},data={data['notes'][x]}")

    converted_data += "}}"

    shutil.copy(filename, str(os.path.dirname(filename))+str(os.path.splitext(os.path.basename(filename))[0])+" - 変換前.json")

    with open("./" + os.path.basename(filename), "w", encoding="utf-8") as g:
        g.write(json.dumps(json.loads(converted_data), indent=2, ensure_ascii=False))
    messagebox.showinfo(title="変換完了", message="変換が完了しました。", detail="保存場所:\n"+os.getcwd().replace('\\', '/')+"/"+os.path.basename(filename))
    fn.config(text="ファイルを選択してください。")
    convert_btn.config(state=tk.DISABLED)

root = tk.Tk()

composer_label = tk.Label(root, text="コンポーザー名を入力してください。")
composer_label.pack()
composer_entry = tk.Entry(root)
composer_entry.pack()

charter_label = tk.Label(root, text="採譜者(譜面制作者)名を入力してください。")
charter_label.pack()
charter_entry = tk.Entry(root)
charter_entry.pack()

levels = ("1", "2", "3", "4", "5", "6", "7", "7+", "8", "8+", "9", "9+", "10", "10+", "11", "11+", "12", "12+", "13", "13+", "14", "14+", "15") 
level_label = tk.Label(root, text="レベルを選択してください。")
level_label.pack()
level_combo = ttk.Combobox(root, justify="center", values=levels)
level_combo.pack()

fn = tk.Label(root, text="\nファイルを選択してください。")
fn.pack()

setfile = ttk.Button(root, text="ファイルを選択", command=set_chart)
setfile.pack()

rtn = tk.Label(root, text="")
rtn.pack()

convert_btn = ttk.Button(root, text="変換", command=process_data, state=tk.DISABLED)
convert_btn.pack()

root.mainloop()

保存したらNoteEditorで作成したJsonファイルをこのスクリプトファイルにドラッグアンドドロップしてください。
(コマンドプロンプトからpython convert.py [変換前のjsonファイルのパス]でも良いです。)
そうすると、Jsonファイルが生成されるのでそれを/Assets/Resources/Chartsに保存してください。
また、adata.csの変数chart.jsonを省いて書き換え、保存してください。

次回

お疲れ様でした。
次回は判定の実装と音声の再生です。
ただ、私自身がそろそろテスト期間に入るので、次回投稿するのは2~3週間後になると思います。
それではまた次回にお会いしましょう~

9
3
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
9
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?