supabase-jsにカウントメソッドを追加する

この記事を書くに至った経緯

3月上旬にPlanescaleの無料プランが廃止することが発表され、Planescaleで開発していたプロジェクトのDBをSupabaseに以降することにしました。 その際、ORM(Object Relational Mapping)にPrismaを採用していたおかげでDBの以降はスムーズに行えたのですが、せっかくならSupabase公式のsupabase-jsを使おうと思った時、supabase-jsにデータの数を取得するメソッドが無いことに気がついたので拡張メソッドとして実装することにしました。

実装

めちゃくちゃシンプルです。 今のところ、SupabaseClientの拡張メソッドとして実装しているため、select()などとメソッドチェーンで繋げられないです。 時間があれば加筆したい...

import { PostgrestResponse, SupabaseClient } from '@supabase/supabase-js';

async function count(this: SupabaseClient, tableName: string): Promise<number | null> {
    try {
        const { data, error, count }: PostgrestResponse<any> = await this.from(tableName)
            .select('*', { count: 'exact', head: true });

        if (error) {
            throw error;
        }

        return count;
    } catch (error) {
        console.error('Error occurred while counting rows:', error);
        throw error;
    }
}

declare module '@supabase/supabase-js' {
    interface SupabaseClient {
        count: (tableName: string) => Promise<number | null>;
    }
}

SupabaseClient.prototype.count = count;

短いですが以上です。 参考になれば!

Addressableでpathを用いてアンロードする

こんばんは。

using System;
using System.Collections.Generic;
using Cysharp.Threading.Tasks;
using UnityEngine.AddressableAssets;
using UnityEngine.ResourceManagement.AsyncOperations;
using Object = UnityEngine.Object;

namespace Ruchi.Data.Repository
{
public static class AddressableRepository
{
private readonly struct AssetEntity
{
///アドレス、ラベル
public readonly string Path;

//参照カウンタ
public readonly int RefCount;

//Addressable のロードハンドラ
public readonly AsyncOperationHandle Handle;

public AssetEntity(string path, int refCount, AsyncOperationHandle handle)
{
Path = path;
RefCount = refCount;
Handle = handle;
}

public AssetEntity IncreaseRefCount()
{
var refCount = checked(RefCount + 1);
var entity = new AssetEntity(Path, refCount, Handle);
return entity;
}

public AssetEntity DecreaseRefCount()
{
if (RefCount == 0)
{
throw new Exception($"{nameof(RefCount)}0以下にできません。");
}
var entity = new AssetEntity(Path, RefCount - 1, Handle);
return entity;
}
}

private static readonly Dictionary<(Type, string path), AssetEntity> Entities = new();

private static bool ContainsKey<T>(string path)
{
return Entities.ContainsKey((typeof(T), path));
}

private static AssetEntity GetEntity<T>(string path)
{
return Entities[(typeof(T), path)];
}

private static void SetEntity<T>(AssetEntity entity)
{
Entities[(typeof(T), entity.Path)] = entity;
}

private static void RemoveEntity<T>(string path)
{
Entities.Remove((typeof(T), path));
}

public static async UniTask<T> LoadAssetAsync<T>(string path) where T : Object
{
var contains = ContainsKey<T>(path);

// すでにロード済み
if (contains)
{
var cachedEntity = GetEntity<T>(path);
cachedEntity = cachedEntity.IncreaseRefCount();
SetEntity<T>(cachedEntity);

return cachedEntity.Handle.Result as T;
}

var handle = Addressables.LoadAssetAsync<T>(path);
await handle;
if (handle.Status != AsyncOperationStatus.Succeeded)
{
throw new ArgumentNullException(handle.Status.ToString());
}

var entity = new AssetEntity(path, 1, handle);
SetEntity<T>(entity);

return handle.Result;
}

// アセットアンロード
public static void Release<T>(string path)
{
if (ContainsKey<T>(path) == false)
{
return;
}

var entity = GetEntity<T>(path);
entity = entity.DecreaseRefCount();
if (entity.RefCount > 0)
{
return;
}

// アンロード
Addressables.Release(entity.Handle);
RemoveEntity<T>(path);
}

}
}

何かあれば@ruchi12377まで!

以上です。

【爆速で作る】Deno × NextJSのチャットアプリ

はじめに

この記事はサーバーにログが残らない匿名形式のチャットアプリ作成するための記事です。 匿名と言っても、WebSocketを
この記事はWebSocketの使い方を学ぶためのものであり、フレームワーク等の詳しい使い方には触れません。

使う技術・フレームワーク

名前 目的
Deno バックエンド実装
Deno Deploy Denoをデプロイする
NextJS サイトで使うフレームワーク
TailwindCSS フロントエンドのCSS
Vercel サイトのデプロイ

この記事で書くこと

  • DenoでWebSocketを使ったチャットアプリのバックエンドを作る方法
  • バックエンドのコードの解説
  • WebSocketの簡単な説明
  • NextJSで作るシンプルなフロントエンドの書き方

この記事で書かないこと

  • 書くこと以外のほぼ全て

バックエンドの作成

バックエンドを先に作ったほうが後々楽なので、先にバックエンドを作ります。
バックエンド用のフォルダを作って直下にmain.tsファイルを作成して以下のようにする。

import { serve } from "https://deno.land/std@0.191.0/http/server.ts";

const useLog = false;

const clients = new Map<string, WebSocket>();

//接続してる全員にメッセージを送る
const sendMessageToAll = (message: string) => {
  clients.forEach((client) => {
    if (client.readyState === client.OPEN) {
      client.send(message);
    }
  });

  log(JSON.parse(message));
};

function wsHandler(ws: WebSocket, url: URL) {
  //ユーザーにUUIDを振る
  const id = crypto.randomUUID();
  clients.set(id, ws);

  ws.onopen = () => {
    log("connected");
  };
  ws.onmessage = (e) => {
    const payload = JSON.parse(e.data);

    if (payload.type === "message") {
      sendMessageToAll(e.data);
    }
  };
  ws.onclose = () => {
    clients.delete(id);
    log("disconnected");
  };
}

//HttpをWebsocketにUpgradeして
//レスポンスを返す関数
function handler(req: Request) {
  if (req.method != "GET") {
    const err = new Response("Method Not Allowed", {
      status: 405,
      headers: { "Content-Type": "text/plain" },
    });
    return err;
  }

  const url = new URL(req.url);
  //Upgrade
  const { response, socket } = Deno.upgradeWebSocket(req);

  //Websocketの処理
  wsHandler(socket, url);
  return response;
}

