20210127のC#に関する記事は6件です。

クラス継承の利点とは何か?(5年間考えた)

3日間考え直した版を投稿しました。

コメントいただいた内容をもとにがんばって考え直してみました。

再考するきっかけとなったコメントは当記事本編より参考になるので是非ご参照ください。

継承のメリットとはなんですか?

あなたはこの質問に答えられますか。
この質問は、未経験歓迎の求人面接でいただいた質問です。

当時、苦し紛れに「コード量が減る…?」と回答しました。(苦笑いされました笑)
それから5年、仕事として開発に取り組んできた自分なりの答えを記録します。

※ マサカリ歓迎です

結論

共通点を「抽象化」し、is-a関係をコードで具体的に表現できることです。

上記結論だけでは何のことか、何がメリットなのか、ピンとこないかもしれません。
まずは具体的に見てみましょう。

クラス継承の例

オブジェクト指向のよくある例にならって「犬」で考えてみます。
さて、実はこの「犬」という概念が「抽象」です。

柴犬であれ、ハスキーであれ「鳴く」「走る」といった共通のできることや属性を持ち、
それらの共通点を抽象化した概念が「犬」と言えます。

実際にコードで表現してみましょう。

C#
class Dog
{
    public virtual void Bark() { Console.WriteLine("ワンワン!"); }
    public virtual void Run() { /* 走る処理 */ }
}
class Shibainu : Dog { }
class Husky : Dog { public override void Bark() { Console.WriteLine("bow-wow!"); } }
Dog dog = new Shibainu();
dog.Bark(); // ワンワン!

dog = new Husky();
dog.Bark(); // bow-wow!

抽象化した概念を Dog クラスで表現し、柴犬であれハスキーであれ犬として抽象的に扱えていますね!(同じ dog として扱えています)

……しかし、まだメリットが見えてこないかもしれません。

継承のメリットってこうじゃないの?

  • 親クラスを修正すると子クラスにも波及して、修正箇所が減らせるよ
  • 親クラスに共通関数を定義できるよ
  • 結果的にコード量を削減できるよ

はい。たしかに、これらが「継承のメリット」なのは間違いないと思います。
ですが、これらは「抽象化」の結果、生じうるメリットだと考えています。

実際、無理やり利点のみを享受しようとして継承すると破綻してしまうでしょう。
たとえば、まったく違う「概念」のクラスを「たまたま似ていた処理を共通化させたいから」と継承してしまったら?

先ほどの例でいうと「柴犬」は「走る」という処理が可能でしたが「自動車」も「走ること」自体はできます。だからと言って「プリウス」に「犬」を継承してはいけません。「犬」の特性をすべて持つ車になってしまいます。

当たり前ですが、「Prius is NOT a Dog.」ですよね。
is-a関係が成立するかどうかが、継承で実装するか否かの判断基準の一つです。

そのため「継承のメリット」は「抽象化して表現できること」であり、
見えやすいメリットは「抽象化」に付随して生まれてくると結論づけました。

今、面接で同じ質問をされたら?

ここまで読み進めていただきありがとうございます。(もうちょっとだけ続きます)
今回の記事の結論では、具体的なソースの確認が必要だったので、言葉足らずでしたね。

ですので、同じ質問されたとしたら、以下のような回答をしたいと思います。

継承のメリットとはなんですか?

実装したいビジネスロジックを抽象化してスーパークラスを定義することで関係性を明示でき、ポリモーフィズムを活用できるようになります。
その結果として、保守性や開発効率の向上が見込めることです。

・・・少しはマシな回答になったでしょうか?

継承以外で共通関数はどうやって実現するの?

さて、is-a関係にないオブジェクト間の共通機能を継承で実現することを否定しました。
ではどうやって実現するのが最適か?

私の意見ですが、

  • 複数のクラスで使用したい業務ロジックは、コンポジションによる実現
  • I/O操作やDBのトランザクション制御など、業務外のロジックは utility クラスで実現

するのが最適だと考えています。
コード例をどうぞ。

コンポジション例(ユーザー検索をA画面でもB画面でも利用したい)
interface IUserSearch{
    User GetUser(Guid Id);
}
class UserSearch : IUserSearch
{
    public User GetUser(Guid Id) { return null/*検索結果*/; }
}

class A
{
    public IUserSearch userSearch { get; }
    /* ほかに必要なプロパティやメソッドを定義 */
}

class B
{
    public IUserSearch userSearch { get; }
    /* ほかに必要なプロパティやメソッドを定義 */
}

このように、共通処理(ユーザー検索)を UserSearch クラスにくくりだし、
プロパティにインスタンスを保持することで利用できるようになり、共通化に成功しました。

ほかにも共通化したい処理があれば同じように共通クラスを定義し、プロパティを増やすとよいでしょう。

なお、今回の投稿には関係ありませんが「UserSearch」の初期化は DI がおすすめです。
参考記事:DI (依存性注入) って何のためにするのかわからない人向けに頑張って説明してみる


つづいて業務ロジック以外の場合です。

Utilityクラス例
namespace Utilities
{
    public static class Encrypter
    {
        public static string Encrypt(string v)
        {
            return /* vを暗号化 */;
        }
    }
}

このように、業務ロジックを含まないような共通処理は
独立したクラスをまとめた Utilities 名前空間に含めると収まりがいいと思います。

また、実装する前に、同様の機能を提供するAPIがないか探してみると
結果的に時間短縮になるかもしれません。

キーワード

これまでの経験や、様々な記事から今回の結論に達しました。
ご興味があれば、以下のようなワードで検索してみてください。

  • SOLID原則
  • 継承よりコンポジション
  • オブジェクト指向エクササイズ

さいごに

「継承はNG」という論も見ますが、利点を見つけられる以上、少し早計だと思います。
ただ、理解せずに使ってしまったり、理屈に振り回されてしまうのもよくありません。

一番難しいところですが、適切な手法の選択が大切ですね。
そのためには多くの選択肢を確保しておきたいです。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【C#】ファイルに文字を出力する方法

どうも、Mottyです。
ファイルに文字を出力する方法を簡単にまとめていきます。といっても中身も簡単ですので記事自体は簡潔になりますが、深堀りについては時間があれば追記していきます。

書き込む方法

・usingステートメントで、System.IOの使用を宣言します。
・StreamWriterオブジェクトを宣言し、WriteLineで書き込んでいきます。

デスクトップにログファイルを書き込む

        /// <summary>
        /// テキストファイルへの書き込み
        /// </summary>
        public static void WriteText(string fpath = @"C:\Users\$UserName$\Desktop\logfile.log") //試しにデスクトップにログを書き込んでいきます。
        {
            // テキストファイルのパス
            using (StreamWriter sw = new StreamWriter(fpath,true))//trueは追記,falseは上書き
            {
                sw.WriteLine("NEC"); 
                sw.WriteLine("SONY");
                sw.WriteLine("DELL");
                sw.WriteLine(text);
            }
        }
Main.cs
static void Main(string[] args){
WriteText();
Console.ReadKey();
}
//output in logfile.log
//NEC
//SONY
//DELL

CSVファイルを書き込む

・配列をカンマでConcatしてあげればOK
配列やlistを引数にして関数内で処理しても良いですが、今回は端折りました(汗)

        public static void WriteText(string content, string fpath = @"C:\Users\$username$\Desktop\csvfile.csv") 
        {

            using (StreamWriter sw = new StreamWriter(fpath, true))//trueは追記,falseは上書き
            {
                sw.WriteLine(content);
            }
        }
Main.cs
        static void Main(string[] args)
        {
            var dataList = new List<List<string>>(){
           new List<string>() {"1-A","1-B","1-C","1-D"},  // 1行目
           new List<string>() {"2-A","2-B","2-C","2-D"},  // 2行目
           new List<string>() {"3-A","3-B","3-C","3-D"},  // 3行目
          };
            string line1 = string.Join(",", dataList[0]);
            string line2 = string.Join(",", dataList[1]);
            string line3 = string.Join(",", dataList[2]);
            WriteText(line1);
            WriteText(line2);
            WriteText(line3);
            Console.ReadKey();
        }

終わりに

ファイルのテキストを読み込む際はStreamReaderオブジェクトからReadLine()で読み込むこととなりますが、それはまたの機会に。。。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

謎の力を棒に与えたかった