//Denoのエントリーポイント
serve(handler, { port: parseInt(Deno.env.get("PORT") ?? "") || 443 });

//開発時はログを表示しないようにするための
//ログのラップ関数
function log(content: string) {
  if (useLog == false) return;
  console.log(content);
}

serve関数がDenoのエントリーポイントで、ここを起点にいろいろな処理をします。
第一引数でhandler関数を呼び出してますが後ほど。
ポート番号は環境変数があればそいつを、なければ443を使うようにしてます。ポート番号443はwssプロトコルでのデフォルトらしいです。参考

handler関数は主にリクエストを捌く部分です。
まず、GET以外のメソッドは弾いて405を返すようにしてます。
そしてプロトコルがwsかwssじゃなければ400を返します。
GETメソッドなら、HTTP通信をWebSocketにUpgradeします。 Upgradeとは、HTTP通信のリクエストとレスポンスが1対1となっている状態から、WebSocket通信という双方向にデータを送受信できる状態にすることです。 UpgradeしたらwsHandlerを呼びます。

wsHandlerはWebSocketクライアントの追加と、各イベントに対応する処理を書きます。 最初にUUIDを生成してユーザーに割り振って、クライアントのリストに追加します。 IDをクライアントのリストに追加するのは、一気にデータを送信するときに使うためです。 また、UUIDは2128通りあるので多分重複しないです。この確率のお陰でチェックする手間が省けます。気になる人は重複がないかチェックする処理書いてください。

ws.onopenはクライアントがWebSocketを開始したときに呼ばれます。今回はログを出すだけで特に処理はしてないです。

ws.onmessageは接続されてるクライアントからデータを受信したときに呼ばれます。今回は受信したデータのtypeがmessageのときにsendMessageToAllを読んで、接続されてるクライアント全員にメッセージを送っています。送ったメッセージをログとして出力してます。ちなみに、接続されてるクライアントには自分も含まれています。

ws.oncloseはクライアントの接続が切れたときに呼ばれます。今回はクライアントのリストから接続されたクライアントのIDを削除して切断された旨のログを出してます。

ここまでできたらOK。
Deno Deployにデプロイします。 やり方は調べてください。先人方のわかりやすい記事がたくさんあります。 デプロイするとき、エントリーポイントをmain.tsにしてください。

フロントエンドの作成

プロジェクトを作成

npx create-next-app chat-app --use-npm --typescript

プロジェクト作成のオプションは以下のとおりです。お好みの場合、()内はおすすめを書いてます。

  • ESLint → お好み(TypeScriptのフォーマットをやってくれるので基本はYesがおすすめです)
  • Tailwind CSS → Yes
  • src directory → お好み(ディレクトリを読み替えるのがめんどくさい場合はNoがおすすめです)
  • App Router → お好み(ディレクトリを読み替えるのがめんどくさい場合はNoがおすすめです)
  • import alias → No

プロジェクトフォルダに移動

cd chat-app

プロジェクトを立ち上げる。 デフォルトだとローカルホストのポートは3000、http://localhost:3000で開くことができる。

npm run dev

以下の画面が出たら成功です。

ここまでできたら爆速でフロントエンドを組み立てて行きましょう。

import { useEffect, useState } from "react";

let messagesArr: any[] = [];
const author = randomString();

//ランダムなユーザーIDを生成するための関数
function randomString(): String {
  let chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
  let name = "";
  for (var i = 0; i < 8; i++) {
    name += chars.charAt(Math.floor(Math.random() * chars.length));
  }

  return name;
}

export default function Home() {
  const [socket, setSocket] = useState<WebSocket | null>(null);
  const [messages, setMessages] = useState<any[]>([]);
  const url = process.env.NEXT_PUBLIC_API_SERVER as string;

  useEffect(() => {
    setSocket((socket: WebSocket | null) => {
      // 既に入ってたら新しくsocketを作成しない
      // これをしないと2回以上接続するという変なことになってしまう
      if (socket) return socket;
      return new WebSocket(url);
    });
  }, [setSocket]);

  useEffect(() => {
    if (socket) {
      // WebSocketからメッセージを受け取った時の処理
      socket.onmessage = (res: MessageEvent) => {
        const payload = JSON.parse(res.data);
        messagesArr = [...messagesArr, payload];
        setMessages(messagesArr);
      };
    }
  }, [socket]);

  // メッセージを送信する処理
  const sendMessage = (content: string) => {
    const packet = {
      type: "message",
      data: {
        author: author,
        text: content,
        ts: Date.now(),
      },
    };

    socket?.send(JSON.stringify(packet));
  };

  function sendMessageWrapper() {
    const field = document.querySelector("#message") as HTMLInputElement;
    if (field == undefined) return;
    const message = field?.value as string;

    if (message == "") return;
    sendMessage(message);
    field.value = "";
  }

  return (
    <main className="flex min-h-screen flex-col items-center justify-between bg-gray-900">
      <div className="overflow-y-scroll overscroll-contain">
        {messages &&
          messages
            .filter((x) => x.type == "message")
            .map((x) => {
              return (
                <div
                  className="m-2 h-full w-96 bg-white px-4 py-2"
                  key={x.data.ts}
                >
                  <div>
                    <p className="text-md justify-items-start text-black">
                      ID : {x.data.author}
                    </p>
                    <p className="text-2xl text-black">{x.data.text}</p>
                  </div>
                </div>
              );
            })}
      </div>
      <div className="sticky bottom-0 flex justify-between">
        <input
          type="text"
          id="message"
          className="w-80 mx-6 text-black md:w-96"
          placeholder="Enter your message"
        ></input>
        <button
          className="w-16 p-2 bg-white text-black"
          onClick={sendMessageWrapper}
        >
          Send
        </button>
      </div>
    </main>
  );
}

Reactにまつわるオマジナイは調べてください。useEffectとか。先人方の(以下略)。

クライアント側では、WebSocketをURL Sendボタンを押したら、sendMessageWrapperがsendMessageを呼んで、その中でWebSocketで送るオブジェクトをJSONにして送信します。あとはバックエンドがよしなに通信してクライアントにメッセージが送られます。

ここまでできたらOK。
Vercelにデプロイします。

Vercelの環境変数、NEXT_PUBLIC_API_SERVERにDeno DeployにデプロイしたURLを貼り付けるのを忘れないでください。 URLの初めにws://かwss://にしないとプロトコルが不正で通信できないし、WebSocketのインスタンス生成するところで死にます。 例 : wss://sample.deno.dev

以上で完成です。お疲れ様でした!

なにか質問・ツッコミがあればコチラまでどうぞ。DMでもリプライでも分かるところまで返します。

参考

【受験生が作る】リアルタイム性を重視した新感覚チャットアプリ「のーろぐちゃっと」 | Next.js + WebSocket

Math.SignとMathf.Signは同じ結果を返さない

はじめに

UnityのMathfの関数群はMathのラッパーだと思っていたのですが、実はSign関数の場合ラッパーではないのでSign関数についての簡単なメモです。

 

発端

 

両者の違い

Math.Signの実装

x > 0の時1を、x = 0の時0を、x < 0の時-1を返します。

Mathf.Signの実装

x ≥ 0の時1を、x < 0の時-1を返します。

 

2つの大きな違いは引数に0を指定された場合、0とするか1とするかの違いですね。

 

終わりに

短いですが以上です。

GraphViewがっつり入門

はじめに

Unity2018ぐらいからExperimentalで追加されたGraphViewの使い方のメモです。

日本のGraphViewに関する記事があんまりないので書こうと思いました。

(最初の部分は引用に書いてある「GraphView完全理解した(2019年末版)」とほぼ同じ内容です)

自己紹介

学生です。

この記事を読む人

GraphViewで何か作りたいけど何から始めればいいかわからんという人におすすめです。

GraphViewについて

このAPIはExperimentalなので公式のリファレンスにも書いてあるとおり、GraphView APIは実験的な物なので将来的に変更されたり削除される可能があります。(公式リファレンスから引用)

GraphViewを使って何ができるの?

簡単にいうとShaderGraphみたいなものを自作できるようになります。

(流石に個人でここまでは難しいかもしれませんが)

(公式より画像を引用)

 

GraphViewの構成要素

Graphviewは大まかに4つの要素でできています。

 ・GraphView
 ・Node
 ・Port
 ・Edge

 

簡単に説明すると、

GraphView

→NodeやPortの親

Node

→下の写真の青で囲ってある物です。

f:id:ruchi12377:20200417033849p:plain

Port

→In と Outとか。後で説明するEdgeの始点と終点になる物です。

Edge

→Node同士を繋ぐカラフルな線のことです。カラフルな理由は後ほど...

実際に作ってみる

バージョン : Unity2019.2.11f1

使用OS : MacOS Catalina 10.15.3

 

この記事はここを参考(ほぼ引用)しながら少し注釈を加えて説明していきます。

qiita.com

まずEditorWindowを作成します。

GraphEditorWindowという名前のスクリプトを作成して以下の内容を入力してください。

これから作成するスクリプトはEditorというフォルダの中に入れてください。

↓写真のように

f:id:ruchi12377:20200417043152p:plain


//GraphEditorWindow.cs
using UnityEditor;
using UnityEngine;

public class GraphEditorWindow : EditorWindow
{
[MenuItem("Window/Open GraphView")]
  public static void Open()
{
GraphEditorWindow graphEditorWindow = CreateInstance<GraphEditorWindow>();
graphEditorWindow.Show();
graphEditorWindow.titleContent = new GUIContent("GraphEditor");
}
}

f:id:ruchi12377:20200417042054p:plain

ここから開くことができます。

f:id:ruchi12377:20200417042140p:plain


こんな感じの画面が開たら成功です!

 

次にお待ちかね、GraphViewを作成していきます

MyGraphViewという名前のスクリプトを作成して以下の内容を入力してください。

"GraphView"という名前だとエラーが出るので注意してください。

//MyGraphView.cs
using
UnityEditor.Experimental.GraphView;
public class MyGraphView : GraphView {
}


一回何も書かなくてOKです。

そして、さっき作成したGraphEditorWindowに以下の内容を入力してください。

//GraphEditorWindow.cs
using UnityEditor;
using UnityEngine;

public class GraphEditorWindow : EditorWindow
{
     [MenuItem("Window/Open GraphView")]
     public static void Open()
     {
           GraphEditorWindow graphEditorWindow = CreateInstance<GraphEditorWindow>();
           graphEditorWindow.Show();
           graphEditorWindow.titleContent = new GUIContent("GraphEditor");
     }

void OnEnable()
     {
          rootVisualElement.Add(new MyGraphView());
}
}

今のところエディターに何にも表示されないはずですがこれでOKです。

 

次にNodeを作成していきます。

MyNodeという名前のスクリプトを作成して以下の内容を入力してください。

//MyNode.cs
using UnityEditor.Experimental.GraphView;

public class MyNode : Node
{

}

そして、さっき作成したMyGraphViewに以下の内容を入力してください。

//MyGraphView.cs
using UnityEditor.Experimental.GraphView;

public class MyGraphView : GraphView
{
public MyGraphView() : base()
{
AddElement(new MyNode());
}
}

確認してみると、あれ?表示されない!!なんで...なんで..

そういう時はUIElements Debuggerを使いましょう。

 

f:id:ruchi12377:20200417050552p:plain

 

やっぱり生成されています。なぜでしょうか。

f:id:ruchi12377:20200417050742p:plain
しかし、理由は簡単です。高さが0になっていて生成されているのに見えていないのです。

 

GraphEditorWindowに以下の内容を入力してください。

//GraphEditorWindow.cs
using UnityEditor;
using UnityEngine;

public class GraphEditorWindow : EditorWindow
{
     [MenuItem("Window/Open GraphView")]
     public static void Open()
     {
          GraphEditorWindow graphEditorWindow = CreateInstance<GraphEditorWindow>();
          graphEditorWindow.Show();
          graphEditorWindow.titleContent = new GUIContent("GraphEditor");
}
void OnEnable()
     {
          var graphView = new MyGraphView()
          {
style = { flexGrow = 1 }
          };
          rootVisualElement.Add(graphView);
      }
}

f:id:ruchi12377:20200417051535p:plain

無事、Nodeが作成されました。感動の瞬間です!

でもこれじゃただの箱ですw

Portを追加してNode同士をつなげられるようにしましょう。

 