抽象的な表現で申し訳ないのですが、棒に謎の力を与える必要がありました。
メモ代わりに使わせていただきます。

Image from Gyazo

  • アンカーポイントの変更
  • 角度を求めて代入

アンカーポイントの変更

Unityでは基本的にGameObjectの真ん中にセンターが来るようにデザインされています。
今回は物体の端にアンカーポイントを置きたかったので、空のGameObjectを配置して、以下のスクリプトをアタッチ。

public class Gizmo : MonoBehaviour
{
   public float gizmoSize = 0.1f;
    public Color gizmoColor = Color.yellow;

    void OnDrawGizmos()
    {
        Gizmos.color = gizmoColor;
        Gizmos.DrawWireSphere (transform.position, gizmoSize);
    }
}

ヒエラルキーの方は、空のゲームオブジェクトの下に置きたいオブジェクトを配置します。
Image from Gyazo

こちらの記事を参考(というかほぼそのまま)利用させていただきました。

角度を求めて代入

原点はわかっていたので、だいたいこのへんから取ろうと決めた座標を使って角度を求めました。

float rad = Mathf.Atan2(modelpos.y,modelpos.x);
float moveY = 90 - rad * Mathf.Rad2Deg;

上記の参考はこちらの記事をを利用させていただきました。

このときに、GameObjectが何でも良いように、

[SerializeField]GameObject targetmodel;

上記のように宣言して、参照元が何でも良いようにUnityでフィールドを見れるようにしました。

Quautanionのりお会が全く追いつかなかったため、今回は Quaternion.Euler を使って値を代入しています。

gizmo.transform.rotation = Quaternion.Euler(2.61f,-7.06f,moveY/2);

rotationは、Quaternionを代入せねばいけませんが、上記のように雑にVector3みたいなのが代入ができるので便利です。
教えていただいたのはLookAt()関数でしたが、今回は使用しませんでした。

以上メモでした。

もっとこうすれば簡単だよ!ということがありましたら教えていただけると幸いです。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Unity C# ランゲーム:敵の攻撃の狙いをランダムにする方法について

こんにちは、連日 Qiitaで質問させて頂いてます。はせがわです。

また、皆さんのお力をお借りしたく質問させて頂きます。

今作っている物

Unityで
「盾一枚で依頼人を敵の攻撃から守りながらゴールまで送り届けるランゲーム」
を作っています。

aaaaa.png

プレイヤーは、自動前進で敵がPlayerまたは依頼人を狙って放物線上に飛んできます。
※現状は、playerのみをターゲットにしています。

実現したい事

敵の攻撃は、3パターン作り、そのパターンをランダムで実行したいと思っています
パターン1は、プレイヤーをそのまま狙ってくるパターン
パターン2は、依頼人を狙ってくるパターン
パターン3は、下記のペライチのような、ランダム範囲で攻撃してくる
無題のプレゼンテーション.jpg

しかし、現状 Z軸が動いてしまい、盾の後ろの方にターゲットが移動してしまいます。
※現在、ターゲットは、プレイヤーの前身移動速度を考慮して、盾の少し前に設定しています。
 Z位置は、そこで固定して YとX軸だけでターゲットがばらついてほしいと思っています。

【参考動画】
https://youtu.be/-LhvG5ULkfk

クリアしたい課題

・3つ目の ランダム攻撃が実装できません。
 どうか、お力を貸してください。。。

[Github]
https://github.com/hasegawadesu/ShieldRunner-VR-.git