MyNodeに以下の内容を入力してください。

//MyNode.cs
using UnityEditor.Experimental.GraphView;

public class MyNode : Node
{
public MyNode()
{
title = "Sample";
var inputPort = Port.Create<Edge>(Orientation.Horizontal, Direction.Input,Port.Capacity.Single, typeof(Port));
inputContainer.Add(inputPort);

var outputPort = Port.Create<Edge>(Orientation.Horizontal, Direction.Output, Port.Capacity.Single, typeof(Port));
outputContainer.Add(outputPort);
}
}

これで確認してみると

f:id:ruchi12377:20200417052911p:plain

お、NodeにPortがつきました!

でも思いませんか?NodeとNodeを繋ぎたいと、でも繋ぐNodeがないですね。

Nodeを量産できるようにしましょう。

MyGraphViewを以下のようにしてください。

//MyGraphView.cs
using UnityEngine.UIElements;//AddManipulatorを使うために必要
using UnityEditor.Experimental.GraphView;

public class MyGraphView : GraphView
{
public MyGraphView() : base()
{
this.AddManipulator(new SelectionDragger());//移動できるように
nodeCreationRequest += context =>
{
AddElement(new MyNode());
};
}
}

これで右クリック>Create NodeでNodeをたくさん生成できるようになって移動することはできますが、Npde同士を接続できません。

 

OutputPortから正しいInputPortにつなげてあげる必要があります。

//MyGraphView.cs
using UnityEngine.UIElements;//AddManipulatorを使うために必要
using System.Collections.Generic;
using UnityEditor.Experimental.GraphView;

public class MyGraphView : GraphView
{
public MyGraphView() : base()
{
this.AddManipulator(new SelectionDragger());//移動できるように

nodeCreationRequest += context =>
{
AddElement(new MyNode());
};
}

public override List<Port> GetCompatiblePorts(Port startAnchor, NodeAdapter nodeAdapter)
{
return ports.ToList();
}
}

でもEdgeが見にくいですね...見えないことはないですけど。
そういうことで背景の色を変えましょう。

f:id:ruchi12377:20200417060033p:plain

まず画像のようにResourcesフォルダを作成

f:id:ruchi12377:20200417061129p:plain

その中にUIElements Editor Window から

f:id:ruchi12377:20200417064941p:plain

C#、UXMLのチェックを外してUSSだけにチェックを入れTextFieldにBackGroundと入れ、Confirmを押します

f:id:ruchi12377:20200417065009p:plain

USSと書かれたファイルが作成できたらOKです

f:id:ruchi12377:20200417065315p:plain

USSの説明ですが、簡単にいうとHTMLのCSSのような物です。

色を変えたりできます。

 

そこで、BackGround.ussを下のようにします。今回はUnityのAnimatorのような色使いにしてみました。

GridBackground{
--grid-background-color:rgb(90,90,90);
--line-color:rgba(80,80,80,1);
--thick-line-color:rgba(40,40,40,0.6);
--spacing:10;
}

--grid-background-color → 背景色

--line-color → 細かい線の色

--thick-line-color → 大まかな線の色
--spacing →細かいマスの長さ

 

期待して見てみると、あれ?何も変わっていない。

だって 作成したUSSをEditorWindowに適応してないんですから。

f:id:ruchi12377:20200417070608p:plain

そこで、MyGraphView.csを次のようにします。

//MyGraphView.cs
using UnityEngine;
using UnityEngine.UIElements;//AddManipulatorを使うために必要
using System.Collections.Generic;
using UnityEditor.Experimental.GraphView;
public class MyGraphView : GraphView
{
public MyGraphView() : base()
{
styleSheets.Add(Resources.Load<StyleSheet>("BackGround"));
GridBackground gridBackground = new GridBackground();
Insert(0, gridBackground);
gridBackground.StretchToParentSize();

SetupZoom(ContentZoomer.DefaultMinScale, ContentZoomer.DefaultMaxScale);

this.AddManipulator(new ContentZoomer());//拡大縮小できるように
this.AddManipulator(new SelectionDragger());//Nodeを移動できるように
this.AddManipulator(new ContentDragger());//画面を移動できるように
this.AddManipulator(new RectangleSelector());//範囲選択
nodeCreationRequest += context =>
{
AddElement(new MyNode());
};
}

public override List<Port> GetCompatiblePorts(Port startAnchor, NodeAdapter nodeAdapter)
{
return ports.ToList();
}
}

そうするとこうなるのですが

f:id:ruchi12377:20200417065655p:plain

一回わかりやすいように色を変えてみます

・背景色の色を

・細かい線の色を

・大まかな線の色を

・細かいマスの長さを15にしてみました

f:id:ruchi12377:20200417070250p:plain

これでわかりやすくなったのではないのでしょうか?

細かいマスの大きさが少し大きくなってるのがわかると思います。

 

SampleじゃないNodeを作成する

 MyNodeを以下のようにします

//MyNode.cs
using
UnityEditor.Experimental.GraphView;

public abstract class MyNode : Node { }

抽象クラスにします。クラスの中身は何にも書かなくて大丈夫です。 

新しくProcessNode.csというものを作り以下のようにします。

//ProcessNode.cs
using UnityEditor.Experimental.GraphView;

public abstract class ProcessNode : MyNode
{
     public ProcessNode()
     {
          var inputPort = Port.Create<Edge>(Orientation.Horizontal, Direction.Input, Port.Capacity.Single, typeof(Port));
inputPort.portName = "In";
          inputContainer.Add(inputPort);

var outputPort = Port.Create<Edge>(Orientation.Horizontal, Direction.Output, Port.Capacity.Single, typeof(Port));
          outputPort.portName = "Out";
          outputContainer.Add(outputPort);
}

public abstract void Execute();
}

 こいつを一回噛ませることでインターフェースじゃないですが、In と Out のPortを強制的に作成してくれます。便利ですね。

 

LogNode.csを作成して以下のようにします。

using UnityEditor.Experimental.GraphView;

public class LogNode : ProcessNode
{
     private Port inputString;

public LogNode() : base()
     {
          title = "Log";

inputString = Port.Create<Edge>(Orientation.Horizontal, Direction.Input, Port.Capacity.Single, typeof(string));
     inputContainer.Add(inputString);
     }

     public override void Execute()
     {

}
}

あれ?Debug.Logがない、と思った人、焦らないでくださいw

その前に出力する文字を指定できるStringNodeを作らなければいけません。
StringNodeというスクリプトを作成し、以下のようにします。

using UnityEngine.UIElements;
using UnityEditor.Experimental.GraphView;

public class StringNode : MyNode
{
private TextField textField;
public string Text { get { return textField.value; } }

public StringNode() : base()
{
title = "String";

var outputPort = Port.Create<Edge>(Orientation.Horizontal, Direction.Output, Port.Capacity.Multi, typeof(string));
outputContainer.Add(outputPort);
textField = new TextField();
mainContainer.Add(textField);
}
}

○○Field系については後で説明します。
そして最後にRootNodeを作成します。これはすべてのNodeの親とも言えます。

RootNodeというスクリプトを作成し、以下のようにします。

using UnityEditor.Experimental.GraphView;

public class RootNode : MyNode
{
public RootNode() : base()
{
title = "Root";

capabilities -= Capabilities.Deletable;
var outputPort = Port.Create<Edge>(Orientation.Horizontal, Direction.Output, Port.Capacity.Single, typeof(Port));
outputPort.portName = "Out";
outputContainer.Add(outputPort);
}
}

親が何個もあるというのはおかしいので、Wndowを生成したときに一個だけ生成するようにします。
RootNodeはcapabilities -= Capabilities.Deletable;の部分で消せないようになっています。
MyGraphViewを以下のようにします。

//MyGraphView.cs
using UnityEngine;
using UnityEngine.UIElements;//AddManipulatorを使うために必要
using System.Collections.Generic;
using UnityEditor.Experimental.GraphView;