[射出Code]

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class throwingscript : MonoBehaviour
{


    //☆
    private float InstantiationTimer = 0f;
    // ターゲットオブジェクトの Transformコンポーネントを格納する変数
    public Transform target;//☆
    // オブジェクトの移動速度を格納する変数
    public float moveSpeed;//☆
    // オブジェクトが停止するターゲットオブジェクトとの距離を格納する変数
    public float stopDistance;//☆
    // オブジェクトがターゲットに向かって移動を開始する距離を格納する変数
    public float moveDistance;//☆  

    //<summary>
    //testtest
    //射出するオブジェクト
    //</summary>
    [SerializeField, Tooltip("ThrowingObject")]
    private GameObject ThrowingObject=null;

    //<summary>
    //標的のオブジェクト
    //</summary>
    [SerializeField, Tooltip("TargetObject")]
    private GameObject TargetObject=null;

    //<summary>
    //射出角度
    //</summary>
    [SerializeField, Range(0F,90F), Tooltip("ThrowingAngle")]
    private float ThrowingAngle=0;

    // Start is called before the first frame update
    private void Start()
    {
     Collider collider = GetComponent<Collider>();
     if(collider != null)
     {
         //干渉しないようにisTriggerをつける 
         collider.isTrigger = true;
     }   
    }

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

     // ゲーム実行中に毎フレーム実行する処理

        //☆ 変数 targetPos を作成してターゲットオブジェクトの座標を格納
        Vector3 targetPos = target.position;//☆
        // 自分自身のY座標を変数 target のY座標に格納
        //(ターゲットオブジェクトのX、Z座標のみ参照)
        targetPos.y = transform.position.y;//☆
        // オブジェクトを変数 targetPos の座標方向に向かせる
        transform.LookAt(targetPos);//☆

        // 変数 distance を作成してオブジェクトの位置とターゲットオブジェクトの距離を格納
        float distance = Vector3.Distance(transform.position, target.position);//☆
        // オブジェクトとターゲットオブジェクトの距離判定
        // 変数 distance(ターゲットオブジェクトとオブジェクトの距離)が変数 moveDistance の値より小さければ
        // さらに変数 distance が変数 stopDistance の値よりも大きい場合



        if (this.transform.position.z-15 < target.transform.position.z)
        {
            Destroy(gameObject);
        }

        else if (distance < moveDistance && distance > stopDistance)//☆
        {
            ThrowingBall();
        }

    }
    //<summary>
    //ボールを射出する
    //</summary>
    private void ThrowingBall()
    {
        InstantiationTimer -= Time.deltaTime;
        if (ThrowingObject !=null && TargetObject !=null && InstantiationTimer <0)
        {
            //Ballオブジェクトの生成
            GameObject ball = Instantiate(ThrowingObject, this.transform.position, Quaternion.identity);
            InstantiationTimer =2f;

            //標的の座標
            float x = Random.Range(0.0f, 2.0f);
      float y = Random.Range(0.0f, 2.0f);
            float z = Random.Range(0.0f, 2.0f);
           Vector3 targetPosition = TargetObject.transform.position;
           TargetObject.transform.position = new Vector3(x, y, z);

            //射出角度
            float angle = ThrowingAngle;

            //射出速度を算出
            Vector3 velocity = CalculateVelocity(this.transform.position, targetPosition, angle);

            //射出
            Rigidbody rid = ball.GetComponent<Rigidbody>();
            rid.AddForce(velocity * rid.mass, ForceMode.Impulse);
        }


            }

    //<summary>
    //標的に命中する射出速度の清算
    //</summary>
    //<param name="pointA">射出開始座標</param>
    //<param name="pointB">標的の座標</param>
    //<returns>射出速度</returns>
    private Vector3 CalculateVelocity(Vector3 pointA, Vector3 pointB, float angle)
    {
        //射出角をラジアンに変換
        float rad = angle * Mathf.PI / 180;

        //水平方向の距離x
        float x = Vector2.Distance(new Vector2(pointA.x,pointA.z),new Vector2(pointB.x, pointB.z));

        //垂直方向の距離y
        float y = pointA.y - pointB.y;

        //斜方投射のこうしきを初速度について解く
        float speed = Mathf.Sqrt(-Physics.gravity.y * Mathf.Pow(x,2)/(2*Mathf.Pow(Mathf.Cos(rad),2)*(x * Mathf.Tan(rad)+y)));

        if(float.IsNaN(speed))
        {
            //条件を満たす初速を算出できなければVector3.zeroを返す
            return Vector3.zero;
        }
        else
        {
            return (new Vector3(pointB.x - pointA.x, x * Mathf.Tan(rad), pointB.z-pointA.z).normalized*speed);    
        }
    }
}


なにとぞ、よろしくお願いします。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

F# and CSVHelper でCSVファイルを読み込んでみた!

Summary

ここ一年近くCSVファイルを読み込むのに苦労してましたが、

だいぶ形になってきたので書いてみたいと思います

F# and CSVHelper でCSVファイルを読み込んでみました。