public class MyGraphView : GraphView
{
public RootNode root;

public MyGraphView() : base()
{
styleSheets.Add(Resources.Load<StyleSheet>("BackGround"));
GridBackground gridBackground = new GridBackground();
Insert(0, gridBackground);
gridBackground.StretchToParentSize();

SetupZoom(ContentZoomer.DefaultMinScale, ContentZoomer.DefaultMaxScale);

this.AddManipulator(new ContentZoomer());//移動できるように
this.AddManipulator(new SelectionDragger());//移動できるように
this.AddManipulator(new ContentDragger());//移動できるように
this.AddManipulator(new RectangleSelector());//範囲選択

root = new RootNode();
AddElement(root);

nodeCreationRequest += context =>
     {
  //AddElement(new MyNode());
};

public override List<Port> GetCompatiblePorts(Port startAnchor, NodeAdapter nodeAdapter)
{
return ports.ToList();
}
}

 

CreateNodeでSampleNodeしか作れないのはおかしいので、選択してから生成できるようにします。

SearchWindowProviderというスクリプトを作成して以下のようにします。

//SearchWindowProvider.cs
using System;
using System.Collections.Generic;
using UnityEngine;
using UnityEditor.Experimental.GraphView;

public class SearchWindowProvider : ScriptableObject, ISearchWindowProvider
{
private MyGraphView graphView;

public void Initialize(MyGraphView graphView)
{
this.graphView = graphView;
}

List<SearchTreeEntry> ISearchWindowProvider.CreateSearchTree(SearchWindowContext context)
{
var entries = new List<SearchTreeEntry>();
entries.Add(new SearchTreeGroupEntry(new GUIContent("Create Node")));

foreach (var assembly in AppDomain.CurrentDomain.GetAssemblies())
     {
foreach (var type in assembly.GetTypes())
{
if (type.IsClass && !type.IsAbstract && type.IsSubclassOf(typeof(MyNode))
&& type != typeof(RootNode))
{
entries.Add(new SearchTreeEntry(new GUIContent(type.Name)){ level = 1, userData = type });
}
}
}
return entries;
}

bool ISearchWindowProvider.OnSelectEntry(SearchTreeEntry searchTreeEntry, SearchWindowContext context)
{
var type = searchTreeEntry.userData as Type;
var node = Activator.CreateInstance(type) as MyNode;
graphView.AddElement(node);
return true;
}
}

そしたら検索できるようにMyGraphViewを次のようにします

//MyGraphView.cs
using UnityEngine;
using UnityEngine.UIElements;//AddManipulatorを使うために必要
using System.Collections.Generic;
using UnityEditor.Experimental.GraphView;

Public class MyGraphView : GraphView
{
     public RootNode root;

public MyGraphView() : base()
     {
     styleSheets.Add(Resources.Load<StyleSheet>("BackGround"));
     GridBackground gridBackground = new GridBackground();
     Insert(0, gridBackground);
     gridBackground.StretchToParentSize();

SetupZoom(ContentZoomer.DefaultMinScale, ContentZoomer.DefaultMaxScale);

this.AddManipulator(new ContentZoomer());//移動できるように
     this.AddManipulator(new SelectionDragger());//移動できるように
     this.AddManipulator(new ContentDragger());//移動できるように


root = new RootNode();
     AddElement(root);

var searchWindowProvider = new SearchWindowProvider();
searchWindowProvider.Initialize(this);

nodeCreationRequest += context =>
     {
          SearchWindow.Open(new SearchWindowContext(context.screenMousePosition), searchWindowProvider);
     };
}

public override List<Port> GetCompatiblePorts(Port startAnchor, NodeAdapter nodeAdapter)
     {
           return ports.ToList();
     }
}

そしたら画面に戻ってCreateNodeして見てください下の写真のようになれば成功です。

f:id:ruchi12377:20200417081740p:plain

でもこれは少しおかしくないですか?

outがstringに、stringがoutに繋がります。これを修正しましょう。f:id:ruchi12377:20200417081936p:plain

 

MyGraphViewを以下のようにします。

//MyGraphView.cs
using UnityEngine;
using UnityEngine.UIElements;//AddManipulatorを使うために必要
using System.Collections.Generic;
using UnityEditor.Experimental.GraphView;

public class MyGraphView : GraphView
{
public RootNode root;

public MyGraphView() : base()
{
styleSheets.Add(Resources.Load<StyleSheet>("BackGround"));
GridBackground gridBackground = new GridBackground();
Insert(0, gridBackground);
gridBackground.StretchToParentSize();

SetupZoom(ContentZoomer.DefaultMinScale, ContentZoomer.DefaultMaxScale);

this.AddManipulator(new ContentZoomer());//移動できるように
this.AddManipulator(new SelectionDragger());//選択できるように
this.AddManipulator(new ContentDragger());//画面を移動できるように
this.AddManipulator(new RectangleSelector());//範囲選択

root = new RootNode();
AddElement(root);

var searchWindowProvider = new SearchWindowProvider();
searchWindowProvider.Initialize(this);

nodeCreationRequest += context =>
{
SearchWindow.Open(new SearchWindowContext(context.screenMousePosition), searchWindowProvider);
};
}

public override List<Port> GetCompatiblePorts(Port startAnchor, NodeAdapter nodeAdapter)
{
var compatiblePorts = new List<Port>();
foreach (var port in ports.ToList())
{
if (startAnchor.node == port.node || startAnchor.direction == port.direction || startAnchor.portType != port.portType)
{
continue;
}

compatiblePorts.Add(port);
}
return compatiblePorts;
}
}

 

これで良い感じですね 。

 

そしたら実際に動くようにしましょう。

ProcessNode、LogNode、RootNodeを以下のようにします

//ProcessNode.cs
using UnityEditor.Experimental.GraphView;

public abstract class ProcessNode : MyNode
{
public Port InputPort;
public Port OutputPort;

public ProcessNode()
{
InputPort = Port.Create<Edge>(Orientation.Horizontal, Direction.Input, Port.Capacity.Single, typeof(Port));
InputPort.portName = "In";
inputContainer.Add(InputPort);

OutputPort = Port.Create<Edge>(Orientation.Horizontal, Direction.Output, Port.Capacity.Single, typeof(Port));
OutputPort.portName = "Out";
outputContainer.Add(OutputPort);
}

public abstract void Execute()
{

}
}

 

 

 

//LogNode.cs

using System.Linq;
using UnityEngine;
using UnityEditor.Experimental.GraphView;

public class LogNode : ProcessNode
{
private Port inputString;

public LogNode() : base()
{
title = "Log";

inputString = Port.Create<Edge>(Orientation.Horizontal, Direction.Input, Port.Capacity.Single, typeof(string));
inputContainer.Add(inputString);
}

public override void Execute()
{
var edge = inputString.connections.FirstOrDefault();
var node = edge.output.node as StringNode;

if (node == null) return;

Debug.Log(node.Text);
}
}




//RootNode.cs
using UnityEditor.Experimental.GraphView;

public class RootNode : MyNode
{
public Port OutputPort;

public RootNode() : base()
{
title = "Root";

capabilities -= Capabilities.Deletable;

OutputPort = Port.Create<Edge>(Orientation.Horizontal, Direction.Output, Port.Capacity.Single, typeof(Port));
OutputPort.portName = "Out";
outputContainer.Add(OutputPort);
}
}

 

これをRootから順に処理していきます。

しかしNodeには繋がってるNodeを取得する機能がないそうなので、Port生成時にキャッシュしておきます。

 

MyGraphViewを以下のようにします

//MyGraphView.cs
using UnityEngine;
using UnityEngine.UIElements;//AddManipulatorを使うために必要
using System.Linq;
using System.Collections.Generic;
using UnityEditor.Experimental.GraphView;

public class MyGraphView : GraphView
{
public RootNode root;

public MyGraphView() : base()
{
styleSheets.Add(Resources.Load<StyleSheet>("BackGround"));
GridBackground gridBackground = new GridBackground();
Insert(0, gridBackground);
gridBackground.StretchToParentSize();

SetupZoom(ContentZoomer.DefaultMinScale, ContentZoomer.DefaultMaxScale);

this.AddManipulator(new ContentZoomer());//移動できるように
this.AddManipulator(new SelectionDragger());//移動できるように
this.AddManipulator(new ContentDragger());//移動できるように

root = new RootNode();
AddElement(root);

var searchWindowProvider = new SearchWindowProvider();
searchWindowProvider.Initialize(this);

nodeCreationRequest += context =>
{
SearchWindow.Open(new SearchWindowContext(context.screenMousePosition), searchWindowProvider);
};
}

public override List<Port> GetCompatiblePorts(Port startAnchor, NodeAdapter nodeAdapter)
{
var compatiblePorts = new List<Port>();
foreach (var port in ports.ToList())
{
if (startAnchor.node == port.node || startAnchor.direction == port.direction || startAnchor.portType != port.portType)
{
continue;
}

compatiblePorts.Add(port);
}
return compatiblePorts;
}

public void Execute()
{
var rootEdge = root.OutputPort.connections.FirstOrDefault();
if (rootEdge == null) return;

var currentNode = rootEdge.input.node as ProcessNode;

while (true)
{
currentNode.Execute();

var edge = currentNode.OutputPort.connections.FirstOrDefault();
if (edge == null) break;

currentNode = edge.input.node as ProcessNode;
}
}
}

 

そして実行するようなボタンを作ります。

GraphEditorWindowを以下のようにします。

//GraphEditorWindow.cs
using UnityEditor;
using UnityEngine;
using UnityEngine.UIElements;

public class GraphEditorWindow : EditorWindow
{
[MenuItem("Window/Open GraphView")]
public static void Open()
{
GraphEditorWindow graphEditorWindow = CreateInstance<GraphEditorWindow>();
graphEditorWindow.Show();
graphEditorWindow.titleContent = new GUIContent("GraphEditor");
}

void OnEnable()
{
var graphView = new MyGraphView()
{
style = { flexGrow = 1 }
};
rootVisualElement.Add(graphView);
rootVisualElement.Add(new Button(graphView.Execute) { text = "Execute" });
}
}

これで完成ですね!!

 

ここまではQiitaにあるのと同じですよね。

 

ここからFieldとかについて細かく説明していきます。

Fieldについて

TextだけではなくIntやFloatEnumや自作コンポーネントもD&Dできるようにしたいですよね?

まず、公式で元々あるのは、

UnityEmgine

-UIElements

  -TextField

 

UnityEditor

-UIElememts

  -BoundsField

  -BoundsIntField

  -ColorField

  -CurveField

  -DoubleField

  -EnumField

  -FloatField

  -GradientField

  -LayerField

  -LayeraMaskField

  -ObjectField

  -TagField

  -Vector2Field

  -Vector2IntField

  -Vector3Field

  -Vector3IntField

  -Vector4Field

使えそうなのはこの辺りですね。BoolFieldがないのは少し引っかかりますが。

 

細かく解説していきます。

TextField

string型の文字を入力できるフィールドを作成できます。

f:id:ruchi12377:20200417222945p:plain

 

BoundsField

Bounds型の数字を入力できるフィールドを作成できます。

BoxCollderの大きさとか設定するやつと似ていますね。

f:id:ruchi12377:20200417224310p:plain

 

BoundsIntField

BoundsInt型の数字を入力できるフィールドを作成できます。

BoundsFieldとの違いはCenterとExtentsに少数を入力できるかどうかです。

f:id:ruchi12377:20200417224351p:plain

 

ColorField

Color32型の色を入力できるフィールドを作成できます。

f:id:ruchi12377:20200417230842p:plain

 

CurveField

Curve型の曲線を入力できるフィールドを作成できます。

f:id:ruchi12377:20200418000115p:plain

 

DoubleField

Double型の少数を入力できるフィールドです。

後述するFloatFieldより細かい値を設定できます。

f:id:ruchi12377:20200418000213p:plain

 

EnumField

Enum型のドロップダウンメニューを入力できるフィールドです。

f:id:ruchi12377:20200418000241p:plain

 

FloatField

Float型の少数を入力できるフィールドです。

f:id:ruchi12377:20200418000405p:plain

 

GradientField

Gradient型のグラデーションを入力できるフィールドです。

f:id:ruchi12377:20200418000449p:plain

 

LayerField

Layer型のレイヤーを入力できるフィールドです。

OnCollisionEnterとかで判定に使えそう。

f:id:ruchi12377:20200418000516p:plain

 

LayeraMaskField

LayerMask型のレイヤーを入力できるフィールドです。

任意のレイヤーとだけ衝突させたい時などに使えます。

f:id:ruchi12377:20200418000651p:plain

 

ObjectField

Object型のObject?を入力できるフィールドです。

こいつをうまく使えばカスタムコンポーネントもフィールドにD&Dできるようになります。これは後でじっくり解説します。

この写真はRigidbodyをフィールドに設定してる状態です。

f:id:ruchi12377:20200418000819p:plain

 

TagField

Tag型のタグを入力できるフィールドです。

f:id:ruchi12377:20200418000923p:plain

Vector2Field

Vector2型の数字を入力できるフィールドです。

f:id:ruchi12377:20200418001052p:plain

 

Vector2IntField

Vector2Fieldと違ってフィールドにIntしか入れることができません。

f:id:ruchi12377:20200418001059p:plain

 

Vector3Field

Vector3型の数字を入力できるフィールドです。

f:id:ruchi12377:20200418001216p:plain

 

Vector3IntField

Vector3Fieldと違ってフィールドにIntしか入れることができません。

f:id:ruchi12377:20200418001225p:plain

 

Vector4Field

Vector4型の数字を入力できるフィールドです。

f:id:ruchi12377:20200418001318p:plain

 

基本的にはこんな感じです。

実装方法はObjectFieldとEnumField以外は同じです。

逆にObjectFieldとEnumFieldが厄介なんですよ。

 

さっき言った二つ以外は、

↓こんな感じです。StringNodeとほぼ同じですね

using UnityEngine;
using UnityEditor.UIElements;
using UnityEditor.Experimental.GraphView;

public class TestNode : MyNode
{
private 型Field testField;
public 型 変数名 { get { return testField.value; } }

public ObjectNode() : base()
{
title = "NodeName";

var outputPort = Port.Create<Edge>(Orientation.Horizontal, Direction.Output, Port.Capacity.Multi, typeof(型));
outputContainer.Add(outputPort);

testField = new 型Field();
mainContainer.Add(testField);
}
}

 

これでOKです。

そしてvar outputPortと書いてある行のPort.Capacityというところがあると思います。Multiにすると何個でも繋げることができますが、Singleにするとひとつしか繋げることができません。

比較GIFです。

左がMultiで右がSingleです。

f:id:ruchi12377:20200418002555g:plain  f:id:ruchi12377:20200418002605g:plain

 

でもやっぱりRigidbodyとか自作コンポーネントもフィールドにD&Dしたいですよね??

そういう時はObjectFieldを使いましょう。

これがまたクセがあるんですよ。今回はRigidbodyを例に解説していきます。

さっきのように記述するとこうなると思います。しかし、みてみると

//ObjectNode.cs
using UnityEngine;
using UnityEditor.UIElements;
using UnityEditor.Experimental.GraphView;

public class ObjectNode : MyNode
{
private ObjectField objectField;
public object Text { get { return objectField.value; } }

public ObjectNode() : base()
{
title = "Object";

var outputPort = Port.Create<Edge>(Orientation.Horizontal, Direction.Output, Port.Capacity.Multi, typeof(object));
outputContainer.Add(outputPort);

objectField = new ObjectField();
mainContainer.Add(objectField);
}
}

f:id:ruchi12377:20200418003050p:plain

あれFieldは生成されてるけどno typeって出てる!?D&Dもできません。

ハマりましたね.....

 

これで1日くらい悩んでました。しかし、公式のリファレンスにはobjectTypeという物で型を指定できると書いてあったのでこうしてみました。

//ObjectNode.cs
using UnityEngine;
using UnityEditor.UIElements;
using UnityEditor.Experimental.GraphView;

public class ObjectNode : MyNode
{
private ObjectField objectField;
public object Text { get { return objectField.value; } }