動作概要

1. CSVファイル(フルーツ.csv)を読み込む
2. 読込んだレコードを出力する

読込元CSV

りんご,,300,2020/12/11
チェリー,,450,2020/12/12
ドラゴンフルーツ,赤紫,"1,200",2020/12/24
キャベツ,,,2020/12/30

出力例

[|seq
    [{ ファイル作成日時 = 2021/01/27 11:33:32
       果物名 = "ドラゴンフルーツ"
        = "赤紫"
       価格 = "1,200"
       購入日 = 2020/12/24 0:00:00 }; { ファイル作成日時 = 2021/01/27 11:33:32
                                     果物名 = "キャベツ"
                                      = "緑"
                                     価格 = ""
                                     購入日 = 2020/12/30 0:00:00 };
     { ファイル作成日時 = 2021/01/27 11:33:32
       果物名 = "りんご"
        = "赤"
       価格 = "300"
       購入日 = 2020/12/11 0:00:00 }; { ファイル作成日時 = 2021/01/27 11:33:32
                                     果物名 = "チェリー"
                                      = "黒"
                                     価格 = "450"
                                     購入日 = 2020/12/12 0:00:00 }]|]

ソースコードの構成

こんな感じ

image.png

csvRead_log.fs

エラーになった場合の受け皿を作ります

namespace CsvRead

module Log =

  type ErrorRecordLog = {
    処置済 : bool
    ファイル作成日時 : System.DateTime
    ファイル名 : string
    エラーレコード : string
  }

csvRead_util.fs

csvを読み込む際のちょっとした自作便利関数をここに書きます

namespace CsvRead

module public CsvUtil =

  module public Encoding =

    open System.Text

    let public shiftJIS =
      Encoding.RegisterProvider(CodePagesEncodingProvider.Instance)
      Encoding.GetEncoding 932

    let public utf8 =
      Encoding.GetEncoding 65001

  module public Files =

    open System.IO
    open System.Text.RegularExpressions

    let targetFiles1 srcDir fileExtension filePattern : option<string[]> =
      System.IO.Directory.EnumerateFiles(srcDir, "*" + fileExtension)
      |> Seq.filter(fun fp -> Regex.IsMatch(fp,filePattern) )
      |> Seq.sortByDescending(fun fp -> File.GetLastWriteTime(fp))
      |> Seq.map(fun fp -> Path.Combine(Directory.GetCurrentDirectory() , fp ) |> fun s -> Path.GetFullPath(s))
      |> fun x -> x |> Seq.toArray
      |> fun x -> if Array.isEmpty x then None else Some x

csvRead_global.fs

CSVを読み込む際の一般的な情報をここに書きます(フォルダ場所とか)

namespace CsvRead

module public GlobalDataStore =
  open System.Collections.Concurrent
  // ★ multiple record type の場合は必要に応じて数量を増やす
  let num = 1
  let cqGoods = Array.init num (fun _ -> new ConcurrentQueue<obj>() )
  let cqBad  = new ConcurrentQueue<obj>()


/// ★読込対象のCSVファイルに関する一般設定
module public GlobalSetting =

  let csvSetting = {|
    csvFolder = "./../csv"
    extension = "*.csv"
    fileRegPattern = ".*フルーツ.*"
    fileEncoding = Encoding.utf8
  |}

csvRead_csv.fs

読込対象CSVの構造をここに書きます

namespace CsvRead

  open System
  open CsvHelper.Configuration

  [<CLIMutable>]
  type public Csv = {

    mutable ファイル作成日時 : DateTime
    果物名 : string
     : string
    価格 : string
    購入日 : Nullable<DateTime>

  }

  [<Sealed>]
  type public CsvMap () as this =
    inherit ClassMap<Csv>()
    do
      this.Map(fun x -> x.ファイル作成日時).Constant( System.DateTime(9999,12,31,12,59,59) ) |> ignore
      this.Map(fun x -> x.果物名).Index(0) |> ignore
      this.Map(fun x -> x.).Index(1) |> ignore
      this.Map(fun x -> x.価格).Index(2) |> ignore
      this.Map(fun x -> x.購入日).Index(3) |> ignore

csvRead_csvhelperWrap.fs

CSVHelperのラッパーをここに書きます

namespace CsvRead

module public CsvHelperWrap =

  open System.Globalization
  open System.IO
  open CsvHelper
  open CsvHelper.Configuration
  open GlobalDataStore
  open CsvRead.Log


  // ★Csv読込設定
  let private csvConfig (fp:string)  : CsvConfiguration  =

    CsvConfiguration(CultureInfo.CurrentCulture)
    |> fun x ->

      x.Delimiter <- ","
      x.HasHeaderRecord <- false
      x.TrimOptions <- TrimOptions.Trim
      x.IgnoreBlankLines <- true
      x.ShouldSkipRecord <- (fun arr -> arr.[0] = "EOF" )

      // ★Multiple Record type の場合はここをレコードタイプ毎に追加していく
      x.RegisterClassMap<CsvMap>() |> ignore

      // カラムが不足していてエラーを出したくない場合は null を設定する
      x.MissingFieldFound <- null

      x.BadDataFound <- fun ctx ->
        [|
          $"データ番号:{ctx.RawRow} : データ形式がおかしいです BadData"
          ctx.RawRecord.Trim()
        |]
        |> fun errRcd ->
          cqBad.Enqueue(
              {
                処置済 = false
                ファイル作成日時 = File.GetCreationTime(fp)
                ファイル名 = fp |> System.IO.Path.GetFileNameWithoutExtension
                エラーレコード = errRcd |> String.concat ","
              }
        )

      x

  let public csvRead (streamReader:StreamReader) (fp:string) =

    use csv = new CsvReader(streamReader , csvConfig fp)

    // ★ヘッダーがある場合
    // let skipRows (csv:CsvReader) i =
    //   for j in [1..i] do
    //     csv.Read() |> ignore
    // skipRows csv 1

    while (csv.Read()) do

      try
        cqGoods
        |> Array.item 0
        |> fun x ->
          let tmp = csv.GetRecord<Csv>()
          tmp.ファイル作成日時 <- File.GetCreationTime(fp)
          x.Enqueue( tmp )
      with _ ->

        [|
          $"データ番号:{csv.Context.RawRow} : 何かしらのエラーです"
          csv.Context.RawRecord.Trim()
        |]
        |> fun errRcd ->
          cqBad.Enqueue(
            {
              処置済 = false
              ファイル作成日時 = File.GetCreationTime(fp)
              ファイル名 = fp |> System.IO.Path.GetFileNameWithoutExtension
              エラーレコード = errRcd |> String.concat ","
            }
        )

csvRead_main.fs

ファイル処理をここに書きます

namespace CsvRead

open GlobalSetting

module Main =

  let public CsvRead1 () =
    CsvUtil.Files.targetFiles1 csvSetting.csvFolder csvSetting.extension csvSetting.fileRegPattern
    |> fun x ->
      match Option.isNone x with
      | true -> ()
      | false ->
        Option.get x
        |> Array.iter(fun fp ->
            new System.IO.StreamReader( fp , csvSetting.fileEncoding)
            |> fun sr -> CsvHelperWrap.csvRead sr fp
        )

main.fs

open CsvRead
open GlobalDataStore

[<EntryPoint>]
let main argv =

  Main.CsvRead1 ()

  cqGoods
  |> printfn "%A"

  0

現場からは以上です

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Unity】InputFieldにTabを押したら次へ行くという処理を追加する

SwitchInputField.cs
using UnityEngine;
using TMPro;

public class SwitchInputField : MonoBehaviour
{
    public TMP_InputField thisInputField;
    public TMP_InputField nextInputField;

    // Start is called before the first frame update
    void Start()
    {
        thisInputField = GetComponent<TMP_InputField>();
    }

    // Update is called once per frame
    void Update()
    {
        if(thisInputField.isFocused && Input.GetKeyDown(KeyCode.Tab))
        {
            nextInputField.Select();
        }
    }
}

//使い方
InputFieldコンポーネントを持っているオブジェクト自体にAdd Componentします。
Tabを押した時に選択されて欲しいオブジェクトをnextInputFieldにアタッチ or スクリプトから代入します。

//TextMeshProを使わない方向け
using TMPro; => using UnityEngine.UI;
TMP_InputField => InputField
という風に読み替えてください。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む