     public ObjectNode() : base()
     {
title = "Object";

var outputPort = Port.Create<Edge>(Orientation.Horizontal, Direction.Output, Port.Capacity.Multi, typeof(object));
outputContainer.Add(outputPort);

objectField = new ObjectField();
objectField.objectType = typeof(Rigidbody);
mainContainer.Add(objectField);
}
}

f:id:ruchi12377:20200418003334p:plain

無事解決しましたね!

 

ここまできたらEnumとかも使いたいですよね。

 

まず普通書いてみます。

//EnumField.cs
using System;
using UnityEditor.UIElements;
using UnityEditor.Experimental.GraphView;

public class EnumNode : MyNode
{
private EnumField enumField;
public int Text { get { return Convert.ToInt32(enumField.value); } }
public Jyanken jyanken;

public enum Jyanken
{
Gu,
Tyoki,
Pa,
}

public EnumNode() : base()
{
title = "Enum";

var outputPort = Port.Create<Edge>(Orientation.Horizontal, Direction.Output, Port.Capacity.Multi, typeof(int));
outputContainer.Add(outputPort);

enumField = new EnumField();
mainContainer.Add(enumField);
}
}

 

選択するEnum、今回はJyankenというものを作ってあげて、画面を見てみると、

f:id:ruchi12377:20200418004030p:plain

なんか...あるんだよね。でもGuとかはないし、選べないですね。

うーん困りましたね....

さっきのObjectFieldのようにobjectTypeならぬSetEnumというものもありませんし。

丸一日ハマりました。

 

実はEnumFieldにInitというものがあり第一引数で選択したいenumを設定すると選べるようになるんですね〜Initは初期化って意味らしいです。

もっとわかりやすい名前にして欲しかったです....

 

コードです。

//EnumField.cs
using UnityEditor.UIElements;
using UnityEditor.Experimental.GraphView;

public class EnumNode : MyNode
{
private EnumField enumField;
public int Text { get { return (int)enumField.userData; } }
public Jyanken jyanken;

public enum Jyanken
{
Gu,
Tyoki,
Pa,
}
public EnumNode() : base()
{
title = "Enum";

var outputPort = Port.Create<Edge>(Orientation.Horizontal, Direction.Output, Port.Capacity.Multi, typeof(int));
outputContainer.Add(outputPort);

enumField = new EnumField();
enumField.Init(jyanken);
mainContainer.Add(enumField);
}
}

f:id:ruchi12377:20200418004528p:plain

無事ドロップダウンを表示することができました!


 Audio再生とかをしたい

Logを出す以外にもEnumFiledとかObjectFieldを使って何かやってみたいですよね?

そこで今回はAudioの再生をやってみます。

まずAudioClipをD&DするためのNodeを作ります。

//AudioClipNode.cs
using UnityEngine;
using UnityEditor.UIElements;
using UnityEditor.Experimental.GraphView;

public class AudioClipNode : MyNode
{
private ObjectField audioClipField;
public AudioClip value { get { return audioClipField.value as AudioClip; } }

public AudioClipNode() : base()
{
title = "AudioClip";

var outputPort = Port.Create<Edge>(Orientation.Horizontal, Direction.Output, Port.Capacity.Multi, typeof(AudioClip));
outputContainer.Add(outputPort);

audioClipField = new ObjectField();
audioClipField.objectType = typeof(AudioClip);
mainContainer.Add(audioClipField);
}
}

 

次にAudioSourceをD&DするためのNodeを作ります

 

//AudioSourceNode.cs
using UnityEngine;
using UnityEditor.UIElements;
using UnityEditor.Experimental.GraphView;

public class AudioSourceNode : MyNode
{
private ObjectField audioSourceField;
public AudioSource value { get { return audioSourceField.value as AudioSource; } }

public AudioSourceNode() : base()
{
title = "AudioSource";
var outputPort = Port.Create<Edge>(Orientation.Horizontal, Direction.Output, Port.Capacity.Multi, typeof(AudioSource));
outputContainer.Add(outputPort);

audioSourceField = new ObjectField();
audioSourceField.objectType = typeof(AudioSource);
mainContainer.Add(audioSourceField);
}
}

 

最後にAudioを再生したりするAudioEventNodeを作成しましょう。

このNodeはStringNodeやLogNodeと違って、自分にもField(Enum)がありますが、InputPortもあるということです。

 

//AudioEventNode.cs
using System;
using System.Linq;
using UnityEngine;
using UnityEditor.UIElements;
using UnityEditor.Experimental.GraphView;

public class AudioEventNode : ProcessNode
{
private Port inputClip;
private Port inputSource;

private EnumField enumField;
public int value { get { return Convert.ToInt32(enumField.value); } }

enum EventType
{
Play,
Stop,
Pause,
UnPause,
Loop,
};

public AudioEventNode() : base()
{
title = "AudioEvent";

inputClip = Port.Create<Edge>(Orientation.Horizontal, Direction.Input, Port.Capacity.Single, typeof(AudioClip));
inputContainer.Add(inputClip);

inputSource = Port.Create<Edge>(Orientation.Horizontal, Direction.Input, Port.Capacity.Single, typeof(AudioSource));
inputContainer.Add(inputSource);

enumField = new EnumField();
EventType eventType = EventType.Play;
enumField.Init(eventType);
mainContainer.Add(enumField);
}

public override void Execute()
{
AudioClip audioClip = null;
AudioSource audioSource = null;
if (inputClip.connections.FirstOrDefault() != null)
{
Edge Clip_edge = inputClip.connections.FirstOrDefault();
var Clip_node = Clip_edge.output.node as AudioClipNode;
audioClip = Clip_node.value;
}

if (inputSource.connections.FirstOrDefault() != null)
{
Edge Source_edge = inputSource.connections.FirstOrDefault();
var Source_node = Source_edge.output.node as AudioSourceNode;
audioSource = Source_node.value;
}

if (audioClip != null && audioSource != null)
{
audioSource.clip = audioClip;
Play(audioSource, audioClip, value);
}
}


private static void Play(AudioSource source, AudioClip clip, int state)
{
source.clip = clip;
source.playOnAwake = false;
source.mute = false;
source.loop = false;

if (state == 0)
{
source.Play();
}
if (state == 1)
{
source.Stop();
}
if (state == 2)
{
source.Pause();
}
if (state == 3)
{
source.UnPause();
}
if (state == 4)
{
source.loop = true;
source.Play();
}
}
}

これでExecuteを押したら音が再生されるようになったでしょうか?

f:id:ruchi12377:20200418184224p:plain

 

今回はここまでです。もう一回言いますが、これはExperimentalなので、将来的に削除、変更される可能性があります。

 

まとめ

これで、ノードベースでスクリプトを作成できるものを作ろうと思った人はいませんか?僕ですね。しかし、もんりぃ先生もんりぃ先生 (@monry) | Twitter)という方がUniflowというものを開発しています。自作するのもロマンがあっていいと思いますがさすがって感じですよね。Uniflowについては青木ととさんが記事にしてくれています。使いたいと思った方はぜひこちらの記事を参考にしてみてください。

[Unity] ノードベースでUnityAPIを制御するライブラリUniFlowを触ってみる - Qiita

 

お疲れ様でした!!

 

Github

中の人が実際に作ってる途中のやつです。

参考程度に〜

github.com

引用

github.com

github.com

qiita.com

www.youtube.com

docs.unity3d.com