TECH PLAY

電通総研

電通総研 の技術ブログ

822

こんにちは、金融ソリューション事業部の岡崎、若本です。 3DCG空間では、オブジェクトに関する情報を過度に表示し過ぎることはユーザー体験を阻害する要因になります。その一方で、あまりに情報が少ないと世界観の欠如に繋がります。そのため、質問応答などのユーザーが求める情報を必要なだけ表示する仕組みが求められることがあります。 そこで今回は、 Unreal Engine (UE)上のオブジェクトの情報についてChatGPTに問い合わせる実装を行ってみたいと思います。 下図のように、あらかじめ 格納した関連文献を基に、UE上でObjectに対する質問をChatGPTに答えさせる ことがゴールです。 準備するもの 処理概要 手順①. GPTを用いたAPIを作成 1.1 APIの環境構築 1.2 エンドポイントの作成 1.3 機能の作成 1.4 動作確認(データ登録) 1.5 動作確認(検索) 手順②. オブジェクトの準備 2.1 チャット可能な範囲に入ったとき、アイコンを表示する機能 2.2 オブジェクト(Actor)をクリックしてチャット画面を表示する機能 手順③. チャットUIの実装 3.1 チャットのページUIを実装 3.2 チャットの吹き出しUIを実装 3.2.1 自分のチャットを表示する 3.2.2 GPTの返信を表示する 手順④. HTTPリクエスト用のC++クラス作成 4.1 C++クラスを作成 4.2 C++クラスを編集 4.3 作成したクラスをブループリント化 手順⑤. 質問送信時の処理を実装 5.1 「送信」ボタンが押されたとき、送信した内容を表示する 5.2 HTTPリクエストを送信する 5.3 GPTの返信内容を表示する デモ動画 おわりに 準備するもの Unreal Engine 5.2.0 OpenAI API GPT-3.5-turbo(0301)を使用します。 事前にユーザー登録と、 API キーの発行を実施しておきます。 FastAPI API として、下記の機能を作成します。 オブジェクトに関連したドキュメントを登録する オブジェクト名と質問を与え、答えを返す Docker API の環境構築に使用します。 処理概要 下記の処理を実装します。 それぞれの機能について、()内の担当者が作業を行いました。 コリジョン 判定(岡崎) チャット可能な範囲に入ったとき、アイコンを表示します。 チャットUIの表示(岡崎、若本) コリジョン 状態でオブジェクト(Actor)をクリックし、チャット画面を表示します。 GPTへのリク エス ト(若本) UE上の情報を基に、GPTにリク エス トを送ります。 結果の表示(若本) API から返ってきたレスポンスをUIに反映します。 以降では、各機能の概要や実装について説明します。 手順①. GPTを用いた API を作成 本手順は若本が説明します。 GPTはUE内のオブジェクトについて情報を持たないため、そのままChatGPTに問い合わせても的確に質問に答えることができません。 そこで、以下の2つの機能を持つ API を作成します。今回は Unreal Engine を実行するPC上で構築することとしました。 オブジェクトに関連したドキュメントを登録する(インデックス登録) オブジェクト名と質問を与え、答えを返す(問い合わせ) それぞれの機能についてエンドポイントを用意し、HTTPリク エス トで実行できるようにします。 最終的には下記のようなファイル構成となります。 1.1 API の環境構築 まずはDockerで API のベースとなる環境を作成します。 以下は最終的に使用したDockerの設定ファイルです。 フォルダ上でdocker containerをupするとアプリケーションが起動します( http://localhost:8000/docs/ からアクセスすることができます)。 docker-compose.yml(クリックで表示) version: '3' services: backend: container_name: gpt-api build: context: . dockerfile: ./Dockerfile volumes: \- type: bind source: './api' target: '/src' ports: \- 8000:8000 stdin_open: true Dockerfile(クリックで表示) FROM python:3.8-slim-buster RUN pip install --upgrade pip RUN pip install --upgrade setuptools COPY requirements.txt / RUN pip install -r requirements.txt WORKDIR /src CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--reload"] requirements.txt(クリックで表示) numpy openai transformers llama_index langchain fastapi uvicorn[standard] pandas 1.2 エンドポイントの作成 次に、 API が持つ2つの機能についてエンドポイントを作成していきます。 routers/index.pyにインデックス登録のエンドポイントを、routers/search.pyに問い合わせ用のエンドポイントを記述しています。 main.py(クリックで表示) from fastapi import FastAPI from routers import search, index import os os.environ[ 'OPENAI_API_KEY' ] = 'YOUR_OPENAI_API_TOKEN' app = FastAPI() app.include_router(search.router) app.include_router(index.router) routers/index.py(クリックで表示) from fastapi import APIRouter from functions.index_operation import create_index router = APIRouter() @ router.post ( "/index" ) def service_create_index (dir_name: str ): _ = create_index(dir_name) return 200 routers/search.py(クリックで表示) from fastapi import APIRouter from pydantic import BaseModel from functions.search_operation import search router = APIRouter() class SearchRequestItem (BaseModel): dir_name: str = None query: str = None class SearchResponseItem (BaseModel): text: str = None @ router.post ( "/search" ) def service_search (item: SearchRequestItem) -> SearchResponseItem: response_text = search(item.dir_name, item.query) response = SearchResponseItem(text=response_text) return response 1.3 機能の作成 最後に、それぞれのエンドポイントで呼び出される処理を記述します。 functions/index_operation.pyにインデックス登録の処理を、functions/search_operation.pyに問い合わせの処理を実装しました。functions/ api _handling.pyでは、使用するmodel(gpt-3.5-turbo)の設定を行っています。 ここで、インデックス登録はドキュメントをあらかじめベクトルにしておき、問い合わせが来た際に検索するために行います。 今回の実装では、フォルダ内の CSV /PDF/TXTファイルについて読み込みベクトル化できるようにしました。 ※データの大きさや権限によってエラーが発生する可能性はありますが、今回そのエラーハンドリングまでは行っていません。 functions/api_handling.py(クリックで表示) from llama_index import LLMPredictor, ServiceContext, PromptHelper from langchain.chat_models import ChatOpenAI def get_service_context (): llm_predictor = LLMPredictor( llm=ChatOpenAI( temperature= 0 , model_name= "gpt-3.5-turbo" , max_tokens= 512 ) ) prompt_helper = PromptHelper( max_input_size= 4096 , num_output= 256 , max_chunk_overlap= 20 ) service_context = ServiceContext.from_defaults(llm_predictor=llm_predictor, prompt_helper=prompt_helper) return service_context functions/index_operation.py(クリックで表示) from llama_index import GPTVectorStoreIndex, SimpleDirectoryReader, download_loader from functions.api_handling import get_service_context import glob def create_index (dir_name: str ): input_dir = f 'data/{dir_name}' service_context = get_service_context() index = None all_files = glob.glob(f '{input_dir}/*' ) for file in all_files: if file .endswith( '.csv' ): Reader = download_loader( "SimpleCSVReader" ) loader = Reader() docs = loader.load_data( file = file ) if file .endswith( '.pdf' ): Reader = download_loader( "CJKPDFReader" ) loader = Reader() docs = loader.load_data( file = file ) if file .endswith( '.txt' ): loader = SimpleDirectoryReader(input_files=[ file ], required_exts=[ 'txt' ]) docs = loader.load_data() if index is None : index = GPTVectorStoreIndex.from_documents( docs, service_context=service_context ) else : for doc in docs: index.insert(doc) # indexを保存する if index: index.storage_context.persist(f "index/{dir_name}" ) return index functions/search_operation.py(クリックで表示) from llama_index import StorageContext, load_index_from_storage from functions.api_handling import get_service_context def load_index (dir_name: str ): \ # インデックスの読み込み storage_context = StorageContext.from_defaults(persist_dir=f "index/{dir_name}" ) index = load_index_from_storage(storage_context, service_context=get_service_context()) return index def search (dir_name: str , query: str ): index = load_index(dir_name) \ # クエリエンジンの作成 query_engine = index.as_query_engine() text_query = f '下記の質問に対して、敬語で簡潔に答えてください。 \n {query}' response = query_engine.query(text_query) return response.response 1.4 動作確認(データ登録) 上記のコードを記述後、動作確認を実施します。 アプリを立ち上げ後、以下のURLにアクセスすることでSwagger UI(下図)が表示され、直接動作確認をすることができます。 http://localhost:8000/docs/ まずはデータを登録してみましょう。 dataフォルダ下に新しいフォルダを作成します。ここでは、「BookInfo」というフォルダを作成しました。 なお、検索時にもこのフォルダ名を使用します。 ※ フォルダ名をもとにした管理やリク エス トは少し危険ですが、今回は簡単な検証のため実装を簡略化しています。 フォルダ作成後、「BookInfo」フォルダ内に CSV /PDF/TXTファイルを格納します。 今回は自作のsample.txtを格納しました。ファイルの中身は下記のようになっています。 格納後、 http://localhost:8000/docs/ にアクセスし、/indexから下記のようにインデックス登録を実行します。 indexフォルダ下に同名のフォルダが作成されます。 下記の3種類の json ファイルが入っていれば正常にインデックスが登録できています。 1.5 動作確認(検索) 検索についても動作確認してみます。 http://localhost:8000/docs/ にアクセスし、/searchを下記のように実行してみます。 少しするとResponseが返ってきます。登録した文章の情報を基に答えられており、動作が確認できました。 複数インデックスを登録した場合はdir_nameのパラメータを変えることで、検索対象を変えて回答させることができます。 手順②. オブジェクトの準備 次に、UE上で使用するオブジェクトの準備を行っていきます。本手順は岡崎が説明します。 ここでは以下の機能や画面を作成します。 チャット可能な範囲に入ったとき、アイコンを表示する機能 オブジェクト(Actor)をクリックしてチャット画面を表示する機能 それでは各機能について説明していきます。 2.1 チャット可能な範囲に入ったとき、アイコンを表示する機能 今回はゲームでよくあるような、プレイヤーがオブジェクトに一定の距離近づくと、「ここにアイテムがあります」という意味のアイコンが表示される処理の作成。 また、さらに近づくと「アイテムにクリックできます」という意味のアイコンに変更し、アイテムがクリックできるようになる処理を作っていきます。 まずはプレイヤーがオブジェクトに近づいたことを判定する処理を作成します。 以前 こちらの記事(UE5でコリジョン(衝突)判定機能を使って色々な機能を作成してみた) でも紹介しましたが、 コリジョン 機能を使っていきます。 まずはアクターブループリントを作成し「BP_P_Item」と 命名 します。 コンポーネント 追加から、「 Sphere Collision」を2つ追加し、それぞれ「BigSphere」と「SmallSphere」とします。 この2つの コリジョン は、「BigSphere」にプレイヤーが入った時、アイテムの上にアイコンを表示し、「SmallSphere」に入った時、アイコンを変更してアイテムをクリックできる状態に変更するために使用します。 上記を再現するため、「BigSphere」を大きく作成し、その中に「SmallSphere」を配置しています。 次にアイテムの上に表示するアイコンを作成します。 ユーザーインターフェース から ウィジェット ブループリントを作成し、「WBP_ItemIcon」と 命名 します。 作成後、下記画像のようにアイコンの ウィジェット を作成し、「Is Variable」にチェックを入れておきます。 次に コリジョン 用のブループリント(BP_P_Item)に戻り、「SmallSphere」の下に「Static Mesh」と「 Widget 」の コンポーネント を追加します。 画像では Widget の名前をWidgetItemIconに変更しています。 「 Widget 」の コンポーネント の詳細タブより、「 Widget Class」を先ほど作成した ウィジェット ブループリントの名前(WBP_ItemIcon)に変更します。 また、同じ詳細タブより「 レンダリング 」内の「Visible」のチェックを外しておきます。 次にイベントグラフに移動し、「On Component Begin Overlap(BigSphere)」と「On Component End Overlap(BigSphere)」のノードを作成します。 このノードは、左側の コンポーネント 一覧から「BigSphere」を右クリックし、イベント追加から選ぶことができます。 下記画像のように、プレイヤーが「BigSphere」内に入った際に、デフォルトで見えない設定にしておいたアイコン ウィジェット の「Visibility」の値を変更し、画面上に レンダリング させる処理を作成します。 同様にプレイヤーが「BigSphere」からプレイヤーが出た際にアイコンを見えなくする処理も追加します。 ここまでで、下記のようにプレイヤーの位置によってアイコンが表示されたり、消したりする処理ができました。 続いてさらに近づいた際(SmallSphereに入った際)にアイコンの表示を変更する処理を作成します。 アイコン用のブループリント(WBP_ItemIcon)に戻り、アイコンを変更するための関数を「AreaEvent」という名前で作成します。 「AreaEvent」には引数として「IsInside」というBoolean値を設定しておきます。 下記画像のように「AreaEvent」から「IsInside」の値によってイメージ画像を変更するために「Set Brush from Texture」ノードを繋ぎます。 「選択する」ノードに、Falseの場合にはSmallSphereに入っていない時用の画像を設定し、Trueの場合はSmallSphereに入っている時用の画像を設定します。 (本プロジェクトの場合は下記画像の「arrow」と「circle」を使用) 「Set Brush from Texture」のターゲットには ウィジェット ブループリントでimageを作成した際に 命名 した名前の変数が左側のタブに追加されているのでそれを使用します。 次に コリジョン 用のブループリント(BP_P_Item)に戻り、「SmallSphere」に入った際にアイコン ウィジェット の画像を変えるために「AreaEvent」の関数を使用する処理を作成します。 「SmallSphere」から「On Component Begin Overlap(SmallSphere)」と「On Component End Overlap(SmallSphere)」のノードを作成します。 下記画像のように「On Component Begin Overlap(SmallSphere)」と「On Component End Overlap(SmallSphere)」を「Area Event」繋げます。 「Area Event」の引数としてプレイヤーが「SmallSphere」の内部にいるかどうかの値を変数にして「In Small Collision Area」とします。 (後述の処理で使用するため変数にしておきます。) ここまでの処理で「BigSphere」の内部にいる時に矢印のアイコンを出現させ、「SmallSphere」の内部にいる時に二重丸のアイコンに変更させる処理ができました。 2.2 オブジェクト(Actor)をクリックしてチャット画面を表示する機能 続いて実際にオブジェクトを配置し、「SmallSphere」の内部にプレイヤーがいる時のみオブジェクトにクリックができる機能と、クリックした後にチャット画面を表示する機能を作成します。 まず初めに、プレイ画面上にマウスカーソルを出現させ、クリックを有効にするために、レベルブループリントを開きます。下の画像のように「イベントBeginPlay」に「Show Mouse Cursor」と「Enable Click Events」の設定をどちらもオンに変更します。 これでプレイ画面上にマウスカーソルを出現させ、クリックができるように設定されました。 次に、先ほど作成した コリジョン 用のブループリント(BP_P_Item)から子ブループリントを作成します。 親のブループリント上で右クリックを行い、「子ブループリントクラスを作成します」ボタンを押し「BP_C_Item_Book」と 命名 します。 子ブループリントを開き、親ブループリント作成時は特に編集しなかった「Static Mesh」に任意のメッシュ素材を選びます。 今回は「Quixel Bridge」内のメッシュを使用しました。 こちらの記事(Unreal Engine 5 を使ってワールドの地形を作成してみました) で、「Quixel Bridge」の使用方法を紹介しています。 イベントグラフを開き、「イベント ActorOnClicked」のノードを追加します。 「In Small Collision Area」の値からIF文を作成し、プレイヤーが「Small Collision」の中にいる場合のみチャット画面用の ウィジェット を作成するために「 ウィジェット を作成」ノードを繋げ、「Add Viewport」を付けます。 ここでチャット画面用の ウィジェット ブループリントを作成します。 (チャット機能の詳細は後述するので、ここでは大枠だけ作成します。) クリックしたいオブジェクト用(今回は本のオブジェクト)の ウィジェット ブループリントを作成し、「WBP_BookInfo」と 命名 します。 「 Canvas Panel」内に、本のタイトルや、サムネイル用画像を置き、チャット用のスペースも確保します。 こちらの記事(UE5でコリジョン(衝突)判定機能を使って色々な機能を作成してみた) で、 ウィジェット の作成方法について触れているので参考にしてください。 今回は閉じるボタン(×マーク)を押すことで ウィジェット を閉じる挙動にしたいので、「 Canvas Panel」内に、ボタンも追加します。 パレット追加欄に「Button」と検索し、ボタン内部に「Text」で「×」を記述し閉じるボタンを作成しました。 (ボタンの名前を「CloseButton」に変更してあります。) 階層タブより「CloseButton」を選択中に、詳細タブ内のイベント>On Click の追加ボタンを押し、イベントグラフに「On Click(CloseButton)」ノードを追加します。 「On Click(CloseButton)」ノードに「Remove from Parent」を繋ぎ、閉じるボタンが押された時チャット画面用の ウィジェット が閉じるようにします。 「BP_C_Item_Book」を再び開き、「 ウィジェット を作成」ノードの「Class」を今作成した「WBP_BookInfo」に変更します。 これで画面上の本のオブジェクトをクリックした際に、画面上にチャット画面の ウィジェット が現れ、閉じるボタンで閉じる処理ができました。 ただこのままだと、本のオブジェクトをクリックした分だけ ウィジェット が開いてしまうので、現在 ウィジェット が開かれている状態かどうかを判別するために「IsOpenDetailWidget」という変数を作成し、下記画像のようにフラグとして使用します。 「WBP_BookInfo」でチャット画面を閉じた後で「IsOpenDetailWidget」の変数をFalseに戻しておきます。 ここまでで、オブジェクト(Actor)をクリックしてチャット画面を表示する機能が完成しました。 手順③. チャットUIの実装 ここまでで、ベースとなるUEのオブジェクトやUI、外部 API の作成が完了しました。 以降の手順は若本が説明します。 本手順では、GPTのリク エス トとのやり取りに必要になる追加のUIを用意していきます。 3.1 チャットのページUIを実装 次に、チャットのやり取りを行うためのUIを作成します。 チャットの 吹き出し は動的に追加されていくため、ここでは必要な コンポーネント のみ用意します。 まずはチャット画面のベースとして、手順②で作成した「WBP_BookInfo」にText_box、button、scroll boxを追加します。 Text_boxとscroll boxをわかりやすい変数名に変更しておきます。 また、呼び出すために「Is Variable」にチェックを入れておきます。 次に、buttonにOnClickイベントを追加します。 詳細画面のOnClickを押すことで、OnClickイベントが有効になります。こちらを押しておきましょう。 詳細な処理は後の手順で作成します。 また、ここでItemの属性値を設定しておきます。 属性値を用いて後段でGPTに問い合わせる際の検索対象を変更するため、その下準備となります。 まずは Widget の「WBP_BookInfo」に変数「WidgetAttributes」を作成します。 次に、「BP_C_ItemBook」クラスに属性値を設定しておきます。ここでは変数名を「ItemAttributes」、デフォルト値を「BookInfo」としました。 最後に、「BP_C_ItemBook」のブループリント上で、 ウィジェット の作成時に自身の持つ「ItemAttributes」を「WidgetAttributes」にセットする処理を追加します。これで完成です。 3.2 チャットの 吹き出し UIを実装 3.1でやりとりを行う画面は作成しましたが、メッセージを入れるための 吹き出し がありません。 そこで、別途チャットの 吹き出し 用UI(自分用/GPT用)を作成します。 それぞれ ユーザーインターフェース から ウィジェット ブループリントを作成しましょう。 作成後、 Canvas PanelとText Box(Multi-Line)を配置します。 こちらもText Boxはわかりやすい変数名に変更し、「Is Variable」にチェックを入れておきます。 3.2.1 自分のチャットを表示する 自分用の 吹き出し は下記のように設定しました。背景色とFontの色/サイズを調整しています。 3.2.2 GPTの返信を表示する GPT用の 吹き出し も同様に作成します。 手順④. HTTPリク エス ト用の C++ クラス作成 4.1 C++ クラスを作成 Unreal Engine では、デフォルトでHTTPへのリク エス トを送信するクラスは用意されていないため、 API にリク エス トを送り、レスポンスを受け取るクラスを新規作成する必要があります。 ブループリントでは実現できないため、今回は C++ で実装することとしました。 まずは、ツールから新規の C++ クラスを作成します。 ここで、親クラスは「Actor」、名前は「HTTPActor」としました。 クラスを作成すると、エディタに飛ばされます。エディタ上でクラスを編集していきます。 4.2 C++ クラスを編集 API にリク エス トを送り、レスポンスを受け取るための処理を追加していきます。 まずは、Build.csのDependencyModuleに"HTTP", " Json ", "JsonUtilities"の3つを追加します。 Build.cs(クリックで表示) // Fill out your copyright notice in the Description page of Project Settings. using UnrealBuildTool; public class TechBlog9 : ModuleRules { public TechBlog9(ReadOnlyTargetRules Target) : base(Target) { PCHUsage = PCHUsageMode.UseExplicitOrSharedPCHs; PublicDependencyModuleNames.AddRange(new string[] { "Core", "CoreUObject", "Engine", "InputCore", "HTTP", "Json", "JsonUtilities" }); PrivateDependencyModuleNames.AddRange(new string[] { }); // Uncomment if you are using Slate UI // PrivateDependencyModuleNames.AddRange(new string[] { "Slate", "SlateCore" }); // Uncomment if you are using online features // PrivateDependencyModuleNames.Add("OnlineSubsystem"); // To include OnlineSubsystemSteam, add it to the plugins section in your uproject file with the Enabled attribute set to true } } また、Engine.iniの末尾にHTTPリク エス トの設定を記載しておきます。 Engine.ini(クリックで表示) [HTTP] HttpTimeout=60 HttpConnectionTimeout=5 HttpReceiveTimeout=-1 HttpSendTimeout=-1 HttpMaxConnectionsPerServer=32 bEnableHttp=true bUseNullHttp=false HttpDelayTime=0.1 上記が終わったら、HTTPActor.hクラスを変更します。 筆者はEpicのcommunityを参考に#include "CoreMinimal.h"を コメントアウト しました。 後ほどブループリント上で呼び出すため、HTTPリク エス トを実行するHttpMethodはUFUNCTIONを、結果を格納する変数である outputStringにはUPROPERTYを設定しておきます。 HTTPActor.h(クリックで表示) // Fill out your copyright notice in the Description page of Project Settings. #pragma once // #include "CoreMinimal.h" #include "GameFramework/Actor.h" #include "Http.h" #include "HTTPActor.generated.h" UCLASS() class TECHBLOG9_API AHTTPActor : public AActor { GENERATED_BODY() public: // Sets default values for this actor's properties AHTTPActor(); protected: // Called when the game starts or when spawned virtual void BeginPlay() override; public: // Called every frame virtual void Tick(float DeltaTime) override; FHttpModule* Http; UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = Default) FString outputString; // HTTP通信 UFUNCTION(BlueprintCallable, Category = "Http") void HttpMethod(FString query, FString dir_name); // レスポンス後のイベント処理 void OnResponseReceived(FHttpRequestPtr Request, FHttpResponsePtr Response, bool bWasSuccessful); }; 最後に、HTTPActor.cppに具体的な処理内容を記述します。 手順①で実装した API の仕様に合わせ、dir_name(インデックスのフォルダ名)とquery(質問)の2つを引数としてHTTPリク エス トを送り、返ってきた Json の「text」をoutputStringに格納する処理を追加しました。 HTTPActor.cpp(クリックで表示) // Fill out your copyright notice in the Description page of Project Settings. #include "HTTPActor.h" // Sets default values AHTTPActor::AHTTPActor() { // Set this actor to call Tick() every frame. You can turn this off to improve performance if you don't need it. PrimaryActorTick.bCanEverTick = true; Http = &FHttpModule::Get(); } // Called when the game starts or when spawned void AHTTPActor::BeginPlay() { Super::BeginPlay(); } // Called every frame void AHTTPActor::Tick(float DeltaTime) { Super::Tick(DeltaTime); } void AHTTPActor::HttpMethod(FString query, FString dir_name) { // Jsonデータ作成 TSharedPtr<FJsonObject> JsonObject = MakeShareable(new FJsonObject); JsonObject->SetStringField("dir_name", dir_name); JsonObject->SetStringField("query", query); // OutputStringに Json 格納 FString OutputString; TSharedRef<TJsonWriter > JsonWriter = TJsonWriterFactory<>::Create(&OutputString); FJsonSerializer::Serialize(JsonObject.ToSharedRef(), JsonWriter); // HTTPリク エス ト TSharedRef Request = Http->CreateRequest(); Request->OnProcessRequestComplete().BindUObject(this, &AHTTPActor::OnResponseReceived); Request->SetURL(" http://localhost:8000/search "); Request->SetVerb("POST"); Request->SetHeader(TEXT("User-Agent"), "X-UnrealEngine-Agent"); Request->SetHeader("Content-Type", TEXT("application/ json ")); Request->SetContentAsString(OutputString); Request->ProcessRequest(); } void AHTTPActor::OnResponseReceived(FHttpRequestPtr Request, FHttpResponsePtr Response, bool bWasSuccessful) { TSharedPtr<FJsonObject> JsonObject; TSharedRef<TJsonReader<>> Reader = TJsonReaderFactory<>::Create(Response->GetContentAsString()); if (FJsonSerializer::Deserialize(Reader, JsonObject)) { // Json からtextを抽出 outputString = JsonObject->GetStringField("text"); } } 上記の変更が終わったらbuildします。 筆者は VS Code で編集していたため、Launch<project_name>Editor(development)でbuildしました。 Unreal Engine が起動され、projectが表示されれば C++ クラスの作成は完了です。 4.3 作成したクラスをブループリント化 最後に、作成した C++ クラスをブループリントから呼び出すため、Blueprintクラスを作成します。 名前は「BP_HTTPActor」としました。 作成した「BP_HTTPActor」をワールドに配置すれば完了です。 手順⑤. 質問送信時の処理を実装 ここまででオブジェクトの準備、GPTの準備、UIの準備が全て終わりました。 最後に、Brueprint上ですべてを繋ぎこむ処理を「BP_C_Item_Book」のブループリント上で実装します。 以下が本手順で作成したブループリントの全体像です。 長くて見づらいため、それぞれ分けて解説します。(見やすさのため、拡大時にレイアウトを調整しています) 3.1で作成したOnclickイベント(チャットの送信ボタン押下)を起点としてブループリントを作成しました。 5.1 「送信」ボタンが押されたとき、送信した内容を表示する 送信ボタンが押下されると、TextBoxの値を取得して新しい 吹き出し として表示します。 ここでは、3.2.1で作成した自分用の 吹き出し を作成し、TextBoxの値を入れてScroll Boxの中に入れています。 5.2 HTTPリク エス トを送信する 4.3で配置した「BP_HTTPActor」クラスを呼び出し、HTTPMethodを実行します。ユーザーの入力と ウィジェット の持つ変数「WidgetAttributes」の値をGPTの API 側へリク エス トとして送信しています。 このとき、outputStringの変数にGPTのレスポンスが格納されるまでは、Delayとloopを使用してwaitします。 また、HTTPリク エス トを送った後、自身が入力したテキストをクリアする処理を記述しました。 5.3 GPTの返信内容を表示する 5.1と同様、今度はGPT用の 吹き出し を作成し、outputStringの値を入れてScroll Boxの中に格納します。 最後に、outputStringの変数をクリアすれば処理は完了です。 デモ動画 本のオブジェクトに近づくことでタッチが可能になり、タッチすることでUIが表示されます。 さらにUI上で問い合わせると、オブジェクトの関連文章を基に回答できていることを確認できます。 これまでの実装により、ストレスの少ない問い合わせをUE上で実現することができました。 おわりに 今回はチャットUIを用いた、オブジェクトに対する問い合わせを実装しました。 以下、実装を担当した岡崎と若本のそれぞれの所感となります。 岡崎:今回の実装で、ブループリントでのチェンジイベント(オンクリックなど)や、 コリジョン イベントに関する使用方法の基礎的な学習を行えました。今後プロダクトを作成する際、プレイヤー起因で任意の情報を出したり、プレイヤー情報を変更させるといった機能を作成する際に、数多く使うことになる機能を作成することができたため、より応用的な使い方など引き続き調査したいと思います。 若本:UE初学者であり、今回広く Python の実装/ブループリントの実装/ C++ の実装を行いましたが、慣れて以降はストレスレスに開発を行うことができました。今回の実装を発展させ、非同期処理の実装や会話履歴の入れ込み、UIのリッチ化など、さらに作りこむことによってよりよいユーザー体験が得られることが期待できると感じました。 現在ISIDは web3領域のグループ横断組織 を立ち上げ、Web3および メタバース 領域のR&Dを行っております(カテゴリー「3DCG」の記事は こちら )。 もし本領域にご興味のある方や、一緒にチャレンジしていきたい方は、ぜひお気軽にご連絡ください! 私たちと同じチームで働いてくれる仲間を、是非お待ちしております! ISID採用ページ 執筆: @wakamoto.ryosuke 、レビュー: @yamada.y ( Shodo で執筆されました )
アバター
こんにちは、金融ソリューション事業部の岡崎、若本です。 3DCG空間では、オブジェクトに関する情報を過度に表示し過ぎることはユーザー体験を阻害する要因になります。その一方で、あまりに情報が少ないと世界観の欠如に繋がります。そのため、質問応答などのユーザーが求める情報を必要なだけ表示する仕組みが求められることがあります。 そこで今回は、 Unreal Engine (UE)上のオブジェクトの情報についてChatGPTに問い合わせる実装を行ってみたいと思います。 下図のように、あらかじめ 格納した関連文献を基に、UE上でObjectに対する質問をChatGPTに答えさせる ことがゴールです。 準備するもの 処理概要 手順①. GPTを用いたAPIを作成 1.1 APIの環境構築 1.2 エンドポイントの作成 1.3 機能の作成 1.4 動作確認(データ登録) 1.5 動作確認(検索) 手順②. オブジェクトの準備 2.1 チャット可能な範囲に入ったとき、アイコンを表示する機能 2.2 オブジェクト(Actor)をクリックしてチャット画面を表示する機能 手順③. チャットUIの実装 3.1 チャットのページUIを実装 3.2 チャットの吹き出しUIを実装 3.2.1 自分のチャットを表示する 3.2.2 GPTの返信を表示する 手順④. HTTPリクエスト用のC++クラス作成 4.1 C++クラスを作成 4.2 C++クラスを編集 4.3 作成したクラスをブループリント化 手順⑤. 質問送信時の処理を実装 5.1 「送信」ボタンが押されたとき、送信した内容を表示する 5.2 HTTPリクエストを送信する 5.3 GPTの返信内容を表示する デモ動画 おわりに 準備するもの Unreal Engine 5.2.0 OpenAI API GPT-3.5-turbo(0301)を使用します。 事前にユーザー登録と、 API キーの発行を実施しておきます。 FastAPI API として、下記の機能を作成します。 オブジェクトに関連したドキュメントを登録する オブジェクト名と質問を与え、答えを返す Docker API の環境構築に使用します。 処理概要 下記の処理を実装します。 それぞれの機能について、()内の担当者が作業を行いました。 コリジョン 判定(岡崎) チャット可能な範囲に入ったとき、アイコンを表示します。 チャットUIの表示(岡崎、若本) コリジョン 状態でオブジェクト(Actor)をクリックし、チャット画面を表示します。 GPTへのリク エス ト(若本) UE上の情報を基に、GPTにリク エス トを送ります。 結果の表示(若本) API から返ってきたレスポンスをUIに反映します。 以降では、各機能の概要や実装について説明します。 手順①. GPTを用いた API を作成 本手順は若本が説明します。 GPTはUE内のオブジェクトについて情報を持たないため、そのままChatGPTに問い合わせても的確に質問に答えることができません。 そこで、以下の2つの機能を持つ API を作成します。今回は Unreal Engine を実行するPC上で構築することとしました。 オブジェクトに関連したドキュメントを登録する(インデックス登録) オブジェクト名と質問を与え、答えを返す(問い合わせ) それぞれの機能についてエンドポイントを用意し、HTTPリク エス トで実行できるようにします。 最終的には下記のようなファイル構成となります。 1.1 API の環境構築 まずはDockerで API のベースとなる環境を作成します。 以下は最終的に使用したDockerの設定ファイルです。 フォルダ上でdocker containerをupするとアプリケーションが起動します( http://localhost:8000/docs/ からアクセスすることができます)。 docker-compose.yml(クリックで表示) version: '3' services: backend: container_name: gpt-api build: context: . dockerfile: ./Dockerfile volumes: \- type: bind source: './api' target: '/src' ports: \- 8000:8000 stdin_open: true Dockerfile(クリックで表示) FROM python:3.8-slim-buster RUN pip install --upgrade pip RUN pip install --upgrade setuptools COPY requirements.txt / RUN pip install -r requirements.txt WORKDIR /src CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--reload"] requirements.txt(クリックで表示) numpy openai transformers llama_index langchain fastapi uvicorn[standard] pandas 1.2 エンドポイントの作成 次に、 API が持つ2つの機能についてエンドポイントを作成していきます。 routers/index.pyにインデックス登録のエンドポイントを、routers/search.pyに問い合わせ用のエンドポイントを記述しています。 main.py(クリックで表示) from fastapi import FastAPI from routers import search, index import os os.environ[ 'OPENAI_API_KEY' ] = 'YOUR_OPENAI_API_TOKEN' app = FastAPI() app.include_router(search.router) app.include_router(index.router) routers/index.py(クリックで表示) from fastapi import APIRouter from functions.index_operation import create_index router = APIRouter() @ router.post ( "/index" ) def service_create_index (dir_name: str ): _ = create_index(dir_name) return 200 routers/search.py(クリックで表示) from fastapi import APIRouter from pydantic import BaseModel from functions.search_operation import search router = APIRouter() class SearchRequestItem (BaseModel): dir_name: str = None query: str = None class SearchResponseItem (BaseModel): text: str = None @ router.post ( "/search" ) def service_search (item: SearchRequestItem) -> SearchResponseItem: response_text = search(item.dir_name, item.query) response = SearchResponseItem(text=response_text) return response 1.3 機能の作成 最後に、それぞれのエンドポイントで呼び出される処理を記述します。 functions/index_operation.pyにインデックス登録の処理を、functions/search_operation.pyに問い合わせの処理を実装しました。functions/ api _handling.pyでは、使用するmodel(gpt-3.5-turbo)の設定を行っています。 ここで、インデックス登録はドキュメントをあらかじめベクトルにしておき、問い合わせが来た際に検索するために行います。 今回の実装では、フォルダ内の CSV /PDF/TXTファイルについて読み込みベクトル化できるようにしました。 ※データの大きさや権限によってエラーが発生する可能性はありますが、今回そのエラーハンドリングまでは行っていません。 functions/api_handling.py(クリックで表示) from llama_index import LLMPredictor, ServiceContext, PromptHelper from langchain.chat_models import ChatOpenAI def get_service_context (): llm_predictor = LLMPredictor( llm=ChatOpenAI( temperature= 0 , model_name= "gpt-3.5-turbo" , max_tokens= 512 ) ) prompt_helper = PromptHelper( max_input_size= 4096 , num_output= 256 , max_chunk_overlap= 20 ) service_context = ServiceContext.from_defaults(llm_predictor=llm_predictor, prompt_helper=prompt_helper) return service_context functions/index_operation.py(クリックで表示) from llama_index import GPTVectorStoreIndex, SimpleDirectoryReader, download_loader from functions.api_handling import get_service_context import glob def create_index (dir_name: str ): input_dir = f 'data/{dir_name}' service_context = get_service_context() index = None all_files = glob.glob(f '{input_dir}/*' ) for file in all_files: if file .endswith( '.csv' ): Reader = download_loader( "SimpleCSVReader" ) loader = Reader() docs = loader.load_data( file = file ) if file .endswith( '.pdf' ): Reader = download_loader( "CJKPDFReader" ) loader = Reader() docs = loader.load_data( file = file ) if file .endswith( '.txt' ): loader = SimpleDirectoryReader(input_files=[ file ], required_exts=[ 'txt' ]) docs = loader.load_data() if index is None : index = GPTVectorStoreIndex.from_documents( docs, service_context=service_context ) else : for doc in docs: index.insert(doc) # indexを保存する if index: index.storage_context.persist(f "index/{dir_name}" ) return index functions/search_operation.py(クリックで表示) from llama_index import StorageContext, load_index_from_storage from functions.api_handling import get_service_context def load_index (dir_name: str ): \ # インデックスの読み込み storage_context = StorageContext.from_defaults(persist_dir=f "index/{dir_name}" ) index = load_index_from_storage(storage_context, service_context=get_service_context()) return index def search (dir_name: str , query: str ): index = load_index(dir_name) \ # クエリエンジンの作成 query_engine = index.as_query_engine() text_query = f '下記の質問に対して、敬語で簡潔に答えてください。 \n {query}' response = query_engine.query(text_query) return response.response 1.4 動作確認(データ登録) 上記のコードを記述後、動作確認を実施します。 アプリを立ち上げ後、以下のURLにアクセスすることでSwagger UI(下図)が表示され、直接動作確認をすることができます。 http://localhost:8000/docs/ まずはデータを登録してみましょう。 dataフォルダ下に新しいフォルダを作成します。ここでは、「BookInfo」というフォルダを作成しました。 なお、検索時にもこのフォルダ名を使用します。 ※ フォルダ名をもとにした管理やリク エス トは少し危険ですが、今回は簡単な検証のため実装を簡略化しています。 フォルダ作成後、「BookInfo」フォルダ内に CSV /PDF/TXTファイルを格納します。 今回は自作のsample.txtを格納しました。ファイルの中身は下記のようになっています。 格納後、 http://localhost:8000/docs/ にアクセスし、/indexから下記のようにインデックス登録を実行します。 indexフォルダ下に同名のフォルダが作成されます。 下記の3種類の json ファイルが入っていれば正常にインデックスが登録できています。 1.5 動作確認(検索) 検索についても動作確認してみます。 http://localhost:8000/docs/ にアクセスし、/searchを下記のように実行してみます。 少しするとResponseが返ってきます。登録した文章の情報を基に答えられており、動作が確認できました。 複数インデックスを登録した場合はdir_nameのパラメータを変えることで、検索対象を変えて回答させることができます。 手順②. オブジェクトの準備 次に、UE上で使用するオブジェクトの準備を行っていきます。本手順は岡崎が説明します。 ここでは以下の機能や画面を作成します。 チャット可能な範囲に入ったとき、アイコンを表示する機能 オブジェクト(Actor)をクリックしてチャット画面を表示する機能 それでは各機能について説明していきます。 2.1 チャット可能な範囲に入ったとき、アイコンを表示する機能 今回はゲームでよくあるような、プレイヤーがオブジェクトに一定の距離近づくと、「ここにアイテムがあります」という意味のアイコンが表示される処理の作成。 また、さらに近づくと「アイテムにクリックできます」という意味のアイコンに変更し、アイテムがクリックできるようになる処理を作っていきます。 まずはプレイヤーがオブジェクトに近づいたことを判定する処理を作成します。 以前 こちらの記事(UE5でコリジョン(衝突)判定機能を使って色々な機能を作成してみた) でも紹介しましたが、 コリジョン 機能を使っていきます。 まずはアクターブループリントを作成し「BP_P_Item」と 命名 します。 コンポーネント 追加から、「 Sphere Collision」を2つ追加し、それぞれ「BigSphere」と「SmallSphere」とします。 この2つの コリジョン は、「BigSphere」にプレイヤーが入った時、アイテムの上にアイコンを表示し、「SmallSphere」に入った時、アイコンを変更してアイテムをクリックできる状態に変更するために使用します。 上記を再現するため、「BigSphere」を大きく作成し、その中に「SmallSphere」を配置しています。 次にアイテムの上に表示するアイコンを作成します。 ユーザーインターフェース から ウィジェット ブループリントを作成し、「WBP_ItemIcon」と 命名 します。 作成後、下記画像のようにアイコンの ウィジェット を作成し、「Is Variable」にチェックを入れておきます。 次に コリジョン 用のブループリント(BP_P_Item)に戻り、「SmallSphere」の下に「Static Mesh」と「 Widget 」の コンポーネント を追加します。 画像では Widget の名前をWidgetItemIconに変更しています。 「 Widget 」の コンポーネント の詳細タブより、「 Widget Class」を先ほど作成した ウィジェット ブループリントの名前(WBP_ItemIcon)に変更します。 また、同じ詳細タブより「 レンダリング 」内の「Visible」のチェックを外しておきます。 次にイベントグラフに移動し、「On Component Begin Overlap(BigSphere)」と「On Component End Overlap(BigSphere)」のノードを作成します。 このノードは、左側の コンポーネント 一覧から「BigSphere」を右クリックし、イベント追加から選ぶことができます。 下記画像のように、プレイヤーが「BigSphere」内に入った際に、デフォルトで見えない設定にしておいたアイコン ウィジェット の「Visibility」の値を変更し、画面上に レンダリング させる処理を作成します。 同様にプレイヤーが「BigSphere」からプレイヤーが出た際にアイコンを見えなくする処理も追加します。 ここまでで、下記のようにプレイヤーの位置によってアイコンが表示されたり、消したりする処理ができました。 続いてさらに近づいた際(SmallSphereに入った際)にアイコンの表示を変更する処理を作成します。 アイコン用のブループリント(WBP_ItemIcon)に戻り、アイコンを変更するための関数を「AreaEvent」という名前で作成します。 「AreaEvent」には引数として「IsInside」というBoolean値を設定しておきます。 下記画像のように「AreaEvent」から「IsInside」の値によってイメージ画像を変更するために「Set Brush from Texture」ノードを繋ぎます。 「選択する」ノードに、Falseの場合にはSmallSphereに入っていない時用の画像を設定し、Trueの場合はSmallSphereに入っている時用の画像を設定します。 (本プロジェクトの場合は下記画像の「arrow」と「circle」を使用) 「Set Brush from Texture」のターゲットには ウィジェット ブループリントでimageを作成した際に 命名 した名前の変数が左側のタブに追加されているのでそれを使用します。 次に コリジョン 用のブループリント(BP_P_Item)に戻り、「SmallSphere」に入った際にアイコン ウィジェット の画像を変えるために「AreaEvent」の関数を使用する処理を作成します。 「SmallSphere」から「On Component Begin Overlap(SmallSphere)」と「On Component End Overlap(SmallSphere)」のノードを作成します。 下記画像のように「On Component Begin Overlap(SmallSphere)」と「On Component End Overlap(SmallSphere)」を「Area Event」繋げます。 「Area Event」の引数としてプレイヤーが「SmallSphere」の内部にいるかどうかの値を変数にして「In Small Collision Area」とします。 (後述の処理で使用するため変数にしておきます。) ここまでの処理で「BigSphere」の内部にいる時に矢印のアイコンを出現させ、「SmallSphere」の内部にいる時に二重丸のアイコンに変更させる処理ができました。 2.2 オブジェクト(Actor)をクリックしてチャット画面を表示する機能 続いて実際にオブジェクトを配置し、「SmallSphere」の内部にプレイヤーがいる時のみオブジェクトにクリックができる機能と、クリックした後にチャット画面を表示する機能を作成します。 まず初めに、プレイ画面上にマウスカーソルを出現させ、クリックを有効にするために、レベルブループリントを開きます。下の画像のように「イベントBeginPlay」に「Show Mouse Cursor」と「Enable Click Events」の設定をどちらもオンに変更します。 これでプレイ画面上にマウスカーソルを出現させ、クリックができるように設定されました。 次に、先ほど作成した コリジョン 用のブループリント(BP_P_Item)から子ブループリントを作成します。 親のブループリント上で右クリックを行い、「子ブループリントクラスを作成します」ボタンを押し「BP_C_Item_Book」と 命名 します。 子ブループリントを開き、親ブループリント作成時は特に編集しなかった「Static Mesh」に任意のメッシュ素材を選びます。 今回は「Quixel Bridge」内のメッシュを使用しました。 こちらの記事(Unreal Engine 5 を使ってワールドの地形を作成してみました) で、「Quixel Bridge」の使用方法を紹介しています。 イベントグラフを開き、「イベント ActorOnClicked」のノードを追加します。 「In Small Collision Area」の値からIF文を作成し、プレイヤーが「Small Collision」の中にいる場合のみチャット画面用の ウィジェット を作成するために「 ウィジェット を作成」ノードを繋げ、「Add Viewport」を付けます。 ここでチャット画面用の ウィジェット ブループリントを作成します。 (チャット機能の詳細は後述するので、ここでは大枠だけ作成します。) クリックしたいオブジェクト用(今回は本のオブジェクト)の ウィジェット ブループリントを作成し、「WBP_BookInfo」と 命名 します。 「 Canvas Panel」内に、本のタイトルや、サムネイル用画像を置き、チャット用のスペースも確保します。 こちらの記事(UE5でコリジョン(衝突)判定機能を使って色々な機能を作成してみた) で、 ウィジェット の作成方法について触れているので参考にしてください。 今回は閉じるボタン(×マーク)を押すことで ウィジェット を閉じる挙動にしたいので、「 Canvas Panel」内に、ボタンも追加します。 パレット追加欄に「Button」と検索し、ボタン内部に「Text」で「×」を記述し閉じるボタンを作成しました。 (ボタンの名前を「CloseButton」に変更してあります。) 階層タブより「CloseButton」を選択中に、詳細タブ内のイベント>On Click の追加ボタンを押し、イベントグラフに「On Click(CloseButton)」ノードを追加します。 「On Click(CloseButton)」ノードに「Remove from Parent」を繋ぎ、閉じるボタンが押された時チャット画面用の ウィジェット が閉じるようにします。 「BP_C_Item_Book」を再び開き、「 ウィジェット を作成」ノードの「Class」を今作成した「WBP_BookInfo」に変更します。 これで画面上の本のオブジェクトをクリックした際に、画面上にチャット画面の ウィジェット が現れ、閉じるボタンで閉じる処理ができました。 ただこのままだと、本のオブジェクトをクリックした分だけ ウィジェット が開いてしまうので、現在 ウィジェット が開かれている状態かどうかを判別するために「IsOpenDetailWidget」という変数を作成し、下記画像のようにフラグとして使用します。 「WBP_BookInfo」でチャット画面を閉じた後で「IsOpenDetailWidget」の変数をFalseに戻しておきます。 ここまでで、オブジェクト(Actor)をクリックしてチャット画面を表示する機能が完成しました。 手順③. チャットUIの実装 ここまでで、ベースとなるUEのオブジェクトやUI、外部 API の作成が完了しました。 以降の手順は若本が説明します。 本手順では、GPTのリク エス トとのやり取りに必要になる追加のUIを用意していきます。 3.1 チャットのページUIを実装 次に、チャットのやり取りを行うためのUIを作成します。 チャットの 吹き出し は動的に追加されていくため、ここでは必要な コンポーネント のみ用意します。 まずはチャット画面のベースとして、手順②で作成した「WBP_BookInfo」にText_box、button、scroll boxを追加します。 Text_boxとscroll boxをわかりやすい変数名に変更しておきます。 また、呼び出すために「Is Variable」にチェックを入れておきます。 次に、buttonにOnClickイベントを追加します。 詳細画面のOnClickを押すことで、OnClickイベントが有効になります。こちらを押しておきましょう。 詳細な処理は後の手順で作成します。 また、ここでItemの属性値を設定しておきます。 属性値を用いて後段でGPTに問い合わせる際の検索対象を変更するため、その下準備となります。 まずは Widget の「WBP_BookInfo」に変数「WidgetAttributes」を作成します。 次に、「BP_C_ItemBook」クラスに属性値を設定しておきます。ここでは変数名を「ItemAttributes」、デフォルト値を「BookInfo」としました。 最後に、「BP_C_ItemBook」のブループリント上で、 ウィジェット の作成時に自身の持つ「ItemAttributes」を「WidgetAttributes」にセットする処理を追加します。これで完成です。 3.2 チャットの 吹き出し UIを実装 3.1でやりとりを行う画面は作成しましたが、メッセージを入れるための 吹き出し がありません。 そこで、別途チャットの 吹き出し 用UI(自分用/GPT用)を作成します。 それぞれ ユーザーインターフェース から ウィジェット ブループリントを作成しましょう。 作成後、 Canvas PanelとText Box(Multi-Line)を配置します。 こちらもText Boxはわかりやすい変数名に変更し、「Is Variable」にチェックを入れておきます。 3.2.1 自分のチャットを表示する 自分用の 吹き出し は下記のように設定しました。背景色とFontの色/サイズを調整しています。 3.2.2 GPTの返信を表示する GPT用の 吹き出し も同様に作成します。 手順④. HTTPリク エス ト用の C++ クラス作成 4.1 C++ クラスを作成 Unreal Engine では、デフォルトでHTTPへのリク エス トを送信するクラスは用意されていないため、 API にリク エス トを送り、レスポンスを受け取るクラスを新規作成する必要があります。 ブループリントでは実現できないため、今回は C++ で実装することとしました。 まずは、ツールから新規の C++ クラスを作成します。 ここで、親クラスは「Actor」、名前は「HTTPActor」としました。 クラスを作成すると、エディタに飛ばされます。エディタ上でクラスを編集していきます。 4.2 C++ クラスを編集 API にリク エス トを送り、レスポンスを受け取るための処理を追加していきます。 まずは、Build.csのDependencyModuleに"HTTP", " Json ", "JsonUtilities"の3つを追加します。 Build.cs(クリックで表示) // Fill out your copyright notice in the Description page of Project Settings. using UnrealBuildTool; public class TechBlog9 : ModuleRules { public TechBlog9(ReadOnlyTargetRules Target) : base(Target) { PCHUsage = PCHUsageMode.UseExplicitOrSharedPCHs; PublicDependencyModuleNames.AddRange(new string[] { "Core", "CoreUObject", "Engine", "InputCore", "HTTP", "Json", "JsonUtilities" }); PrivateDependencyModuleNames.AddRange(new string[] { }); // Uncomment if you are using Slate UI // PrivateDependencyModuleNames.AddRange(new string[] { "Slate", "SlateCore" }); // Uncomment if you are using online features // PrivateDependencyModuleNames.Add("OnlineSubsystem"); // To include OnlineSubsystemSteam, add it to the plugins section in your uproject file with the Enabled attribute set to true } } また、Engine.iniの末尾にHTTPリク エス トの設定を記載しておきます。 Engine.ini(クリックで表示) [HTTP] HttpTimeout=60 HttpConnectionTimeout=5 HttpReceiveTimeout=-1 HttpSendTimeout=-1 HttpMaxConnectionsPerServer=32 bEnableHttp=true bUseNullHttp=false HttpDelayTime=0.1 上記が終わったら、HTTPActor.hクラスを変更します。 筆者はEpicのcommunityを参考に#include "CoreMinimal.h"を コメントアウト しました。 後ほどブループリント上で呼び出すため、HTTPリク エス トを実行するHttpMethodはUFUNCTIONを、結果を格納する変数である outputStringにはUPROPERTYを設定しておきます。 HTTPActor.h(クリックで表示) // Fill out your copyright notice in the Description page of Project Settings. #pragma once // #include "CoreMinimal.h" #include "GameFramework/Actor.h" #include "Http.h" #include "HTTPActor.generated.h" UCLASS() class TECHBLOG9_API AHTTPActor : public AActor { GENERATED_BODY() public: // Sets default values for this actor's properties AHTTPActor(); protected: // Called when the game starts or when spawned virtual void BeginPlay() override; public: // Called every frame virtual void Tick(float DeltaTime) override; FHttpModule* Http; UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = Default) FString outputString; // HTTP通信 UFUNCTION(BlueprintCallable, Category = "Http") void HttpMethod(FString query, FString dir_name); // レスポンス後のイベント処理 void OnResponseReceived(FHttpRequestPtr Request, FHttpResponsePtr Response, bool bWasSuccessful); }; 最後に、HTTPActor.cppに具体的な処理内容を記述します。 手順①で実装した API の仕様に合わせ、dir_name(インデックスのフォルダ名)とquery(質問)の2つを引数としてHTTPリク エス トを送り、返ってきた Json の「text」をoutputStringに格納する処理を追加しました。 HTTPActor.cpp(クリックで表示) // Fill out your copyright notice in the Description page of Project Settings. #include "HTTPActor.h" // Sets default values AHTTPActor::AHTTPActor() { // Set this actor to call Tick() every frame. You can turn this off to improve performance if you don't need it. PrimaryActorTick.bCanEverTick = true; Http = &FHttpModule::Get(); } // Called when the game starts or when spawned void AHTTPActor::BeginPlay() { Super::BeginPlay(); } // Called every frame void AHTTPActor::Tick(float DeltaTime) { Super::Tick(DeltaTime); } void AHTTPActor::HttpMethod(FString query, FString dir_name) { // Jsonデータ作成 TSharedPtr<FJsonObject> JsonObject = MakeShareable(new FJsonObject); JsonObject->SetStringField("dir_name", dir_name); JsonObject->SetStringField("query", query); // OutputStringに Json 格納 FString OutputString; TSharedRef<TJsonWriter > JsonWriter = TJsonWriterFactory<>::Create(&OutputString); FJsonSerializer::Serialize(JsonObject.ToSharedRef(), JsonWriter); // HTTPリク エス ト TSharedRef Request = Http->CreateRequest(); Request->OnProcessRequestComplete().BindUObject(this, &AHTTPActor::OnResponseReceived); Request->SetURL(" http://localhost:8000/search "); Request->SetVerb("POST"); Request->SetHeader(TEXT("User-Agent"), "X-UnrealEngine-Agent"); Request->SetHeader("Content-Type", TEXT("application/ json ")); Request->SetContentAsString(OutputString); Request->ProcessRequest(); } void AHTTPActor::OnResponseReceived(FHttpRequestPtr Request, FHttpResponsePtr Response, bool bWasSuccessful) { TSharedPtr<FJsonObject> JsonObject; TSharedRef<TJsonReader<>> Reader = TJsonReaderFactory<>::Create(Response->GetContentAsString()); if (FJsonSerializer::Deserialize(Reader, JsonObject)) { // Json からtextを抽出 outputString = JsonObject->GetStringField("text"); } } 上記の変更が終わったらbuildします。 筆者は VS Code で編集していたため、Launch<project_name>Editor(development)でbuildしました。 Unreal Engine が起動され、projectが表示されれば C++ クラスの作成は完了です。 4.3 作成したクラスをブループリント化 最後に、作成した C++ クラスをブループリントから呼び出すため、Blueprintクラスを作成します。 名前は「BP_HTTPActor」としました。 作成した「BP_HTTPActor」をワールドに配置すれば完了です。 手順⑤. 質問送信時の処理を実装 ここまででオブジェクトの準備、GPTの準備、UIの準備が全て終わりました。 最後に、Brueprint上ですべてを繋ぎこむ処理を「BP_C_Item_Book」のブループリント上で実装します。 以下が本手順で作成したブループリントの全体像です。 長くて見づらいため、それぞれ分けて解説します。(見やすさのため、拡大時にレイアウトを調整しています) 3.1で作成したOnclickイベント(チャットの送信ボタン押下)を起点としてブループリントを作成しました。 5.1 「送信」ボタンが押されたとき、送信した内容を表示する 送信ボタンが押下されると、TextBoxの値を取得して新しい 吹き出し として表示します。 ここでは、3.2.1で作成した自分用の 吹き出し を作成し、TextBoxの値を入れてScroll Boxの中に入れています。 5.2 HTTPリク エス トを送信する 4.3で配置した「BP_HTTPActor」クラスを呼び出し、HTTPMethodを実行します。ユーザーの入力と ウィジェット の持つ変数「WidgetAttributes」の値をGPTの API 側へリク エス トとして送信しています。 このとき、outputStringの変数にGPTのレスポンスが格納されるまでは、Delayとloopを使用してwaitします。 また、HTTPリク エス トを送った後、自身が入力したテキストをクリアする処理を記述しました。 5.3 GPTの返信内容を表示する 5.1と同様、今度はGPT用の 吹き出し を作成し、outputStringの値を入れてScroll Boxの中に格納します。 最後に、outputStringの変数をクリアすれば処理は完了です。 デモ動画 本のオブジェクトに近づくことでタッチが可能になり、タッチすることでUIが表示されます。 さらにUI上で問い合わせると、オブジェクトの関連文章を基に回答できていることを確認できます。 これまでの実装により、ストレスの少ない問い合わせをUE上で実現することができました。 おわりに 今回はチャットUIを用いた、オブジェクトに対する問い合わせを実装しました。 以下、実装を担当した岡崎と若本のそれぞれの所感となります。 岡崎:今回の実装で、ブループリントでのチェンジイベント(オンクリックなど)や、 コリジョン イベントに関する使用方法の基礎的な学習を行えました。今後プロダクトを作成する際、プレイヤー起因で任意の情報を出したり、プレイヤー情報を変更させるといった機能を作成する際に、数多く使うことになる機能を作成することができたため、より応用的な使い方など引き続き調査したいと思います。 若本:UE初学者であり、今回広く Python の実装/ブループリントの実装/ C++ の実装を行いましたが、慣れて以降はストレスレスに開発を行うことができました。今回の実装を発展させ、非同期処理の実装や会話履歴の入れ込み、UIのリッチ化など、さらに作りこむことによってよりよいユーザー体験が得られることが期待できると感じました。 現在ISIDは web3領域のグループ横断組織 を立ち上げ、Web3および メタバース 領域のR&Dを行っております(カテゴリー「3DCG」の記事は こちら )。 もし本領域にご興味のある方や、一緒にチャレンジしていきたい方は、ぜひお気軽にご連絡ください! 私たちと同じチームで働いてくれる仲間を、是非お待ちしております! 私たちは共に働いていただける仲間を募集しています! みなさまのご応募、お待ちしています! 最新テクノロジー事業企画・推進担当 ◎Web3/メタバース/AI AIソリューション開発エンジニア ◎AIビジネス創出・推進に関われる    株式会社電通総研 新卒採用 執筆: @wakamoto.ryosuke 、レビュー: @yamada.y ( Shodo で執筆されました )
アバター
こんにちは、ISID金融ソリューション事業部の孫です。 この記事は、私が Unreal Engine (以下UE)のネットワーク同期(以下 レプリケーション )に関する知識を学んだ知見です。 UEの レプリケーション 機能は、 マルチプレイヤー ゲームの開発において非常に重要なコアな機能です。 Web上に公開されているUEの レプリケーション プログラミングは、現在BluePrintを用いたビジュアルプログラミングが主となっています。 確かにBluePrintは便利で迅速な開発が可能ですが、UEの内部動作ロジックをより深く理解するためには C++ プログラミングが不可欠です。 この記事では、 C++ を使用してシンプルなネットワーク同期のデモを実装する方法を紹介します。このデモの開発を通じて、UEの レプリケーション 機能の実装方法を学ぶことができます。 はじめに UEの レプリケーション 部分について触れると、Dedicated Serverという概念をまず理解する必要があります。 ゲームネットワーク アーキテクチャ においてDedicated Serverが導入された背景については、 金融ソリューション事業部の山下さんの記事 を参照していただければと思います。 UEのDedicated Serverについて、UEのクライアントコードとサーバーコードは一体であることが特徴です。 通常、一般的なフロントエンドとバックエンドの分離とは異なり、UEではクライアントとサーバーが同じプロジェクト内に存在します。このため、クライアントとサーバーのコードは混在していることになります。 ※コードの間で以下のマクロを使ってサーバーコードとクライアントコードの区別が可能: WITH_EDITOR : コードがエディタ環境で動作しているときにTrueになります。 UE_SERVER : コードがサーバー環境で動作しているときにTrueになります。 UE_CLIENT : コードがクライアント環境で動作しているときにTrueになります。 Dedicated Serverが必要な理由は、C/S(クライアント/サーバー)モードではサーバーがクライアントの業務も担当するため、運用負荷が高くなるからです。Dedicated Serverの導入により、クライアントとサーバーの役割が分離され、負荷を軽減できます。 Dedicated Serverは、UEが FPS の同期問題を解決するために設計された専用のサーバーです。また、Dedicated ServerはEpicが開発した特別な最適化されたネットワーク プロトコル を使用しており、高性能な同期(遅延問題の解決)を実現できます。 Dedicated Serverの構築方法については、以前の 記事 を参照してください。 開発環境/ツール Unreal Engine 5.2.0 Windows10 21H2 x64 RAM 16GM, SSD 1TB NVIDIA GeForce GTX 3080 Visual Studio 2019 version 16.11.21(以下VS2019) それでは、デモの制作を開始しましょう。以下の手順で進めていきます。 ※デモはUEのサードパーソンテンプレートの上で レプリケーション 機能を実装 UEのネットワーク知識とActorの権限確認(ユーザーネーム表示用キャ ラク ターの作成含め) ユーザー名入力画面の作成 ユーザーネームの レプリケーション 実装 Dedicated Server側の実装 デモの確認 このような手順でデモの制作を進めていくと、ユーザーネームを持つキャ ラク ターを生成し、それを全てのクライアントで同期できます。 1.UEのネットワーク知識とActor権限の確認 本番の作成を開始する前に、まずは2点の前提知識を説明します。 UEのネットワークモデル UEのネットワークモデルでは、Actorの レプリケーション を通じてゲームオブジェクトの状態をクライアント間で同期します。これにより、各クライアントが一貫したゲームワールドを見ることができます。 UEのネットワークモデルには、いくつかのキーポイントと概念があります: Actor Replication(アクターレプリケーション) : Unreal Engine では、各ゲームオブジェクトはActorと呼ばれます。サーバー内のActorの状態はクライアントに レプリケーション (複製)される可能性があり、これを「 レプリケーション 」と呼びます。どのActorが レプリケーション され、どのように レプリケーション されるかは、開発者が特定の属性と関数を設定することで決定されます。 State Synchronization(状態同期) :サーバーは自身の状態をネットワークを通じて各クライアントに送信し、全てのクライアントが一貫したゲームワールドを見ることができるようにします。この過程を状態同期と呼びます。これがサーバーコンテンツを各クライアントに分散する必要性の理由です。この過程がなければ、クライアントは古いか、または一貫性のないゲームワールドの状態を見ることになりゲーム体験が低下します。 Client Prediction(クライアント予測) :ネットワークの遅延がゲーム体験に影響を与えるのを減らすために、クライアントは「クライアント予測」と呼ばれる技術を使用します。つまり、サーバーからの応答が到着する前に、クライアントはあらかじめいくつかのアクションを実行します。サーバーからの応答を受け取ったら、クライアントは自身の状態を調整してサーバーの状態に一致させます。 Lag Compensation(ラグ補償) :これはネットワーク遅延の影響を減らす別のテクニックです。サーバーは、クライアントがリク エス トを発行した時点のゲーム状態に戻って、その状態でリク エス トされた操作を実行します。 これらの コンポーネント やテクニックを組み合わせることで、UEのネットワークモデルは安定性と効率性を持ち、多人数プレイにおける一貫したゲーム体験を実現します。 各 コンポーネント は重要な役割を果たしますが、その中でも核心的な概念は「状態同期」です。状態同期は、すべてのプレイヤーが一貫したゲームワールドを見ることを保証し、各自異なる視覚体験やインタ ラク ション体験が生じることを防ぎます。 Actorの所有権-ROLE クライアントとサーバーの間に区別がある以上、Actorの所有権においてもクライアントとサーバーの区別があることは当然です。 UEでは、Actorの制御権限を3つのカテゴリに分けています。それらは以下のとおりです: ROLE_None :特定の制御権限に属さない状態を表します。 ROLE_Authority :サーバー側でActorの制御権を持つことを示します。 ROLE_AutonomousProxy :クライアント側でローカルなActorの制御権を持つことを示します。 ROLE_SimulatedProxy :他クライアントがActor制御権を持つことを示します。 これらの三つの属性はUEがActorを設計する際に、Actorに固有属性として設計されています。これはActorがどこに存在するかを判断するために使われます。 UEでは、サーバーのコードとクライアントのコードが一体化しているため、Actorがこの属性を持つことは非常に必要です。 その辺の権限関係をテストしてみましょう。 テンプレートのCharacterにRendertextを追加します。 xxxCharacter.h #include "Components/TextRenderComponent.h" ... public: UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = playername, meta = (AllowPrivateAccess = "true")) class UTextRenderComponent* playerNameTag; ... xxxCharacter.cpp #include "Components/SkeletalMeshComponent.h" // コンストラクタ関数にキャラクターのSkeletalメッシュ配下にTextRenderComponent追加 xxxCharacter::xxxCharacter() { ... // Create a Text Component playerNameTag = CreateDefaultSubobject<UTextRenderComponent>(TEXT("playerName")); USkeletalMeshComponent* SkeletalMesh = GetMesh(); playerNameTag->SetupAttachment(SkeletalMesh); playerNameTag->SetText(FText::FromString("test")); // Set Component Location Rotation playerNameTag->SetHorizontalAlignment(EHTA_Center); playerNameTag->SetRelativeLocation(FVector(0.0f, 0.0f, 180.0f)); playerNameTag->SetRelativeRotation(FRotator(0.0f, 90.0f, 0.0f)); ... 制御Roleを表示します。 xxxCharacter.cpp void Atest_DEServerCharacter::BeginPlay() { if (GetLocalRole() == ROLE_Authority) { playerNameTag->SetText(FText::FromString("ROLE_Authority")); UE_LOG(LogTemp, Warning, TEXT("This Actor is on the server.")); } else if (GetLocalRole() == ROLE_AutonomousProxy) { playerNameTag->SetText(FText::FromString("ROLE_AutonomousProxy")); UE_LOG(LogTemp, Warning, TEXT("This Actor is on the owning client.")); } else if (GetLocalRole()== ROLE_SimulatedProxy) { playerNameTag->SetText(FText::FromString("ROLE_SimulatedProxy")); UE_LOG(LogTemp, Warning, TEXT("Other Client Actor is ROLE_SimulatedProxy.")); } else { playerNameTag->SetText(FText::FromString("ROLE_None")); UE_LOG(LogTemp, Warning, TEXT("This Actor is on a non-owning client.")); } } 権限Roleを確認します。 サーバーのウィンドウでは、すべてのキャ ラク ターが「ROLE_Authority」と表示されていることがわかります。 それに対して二つのクライアントのウィンドウでは、自分が制御しているキャ ラク ターだけ「ROLE_AutonomousProxy」と表示され、他のすべては「ROLE_SimulatedProxy」と表示されています。 その中にはサーバーが生成したキャ ラク ターも含まれていますが、このクライアントにとってはそれも他のエンドのActorに属するものとなります。 次に、ステップ バイス テップで レプリケーション デモを作成します。 2.ユーザー名入力画面の作成 入力用 Widget UIの作成 Content Browserで Content フォルダを開き、右側の空白部分で右クリックして User Interface -> Widget Blueprintを選択してUIを作成します。 新しく作成したBlueprintをダブルクリックして開き、以下の画像のように「ゲーム開始Button」「ユーザー名入力のEditableText」とタイトル表示の「TextBlock ウィジェット 」を追加します。 Widget UIの親Classファイルの作成 Content Browser で C++Classes フォルダを開き、右側の空白部分で右クリックし New C++ Class -> UserWidget を選択します。 新しく作成したクラスファイルが自動的に Visual Studio で開かれます。 このクラスがBlueprintのUIを制御するために、Blueprintの親クラスを新しく作成したクラスに変更します。 UIのBlueprintをダブルクリックして開き、 File -> Reparent Blueprint をクリックし、表示されるダイアログで新しく作成したクラスを選択します。 ユーザー名の取得コードの実装 制御する ウィジェット の定義を追加します。 定義に UPROPERTY(EditAnywhere, BlueprintReadWrite, meta = (BindWidget)) を追加して、Blueprint Widget 内の対応する ウィジェット にバインドできるようにします。 //LoginHUDWidget.h UCLASS() class TEST_DESERVER_API ULoginHUDWidget : public UUserWidget { GENERATED_BODY() public: void NativePreConstruct(); UFUNCTION() void OnPlayGameButtonClicked(); UPROPERTY(EditAnywhere, BlueprintReadWrite, meta = (BindWidget)) class UEditableText* inputName; UPROPERTY(EditAnywhere, BlueprintReadWrite, meta = (BindWidget)) class UTextBlock* statusLabel; UPROPERTY(EditAnywhere, BlueprintReadWrite, meta = (BindWidget)) class UTextBlock* playLabel; UPROPERTY(EditAnywhere, BlueprintReadWrite, meta = (BindWidget)) class UButton* playBtn; }; コンスト ラク タ関数で ウィジェット を初期化します。 Button ウィジェット には OnPlayGameButtonClicked という処理関数を追加します。 //LoginHUDWidget.cpp void ULoginHUDWidget::NativePreConstruct() { Super::NativePreConstruct(); inputName->SetHintText(FText::FromString("Please input your name")); statusLabel->SetText(FText::FromString("Test Replication Demo")); playLabel->SetText(FText::FromString("Play")); FScriptDelegate StartPlayDelegate; StartPlayDelegate.BindUFunction(this, "OnPlayGameButtonClicked"); playBtn->OnClicked.Add(StartPlayDelegate); }; ゲーム開始ボタンがクリックされた後の処理ロジックを追加します。 UGameplayStatics::OpenLevel 関数を使用してDedicated Serverに接続します。 ユーザーが入力したプレイヤー名はOptionsを通じてDedicated Serverに渡されます。 void ULoginHUDWidget::OnPlayGameButtonClicked() { FString NickName = inputName->GetText().ToString(); FString LevelName = "127.0.0.1:7777"; FString Options = FString::Printf(TEXT("?NickName=%s"), *NickName); UGameplayStatics::OpenLevel(GetWorld(), FName(*LevelName), false, Options); } GameModeで Widget コンポーネント をロードします。 //xxxGameMode.h protected: UPROPERTY(EditAnywhere, Category = "UI") TSubclassOf<UUserWidget> LoginWidgetClass; private: UPROPERTY() ULoginHUDWidget* loginWidget; //xxxGameMode.cpp AxxxGameMode::AxxxGameMode() { static ConstructorHelpers::FClassFinder<UUserWidget> LoginWidgetObj(TEXT("/Game/UI/LoginHUD_UI")); LoginWidgetClass = LoginWidgetObj.Class; }; void AxxxGameMode::BeginPlay() { Super::BeginPlay(); APlayerController* PlayerController = GetWorld()->GetFirstPlayerController(); if (PlayerController != nullptr) { PlayerController->bShowMouseCursor = true; } if (LoginWidgetClass != nullptr) { UUserWidget* loginWidget = CreateWidget<UUserWidget>(GetWorld(), LoginWidgetClass); if (loginWidget != nullptr) { loginWidget->AddToViewport(); } } } 3.ユーザーネームの レプリケーション 実装 クライアントの属性が変更されたときに、その属性値が他のクライアントに同期するためには、以下の2点を覚えておく必要があります: ① 属性のReplicationはReplicated or ReplicatedUsingに設定すべきです ② 属性を変更するコードはDedicated Server上で実行されます Actorの レプリケーション ReplicationはUObjectから派生した任意クラスの変数の固有属性で、この変数がネットワーク同期を許可するかどうかを指定します。 ブループリントでは、以下の3つの項目がReplication属性に対して設定可能です: None :ネットワーク同期を許可しません。 Replication :ネットワーク同期を許可します。 RepNotify :属性はネットワーク同期を許可し、さらにコールバック関数にバインドします。属性が変化すると、このコールバックが呼び出されます。ブループリントでは、このコールバック関数はFUNCTION内に自動的に作成され、OnRep_で始まり属性名で終わるようになっています。例えば、pos属性のコールバック関数はOnRep_posとなります。 C++ でこの部分は、 APlayerState クラスで実装されます。 APlayerState は Unreal Engine のクラスであり、各プレイヤーに関連するゲーム情報を格納および管理するために使用されます。 この情報は、プレイヤーが現在のシーンにいるかどうかに関係なく、通常はゲームセッション全体で永続的です。この設計により、 APlayerState はゲームセッション全体のレベル内でプレイヤー情報を格納する理想的な場所となります。 ※ 注意 : APlayerState はプレイヤーの入力やゲームワールド内での状態(位置、速度、アニメーションの状態など)を格納するためのものではありません。これらの情報は APlayerController に格納する必要があります。 該当する C++ コードの例は以下のようになります。 # ネットワーク同期を許可しません UPROPERTY() # ネットワーク同期を許可します UPROPERTY(Replicated) # 属性はネットワーク同期を許可し、さらにコールバック関数にバインドします UPROPERTY(ReplicatedUsing=OnRep_xxx) ユーザーネームの レプリケーション 実装 Playstateのサブクラスを新規作成します。 Widget Classを作成したのと同じ手順で、 C++ Classesフォルダで右クリックし、 New C++ Class -> playerState を選択します。 作成が完了すると、 Visual Studio が自動的に新しいクラスファイルを開きます。 Replicatedとして定義します。 DOREPLIFETIME Unreal Engine のネットワークプログラミングにおけるマクロであり、特定のプロパティがネットワーク上で複製可能であることを設定するために使用されます。 //playerstate.h public: UPROPERTY(Replicated) FString NickName; virtual void GetLifetimeReplicatedProps(TArray<FLifetimeProperty>& OutLifetimeProps) const override; //playerstate.cpp void AMetaPlayerState::GetLifetimeReplicatedProps(TArray<FLifetimeProperty>& OutLifetimeProps) const { Super::GetLifetimeReplicatedProps(OutLifetimeProps); DOREPLIFETIME(AMetaPlayerState, NickName); } GameModeのコンスト ラク タ関数に PlayerState をロードします。 AxxxGameMode::AxxxGameMode() { PlayerStateClass = AMetaPlayerState::StaticClass(); }; サーバーからの更新メッセージを受け取り、キャ ラク ターに値を割り当てます。 コン トロール しているキャ ラク ターについて、上記で定義したNickNameが変更された場合、 OnRep_PlayerState 関数が実行されます。 OnRep_PlayerState 関数は APlayerState クラスに定義されており、それを継承する必要があります。 //xxxCharacter.h private: virtual void OnRep_PlayerState() override; //xxxCharacter.cpp void AxxxCharacter::OnRep_PlayerState() { Super::OnRep_PlayerState(); APlayerState* OwningPlayerState = GetPlayerState(); if (OwningPlayerState != nullptr) { AMetaPlayerState* MetaPlayerState = Cast<AMetaPlayerState>(OwningPlayerState); if (MetaPlayerState != nullptr) { FString NickName = MetaPlayerState->NickName; if (NickName.Len() > 0 ) { playerNameTag->SetText(FText::FromString(NickName)); } } } } 4.Dedicated Server側の実装 UEの AGameModeBase および AGameMode クラスは、ゲームの基本ルールとロジックを定義するために使用されます。 以下に、これらのクラスでいくつかの重要なライフサイクル関数と、それらが通常どのような役割を果たすかを示します。 InitGame() :この関数はサーバーが起動し、すべてのオブジェクトがロードされ、ゲームがまだ実行されていない状態で呼び出されます。ここで変数や状態を初期化できます。 PreLogin() :これはクライアントが接続を許可される前にサーバーで呼び出される関数です。プレイヤーの資格情報(例:ユーザー名やパスワードの確認)を検証したり、プレイヤーの接続を他の形式で事前検証したりするために使用できます。検証が失敗した場合、ここでプレイヤーの接続を拒否できます。 PostLogin() :プレイヤーが正常に接続され、検証された後、PostLogin()関数が呼び出されます。ここでは、プレイヤーがゲームに参加した後すぐに実行する必要のあるコードを実行できます。例えば、歓迎メッセージを送信したり、プレイヤーのゲームデータを初期化したりできます。 InitNewPlayer() :この関数はプレイヤーがサーバーに接続して初期化されたときに呼び出されます。この関数では、「プレイヤーの属性の初期化」「プレイヤーのチームの割り当て」「新しいプレイヤーに必要なゲーム情報の送信」など、多くのタスクを実行できます。 BeginPlay() :この関数はゲームの開始時に呼び出されます。ゲーム開始時に実行する必要があるコードをここで実行できます。 Logout() :プレイヤーがゲームから退出するときにこの関数が呼び出されます。ここでは、プレイヤーがゲームから退出する際に実行する必要のあるコード(例:プレイヤーのゲームデータの保存や、プレイヤーの退出メッセージの送信など)を実行できます。 これらの関数は最も一般的に使用され、ゲームの異なる段階で何を実行するかをサーバーサイドで処理するために使用されます。    これらの関数により、ゲームのロジックや要件に合わせて特定のコードを適切なタイミングで実行できます。 前述のように、同期を実現するには2つの条件を満たす必要があります。 「② 属性の変更コードはDedicated Server上で実行される」 という条件を満たすために、上記の関数の中で、特に InitNewPlayer() 関数を選択する必要があります。 なぜなら、この関数はプレイヤーの PlayState 初期化が行われるタイミングですから。 //.xxxGameMode.h virtual FString InitNewPlayer(APlayerController* NewPlayerController, const FUniqueNetIdRepl& UniqueId, const FString& Options, const FString& Portal) override; //.xxxGameMode.cpp FString xxxServerGameMode::InitNewPlayer(APlayerController* NewPlayerController, const FUniqueNetIdRepl& UniqueId, const FString& Options, const FString& Portal) { FString InitializedString = Super::InitNewPlayer(NewPlayerController, UniqueId, Options, Portal); const FString& nickName = UGameplayStatics::ParseOption(Options, "NickName"); if (NewPlayerController != nullptr) { APlayerState* PlayerState = NewPlayerController->PlayerState; if (PlayerState != nullptr) { AMetaPlayerState* ServerPlayerState = Cast<AMetaPlayerState>(PlayerState); if (ServerPlayerState) { ServerPlayerState->NickName = nickName; } } } return InitializedString; } 5.デモの確認 ここまでで、ユーザーネームの レプリケーション に関する実装が全部完了しました! Dedicated Serverをパッケージ化して試してみましょう。確認ポイントは以下となります: 各クライアントでユーザーがユーザーネームを入力し、ゲームが開始できること 各クライアントで対応するキャ ラク ターの名前が表示されること 上記の確認が取れましたら、いわゆるネットワーク上での状態同期が成功し、すべてのクライアントが一貫したゲーム世界を見ることができるようになりました! ※パッケージング手順については Amazon GameLift × Unreal Engines 5 でオンラインマルチプレイゲームを作る の記事を参照してください。 注意事項 属性の同期を実装する際には、以下の点を注意してください。 それは、キャ ラク ターモデルの変更をAPlayerStateクラスに実装しないということです。 筆者がコードを書く際、最初に APlayerState クラスに以下のようなコールバック関数を書いたことがありました。 void ATestPlayerState::OnRep_NickName() { APlayerController* PC = GetGameInstance()->GetFirstLocalPlayerController(); if (PC && PC->GetPawn()) { AMetaPlayerController* PlayerController = Cast<AMetaPlayerController>(PC); if (PlayerController) { Atest_DEServerCharacter* MyCharacter = Cast<Atest_DEServerCharacter>(PlayerController->GetPawn()); if (MyCharacter) { MyCharacter->playerNameTag->SetText(FText::FromString(NickName)); } } } } 実行結果として、もともとPlayer1を制御していたユーザーが、Player2がログインした後に制御しているキャ ラク ターの表示がPlayer2のユーザー名になってしまうという問題が発生しました。    これは、私が APlayerState の OnRep_NickName 関数でキャ ラク ターを取得し、名前ラベルを変更していたためです。 この関数は、NickNameフィールドがクライアントに複製されたときに呼び出されます。しかし一部の場合では、NickNameフィールドがクライアントに複製された時点では、クライアントが新しいキャ ラク ターの情報をまだ受信していない可能性があります。つまり、GetPawn()関数がnullを返すか、もしくはキャ ラク ターが存在していても既に存在する他のプレイヤーのキャ ラク ターになるかもしれません。 この問題を解決するための方法は、 APlayerState のOnRep関数内でキャ ラク ターを取得し、名前ラベルを変更しないことです。 代わりに、キャ ラク ターのOnRep_PlayerState関数内で、キャ ラク ター自身のPlayerStateを取得しキャ ラク ターの名前ラベルを変更する必要があります。先ほど実装したコードと同様に、キャ ラク ター自身のOnRep_PlayerState関数でこれを行ってください。 終わりに このユーザーネーム属性の レプリケーション デモを通じて、 Unreal Engine のネットワーク同期モデルについて一定の理解を得ることができました。 Unreal Engine は、さまざまなタイプのゲームや仮想現実アプリケーションをサポートする強力な ゲームエンジン です。3Dおよび メタバース の開発領域では、ネットワーク同期、 マルチプレイヤー ゲーム、物理シミュレーション、シーンの レンダリング など、さまざまな技術と応用を探求できます。学習と研究を続けることで、この領域においてより深い理解と高い技術力を身につけることができます。 現在ISIDは web3領域のグループ横断組織 を立ち上げ、Web3および メタバース 領域のR&Dを行っております(カテゴリー「3DCG」の記事は こちら )。 もし本領域にご興味のある方や、一緒にチャレンジしていきたい方は、ぜひお気軽にご連絡ください! 私たちと同じチームで働いてくれる仲間を、是非お待ちしております! ISID採用ページ(Web3/メタバース/AI) 参考文献 https://docs.unrealengine.com/5.2/en-US/networking-overview-for-unreal-engine/ 執筆: @chen.sun 、レビュー: @yamashita.yuki ( Shodo で執筆されました )
アバター
こんにちは、ISID金融ソリューション事業部の孫です。 この記事は、私が Unreal Engine (以下UE)のネットワーク同期(以下 レプリケーション )に関する知識を学んだ知見です。 UEの レプリケーション 機能は、 マルチプレイヤー ゲームの開発において非常に重要なコアな機能です。 Web上に公開されているUEの レプリケーション プログラミングは、現在BluePrintを用いたビジュアルプログラミングが主となっています。 確かにBluePrintは便利で迅速な開発が可能ですが、UEの内部動作ロジックをより深く理解するためには C++ プログラミングが不可欠です。 この記事では、 C++ を使用してシンプルなネットワーク同期のデモを実装する方法を紹介します。このデモの開発を通じて、UEの レプリケーション 機能の実装方法を学ぶことができます。 はじめに UEの レプリケーション 部分について触れると、Dedicated Serverという概念をまず理解する必要があります。 ゲームネットワーク アーキテクチャ においてDedicated Serverが導入された背景については、 金融ソリューション事業部の山下さんの記事 を参照していただければと思います。 UEのDedicated Serverについて、UEのクライアントコードとサーバーコードは一体であることが特徴です。 通常、一般的なフロントエンドとバックエンドの分離とは異なり、UEではクライアントとサーバーが同じプロジェクト内に存在します。このため、クライアントとサーバーのコードは混在していることになります。 ※コードの間で以下のマクロを使ってサーバーコードとクライアントコードの区別が可能: WITH_EDITOR : コードがエディタ環境で動作しているときにTrueになります。 UE_SERVER : コードがサーバー環境で動作しているときにTrueになります。 UE_CLIENT : コードがクライアント環境で動作しているときにTrueになります。 Dedicated Serverが必要な理由は、C/S(クライアント/サーバー)モードではサーバーがクライアントの業務も担当するため、運用負荷が高くなるからです。Dedicated Serverの導入により、クライアントとサーバーの役割が分離され、負荷を軽減できます。 Dedicated Serverは、UEが FPS の同期問題を解決するために設計された専用のサーバーです。また、Dedicated ServerはEpicが開発した特別な最適化されたネットワーク プロトコル を使用しており、高性能な同期(遅延問題の解決)を実現できます。 Dedicated Serverの構築方法については、以前の 記事 を参照してください。 開発環境/ツール Unreal Engine 5.2.0 Windows10 21H2 x64 RAM 16GM, SSD 1TB NVIDIA GeForce GTX 3080 Visual Studio 2019 version 16.11.21(以下VS2019) それでは、デモの制作を開始しましょう。以下の手順で進めていきます。 ※デモはUEのサードパーソンテンプレートの上で レプリケーション 機能を実装 UEのネットワーク知識とActorの権限確認(ユーザーネーム表示用キャ ラク ターの作成含め) ユーザー名入力画面の作成 ユーザーネームの レプリケーション 実装 Dedicated Server側の実装 デモの確認 このような手順でデモの制作を進めていくと、ユーザーネームを持つキャ ラク ターを生成し、それを全てのクライアントで同期できます。 1.UEのネットワーク知識とActor権限の確認 本番の作成を開始する前に、まずは2点の前提知識を説明します。 UEのネットワークモデル UEのネットワークモデルでは、Actorの レプリケーション を通じてゲームオブジェクトの状態をクライアント間で同期します。これにより、各クライアントが一貫したゲームワールドを見ることができます。 UEのネットワークモデルには、いくつかのキーポイントと概念があります: Actor Replication(アクターレプリケーション) : Unreal Engine では、各ゲームオブジェクトはActorと呼ばれます。サーバー内のActorの状態はクライアントに レプリケーション (複製)される可能性があり、これを「 レプリケーション 」と呼びます。どのActorが レプリケーション され、どのように レプリケーション されるかは、開発者が特定の属性と関数を設定することで決定されます。 State Synchronization(状態同期) :サーバーは自身の状態をネットワークを通じて各クライアントに送信し、全てのクライアントが一貫したゲームワールドを見ることができるようにします。この過程を状態同期と呼びます。これがサーバーコンテンツを各クライアントに分散する必要性の理由です。この過程がなければ、クライアントは古いか、または一貫性のないゲームワールドの状態を見ることになりゲーム体験が低下します。 Client Prediction(クライアント予測) :ネットワークの遅延がゲーム体験に影響を与えるのを減らすために、クライアントは「クライアント予測」と呼ばれる技術を使用します。つまり、サーバーからの応答が到着する前に、クライアントはあらかじめいくつかのアクションを実行します。サーバーからの応答を受け取ったら、クライアントは自身の状態を調整してサーバーの状態に一致させます。 Lag Compensation(ラグ補償) :これはネットワーク遅延の影響を減らす別のテクニックです。サーバーは、クライアントがリク エス トを発行した時点のゲーム状態に戻って、その状態でリク エス トされた操作を実行します。 これらの コンポーネント やテクニックを組み合わせることで、UEのネットワークモデルは安定性と効率性を持ち、多人数プレイにおける一貫したゲーム体験を実現します。 各 コンポーネント は重要な役割を果たしますが、その中でも核心的な概念は「状態同期」です。状態同期は、すべてのプレイヤーが一貫したゲームワールドを見ることを保証し、各自異なる視覚体験やインタ ラク ション体験が生じることを防ぎます。 Actorの所有権-ROLE クライアントとサーバーの間に区別がある以上、Actorの所有権においてもクライアントとサーバーの区別があることは当然です。 UEでは、Actorの制御権限を3つのカテゴリに分けています。それらは以下のとおりです: ROLE_None :特定の制御権限に属さない状態を表します。 ROLE_Authority :サーバー側でActorの制御権を持つことを示します。 ROLE_AutonomousProxy :クライアント側でローカルなActorの制御権を持つことを示します。 ROLE_SimulatedProxy :他クライアントがActor制御権を持つことを示します。 これらの三つの属性はUEがActorを設計する際に、Actorに固有属性として設計されています。これはActorがどこに存在するかを判断するために使われます。 UEでは、サーバーのコードとクライアントのコードが一体化しているため、Actorがこの属性を持つことは非常に必要です。 その辺の権限関係をテストしてみましょう。 テンプレートのCharacterにRendertextを追加します。 xxxCharacter.h #include "Components/TextRenderComponent.h" ... public: UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = playername, meta = (AllowPrivateAccess = "true")) class UTextRenderComponent* playerNameTag; ... xxxCharacter.cpp #include "Components/SkeletalMeshComponent.h" // コンストラクタ関数にキャラクターのSkeletalメッシュ配下にTextRenderComponent追加 xxxCharacter::xxxCharacter() { ... // Create a Text Component playerNameTag = CreateDefaultSubobject<UTextRenderComponent>(TEXT("playerName")); USkeletalMeshComponent* SkeletalMesh = GetMesh(); playerNameTag->SetupAttachment(SkeletalMesh); playerNameTag->SetText(FText::FromString("test")); // Set Component Location Rotation playerNameTag->SetHorizontalAlignment(EHTA_Center); playerNameTag->SetRelativeLocation(FVector(0.0f, 0.0f, 180.0f)); playerNameTag->SetRelativeRotation(FRotator(0.0f, 90.0f, 0.0f)); ... 制御Roleを表示します。 xxxCharacter.cpp void Atest_DEServerCharacter::BeginPlay() { if (GetLocalRole() == ROLE_Authority) { playerNameTag->SetText(FText::FromString("ROLE_Authority")); UE_LOG(LogTemp, Warning, TEXT("This Actor is on the server.")); } else if (GetLocalRole() == ROLE_AutonomousProxy) { playerNameTag->SetText(FText::FromString("ROLE_AutonomousProxy")); UE_LOG(LogTemp, Warning, TEXT("This Actor is on the owning client.")); } else if (GetLocalRole()== ROLE_SimulatedProxy) { playerNameTag->SetText(FText::FromString("ROLE_SimulatedProxy")); UE_LOG(LogTemp, Warning, TEXT("Other Client Actor is ROLE_SimulatedProxy.")); } else { playerNameTag->SetText(FText::FromString("ROLE_None")); UE_LOG(LogTemp, Warning, TEXT("This Actor is on a non-owning client.")); } } 権限Roleを確認します。 サーバーのウィンドウでは、すべてのキャ ラク ターが「ROLE_Authority」と表示されていることがわかります。 それに対して二つのクライアントのウィンドウでは、自分が制御しているキャ ラク ターだけ「ROLE_AutonomousProxy」と表示され、他のすべては「ROLE_SimulatedProxy」と表示されています。 その中にはサーバーが生成したキャ ラク ターも含まれていますが、このクライアントにとってはそれも他のエンドのActorに属するものとなります。 次に、ステップ バイス テップで レプリケーション デモを作成します。 2.ユーザー名入力画面の作成 入力用 Widget UIの作成 Content Browserで Content フォルダを開き、右側の空白部分で右クリックして User Interface -> Widget Blueprintを選択してUIを作成します。 新しく作成したBlueprintをダブルクリックして開き、以下の画像のように「ゲーム開始Button」「ユーザー名入力のEditableText」とタイトル表示の「TextBlock ウィジェット 」を追加します。 Widget UIの親Classファイルの作成 Content Browser で C++Classes フォルダを開き、右側の空白部分で右クリックし New C++ Class -> UserWidget を選択します。 新しく作成したクラスファイルが自動的に Visual Studio で開かれます。 このクラスがBlueprintのUIを制御するために、Blueprintの親クラスを新しく作成したクラスに変更します。 UIのBlueprintをダブルクリックして開き、 File -> Reparent Blueprint をクリックし、表示されるダイアログで新しく作成したクラスを選択します。 ユーザー名の取得コードの実装 制御する ウィジェット の定義を追加します。 定義に UPROPERTY(EditAnywhere, BlueprintReadWrite, meta = (BindWidget)) を追加して、Blueprint Widget 内の対応する ウィジェット にバインドできるようにします。 //LoginHUDWidget.h UCLASS() class TEST_DESERVER_API ULoginHUDWidget : public UUserWidget { GENERATED_BODY() public: void NativePreConstruct(); UFUNCTION() void OnPlayGameButtonClicked(); UPROPERTY(EditAnywhere, BlueprintReadWrite, meta = (BindWidget)) class UEditableText* inputName; UPROPERTY(EditAnywhere, BlueprintReadWrite, meta = (BindWidget)) class UTextBlock* statusLabel; UPROPERTY(EditAnywhere, BlueprintReadWrite, meta = (BindWidget)) class UTextBlock* playLabel; UPROPERTY(EditAnywhere, BlueprintReadWrite, meta = (BindWidget)) class UButton* playBtn; }; コンスト ラク タ関数で ウィジェット を初期化します。 Button ウィジェット には OnPlayGameButtonClicked という処理関数を追加します。 //LoginHUDWidget.cpp void ULoginHUDWidget::NativePreConstruct() { Super::NativePreConstruct(); inputName->SetHintText(FText::FromString("Please input your name")); statusLabel->SetText(FText::FromString("Test Replication Demo")); playLabel->SetText(FText::FromString("Play")); FScriptDelegate StartPlayDelegate; StartPlayDelegate.BindUFunction(this, "OnPlayGameButtonClicked"); playBtn->OnClicked.Add(StartPlayDelegate); }; ゲーム開始ボタンがクリックされた後の処理ロジックを追加します。 UGameplayStatics::OpenLevel 関数を使用してDedicated Serverに接続します。 ユーザーが入力したプレイヤー名はOptionsを通じてDedicated Serverに渡されます。 void ULoginHUDWidget::OnPlayGameButtonClicked() { FString NickName = inputName->GetText().ToString(); FString LevelName = "127.0.0.1:7777"; FString Options = FString::Printf(TEXT("?NickName=%s"), *NickName); UGameplayStatics::OpenLevel(GetWorld(), FName(*LevelName), false, Options); } GameModeで Widget コンポーネント をロードします。 //xxxGameMode.h protected: UPROPERTY(EditAnywhere, Category = "UI") TSubclassOf<UUserWidget> LoginWidgetClass; private: UPROPERTY() ULoginHUDWidget* loginWidget; //xxxGameMode.cpp AxxxGameMode::AxxxGameMode() { static ConstructorHelpers::FClassFinder<UUserWidget> LoginWidgetObj(TEXT("/Game/UI/LoginHUD_UI")); LoginWidgetClass = LoginWidgetObj.Class; }; void AxxxGameMode::BeginPlay() { Super::BeginPlay(); APlayerController* PlayerController = GetWorld()->GetFirstPlayerController(); if (PlayerController != nullptr) { PlayerController->bShowMouseCursor = true; } if (LoginWidgetClass != nullptr) { UUserWidget* loginWidget = CreateWidget<UUserWidget>(GetWorld(), LoginWidgetClass); if (loginWidget != nullptr) { loginWidget->AddToViewport(); } } } 3.ユーザーネームの レプリケーション 実装 クライアントの属性が変更されたときに、その属性値が他のクライアントに同期するためには、以下の2点を覚えておく必要があります: ① 属性のReplicationはReplicated or ReplicatedUsingに設定すべきです ② 属性を変更するコードはDedicated Server上で実行されます Actorの レプリケーション ReplicationはUObjectから派生した任意クラスの変数の固有属性で、この変数がネットワーク同期を許可するかどうかを指定します。 ブループリントでは、以下の3つの項目がReplication属性に対して設定可能です: None :ネットワーク同期を許可しません。 Replication :ネットワーク同期を許可します。 RepNotify :属性はネットワーク同期を許可し、さらにコールバック関数にバインドします。属性が変化すると、このコールバックが呼び出されます。ブループリントでは、このコールバック関数はFUNCTION内に自動的に作成され、OnRep_で始まり属性名で終わるようになっています。例えば、pos属性のコールバック関数はOnRep_posとなります。 C++ でこの部分は、 APlayerState クラスで実装されます。 APlayerState は Unreal Engine のクラスであり、各プレイヤーに関連するゲーム情報を格納および管理するために使用されます。 この情報は、プレイヤーが現在のシーンにいるかどうかに関係なく、通常はゲームセッション全体で永続的です。この設計により、 APlayerState はゲームセッション全体のレベル内でプレイヤー情報を格納する理想的な場所となります。 ※ 注意 : APlayerState はプレイヤーの入力やゲームワールド内での状態(位置、速度、アニメーションの状態など)を格納するためのものではありません。これらの情報は APlayerController に格納する必要があります。 該当する C++ コードの例は以下のようになります。 # ネットワーク同期を許可しません UPROPERTY() # ネットワーク同期を許可します UPROPERTY(Replicated) # 属性はネットワーク同期を許可し、さらにコールバック関数にバインドします UPROPERTY(ReplicatedUsing=OnRep_xxx) ユーザーネームの レプリケーション 実装 Playstateのサブクラスを新規作成します。 Widget Classを作成したのと同じ手順で、 C++ Classesフォルダで右クリックし、 New C++ Class -> playerState を選択します。 作成が完了すると、 Visual Studio が自動的に新しいクラスファイルを開きます。 Replicatedとして定義します。 DOREPLIFETIME Unreal Engine のネットワークプログラミングにおけるマクロであり、特定のプロパティがネットワーク上で複製可能であることを設定するために使用されます。 //playerstate.h public: UPROPERTY(Replicated) FString NickName; virtual void GetLifetimeReplicatedProps(TArray<FLifetimeProperty>& OutLifetimeProps) const override; //playerstate.cpp void AMetaPlayerState::GetLifetimeReplicatedProps(TArray<FLifetimeProperty>& OutLifetimeProps) const { Super::GetLifetimeReplicatedProps(OutLifetimeProps); DOREPLIFETIME(AMetaPlayerState, NickName); } GameModeのコンスト ラク タ関数に PlayerState をロードします。 AxxxGameMode::AxxxGameMode() { PlayerStateClass = AMetaPlayerState::StaticClass(); }; サーバーからの更新メッセージを受け取り、キャ ラク ターに値を割り当てます。 コン トロール しているキャ ラク ターについて、上記で定義したNickNameが変更された場合、 OnRep_PlayerState 関数が実行されます。 OnRep_PlayerState 関数は APlayerState クラスに定義されており、それを継承する必要があります。 //xxxCharacter.h private: virtual void OnRep_PlayerState() override; //xxxCharacter.cpp void AxxxCharacter::OnRep_PlayerState() { Super::OnRep_PlayerState(); APlayerState* OwningPlayerState = GetPlayerState(); if (OwningPlayerState != nullptr) { AMetaPlayerState* MetaPlayerState = Cast<AMetaPlayerState>(OwningPlayerState); if (MetaPlayerState != nullptr) { FString NickName = MetaPlayerState->NickName; if (NickName.Len() > 0 ) { playerNameTag->SetText(FText::FromString(NickName)); } } } } 4.Dedicated Server側の実装 UEの AGameModeBase および AGameMode クラスは、ゲームの基本ルールとロジックを定義するために使用されます。 以下に、これらのクラスでいくつかの重要なライフサイクル関数と、それらが通常どのような役割を果たすかを示します。 InitGame() :この関数はサーバーが起動し、すべてのオブジェクトがロードされ、ゲームがまだ実行されていない状態で呼び出されます。ここで変数や状態を初期化できます。 PreLogin() :これはクライアントが接続を許可される前にサーバーで呼び出される関数です。プレイヤーの資格情報(例:ユーザー名やパスワードの確認)を検証したり、プレイヤーの接続を他の形式で事前検証したりするために使用できます。検証が失敗した場合、ここでプレイヤーの接続を拒否できます。 PostLogin() :プレイヤーが正常に接続され、検証された後、PostLogin()関数が呼び出されます。ここでは、プレイヤーがゲームに参加した後すぐに実行する必要のあるコードを実行できます。例えば、歓迎メッセージを送信したり、プレイヤーのゲームデータを初期化したりできます。 InitNewPlayer() :この関数はプレイヤーがサーバーに接続して初期化されたときに呼び出されます。この関数では、「プレイヤーの属性の初期化」「プレイヤーのチームの割り当て」「新しいプレイヤーに必要なゲーム情報の送信」など、多くのタスクを実行できます。 BeginPlay() :この関数はゲームの開始時に呼び出されます。ゲーム開始時に実行する必要があるコードをここで実行できます。 Logout() :プレイヤーがゲームから退出するときにこの関数が呼び出されます。ここでは、プレイヤーがゲームから退出する際に実行する必要のあるコード(例:プレイヤーのゲームデータの保存や、プレイヤーの退出メッセージの送信など)を実行できます。 これらの関数は最も一般的に使用され、ゲームの異なる段階で何を実行するかをサーバーサイドで処理するために使用されます。    これらの関数により、ゲームのロジックや要件に合わせて特定のコードを適切なタイミングで実行できます。 前述のように、同期を実現するには2つの条件を満たす必要があります。 「② 属性の変更コードはDedicated Server上で実行される」 という条件を満たすために、上記の関数の中で、特に InitNewPlayer() 関数を選択する必要があります。 なぜなら、この関数はプレイヤーの PlayState 初期化が行われるタイミングですから。 //.xxxGameMode.h virtual FString InitNewPlayer(APlayerController* NewPlayerController, const FUniqueNetIdRepl& UniqueId, const FString& Options, const FString& Portal) override; //.xxxGameMode.cpp FString xxxServerGameMode::InitNewPlayer(APlayerController* NewPlayerController, const FUniqueNetIdRepl& UniqueId, const FString& Options, const FString& Portal) { FString InitializedString = Super::InitNewPlayer(NewPlayerController, UniqueId, Options, Portal); const FString& nickName = UGameplayStatics::ParseOption(Options, "NickName"); if (NewPlayerController != nullptr) { APlayerState* PlayerState = NewPlayerController->PlayerState; if (PlayerState != nullptr) { AMetaPlayerState* ServerPlayerState = Cast<AMetaPlayerState>(PlayerState); if (ServerPlayerState) { ServerPlayerState->NickName = nickName; } } } return InitializedString; } 5.デモの確認 ここまでで、ユーザーネームの レプリケーション に関する実装が全部完了しました! Dedicated Serverをパッケージ化して試してみましょう。確認ポイントは以下となります: 各クライアントでユーザーがユーザーネームを入力し、ゲームが開始できること 各クライアントで対応するキャ ラク ターの名前が表示されること 上記の確認が取れましたら、いわゆるネットワーク上での状態同期が成功し、すべてのクライアントが一貫したゲーム世界を見ることができるようになりました! ※パッケージング手順については Amazon GameLift × Unreal Engines 5 でオンラインマルチプレイゲームを作る の記事を参照してください。 注意事項 属性の同期を実装する際には、以下の点を注意してください。 それは、キャ ラク ターモデルの変更をAPlayerStateクラスに実装しないということです。 筆者がコードを書く際、最初に APlayerState クラスに以下のようなコールバック関数を書いたことがありました。 void ATestPlayerState::OnRep_NickName() { APlayerController* PC = GetGameInstance()->GetFirstLocalPlayerController(); if (PC && PC->GetPawn()) { AMetaPlayerController* PlayerController = Cast<AMetaPlayerController>(PC); if (PlayerController) { Atest_DEServerCharacter* MyCharacter = Cast<Atest_DEServerCharacter>(PlayerController->GetPawn()); if (MyCharacter) { MyCharacter->playerNameTag->SetText(FText::FromString(NickName)); } } } } 実行結果として、もともとPlayer1を制御していたユーザーが、Player2がログインした後に制御しているキャ ラク ターの表示がPlayer2のユーザー名になってしまうという問題が発生しました。    これは、私が APlayerState の OnRep_NickName 関数でキャ ラク ターを取得し、名前ラベルを変更していたためです。 この関数は、NickNameフィールドがクライアントに複製されたときに呼び出されます。しかし一部の場合では、NickNameフィールドがクライアントに複製された時点では、クライアントが新しいキャ ラク ターの情報をまだ受信していない可能性があります。つまり、GetPawn()関数がnullを返すか、もしくはキャ ラク ターが存在していても既に存在する他のプレイヤーのキャ ラク ターになるかもしれません。 この問題を解決するための方法は、 APlayerState のOnRep関数内でキャ ラク ターを取得し、名前ラベルを変更しないことです。 代わりに、キャ ラク ターのOnRep_PlayerState関数内で、キャ ラク ター自身のPlayerStateを取得しキャ ラク ターの名前ラベルを変更する必要があります。先ほど実装したコードと同様に、キャ ラク ター自身のOnRep_PlayerState関数でこれを行ってください。 終わりに このユーザーネーム属性の レプリケーション デモを通じて、 Unreal Engine のネットワーク同期モデルについて一定の理解を得ることができました。 Unreal Engine は、さまざまなタイプのゲームや仮想現実アプリケーションをサポートする強力な ゲームエンジン です。3Dおよび メタバース の開発領域では、ネットワーク同期、 マルチプレイヤー ゲーム、物理シミュレーション、シーンの レンダリング など、さまざまな技術と応用を探求できます。学習と研究を続けることで、この領域においてより深い理解と高い技術力を身につけることができます。 現在ISIDは web3領域のグループ横断組織 を立ち上げ、Web3および メタバース 領域のR&Dを行っております(カテゴリー「3DCG」の記事は こちら )。 もし本領域にご興味のある方や、一緒にチャレンジしていきたい方は、ぜひお気軽にご連絡ください! 私たちと同じチームで働いてくれる仲間を、是非お待ちしております! ISID採用ページ(Web3/メタバース/AI) 参考文献 https://docs.unrealengine.com/5.2/en-US/networking-overview-for-unreal-engine/ 執筆: @chen.sun 、レビュー: @yamashita.yuki ( Shodo で執筆されました )
アバター
電通国際情報サービス 、オープン イノベーション ラボの 比嘉康雄 です。 今回は、 Expoの公式チュートリアル をやっていきたいと思います。 プロジェクトの作成 Download assets Install dependencies アプリの実行 コードの編集 テキストの文字色の変更 イメージの表示 ImageViewerコンポーネント Buttonコンポーネント PhotoButtonコンポーネント 画像の選択 まとめ 仲間募集 プロジェクトの作成 StickerSmash プロジェクトを作成しましょう。 npx create-expo-app StickerSmash && cd StickerSmash Download assets https://docs.expo.dev/static/images/tutorial/sticker-smash-assets.zip をダウンロードしてから解凍し、StickerSmash/assetsに格納します。既存のファイルは置き換えてください。 Install dependencies Webでも実行できるようにするために、必要なモジュールをインストールします。 npx expo install react-dom react-native-web @expo/webpack-config アプリの実行 プロジェクトの ディレクト リで、 npx expo start を実行します。 wを押すと、Webアプリを試すことができます。 次のように表示されました。 コードの編集 プロジェクトの ディレクト リで、 code . を実行して、 VS Code を立ち上げます。 背景を #25292e に変更しましょう。 const styles = StyleSheet.create({ container: { flex: 1, backgroundColor: "#25292e", alignItems: "center", justifyContent: "center", }, }); 次のように表示されました。 テキストのデフォルトの文字色は黒なので、背景が黒っぽくなったことで、文字が見え辛くなってしまいました。 テキストの文字色の変更 Textタグのstyle属性で、文字の色を白に指定しましょう。 <Text style={{ color: "#fff" }}> Open up App.js to start working on your app! </Text> 次のように表示されました。 イメージの表示 イメージを表示しましょう。 Image コンポーネント をインポートします。 import { StyleSheet, View, Image } from 'react-native'; 次は、イメージパスの設定です。 const PlaceholderImage = require('./assets/images/background-image.png'); Imageタグを使う前に、 スタイルシート の設定をしましょう。 const styles = StyleSheet.create({ container: { flex: 1, backgroundColor: "#25292e", alignItems: "center", }, imageContainer: { flex: 1, paddingTop: 58, }, image: { width: 320, height: 440, borderRadius: 18, }, }); 準備ができたので、Image コンポーネント を使ってみましょう。 <View style={styles.imageContainer}> <Image source={PlaceholderImage} style={styles.image} /> </View> 下記のように表示されました。 ImageViewer コンポーネント 先ほど作ったイメージ表示機能を コンポーネント 化しましょう。 StickSmash ディレクト リの中にcomponents ディレクト リを作成します。 .../StickerSmash$ mkdir components VS Code で ImageViewer.js を作成します。 code components/ImageViewer.js 下記のコードを ImageViewer.js に書き込みます。 import { StyleSheet, Image } from 'react-native'; export default function ImageViewer({ placeholderImageSource }) { return ( <Image source={placeholderImageSource} style={styles.image} /> ); } const styles = StyleSheet.create({ image: { width: 320, height: 440, borderRadius: 18, }, }); App.js から ImageViewer.js を呼び出します。 import ImageViewer from './components/ImageViewer'; const PlaceholderImage = require('./assets/images/background-image.png'); export default function App() { return ( <View style={styles.container}> <View style={styles.imageContainer}> <ImageViewer placeholderImageSource={PlaceholderImage} /> </View> <StatusBar style="auto" /> </View> ); } 見た目はもちろん以前と一緒です。 Button コンポーネント これからボタンを2つ作りますが、その前に、Button コンポーネント を作ってそれを利用することにしましょう。 VS Code で Button.js を作成します。 code components/Button.js 下記のコードを Button.js に書き込みます。 import { StyleSheet, View, Pressable, Text } from 'react-native'; export default function Button({ label }) { return ( <View style={styles.buttonContainer}> <Pressable style={styles.button} onPress={() => alert('You pressed a button.')}> <Text style={styles.buttonLabel}>{label}</Text> </Pressable> </View> ); } const styles = StyleSheet.create({ buttonContainer: { width: 320, height: 68, marginHorizontal: 20, alignItems: 'center', justifyContent: 'center', padding: 3, }, button: { borderRadius: 10, width: '100%', height: '100%', alignItems: 'center', justifyContent: 'center', flexDirection: 'row', }, buttonLabel: { color: '#fff', fontSize: 16, }, }); App.js から Button.js を呼び出します。 import Button from './components/Button'; <View style={styles.footerContainer}> <Button label="Choose a photo" /> <Button label="Use this photo" /> </View> const styles = StyleSheet.create({ // Styles that are unchanged from previous step are hidden for brevity. footerContainer: { flex: 1 / 3, alignItems: 'center', }, }); 下記のように表示されました。 PhotoButton コンポーネント 写真アイコンが表示されるボタンを作りましょう。 @expo/vector-icon をインストールします。 npx expo install @expo/vector-icons VS Code で PhotoButton.js を作成します。 code components/PhotoButton.js 下記のコードを PhotoButton.js に書き込みます。 import { StyleSheet, View, Pressable, Text } from "react-native"; import FontAwesome from "@expo/vector-icons/FontAwesome"; export default function PhotoButton({ label }) { return ( <View style={styles.buttonContainer}> <Pressable style={styles.button} onPress={() => alert("You pressed a button.")} > <FontAwesome name="picture-o" size={18} color="#25292e" style={styles.buttonIcon} /> <Text style={[styles.buttonLabel, {}]}>{label}</Text> </Pressable> </View> ); } const styles = StyleSheet.create({ buttonContainer: { width: 320, height: 68, marginHorizontal: 20, alignItems: "center", justifyContent: "center", padding: 3, borderWidth: 4, borderColor: "#ffd33d", borderRadius: 18, }, button: { borderRadius: 10, width: "100%", height: "100%", alignItems: "center", justifyContent: "center", flexDirection: "row", backgroundColor: "#fff", }, buttonIcon: { paddingRight: 8, }, buttonLabel: { color: "#25292e", fontSize: 16, }, }); 下記のように表示されました。 画像の選択 画像を選択する機能を実装しましょう。 expo-image-picker をインストールします。 npx expo install expo-image-picker App.js で pickImageAsync ファンクションを実装しましょう。選択した画像は、 result.assets[0].uri に入っています。 import * as ImagePicker from 'expo-image-picker'; const pickImageAsync = async () => { const result = await ImagePicker.launchImageLibraryAsync({ allowsEditing: true, quality: 1, }); if (!result.canceled) { alert(result.assets[0].uri); } else { alert('You did not select any image.'); } }; PhotoButton でボタンを押したときの動作が定義できるように、 onPress 属性を追加しましょう。 export default function PhotoButton({ label, onPress }) { return ( <View style={styles.buttonContainer}> <Pressable style={styles.button} onPress={onPress} > <FontAwesome name="picture-o" size={18} color="#25292e" style={styles.buttonIcon} /> <Text style={styles.buttonLabel>{label}</Text> </Pressable> </View> ); } App.js で PhotoButton が押されたときに、 pickImageAsync ファンクションを呼び出します。 <PhotoButton label="Choose a photo" onPress={pickImageAsync} /> ImageViewer で選択した画像を表示できるように、 selectedImage 属性を追加します。 export default function ImageViewer( { placeholderImageSource, selectedImage }) { const imageSource = selectedImage ? { uri: selectedImage } : placeholderImageSource; return <Image source={imageSource} style={styles.image} />; } App.js で、写真が選択されたら、 ImageViewer に表示します。 import { useState } from "react"; const PlaceholderImage = require("./assets/images/background-image.png"); export default function App() { const [selectedImage, setSelectedImage] = useState(null); const pickImageAsync = async () => { const result = await ImagePicker.launchImageLibraryAsync({ allowsEditing: true, quality: 1, }); if (!result.canceled) { setSelectedImage(result.assets[0].uri); } else { alert("You did not select any image."); } }; return ( <View style={styles.container}> <View style={styles.imageContainer}> <ImageViewer placeholderImageSource={PlaceholderImage} selectedImage={selectedImage} /> </View> <View style={styles.footerContainer}> <PhotoButton label="Choose a photo" onPress={pickImageAsync} /> <Button label="Use this photo" /> </View> <StatusBar style="auto" /> </View> ); } 下記のように表示されました。 切りが良いので、前編はここまでとします。 まとめ Expoの公式チュートリアル は、難易度もちょうどいい感じで、実践的な力が身につくなと感じました。ただ、 JavaScript じゃなくTypeScriptにしてほしかったところです。 仲間募集 私たちは同じグループで共に働いていただける仲間を募集しています。 現在、以下のような職種を募集しています。 ソリューションアーキテクト AIエンジニア 執筆: @higa ( Shodo で執筆されました )
アバター
電通国際情報サービス 、オープン イノベーション ラボの 比嘉康雄 です。 今回は、 Expoの公式チュートリアル をやっていきたいと思います。 プロジェクトの作成 Download assets Install dependencies アプリの実行 コードの編集 テキストの文字色の変更 イメージの表示 ImageViewerコンポーネント Buttonコンポーネント PhotoButtonコンポーネント 画像の選択 まとめ 仲間募集 プロジェクトの作成 StickerSmash プロジェクトを作成しましょう。 npx create-expo-app StickerSmash && cd StickerSmash Download assets https://docs.expo.dev/static/images/tutorial/sticker-smash-assets.zip をダウンロードしてから解凍し、StickerSmash/assetsに格納します。既存のファイルは置き換えてください。 Install dependencies Webでも実行できるようにするために、必要なモジュールをインストールします。 npx expo install react-dom react-native-web @expo/webpack-config アプリの実行 プロジェクトの ディレクト リで、 npx expo start を実行します。 wを押すと、Webアプリを試すことができます。 次のように表示されました。 コードの編集 プロジェクトの ディレクト リで、 code . を実行して、 VS Code を立ち上げます。 背景を #25292e に変更しましょう。 const styles = StyleSheet.create({ container: { flex: 1, backgroundColor: "#25292e", alignItems: "center", justifyContent: "center", }, }); 次のように表示されました。 テキストのデフォルトの文字色は黒なので、背景が黒っぽくなったことで、文字が見え辛くなってしまいました。 テキストの文字色の変更 Textタグのstyle属性で、文字の色を白に指定しましょう。 <Text style={{ color: "#fff" }}> Open up App.js to start working on your app! </Text> 次のように表示されました。 イメージの表示 イメージを表示しましょう。 Image コンポーネント をインポートします。 import { StyleSheet, View, Image } from 'react-native'; 次は、イメージパスの設定です。 const PlaceholderImage = require('./assets/images/background-image.png'); Imageタグを使う前に、 スタイルシート の設定をしましょう。 const styles = StyleSheet.create({ container: { flex: 1, backgroundColor: "#25292e", alignItems: "center", }, imageContainer: { flex: 1, paddingTop: 58, }, image: { width: 320, height: 440, borderRadius: 18, }, }); 準備ができたので、Image コンポーネント を使ってみましょう。 <View style={styles.imageContainer}> <Image source={PlaceholderImage} style={styles.image} /> </View> 下記のように表示されました。 ImageViewer コンポーネント 先ほど作ったイメージ表示機能を コンポーネント 化しましょう。 StickSmash ディレクト リの中にcomponents ディレクト リを作成します。 .../StickerSmash$ mkdir components VS Code で ImageViewer.js を作成します。 code components/ImageViewer.js 下記のコードを ImageViewer.js に書き込みます。 import { StyleSheet, Image } from 'react-native'; export default function ImageViewer({ placeholderImageSource }) { return ( <Image source={placeholderImageSource} style={styles.image} /> ); } const styles = StyleSheet.create({ image: { width: 320, height: 440, borderRadius: 18, }, }); App.js から ImageViewer.js を呼び出します。 import ImageViewer from './components/ImageViewer'; const PlaceholderImage = require('./assets/images/background-image.png'); export default function App() { return ( <View style={styles.container}> <View style={styles.imageContainer}> <ImageViewer placeholderImageSource={PlaceholderImage} /> </View> <StatusBar style="auto" /> </View> ); } 見た目はもちろん以前と一緒です。 Button コンポーネント これからボタンを2つ作りますが、その前に、Button コンポーネント を作ってそれを利用することにしましょう。 VS Code で Button.js を作成します。 code components/Button.js 下記のコードを Button.js に書き込みます。 import { StyleSheet, View, Pressable, Text } from 'react-native'; export default function Button({ label }) { return ( <View style={styles.buttonContainer}> <Pressable style={styles.button} onPress={() => alert('You pressed a button.')}> <Text style={styles.buttonLabel}>{label}</Text> </Pressable> </View> ); } const styles = StyleSheet.create({ buttonContainer: { width: 320, height: 68, marginHorizontal: 20, alignItems: 'center', justifyContent: 'center', padding: 3, }, button: { borderRadius: 10, width: '100%', height: '100%', alignItems: 'center', justifyContent: 'center', flexDirection: 'row', }, buttonLabel: { color: '#fff', fontSize: 16, }, }); App.js から Button.js を呼び出します。 import Button from './components/Button'; <View style={styles.footerContainer}> <Button label="Choose a photo" /> <Button label="Use this photo" /> </View> const styles = StyleSheet.create({ // Styles that are unchanged from previous step are hidden for brevity. footerContainer: { flex: 1 / 3, alignItems: 'center', }, }); 下記のように表示されました。 PhotoButton コンポーネント 写真アイコンが表示されるボタンを作りましょう。 @expo/vector-icon をインストールします。 npx expo install @expo/vector-icons VS Code で PhotoButton.js を作成します。 code components/PhotoButton.js 下記のコードを PhotoButton.js に書き込みます。 import { StyleSheet, View, Pressable, Text } from "react-native"; import FontAwesome from "@expo/vector-icons/FontAwesome"; export default function PhotoButton({ label }) { return ( <View style={styles.buttonContainer}> <Pressable style={styles.button} onPress={() => alert("You pressed a button.")} > <FontAwesome name="picture-o" size={18} color="#25292e" style={styles.buttonIcon} /> <Text style={[styles.buttonLabel, {}]}>{label}</Text> </Pressable> </View> ); } const styles = StyleSheet.create({ buttonContainer: { width: 320, height: 68, marginHorizontal: 20, alignItems: "center", justifyContent: "center", padding: 3, borderWidth: 4, borderColor: "#ffd33d", borderRadius: 18, }, button: { borderRadius: 10, width: "100%", height: "100%", alignItems: "center", justifyContent: "center", flexDirection: "row", backgroundColor: "#fff", }, buttonIcon: { paddingRight: 8, }, buttonLabel: { color: "#25292e", fontSize: 16, }, }); 下記のように表示されました。 画像の選択 画像を選択する機能を実装しましょう。 expo-image-picker をインストールします。 npx expo install expo-image-picker App.js で pickImageAsync ファンクションを実装しましょう。選択した画像は、 result.assets[0].uri に入っています。 import * as ImagePicker from 'expo-image-picker'; const pickImageAsync = async () => { const result = await ImagePicker.launchImageLibraryAsync({ allowsEditing: true, quality: 1, }); if (!result.canceled) { alert(result.assets[0].uri); } else { alert('You did not select any image.'); } }; PhotoButton でボタンを押したときの動作が定義できるように、 onPress 属性を追加しましょう。 export default function PhotoButton({ label, onPress }) { return ( <View style={styles.buttonContainer}> <Pressable style={styles.button} onPress={onPress} > <FontAwesome name="picture-o" size={18} color="#25292e" style={styles.buttonIcon} /> <Text style={styles.buttonLabel>{label}</Text> </Pressable> </View> ); } App.js で PhotoButton が押されたときに、 pickImageAsync ファンクションを呼び出します。 <PhotoButton label="Choose a photo" onPress={pickImageAsync} /> ImageViewer で選択した画像を表示できるように、 selectedImage 属性を追加します。 export default function ImageViewer( { placeholderImageSource, selectedImage }) { const imageSource = selectedImage ? { uri: selectedImage } : placeholderImageSource; return <Image source={imageSource} style={styles.image} />; } App.js で、写真が選択されたら、 ImageViewer に表示します。 import { useState } from "react"; const PlaceholderImage = require("./assets/images/background-image.png"); export default function App() { const [selectedImage, setSelectedImage] = useState(null); const pickImageAsync = async () => { const result = await ImagePicker.launchImageLibraryAsync({ allowsEditing: true, quality: 1, }); if (!result.canceled) { setSelectedImage(result.assets[0].uri); } else { alert("You did not select any image."); } }; return ( <View style={styles.container}> <View style={styles.imageContainer}> <ImageViewer placeholderImageSource={PlaceholderImage} selectedImage={selectedImage} /> </View> <View style={styles.footerContainer}> <PhotoButton label="Choose a photo" onPress={pickImageAsync} /> <Button label="Use this photo" /> </View> <StatusBar style="auto" /> </View> ); } 下記のように表示されました。 切りが良いので、前編はここまでとします。 まとめ Expoの公式チュートリアル は、難易度もちょうどいい感じで、実践的な力が身につくなと感じました。ただ、 JavaScript じゃなくTypeScriptにしてほしかったところです。 仲間募集 私たちは同じグループで共に働いていただける仲間を募集しています。 現在、以下のような職種を募集しています。 ソリューションアーキテクト AIエンジニア 執筆: @higa ( Shodo で執筆されました )
アバター
こんにちは!コミュニケーションIT事業部5年目の石田です。 普段は、 iPLAss というローコード開発プラットフォームの企画・開発・利用者サポートを主に担当しています。 この記事では、「若手による仕事の紹介」シリーズの一環として、私の業務内容や働き方についてお話しします。 自己紹介 まずは、簡単に自己紹介です。 2019 年に新卒入社し、2023 年で 5 年目を迎えました。 大学時代は 情報科学 を専攻しており、コンピュータやネットワークに関する知識・技術を系統的に学びつつ、 C言語 や Java を用いたプログラミング・研究開発といった実習も行っていました。 就職活動時点での方向性としては、専門性(技術力)を高めていくというよりは、ビジネス応用(IT・システムを手段として、如何に顧客の課題を解決していくか)により興味・関心がありました。 そこで、多種多様な業種・業態の顧客と接するチャンスがあり、 システム開発 の上流工程にも携わることのできると捉えていた システムインテグレーター ( SIer )をターゲットに就職活動を進め、最終的には縁あってISIDに入社を決めました。 ところが、、、。 入社後に行われた システム開発 研修において、「実際に世の中で使用されているような実用的なWebアプリケーションを複数人のチームで開発する」という大学時代は得られなかった経験を通じて、すっかり「ものづくり」の面白さに魅了されてしまいました。 そこからは、専門性を磨き上げ、「自分で手を動かしてものづくりがしたい」、「高度な専門性をもって事業に貢献したい」という 就活時点とは全く異なるキャリア を志すようになりました。 これから業務内容に触れていきますが、そんな私にぴったりの業務を今は任せられており、毎日楽しく充実して働くことができています。 そして、 SIer という枠にはとどまらないほど、ISIDには多様な仕事があるということを入社して改めて知ることができました。各社員の強みや適性に合った業務があり、まさに今の私はその恩恵を受けることができているのだと思います。 製品開発という仕事 iPLAssとは 冒頭で述べた通り、配属後から一貫して「iPLAss」を軸に業務を行っています。 iPLAssとは、簡単に言えば、「複雑なシステムを早く作る」を実現する ISID製の ローコード開発プラットフォームです。公共分野や金融分野を中心に幅広い適用実績を持つ製品です。 iPLAssの詳細が知りたい方は、是非、 iPLAssのホームページ や ハンズオン記事 をご覧ください。 業務内容 製品開発の仕事は多岐に渡ります。例えば、チームマネジメント、新規機能の企画・開発、バグ報告への対処、品質向上、ホームページや開発者ドキュメントの整備、対外発信活動(技術記事の執筆など)、利用者サポート(問い合わせ対応、不具合調査など)が主な業務内容になります。 製品開発に終わりはなく、iPLAssは日々進化を続けています。iPLAssの利用者(iPLAssを利用してシステムを開発し、顧客に提供するチーム)は、現状、ISID社内のプロジェクトチームであることがほとんどです。その為、開発者と利用者とで密にコミュニケーションを取り、利用者が求める機能を迅速に開発したり、利用者から得られたフィードバックを基に製品の改良を重ねています。iPLAss開発チーム内でも、iPLAss利用者の利便性向上を目的とした機能追加や開発プラットフォームとしての機能充実化・既存機能の強化を日々検討し、改善に取り組んでいます。 また、iPLAssの開発を通じて得られたソフトウェア開発に関する知識や経験を、技術課題の解消やプロトタイピングなどといった形でプロジェクトチームに還元するといったことも自身の役割として上司からは期待されています。 やりがい やりがいは大きく2つあります。 1つ目は、iPLAssを利用して開発されたアプリケーションが世の中に公開され、多くの方に利用されていることが実感できた時にやりがいを感じます。やはり、自分の作ったものが広く利用されれば嬉しいものです! 2つ目は、技術的な探究です。iPLAssの開発には、高度かつ幅広いプログラミングスキル・設計スキルが要求されます。また、 クラウド 連携など、技術トレンドに追随する能力も求められます。このように技術を突き詰める環境にいられることは、専門性を高めたいと考える私にとっては大きなやりがいとなっています。 働き方 業務の性質上、顧客と直接関わる機会は多くありません。また、社内外問わず、会議の頻度も他の社員と比べて多くはなく、どちらかと言えば1人でもくもくと作業をする時間が多いです。 ただ、毎週グループ内で雑談や情報共有を行う機会があったり、毎月上司と1on1ミーティングを実施していたりとコミュニケーションの不足をそれほど感じてはいません。 最後に 最後までお読みいただきありがとうございます。 ISIDでは、新卒・中途関わらず一緒に働く仲間を募集しています! この記事を読んで、ISIDに少しでも興味を持っていただけたら、是非新卒採用サイトを覗いてみてください。 www.isid.co.jp 執筆: @ishida_yuma 、レビュー: Ishizawa Kento (@kent) ( Shodo で執筆されました )
アバター
こんにちは!コミュニケーションIT事業部5年目の石田です。 普段は、 iPLAss というローコード開発プラットフォームの企画・開発・利用者サポートを主に担当しています。 この記事では、「若手による仕事の紹介」シリーズの一環として、私の業務内容や働き方についてお話しします。 自己紹介 まずは、簡単に自己紹介です。 2019 年に新卒入社し、2023 年で 5 年目を迎えました。 大学時代は 情報科学 を専攻しており、コンピュータやネットワークに関する知識・技術を系統的に学びつつ、 C言語 や Java を用いたプログラミング・研究開発といった実習も行っていました。 就職活動時点での方向性としては、専門性(技術力)を高めていくというよりは、ビジネス応用(IT・システムを手段として、如何に顧客の課題を解決していくか)により興味・関心がありました。 そこで、多種多様な業種・業態の顧客と接するチャンスがあり、 システム開発 の上流工程にも携わることのできると捉えていた システムインテグレーター ( SIer )をターゲットに就職活動を進め、最終的には縁あってISIDに入社を決めました。 ところが、、、。 入社後に行われた システム開発 研修において、「実際に世の中で使用されているような実用的なWebアプリケーションを複数人のチームで開発する」という大学時代は得られなかった経験を通じて、すっかり「ものづくり」の面白さに魅了されてしまいました。 そこからは、専門性を磨き上げ、「自分で手を動かしてものづくりがしたい」、「高度な専門性をもって事業に貢献したい」という 就活時点とは全く異なるキャリア を志すようになりました。 これから業務内容に触れていきますが、そんな私にぴったりの業務を今は任せられており、毎日楽しく充実して働くことができています。 そして、 SIer という枠にはとどまらないほど、ISIDには多様な仕事があるということを入社して改めて知ることができました。各社員の強みや適性に合った業務があり、まさに今の私はその恩恵を受けることができているのだと思います。 製品開発という仕事 iPLAssとは 冒頭で述べた通り、配属後から一貫して「iPLAss」を軸に業務を行っています。 iPLAssとは、簡単に言えば、「複雑なシステムを早く作る」を実現する ISID製の ローコード開発プラットフォームです。公共分野や金融分野を中心に幅広い適用実績を持つ製品です。 iPLAssの詳細が知りたい方は、是非、 iPLAssのホームページ や ハンズオン記事 をご覧ください。 業務内容 製品開発の仕事は多岐に渡ります。例えば、チームマネジメント、新規機能の企画・開発、バグ報告への対処、品質向上、ホームページや開発者ドキュメントの整備、対外発信活動(技術記事の執筆など)、利用者サポート(問い合わせ対応、不具合調査など)が主な業務内容になります。 製品開発に終わりはなく、iPLAssは日々進化を続けています。iPLAssの利用者(iPLAssを利用してシステムを開発し、顧客に提供するチーム)は、現状、ISID社内のプロジェクトチームであることがほとんどです。その為、開発者と利用者とで密にコミュニケーションを取り、利用者が求める機能を迅速に開発したり、利用者から得られたフィードバックを基に製品の改良を重ねています。iPLAss開発チーム内でも、iPLAss利用者の利便性向上を目的とした機能追加や開発プラットフォームとしての機能充実化・既存機能の強化を日々検討し、改善に取り組んでいます。 また、iPLAssの開発を通じて得られたソフトウェア開発に関する知識や経験を、技術課題の解消やプロトタイピングなどといった形でプロジェクトチームに還元するといったことも自身の役割として上司からは期待されています。 やりがい やりがいは大きく2つあります。 1つ目は、iPLAssを利用して開発されたアプリケーションが世の中に公開され、多くの方に利用されていることが実感できた時にやりがいを感じます。やはり、自分の作ったものが広く利用されれば嬉しいものです! 2つ目は、技術的な探究です。iPLAssの開発には、高度かつ幅広いプログラミングスキル・設計スキルが要求されます。また、 クラウド 連携など、技術トレンドに追随する能力も求められます。このように技術を突き詰める環境にいられることは、専門性を高めたいと考える私にとっては大きなやりがいとなっています。 働き方 業務の性質上、顧客と直接関わる機会は多くありません。また、社内外問わず、会議の頻度も他の社員と比べて多くはなく、どちらかと言えば1人でもくもくと作業をする時間が多いです。 ただ、毎週グループ内で雑談や情報共有を行う機会があったり、毎月上司と1on1ミーティングを実施していたりとコミュニケーションの不足をそれほど感じてはいません。 最後に 最後までお読みいただきありがとうございます。 ISIDでは、新卒・中途関わらず一緒に働く仲間を募集しています! この記事を読んで、ISIDに少しでも興味を持っていただけたら、是非新卒採用サイトを覗いてみてください。 www.isid.co.jp 執筆: @ishida_yuma 、レビュー: Ishizawa Kento (@kent) ( Shodo で執筆されました )
アバター
こんにちは、ISID 金融ソリューション事業部の岡崎です。 今回はUE5でワープ機能(キャ ラク ターの移動とワープ時のカメラワーク)を作成します。 はじめに UEでは、ロードを挟まないひとつづきのマップのことを「レベル」と呼びます。 今回はそのような、同一のレベル内でプレイヤーが操作するキャ ラク ターを任意の場所へ移動させるワープの機能を紹介します。 レベル間移動をする場合(ロードをはさむ違うマップへの移動)には、違った処理が必要になるので、今回は同一レベル内でのみの移動について、紹介します。 検証環境/ツール OS: Windows 11 pro GPU : NVIDIA GeForce RTX 4070 Ti Game Engine: Unreal Engine 5.2.0 実装手順 ワープゲートの作成 ワープ機能の作成 ワープ時の演出(カメラワーク) ワープ時の演出(フェードアウト) 1. ワープゲートの作成 今回はThirdPersonテンプレートを利用したプロジェクトで進めていきます。 まずはキャ ラク ターがワープをする際に使用するゲートを作成します。 ゲートのメッシュを作成しても良いですが、今回は簡略化のためにスターターコンテンツに入っている「SM_Doorframe」を使用します。 レベル上に入り口用と、出口用に2つ配置し、わかりやすいように「WarpGate1」「WarpGate2」と 命名 します。 それぞれのゲート用にマテリアルを作成します。 コンテンツドロワーよりマテリアルを作成し、下記画像のように設定を行いました。 詳細画面より「WarpGate1」のマテリアルに先ほど作成したマテリアルを適応させゲートを黄色くします。この黄色いゲートをワープの入り口として使用します。 同様に「WarpGate2」用のマテリアルを作成し適応させます。今回は青色のゲートにし、これをワープの出口として使用します。 本筋とは関係ないですが、もう少しワープゲートをリッチにしていきます。 コンテンツ追加からキューブを追加します。 サイズと位置を調整し、ワープゲートの内側と同じサイズに変更します。 また当たり判定をなくすために コリジョン プリセットの値を「No Collision」に変更しておきます。 次にマテリアルを作成し、「M_WarpEffect」と 命名 します。 「エミッシブカラー」のピンを「Multiply」に接続し、「A」のピンと「Vertex Color」と繋げます。さらに「B」のピンには「Constant」と繋げ、値を「50」に設定します。これにより光っているマテリアルが作成できます。 先ほど作成したキューブのマテリアルに適応させることで、ワープゲートがよりリッチになります。 複製して出口の方のゲートにも追加しておきます。 続いてワープの機能を作成していきましょう。 2. ワープ機能の作成 ワープの機能を実装するためにBlueprintを作成します。 コンテンツドロワーよりアクターのBlueprintを作成し「BP_ Warp 」とします。 コンテンツ追加より「Box Collision」を2つ追加し、「In」「Out」と 命名 します。 「DefaultSceneRoot」の大きさを、ワープゲートの内側の大きさに合うように調整します。 次に「BP_ Warp 」をレベル上に配置し、入り口の黄色いゲートの内側に配置します。 (下の画像で、出口の青いゲートはキャ ラク ターのワープをわかりやすくするために少し遠くに配置し直しています。) また、右側の詳細画面よりBoxCollisionの「Out」を選択し、出口の青いゲートに移動します。 このときBlueprint全体ではなく、「Out」だけを選択していることを確認してください。 次にキャ ラク ターが「In」の コリジョン に侵入した際に、キャ ラク ターの座標を「Out」の位置に変更する処理を作っていきます。 まずは「BP_ Warp 」を開き、 コンポーネント 一覧から「In」の コリジョン を右クリックして、 イベントを追加>OnComponentBeginOverlapを選択します。 ノードが配置されたら、「Set Actor Location」ノードを配置して「On Component Begin Overlap」と繋ぎます。 「Set Actor Location」の「New Location」には出口のゲートの座標をセットするために「Get World Location」ノードを追加して、ターゲットに「Out」を繋げます。 開発中はキャ ラク ターの動きが見づらかったので、光るゲートの演出をオフにしています。 3. ワープ時の演出(カメラワーク) 続いて、キャ ラク ターがワープした際の演出を作成していきます。 アクタを配置からCineカメラアクタを選択して設置します。 設置をすると画面の右下に設置したカメラから見える映像が出てくるので、出口ゲートが見えるアングルへ移動させます。 (Current Focal Length の値を変えることで 焦点距離 を変えることができ、カメラの写りを調整できます。) 次にキャ ラク ターがワープをした時にカメラワークを切り替えたいので「BP_ Warp 」内に、ワープの最中かどうかを判定するためのフラグを「InWarpZone」という名前で作成します。また、後続の処理のために「Delay」を3秒に設定して下記画像のように設定します。 次にレベルブループリントを開きます。 キャ ラク ターがワープをしているかチェックするために「イベントTick」を使用して「InWarpZone」の変数を監視します。 「InWarpZone」がTrueになった場合に、カメラワークを切り替えるため「Set View Target with Blend」ノードを追加します。 「Set View Target with Blend」の「New View Target」には先ほど設置したCineカメラアクタを繋げます。 下記画像のようにレベルのViewPort詳細画面からイベントグラフへ ドラッグ&ドロップ を行います。 ターゲットには「Get Player Controller」をつなげます。 ここまででキャ ラク ターがワープをした時に、出口のワープゲートにカメラが移動し、ワープを演出するカメラワークができました。 ワープ完了後、視点を元に戻すため「Delay」を2秒でセットし、再び「Set View Target with Blend」ノードを作成します。 ターゲットには「Get Player Controller」、「New View Target」に「Get Player Character」をセットします。 4. ワープ時の演出(フェードアウト) ここまでの工程でカメラワークの設定が完了しましたが、もうすこし演出を加えるためにフェードアウト機能を追加します。 ユーザーインターフェイス より ウィジェット ブループリントを作成し「WBP_FadeOut」と 命名 します。 キャンバスパネル上にImageを追加し、画面いっぱいに広げます。 Tintのカラーピッカーを使用してimageの色を黒く変更します。 続いてフェードアウトのアニメーションを作成するため、タブからアニメーションを選択します。 アニメーション追加ボタンを押し「FadeOutAnimation」と 命名 します。トラック追加から「Image_31」を追加します。 さらに追加したレコードのプラスボタンから「Color and Opacity」を選択します。 追加された「Color and Opacity」の編集画面で、 RGBAのAlfaの値を下記のように設定します。 0秒:0 3秒:1 これにより、3秒間かけてゆっくりと暗転していくFadeOutのアニメーションができました。 再度レベルブループリントを開き、アニメーションを追加します。 カメラを移動させる1度目の「Set View Target with Blend」の前にフェードアウトを入れます。 「WBP Fade Out ウィジェット を作成」ノードから「Add Viewport」を繋げ、さらにアニメーションを再生するために「Play Animation」ノードを繋げます。 「Play Animation」のターゲットには「WBP Fade Out ウィジェット を作成」の返り値を繋げ、「In Animation」には「WBP Fade Out ウィジェット を作成」より引き出した「Fade out Animation」を使用します。 (細かい調整なので本筋とは関係ないですが、フェードアウトのアニメーションの後に3秒間の「Delay」を置いた方が綺麗な演出だったので「Delay」を追加しました。) カメラワークがゲートの出口に移動したタイミングでフェードアウトの ウィジェット を「Remove from Parent」を使用して削除します。 「イベントTick」で ウィジェット を作成しているため、「InWarpZone」のフラグがTrueのままだと ウィジェット を作り続けてしまうので、下記画像の位置でフラグをFalseに戻す処理も追加しておきます。 最後にウェジェットを作成しアニメーションを再生している最中はプレイヤーのキャ ラク ター操作をできないようにしておきたいので、 「WBP Fade Out ウィジェット を作成」の前に「Disable Input」ノードを追加します。 アニメーションが終わったタイミングでキャ ラク ター操作を有効化させるために、「Enable Input」を「Remove from Parent」の後に繋げます。 以上でワープ機能(キャ ラク ターの移動とワープ時のカメラワーク)が完成しました。 所感 UEを使用したゲームや ノンゲーム の領域でも、ワープ機能はUXを高めるために有効な手段だと考えているので、今回の実装でキャ ラク ターのワープ方法を学べてとてもためになりました。 またそれに伴ったカメラワークの方法や、フェードアウトなどのアニメーションの追加も、ユーザーがストレスなく体験するためにはとても重要なことだと感じました。 違和感なくユーザーが操作するためには、必要な要素がまだまだたくさんあるので、引き続きUEの勉強を続けようと思います。 現在ISIDは web3領域のグループ横断組織 を立ち上げ、Web3および メタバース 領域のR&Dを行っております(カテゴリー「3DCG」の記事は こちら )。 もし本領域にご興味のある方や、一緒にチャレンジしていきたい方は、ぜひお気軽にご連絡ください! 私たちと同じチームで働いてくれる仲間を、是非お待ちしております! ISID採用ページ(Web3/メタバース/AI) 最後まで読んでいただきありがとうございました。 参考 https://historia.co.jp/archives/3198/ https://mostoad.com/ue4-part15 執筆: @okazaki.wataru 、レビュー: @wakamoto.ryosuke ( Shodo で執筆されました )
アバター
こんにちは、ISID 金融ソリューション事業部の岡崎です。 今回はUE5でワープ機能(キャ ラク ターの移動とワープ時のカメラワーク)を作成します。 はじめに UEでは、ロードを挟まないひとつづきのマップのことを「レベル」と呼びます。 今回はそのような、同一のレベル内でプレイヤーが操作するキャ ラク ターを任意の場所へ移動させるワープの機能を紹介します。 レベル間移動をする場合(ロードをはさむ違うマップへの移動)には、違った処理が必要になるので、今回は同一レベル内でのみの移動について、紹介します。 検証環境/ツール OS: Windows 11 pro GPU : NVIDIA GeForce RTX 4070 Ti Game Engine: Unreal Engine 5.2.0 実装手順 ワープゲートの作成 ワープ機能の作成 ワープ時の演出(カメラワーク) ワープ時の演出(フェードアウト) 1. ワープゲートの作成 今回はThirdPersonテンプレートを利用したプロジェクトで進めていきます。 まずはキャ ラク ターがワープをする際に使用するゲートを作成します。 ゲートのメッシュを作成しても良いですが、今回は簡略化のためにスターターコンテンツに入っている「SM_Doorframe」を使用します。 レベル上に入り口用と、出口用に2つ配置し、わかりやすいように「WarpGate1」「WarpGate2」と 命名 します。 それぞれのゲート用にマテリアルを作成します。 コンテンツドロワーよりマテリアルを作成し、下記画像のように設定を行いました。 詳細画面より「WarpGate1」のマテリアルに先ほど作成したマテリアルを適応させゲートを黄色くします。この黄色いゲートをワープの入り口として使用します。 同様に「WarpGate2」用のマテリアルを作成し適応させます。今回は青色のゲートにし、これをワープの出口として使用します。 本筋とは関係ないですが、もう少しワープゲートをリッチにしていきます。 コンテンツ追加からキューブを追加します。 サイズと位置を調整し、ワープゲートの内側と同じサイズに変更します。 また当たり判定をなくすために コリジョン プリセットの値を「No Collision」に変更しておきます。 次にマテリアルを作成し、「M_WarpEffect」と 命名 します。 「エミッシブカラー」のピンを「Multiply」に接続し、「A」のピンと「Vertex Color」と繋げます。さらに「B」のピンには「Constant」と繋げ、値を「50」に設定します。これにより光っているマテリアルが作成できます。 先ほど作成したキューブのマテリアルに適応させることで、ワープゲートがよりリッチになります。 複製して出口の方のゲートにも追加しておきます。 続いてワープの機能を作成していきましょう。 2. ワープ機能の作成 ワープの機能を実装するためにBlueprintを作成します。 コンテンツドロワーよりアクターのBlueprintを作成し「BP_ Warp 」とします。 コンテンツ追加より「Box Collision」を2つ追加し、「In」「Out」と 命名 します。 「DefaultSceneRoot」の大きさを、ワープゲートの内側の大きさに合うように調整します。 次に「BP_ Warp 」をレベル上に配置し、入り口の黄色いゲートの内側に配置します。 (下の画像で、出口の青いゲートはキャ ラク ターのワープをわかりやすくするために少し遠くに配置し直しています。) また、右側の詳細画面よりBoxCollisionの「Out」を選択し、出口の青いゲートに移動します。 このときBlueprint全体ではなく、「Out」だけを選択していることを確認してください。 次にキャ ラク ターが「In」の コリジョン に侵入した際に、キャ ラク ターの座標を「Out」の位置に変更する処理を作っていきます。 まずは「BP_ Warp 」を開き、 コンポーネント 一覧から「In」の コリジョン を右クリックして、 イベントを追加>OnComponentBeginOverlapを選択します。 ノードが配置されたら、「Set Actor Location」ノードを配置して「On Component Begin Overlap」と繋ぎます。 「Set Actor Location」の「New Location」には出口のゲートの座標をセットするために「Get World Location」ノードを追加して、ターゲットに「Out」を繋げます。 開発中はキャ ラク ターの動きが見づらかったので、光るゲートの演出をオフにしています。 3. ワープ時の演出(カメラワーク) 続いて、キャ ラク ターがワープした際の演出を作成していきます。 アクタを配置からCineカメラアクタを選択して設置します。 設置をすると画面の右下に設置したカメラから見える映像が出てくるので、出口ゲートが見えるアングルへ移動させます。 (Current Focal Length の値を変えることで 焦点距離 を変えることができ、カメラの写りを調整できます。) 次にキャ ラク ターがワープをした時にカメラワークを切り替えたいので「BP_ Warp 」内に、ワープの最中かどうかを判定するためのフラグを「InWarpZone」という名前で作成します。また、後続の処理のために「Delay」を3秒に設定して下記画像のように設定します。 次にレベルブループリントを開きます。 キャ ラク ターがワープをしているかチェックするために「イベントTick」を使用して「InWarpZone」の変数を監視します。 「InWarpZone」がTrueになった場合に、カメラワークを切り替えるため「Set View Target with Blend」ノードを追加します。 「Set View Target with Blend」の「New View Target」には先ほど設置したCineカメラアクタを繋げます。 下記画像のようにレベルのViewPort詳細画面からイベントグラフへ ドラッグ&ドロップ を行います。 ターゲットには「Get Player Controller」をつなげます。 ここまででキャ ラク ターがワープをした時に、出口のワープゲートにカメラが移動し、ワープを演出するカメラワークができました。 ワープ完了後、視点を元に戻すため「Delay」を2秒でセットし、再び「Set View Target with Blend」ノードを作成します。 ターゲットには「Get Player Controller」、「New View Target」に「Get Player Character」をセットします。 4. ワープ時の演出(フェードアウト) ここまでの工程でカメラワークの設定が完了しましたが、もうすこし演出を加えるためにフェードアウト機能を追加します。 ユーザーインターフェイス より ウィジェット ブループリントを作成し「WBP_FadeOut」と 命名 します。 キャンバスパネル上にImageを追加し、画面いっぱいに広げます。 Tintのカラーピッカーを使用してimageの色を黒く変更します。 続いてフェードアウトのアニメーションを作成するため、タブからアニメーションを選択します。 アニメーション追加ボタンを押し「FadeOutAnimation」と 命名 します。トラック追加から「Image_31」を追加します。 さらに追加したレコードのプラスボタンから「Color and Opacity」を選択します。 追加された「Color and Opacity」の編集画面で、 RGBAのAlfaの値を下記のように設定します。 0秒:0 3秒:1 これにより、3秒間かけてゆっくりと暗転していくFadeOutのアニメーションができました。 再度レベルブループリントを開き、アニメーションを追加します。 カメラを移動させる1度目の「Set View Target with Blend」の前にフェードアウトを入れます。 「WBP Fade Out ウィジェット を作成」ノードから「Add Viewport」を繋げ、さらにアニメーションを再生するために「Play Animation」ノードを繋げます。 「Play Animation」のターゲットには「WBP Fade Out ウィジェット を作成」の返り値を繋げ、「In Animation」には「WBP Fade Out ウィジェット を作成」より引き出した「Fade out Animation」を使用します。 (細かい調整なので本筋とは関係ないですが、フェードアウトのアニメーションの後に3秒間の「Delay」を置いた方が綺麗な演出だったので「Delay」を追加しました。) カメラワークがゲートの出口に移動したタイミングでフェードアウトの ウィジェット を「Remove from Parent」を使用して削除します。 「イベントTick」で ウィジェット を作成しているため、「InWarpZone」のフラグがTrueのままだと ウィジェット を作り続けてしまうので、下記画像の位置でフラグをFalseに戻す処理も追加しておきます。 最後にウェジェットを作成しアニメーションを再生している最中はプレイヤーのキャ ラク ター操作をできないようにしておきたいので、 「WBP Fade Out ウィジェット を作成」の前に「Disable Input」ノードを追加します。 アニメーションが終わったタイミングでキャ ラク ター操作を有効化させるために、「Enable Input」を「Remove from Parent」の後に繋げます。 以上でワープ機能(キャ ラク ターの移動とワープ時のカメラワーク)が完成しました。 所感 UEを使用したゲームや ノンゲーム の領域でも、ワープ機能はUXを高めるために有効な手段だと考えているので、今回の実装でキャ ラク ターのワープ方法を学べてとてもためになりました。 またそれに伴ったカメラワークの方法や、フェードアウトなどのアニメーションの追加も、ユーザーがストレスなく体験するためにはとても重要なことだと感じました。 違和感なくユーザーが操作するためには、必要な要素がまだまだたくさんあるので、引き続きUEの勉強を続けようと思います。 現在ISIDは web3領域のグループ横断組織 を立ち上げ、Web3および メタバース 領域のR&Dを行っております(カテゴリー「3DCG」の記事は こちら )。 もし本領域にご興味のある方や、一緒にチャレンジしていきたい方は、ぜひお気軽にご連絡ください! 私たちと同じチームで働いてくれる仲間を、是非お待ちしております! ISID採用ページ(Web3/メタバース/AI) 最後まで読んでいただきありがとうございました。 参考 https://historia.co.jp/archives/3198/ https://mostoad.com/ue4-part15 執筆: @okazaki.wataru 、レビュー: @wakamoto.ryosuke ( Shodo で執筆されました )
アバター
こんにちは。X(クロス) イノベーション 本部 ソフトウェアデザインセンター の山下です。 皆さん kubernetes (以降 k8s )は使っていますか? 今回はGKEなどのマネージドな k8s ではなく、オンプレミス環境での k8s 向けのノウハウを紹介する記事になります。 オンプレミス環境の Ingress で社内向けサービスと社外向けサービスを公開したい k8s で様々なサービスを提供していると、特定のサービスの公開先を制限したい場合があります。 認証などで制限を行うことも可能ですが、 IPアドレス などで制限したい場合もあります。具体的には、社外向けサービスと社内向けサービスでアクセスする先の IPアドレス が違うように設定することで、 ファイアウォール などの設定を行えるような状況が望ましいです。 マネージドサービスであれば、EKSだと標準の Ingress (Application LoadBalancer)を使って簡単に実現できる話だと思います。今回は同じようなことを Ingress -Nginx Controllerを使って実現する方法について紹介します。 オンプレミス環境における Ingress Controller k8s には公式の機能として Ingress Controllerは提供されていません。例えば、 kubeadm という k8s の公式な インストーラ があります。 このkubeadmで構築された k8s には Ingress Controllerはインストールされていません。 Ingress Controlerが存在しない状態だとユーザーは Ingress のリソースを作っても上手く動きません。 Ingress リソースをデプロイしてもControllerがないためPendingのまま起動しないといった結果になります。 マネージドな k8s の環境だとインフラとして、LoadBalancerが提供されておりそれらを k8s に統合するような Ingress Controllerの提供がされていることが多いです。例えば、 AWS ではApplication LoadBalancerを利用する Ingress Controlerの AWS Load Balancer Controllerが提供されており、ユーザーは自前でそれをインストールして使うといった具合です。 オンプレミス環境で利用可能な Ingress Controllerも存在しています。ソフトウェアのLoadBalancerと k8s を統合するような仕組みになっているものが多いようです。例えば、以下のようなものが比較的利用されていることが多い印象です。 Ingress -Nginx Controller ( https://kubernetes.github.io/ingress-nginx/ ) Traefik ( https://github.com/traefik/traefik ) contour ( https://github.com/projectcontour/contour ) 他にも様々な Ingress Controllerがあります。 Learnk8sという企業の方が、 Ingress Controllerを比較した資料( Google Spread Sheets)があります。 もし、これから Ingress Controllerを選択するという場合には参考になるかもしれません。 https://docs.google.com/spreadsheets/d/191WWNpjJ2za6-nbG4ZoUMXMpUK8KlCIosvQB0f-oq3k/ 今回は、このなかの Ingress -Nginx Controllerについてのノウハウを公開するものになります。 また、 Ingress -Nginx Controllerは Type: Loadbalancer を指定した Service を作ることができる k8s でなければ上手く動作しません。今回は、 MetalLB を採用している環境を想定しています。 Ingress -Nginx Controllerのインストール Ingress -Nginx Controller は helm を使ってインストールすることが出来ます。 helm repo add ingress-nginx https://kubernetes.github.io/ingress-nginx helm repo update helm install ingress-nginx ingress-nginx/ingress-nginx --create-namespace -n ingress-system 上記の手順で、 ingress -systemというnamespaceに ingress -nginxがインストールされます。 インストール後、 kubectl get pod -n ingress-system などを実行すると 動作が確認できるかと思います。また、 ingressclass というリソースも作成されます。 上記の手順では、 nginx という名前のingressclassが作られるかと思います。 Ingress -Nginx で Ingress を複数作った場合の動作 さて、これで Ingress を利用できる k8s 環境になりました。 例えば以下のような形で Ingress を作ることができる様になりました。 apiVersion : networking.k8s.io/v1 kind : Ingress metadata : name : public-service-ingress namespace : public-service spec : ingressClassName : nginx rules : - host : public.example.com http : paths : - backend : service : name : nginx port : number : 80 path : / pathType : Prefix これで、外部公開しても良いサービスを Ingress で公開できました。 次に、内部に限定公開するサービスを同様に Ingress で作ろうとします。 例えば、以下のようなリソースになるでしょうか。 apiVersion : networking.k8s.io/v1 kind : Ingress metadata : name : private-service-ingress namespace : private-service spec : ingressClassName : nginx rules : - host : private.example.com http : paths : - backend : service : name : nginx port : number : 80 path : / pathType : Prefix うまく動くように思いますが、実際に適用すると問題があります。 何が問題なのでしょうか? kubectl get ingress -A などで ingress の状況を確認してみます。 以下のような結果が得られます。(若干編集してあります) NAMESPACE NAME CLASS HOSTS ADDRESS PORTS AGE public-service public-service-ingress nginx public.example.com 192.168.1.10 80, 443 XXd private-service private-service-ingress nginx private.example.com 192.168.1.10 80, 443 XXd ADDRESSのところを見てみると、なんと同じ IPアドレス が使われていますね。 IPアドレス なども含めて社外と社内のサービスを分けたいと考えていたのでこれは困ります。 これは、 Ingress -Nginxの仕様でingressclassが同じものは同一のnginxにまとめてしまう動作によるものです。 マネージドな k8s だと Ingress に対応してロードバランサが払い出されるものが多いです。これと同じ挙動を期待してはいけないようです。 IPが別の Ingress を作成するには? Ingress -NginxでIPが異なるNginxを払い出すにはどうすればいいのでしょうか? 公式のマニュアルに解説があります。 https://docs.nginx.com/nginx-ingress-controller/installation/running-multiple-ingress-controllers/ Ingress -NginxはIngressClassが別に指定されているControllerを追加で起動する必要があるようです。 controllerから完全に別のものをインストールする必要があるので、helmを使って別途インストールするのが手っ取り早そうです。 private-ingress-nginx という ingressclass を指定した ingress -nginx をhelmでインストールします。 helm install -n private-ingress \ --set controller.ingressClass="private-ingress-nginx" \ --set controller.ingressClassResource.name="private-ingress-nginx" \ --set controller.ingressClassResource.controllerValue="k8s.io/private-ingress-nginx" \ --create-namespace \ private-ingress-nginx \ ingress-nginx/ingress-nginx インストール後、 kubectl get pod -n private-ingress-system などを実行してコントローラが動作していることを確認します。さらに、ingressclassが新しく生成されているかを kubectl get ingressclass で確認します。 ingressclassが2種類出力されれば無事設定できています。 NAME CONTROLLER PARAMETERS AGE private-ingress-nginx k8s.io/private-ingress-nginx <none> XXXd nginx k8s.io/ingress-nginx <none> XXXd この環境で先ほどと同じように複数 Ingress を作成してみましょう。 まずは、publicな Ingress です。これは前回と同じものを作成します。 apiVersion : networking.k8s.io/v1 kind : Ingress metadata : name : public-service-ingress namespace : public-service spec : ingressClassName : nginx rules : - host : public.example.com http : paths : - backend : service : name : nginx port : number : 80 path : / pathType : Prefix 次に、内部に限定公開するサービスを作ります。 今回は、 ingressClassName に private-ingress-nginx を指定します。 apiVersion : networking.k8s.io/v1 kind : Ingress metadata : name : private-service-ingress namespace : private-service spec : ingressClassName : private-ingress-nginx rules : - host : private.example.com http : paths : - backend : service : name : nginx port : number : 80 path : / pathType : Prefix さて、 ingress にちゃんと別の IPアドレス が付与されたか確認してみます。 kubectl get ingress -A を実行してみます。 NAMESPACE NAME CLASS HOSTS ADDRESS PORTS AGE public-service public-service-ingress nginx public.example.com 192.168.1.10 80, 443 XXXd private-service private-service-ingress private-ingress-nginx private.example.com 192.168.1.11 80, 443 XXXd ちゃんと別の IPアドレス が付与されていることを確認できました! これで、アクセス先の IPアドレス が社外サービスと社内サービスで違う状態になったので、アクセス制御などが実施しやすくなりました。 まとめ 今回は、オンプレミス環境の k8s 上で Ingress を使う方法、 Ingress -Nginx controllerを使う場合に社外向け、社内向けといった区分けで異なるIPを持っている Ingress を作成する方法について紹介しました。オンプレミス環境で k8s を運用している方の参考になれば幸いです。 私たちは同じチームで働いてくれる仲間を探しています。今回のエントリで紹介したような仕事に興味のある方、ご応募お待ちしています。 ソリューションアーキテクト 執筆: @yamashita.tsuyoshi 、レビュー: @wakamoto.ryosuke ( Shodo で執筆されました )
アバター
こんにちは。X(クロス) イノベーション 本部 ソフトウェアデザインセンター の山下です。 皆さん kubernetes (以降 k8s )は使っていますか? 今回はGKEなどのマネージドな k8s ではなく、オンプレミス環境での k8s 向けのノウハウを紹介する記事になります。 オンプレミス環境の Ingress で社内向けサービスと社外向けサービスを公開したい k8s で様々なサービスを提供していると、特定のサービスの公開先を制限したい場合があります。 認証などで制限を行うことも可能ですが、 IPアドレス などで制限したい場合もあります。具体的には、社外向けサービスと社内向けサービスでアクセスする先の IPアドレス が違うように設定することで、 ファイアウォール などの設定を行えるような状況が望ましいです。 マネージドサービスであれば、EKSだと標準の Ingress (Application LoadBalancer)を使って簡単に実現できる話だと思います。今回は同じようなことを Ingress -Nginx Controllerを使って実現する方法について紹介します。 オンプレミス環境における Ingress Controller k8s には公式の機能として Ingress Controllerは提供されていません。例えば、 kubeadm という k8s の公式な インストーラ があります。 このkubeadmで構築された k8s には Ingress Controllerはインストールされていません。 Ingress Controlerが存在しない状態だとユーザーは Ingress のリソースを作っても上手く動きません。 Ingress リソースをデプロイしてもControllerがないためPendingのまま起動しないといった結果になります。 マネージドな k8s の環境だとインフラとして、LoadBalancerが提供されておりそれらを k8s に統合するような Ingress Controllerの提供がされていることが多いです。例えば、 AWS ではApplication LoadBalancerを利用する Ingress Controlerの AWS Load Balancer Controllerが提供されており、ユーザーは自前でそれをインストールして使うといった具合です。 オンプレミス環境で利用可能な Ingress Controllerも存在しています。ソフトウェアのLoadBalancerと k8s を統合するような仕組みになっているものが多いようです。例えば、以下のようなものが比較的利用されていることが多い印象です。 Ingress -Nginx Controller ( https://kubernetes.github.io/ingress-nginx/ ) Traefik ( https://github.com/traefik/traefik ) contour ( https://github.com/projectcontour/contour ) 他にも様々な Ingress Controllerがあります。 Learnk8sという企業の方が、 Ingress Controllerを比較した資料( Google Spread Sheets)があります。 もし、これから Ingress Controllerを選択するという場合には参考になるかもしれません。 https://docs.google.com/spreadsheets/d/191WWNpjJ2za6-nbG4ZoUMXMpUK8KlCIosvQB0f-oq3k/ 今回は、このなかの Ingress -Nginx Controllerについてのノウハウを公開するものになります。 また、 Ingress -Nginx Controllerは Type: Loadbalancer を指定した Service を作ることができる k8s でなければ上手く動作しません。今回は、 MetalLB を採用している環境を想定しています。 Ingress -Nginx Controllerのインストール Ingress -Nginx Controller は helm を使ってインストールすることが出来ます。 helm repo add ingress-nginx https://kubernetes.github.io/ingress-nginx helm repo update helm install ingress-nginx ingress-nginx/ingress-nginx --create-namespace -n ingress-system 上記の手順で、 ingress -systemというnamespaceに ingress -nginxがインストールされます。 インストール後、 kubectl get pod -n ingress-system などを実行すると 動作が確認できるかと思います。また、 ingressclass というリソースも作成されます。 上記の手順では、 nginx という名前のingressclassが作られるかと思います。 Ingress -Nginx で Ingress を複数作った場合の動作 さて、これで Ingress を利用できる k8s 環境になりました。 例えば以下のような形で Ingress を作ることができる様になりました。 apiVersion : networking.k8s.io/v1 kind : Ingress metadata : name : public-service-ingress namespace : public-service spec : ingressClassName : nginx rules : - host : public.example.com http : paths : - backend : service : name : nginx port : number : 80 path : / pathType : Prefix これで、外部公開しても良いサービスを Ingress で公開できました。 次に、内部に限定公開するサービスを同様に Ingress で作ろうとします。 例えば、以下のようなリソースになるでしょうか。 apiVersion : networking.k8s.io/v1 kind : Ingress metadata : name : private-service-ingress namespace : private-service spec : ingressClassName : nginx rules : - host : private.example.com http : paths : - backend : service : name : nginx port : number : 80 path : / pathType : Prefix うまく動くように思いますが、実際に適用すると問題があります。 何が問題なのでしょうか? kubectl get ingress -A などで ingress の状況を確認してみます。 以下のような結果が得られます。(若干編集してあります) NAMESPACE NAME CLASS HOSTS ADDRESS PORTS AGE public-service public-service-ingress nginx public.example.com 192.168.1.10 80, 443 XXd private-service private-service-ingress nginx private.example.com 192.168.1.10 80, 443 XXd ADDRESSのところを見てみると、なんと同じ IPアドレス が使われていますね。 IPアドレス なども含めて社外と社内のサービスを分けたいと考えていたのでこれは困ります。 これは、 Ingress -Nginxの仕様でingressclassが同じものは同一のnginxにまとめてしまう動作によるものです。 マネージドな k8s だと Ingress に対応してロードバランサが払い出されるものが多いです。これと同じ挙動を期待してはいけないようです。 IPが別の Ingress を作成するには? Ingress -NginxでIPが異なるNginxを払い出すにはどうすればいいのでしょうか? 公式のマニュアルに解説があります。 https://docs.nginx.com/nginx-ingress-controller/installation/running-multiple-ingress-controllers/ Ingress -NginxはIngressClassが別に指定されているControllerを追加で起動する必要があるようです。 controllerから完全に別のものをインストールする必要があるので、helmを使って別途インストールするのが手っ取り早そうです。 private-ingress-nginx という ingressclass を指定した ingress -nginx をhelmでインストールします。 helm install -n private-ingress \ --set controller.ingressClass="private-ingress-nginx" \ --set controller.ingressClassResource.name="private-ingress-nginx" \ --set controller.ingressClassResource.controllerValue="k8s.io/private-ingress-nginx" \ --create-namespace \ private-ingress-nginx \ ingress-nginx/ingress-nginx インストール後、 kubectl get pod -n private-ingress-system などを実行してコントローラが動作していることを確認します。さらに、ingressclassが新しく生成されているかを kubectl get ingressclass で確認します。 ingressclassが2種類出力されれば無事設定できています。 NAME CONTROLLER PARAMETERS AGE private-ingress-nginx k8s.io/private-ingress-nginx <none> XXXd nginx k8s.io/ingress-nginx <none> XXXd この環境で先ほどと同じように複数 Ingress を作成してみましょう。 まずは、publicな Ingress です。これは前回と同じものを作成します。 apiVersion : networking.k8s.io/v1 kind : Ingress metadata : name : public-service-ingress namespace : public-service spec : ingressClassName : nginx rules : - host : public.example.com http : paths : - backend : service : name : nginx port : number : 80 path : / pathType : Prefix 次に、内部に限定公開するサービスを作ります。 今回は、 ingressClassName に private-ingress-nginx を指定します。 apiVersion : networking.k8s.io/v1 kind : Ingress metadata : name : private-service-ingress namespace : private-service spec : ingressClassName : private-ingress-nginx rules : - host : private.example.com http : paths : - backend : service : name : nginx port : number : 80 path : / pathType : Prefix さて、 ingress にちゃんと別の IPアドレス が付与されたか確認してみます。 kubectl get ingress -A を実行してみます。 NAMESPACE NAME CLASS HOSTS ADDRESS PORTS AGE public-service public-service-ingress nginx public.example.com 192.168.1.10 80, 443 XXXd private-service private-service-ingress private-ingress-nginx private.example.com 192.168.1.11 80, 443 XXXd ちゃんと別の IPアドレス が付与されていることを確認できました! これで、アクセス先の IPアドレス が社外サービスと社内サービスで違う状態になったので、アクセス制御などが実施しやすくなりました。 まとめ 今回は、オンプレミス環境の k8s 上で Ingress を使う方法、 Ingress -Nginx controllerを使う場合に社外向け、社内向けといった区分けで異なるIPを持っている Ingress を作成する方法について紹介しました。オンプレミス環境で k8s を運用している方の参考になれば幸いです。 私たちは同じチームで働いてくれる仲間を探しています。今回のエントリで紹介したような仕事に興味のある方、ご応募お待ちしています。 ソリューションアーキテクト 執筆: @yamashita.tsuyoshi 、レビュー: @wakamoto.ryosuke ( Shodo で執筆されました )
アバター
ISID X(クロス) イノベーション 本部 の三浦です。 筆者の関わっている案件では、多数の AWS アカウント、ec2、ecsが存在しています。 で、大量のリソースがあるとSavings Plansの管理も大変であり、下記のようなツールを作成してSavings Plansの購入をしております(以後Savings PlansはSPと省略します)。 フェデレーションでアカウントの認証情報一括取得(200アカウント弱) ec2一覧、ecs task一覧を作成(SP購入不要の場合は、タグによって除外) 各アカウントごとに必要なSP量を計算 現在購入済みSP料と比べ、SP不足分を算出 不足分のSPを購入 で、上記のような作業をエクセルベースでやっている方も多いかと思いますが、 AWS のホームページから手動でSPの額をコピーするのは骨が折れる作業です。 なので、今回は、上記の情報に該当するデータを csv として取得する方法について記述します。 目次 目次 スクリプト内容解説 その他SP運用に関するFAQ まとめ スクリプト 内容解説 下記、リンクはcloudshell 上で使うことを前提とした スクリプト です。 ############ ステップ1:まずはこのコマンドをCloudShellで実行してください ############ pwsh ############ ステップ2:ここから下を全てコピーして実行してください ############ Import-Module AWSPowerShell.NetCore function Get-Ec2ComputeSavingsPlans { [CmdletBinding()] param ( $region , $productDescription ) $fileter = @( @{Name = "tenancy" ; Values = "shared" } , @{Name = "region" ; Values = $region } , @{Name = "productDescription" ; Values = $productDescription } ) $sp = Get-SPSavingsPlansOfferingRate -Product EC2 -SavingsPlanPaymentOption "All Upfront" -ServiceCode AmazonEC2 -SavingsPlanType Compute -region ap-northeast-1 -MaxResult 1000 -Filter $fileter $result = $sp | % { $xxx = "" | select Region , Rate , instanceType , productDescription , UsageType , UsageType_ARC_CPU_GB , OfferingId , PaymentOption , PlanDescription , PlanType , DurationSeconds , KEY $xxx .Region = $_ .Properties | ? Name -eq region | % { $_ .Value } $xxx .Rate = $_ .Rate $xxx .UsageType = $_ .UsageType $xxx .instanceType = ( $_ .Properties | ? Name -eq instanceType | % { $_ .Value }) $xxx .productDescription = $productDescription $xxx .UsageType_ARC_CPU_GB = ( $_ .Properties | ? Name -eq productDescription | % { $_ .Value }) + "_" + ( $_ .Properties | ? Name -eq instanceType | % { $_ .Value }) $xxx .OfferingId = $_ .SavingsPlanOffering.OfferingId $xxx .PaymentOption = $_ .SavingsPlanOffering.PaymentOption.Value $xxx .PlanDescription = $_ .SavingsPlanOffering.PlanDescription $xxx .PlanType = $_ .SavingsPlanOffering.PlanType.Value $xxx .DurationSeconds = $_ .SavingsPlanOffering.DurationSeconds $xxx .Key = $xxx .Region + "_" + $xxx .DurationSeconds + "_" + $xxx .UsageType_ARC_CPU_GB $xxx } return $result } $productList = @( "Linux/UNIX" , "Windows" , "Red Hat Enterprise Linux" ) $resultList = @() $productList |% { $p = $_ ## 一部の条件は決め打ち ## 全額前払い、共有テナンシー、 ## 対象は、Amazon EC2 の Compute Savings PlansのみでFargate の Compute Savings Planや ## インスタンスファミリー Savings Plan の料金は取得対象外 $l = Get-Ec2ComputeSavingsPlans -region ap-northeast-1 -productDescription $p $resultList += $l } $exportPath = "sp_list.csv" $resultList | Export-Csv -Path $exportPath -Encoding UTF8 -NoTypeInformation なお、コアの部分は下記となります。各社、必要な条件に変えご利用ください(ISIDの場合、全額前払いでよいので割引率重視で全額前払い固定にしてます)。 #$productList = @("Linux/UNIX","Windows","Red Hat Enterprise Linux") $fileter = @( @{Name = "tenancy" ; Values = "shared" } , @{Name = "region" ; Values = $region } , @{Name = "productDescription" ; Values = $productDescription } ) $sp = Get-SPSavingsPlansOfferingRate -Product EC2 -SavingsPlanPaymentOption "All Upfront" -ServiceCode AmazonEC2 -SavingsPlanType Compute -region ap-northeast-1 -MaxResult 1000 -Filter $fileter 下記に実行イメージを示します。 CloudShellの右上の「Actions > Dwonload file」から、下記のようなダイアログでファイルを取得できます。 コア部分は上ぐらいの非常にシンプルなコードです。productDescriptionはかなりの種別が存在しますので、上記以外のプロダクトタイプが必要な場合は追記してください。 その他SP運用に関するFAQ Q1.  「Savings Plans > 推奨事項」 で簡易に必要SP量が見積もれるはずですが、なぜいちいち計算しているのですか? A1.  上記の機能は、一括請求配下ですと他のアカウントの リザーブ ド インスタンス 、SPの影響を受けてしまい正しい予測ができません。アカウント単位で必要なSP量を都度計算するようにしています。 Q2.  一括請求有効化、 リザーブ ド インスタンス 共有の場合、アカウント単位の正しいコストを把握するのは非常に難しい認識ですがどのように計算しているのですか? A2. 専用の会計ツールを導入しております。 リザーブ ド インスタンス を共有している場合、 AWS 純正の機能で正しいコストを判断するのは非常に難しいです。 まとめ ということで、アカウントが多い場合、SP購入業務は結構大変な業務だと感じておりますが、自動化の一助になるかと思いSPの価格取得について記述しました。 なお、実業務としては↑全体を スクリプト 化して運用しております。しかし、バグがあるとかなり大変なので、この業務の自動化を検討される方は入念なテストを強くお勧めします。 私たちは同じチームで働いてくれる仲間を探しています。 クラウド アーキテクトの業務に興味がある方のご応募をお待ちしています。 クラウドアーキテクト 執筆: @miura.toshihiko 、レビュー: 寺山 輝 (@terayama.akira) ( Shodo で執筆されました )
アバター
ISID X(クロス) イノベーション 本部 の三浦です。 筆者の関わっている案件では、多数の AWS アカウント、ec2、ecsが存在しています。 で、大量のリソースがあるとSavings Plansの管理も大変であり、下記のようなツールを作成してSavings Plansの購入をしております(以後Savings PlansはSPと省略します)。 フェデレーションでアカウントの認証情報一括取得(200アカウント弱) ec2一覧、ecs task一覧を作成(SP購入不要の場合は、タグによって除外) 各アカウントごとに必要なSP量を計算 現在購入済みSP料と比べ、SP不足分を算出 不足分のSPを購入 で、上記のような作業をエクセルベースでやっている方も多いかと思いますが、 AWS のホームページから手動でSPの額をコピーするのは骨が折れる作業です。 なので、今回は、上記の情報に該当するデータを csv として取得する方法について記述します。 目次 目次 スクリプト内容解説 その他SP運用に関するFAQ まとめ スクリプト 内容解説 下記、リンクはcloudshell 上で使うことを前提とした スクリプト です。 ############ ステップ1:まずはこのコマンドをCloudShellで実行してください ############ pwsh ############ ステップ2:ここから下を全てコピーして実行してください ############ Import-Module AWSPowerShell.NetCore function Get-Ec2ComputeSavingsPlans { [CmdletBinding()] param ( $region , $productDescription ) $fileter = @( @{Name = "tenancy" ; Values = "shared" } , @{Name = "region" ; Values = $region } , @{Name = "productDescription" ; Values = $productDescription } ) $sp = Get-SPSavingsPlansOfferingRate -Product EC2 -SavingsPlanPaymentOption "All Upfront" -ServiceCode AmazonEC2 -SavingsPlanType Compute -region ap-northeast-1 -MaxResult 1000 -Filter $fileter $result = $sp | % { $xxx = "" | select Region , Rate , instanceType , productDescription , UsageType , UsageType_ARC_CPU_GB , OfferingId , PaymentOption , PlanDescription , PlanType , DurationSeconds , KEY $xxx .Region = $_ .Properties | ? Name -eq region | % { $_ .Value } $xxx .Rate = $_ .Rate $xxx .UsageType = $_ .UsageType $xxx .instanceType = ( $_ .Properties | ? Name -eq instanceType | % { $_ .Value }) $xxx .productDescription = $productDescription $xxx .UsageType_ARC_CPU_GB = ( $_ .Properties | ? Name -eq productDescription | % { $_ .Value }) + "_" + ( $_ .Properties | ? Name -eq instanceType | % { $_ .Value }) $xxx .OfferingId = $_ .SavingsPlanOffering.OfferingId $xxx .PaymentOption = $_ .SavingsPlanOffering.PaymentOption.Value $xxx .PlanDescription = $_ .SavingsPlanOffering.PlanDescription $xxx .PlanType = $_ .SavingsPlanOffering.PlanType.Value $xxx .DurationSeconds = $_ .SavingsPlanOffering.DurationSeconds $xxx .Key = $xxx .Region + "_" + $xxx .DurationSeconds + "_" + $xxx .UsageType_ARC_CPU_GB $xxx } return $result } $productList = @( "Linux/UNIX" , "Windows" , "Red Hat Enterprise Linux" ) $resultList = @() $productList |% { $p = $_ ## 一部の条件は決め打ち ## 全額前払い、共有テナンシー、 ## 対象は、Amazon EC2 の Compute Savings PlansのみでFargate の Compute Savings Planや ## インスタンスファミリー Savings Plan の料金は取得対象外 $l = Get-Ec2ComputeSavingsPlans -region ap-northeast-1 -productDescription $p $resultList += $l } $exportPath = "sp_list.csv" $resultList | Export-Csv -Path $exportPath -Encoding UTF8 -NoTypeInformation なお、コアの部分は下記となります。各社、必要な条件に変えご利用ください(ISIDの場合、全額前払いでよいので割引率重視で全額前払い固定にしてます)。 #$productList = @("Linux/UNIX","Windows","Red Hat Enterprise Linux") $fileter = @( @{Name = "tenancy" ; Values = "shared" } , @{Name = "region" ; Values = $region } , @{Name = "productDescription" ; Values = $productDescription } ) $sp = Get-SPSavingsPlansOfferingRate -Product EC2 -SavingsPlanPaymentOption "All Upfront" -ServiceCode AmazonEC2 -SavingsPlanType Compute -region ap-northeast-1 -MaxResult 1000 -Filter $fileter 下記に実行イメージを示します。 CloudShellの右上の「Actions > Dwonload file」から、下記のようなダイアログでファイルを取得できます。 コア部分は上ぐらいの非常にシンプルなコードです。productDescriptionはかなりの種別が存在しますので、上記以外のプロダクトタイプが必要な場合は追記してください。 その他SP運用に関するFAQ Q1.  「Savings Plans > 推奨事項」 で簡易に必要SP量が見積もれるはずですが、なぜいちいち計算しているのですか? A1.  上記の機能は、一括請求配下ですと他のアカウントの リザーブ ド インスタンス 、SPの影響を受けてしまい正しい予測ができません。アカウント単位で必要なSP量を都度計算するようにしています。 Q2.  一括請求有効化、 リザーブ ド インスタンス 共有の場合、アカウント単位の正しいコストを把握するのは非常に難しい認識ですがどのように計算しているのですか? A2. 専用の会計ツールを導入しております。 リザーブ ド インスタンス を共有している場合、 AWS 純正の機能で正しいコストを判断するのは非常に難しいです。 まとめ ということで、アカウントが多い場合、SP購入業務は結構大変な業務だと感じておりますが、自動化の一助になるかと思いSPの価格取得について記述しました。 なお、実業務としては↑全体を スクリプト 化して運用しております。しかし、バグがあるとかなり大変なので、この業務の自動化を検討される方は入念なテストを強くお勧めします。 私たちは同じチームで働いてくれる仲間を探しています。 クラウド アーキテクトの業務に興味がある方のご応募をお待ちしています。 クラウドアーキテクト 執筆: @miura.toshihiko 、レビュー: 寺山 輝 (@terayama.akira) ( Shodo で執筆されました )
アバター
こんにちは。X(クロス) イノベーション 本部 ソフトウェアデザインセンター セキュリティグループの耿です。 1年ほど前に書いたブログ では、組織横断的に Security Hub の利用をサポートするための準備について書きました。あれから1年が経ち、今は社内の数百アカウントの Security Hub 利用をサポートしています。この記事では、 Security Hub の社内展開の運用で利用しているツール群をご紹介します。 なお、Security Hub の利用サポートは社内の SOC という組織で行っています。 SOC の組織的な立ち位置や役割は こちらのブログ でご紹介しています。 改めてまとめると、SOC は社内の各事業部のプロジェクトに対し、以下の2つのサービスを提供しています。 クラウド サービスのセキュリティ設定不備のチェック(Security Hubのセキュリティ基準) クラウド アカウントに対する脅威の検出(Security Hub + GuardDuty) 概要 ①アカウント連携申請フォーム(Jira Service Management) ②アカウント管理台帳(Excel) ③アカウント連携招待ツール(Lambda関数) ④アカウント連携セットアップスクリプト(PowerShellスクリプト) ⑤アカウント連携チェックツール(Lambda関数) ⑥検出結果配信ツール (ECSバッチアプリ) ⑦脅威検知通知ツール(Lambda関数) ⑧アカウント連携解除ツール(PowerShellスクリプト) ⑨セキュリティコントロールの対応方針、 セキュリティ設定ガイド(Excel, PowerPoint) まとめ 概要 SOC のサービスの運用で使用しているツールなどを1つの図にまとめました。 各プロジェクトが使用している AWS アカウントの Security Hub から、SOC の AWS アカウントへクロスアカウント連携することで、 SOC 側で一括してセキュリティ設定やアラートを取得し、情報配信や監視を行う組織体制です。各ツールの詳細を図の番号順で見ていきます。 ①アカウント連携申請フォーム(Jira Service Management) SOC のサービスを利用するかどうかは、各プロジェクトの判断に委ねています。 SOC を利用する場合は社内向けの Jira Service Management を利用し、申請を受け付けています。申請の種類には AWS アカウントの追加、変更、解除が含まれます。 ②アカウント管理台帳( Excel ) AWS アカウントIDとプロジェクトの連絡先などの マッピング を記載したアカウント管理台帳を用意し、プロジェクトから申請を受け付けたら更新します。 現在の申請数であれば特に運用負荷となっておらず、自動化するとなると申請者の権限管理が逆に難しそうなので、今のところ手動作業です。 ③アカウント連携招待ツール(Lambda関数) ②のアカウント管理台帳を更新したら、SOC の AWS アカウントにある S3 バケット にファイルをアップロードします。 S3 バケットのイベント通知機能 を利用し、ファイルがアップロードされると自動でLambda関数を起動し、ファイルの内容を読み取って、未招待のプロジェクトアカウントへ Security Hub の連携招待 を送信します。 ④アカウント連携セットアップ スクリプト ( PowerShell スクリプト ) Security Hub の招待を受けたメンバーアカウントではいくつかのセットアップ作業が必要です。 Security Hub の有効化と、セキュリティ基準の有効化(まだ有効化していない場合) Security Hub 招待の承諾 AWS Config の有効化(まだ有効化していない場合) GuardDuty の有効化(まだ有効化していない場合) これらを社内のプロジェクト担当者に簡単に実施してもらえるように、CloudShell で流すだけで一連のセットアップが完了する PowerShell スクリプト を用意しています。 ⑤アカウント連携チェックツール(Lambda関数) プロジェクト担当者が④の スクリプト を実行するタイミングは任意です。そこで②のアカウント管理台帳や SOC アカウントの Security Hub などからデータを取得し、セットアップが完了していないアカウントがあるかどうか Lambda 関数で定期的にチェック、結果を SOC 担当のチャットに通知しています。 ⑥検出結果配信ツール (ECSバッチアプリ) プロジェクトアカウントに対して定期的に Security Hub の検出結果を Excel レポートとして配信するバッチアプリを ECS Fargate で実行します。 詳細は 以前のブログ でご説明しています。 ⑦脅威検知通知ツール(Lambda関数) クラウド アカウントに対する緊急度の高い脅威(今まさに侵入されている可能性がある、など)が発生した場合、⑥の検出結果のレポート配信のタイミングに関わらず、なるべく早い段階でプロジェクト担当者に通知されるべきです。Security Hub の連携を行うと、メンバーアカウントの GuardDuty アラートも SOC アカウントにニアリアルタイムで連携されるため、 EventBridge と Lambda 関数によるイベントドリブンで緊急度の高い GuardDuty アラートを SOC 担当のチャットに通知します。 SOC 担当はアラートの内容を確認し、プロジェクト担当者に連絡を取ります。 ⑧アカウント連携解除ツール( PowerShell スクリプト ) プロジェクトアカウントを SOC サービスから解除する場合は、 SOC アカウントの CloudShell で実行することで連携を解除できる PowerShell スクリプト を利用します。プロジェクトアカウント側の設定(Security Hub の無効化)などはプロジェクト側の作業に委ねています。 ⑨セキュリティコン トロール の対応方針、 セキュリティ設定ガイド( Excel , PowerPoint ) Security Hub のアラートに対する理解は、プロジェクトによってまちまちです。プロジェクト側のアラートへの対応をサポートするため、 SOC 担当で Security Hub のコン トロール を調査し、対応方針やノウハウの資料をプロジェクト向けに公開しています。 まとめ ISID 社内で Security Hub の利用をサポートするためのツール群(資料、 スクリプト 含む)をざっとご紹介しました。 ただし今の形がベストだと思っているわけではなく、運用に必要なツールやその仕組みは、今後の SOC の成長に応じて変わってくるだろうなと思っています。 私たちは同じチームで働いてくれる仲間を大募集しています!たくさんのご応募をお待ちしています。 セキュリティエンジニア(セキュリティ設計) 執筆: @kou.kinyo 、レビュー: 寺山 輝 (@terayama.akira) ( Shodo で執筆されました )
アバター
こんにちは。X(クロス) イノベーション 本部 ソフトウェアデザインセンター セキュリティグループの耿です。 1年ほど前に書いたブログ では、組織横断的に Security Hub の利用をサポートするための準備について書きました。あれから1年が経ち、今は社内の数百アカウントの Security Hub 利用をサポートしています。この記事では、 Security Hub の社内展開の運用で利用しているツール群をご紹介します。 なお、Security Hub の利用サポートは社内の SOC という組織で行っています。 SOC の組織的な立ち位置や役割は こちらのブログ でご紹介しています。 改めてまとめると、SOC は社内の各事業部のプロジェクトに対し、以下の2つのサービスを提供しています。 クラウド サービスのセキュリティ設定不備のチェック(Security Hubのセキュリティ基準) クラウド アカウントに対する脅威の検出(Security Hub + GuardDuty) 概要 ①アカウント連携申請フォーム(Jira Service Management) ②アカウント管理台帳(Excel) ③アカウント連携招待ツール(Lambda関数) ④アカウント連携セットアップスクリプト(PowerShellスクリプト) ⑤アカウント連携チェックツール(Lambda関数) ⑥検出結果配信ツール (ECSバッチアプリ) ⑦脅威検知通知ツール(Lambda関数) ⑧アカウント連携解除ツール(PowerShellスクリプト) ⑨セキュリティコントロールの対応方針、 セキュリティ設定ガイド(Excel, PowerPoint) まとめ 概要 SOC のサービスの運用で使用しているツールなどを1つの図にまとめました。 各プロジェクトが使用している AWS アカウントの Security Hub から、SOC の AWS アカウントへクロスアカウント連携することで、 SOC 側で一括してセキュリティ設定やアラートを取得し、情報配信や監視を行う組織体制です。各ツールの詳細を図の番号順で見ていきます。 ①アカウント連携申請フォーム(Jira Service Management) SOC のサービスを利用するかどうかは、各プロジェクトの判断に委ねています。 SOC を利用する場合は社内向けの Jira Service Management を利用し、申請を受け付けています。申請の種類には AWS アカウントの追加、変更、解除が含まれます。 ②アカウント管理台帳( Excel ) AWS アカウントIDとプロジェクトの連絡先などの マッピング を記載したアカウント管理台帳を用意し、プロジェクトから申請を受け付けたら更新します。 現在の申請数であれば特に運用負荷となっておらず、自動化するとなると申請者の権限管理が逆に難しそうなので、今のところ手動作業です。 ③アカウント連携招待ツール(Lambda関数) ②のアカウント管理台帳を更新したら、SOC の AWS アカウントにある S3 バケット にファイルをアップロードします。 S3 バケットのイベント通知機能 を利用し、ファイルがアップロードされると自動でLambda関数を起動し、ファイルの内容を読み取って、未招待のプロジェクトアカウントへ Security Hub の連携招待 を送信します。 ④アカウント連携セットアップ スクリプト ( PowerShell スクリプト ) Security Hub の招待を受けたメンバーアカウントではいくつかのセットアップ作業が必要です。 Security Hub の有効化と、セキュリティ基準の有効化(まだ有効化していない場合) Security Hub 招待の承諾 AWS Config の有効化(まだ有効化していない場合) GuardDuty の有効化(まだ有効化していない場合) これらを社内のプロジェクト担当者に簡単に実施してもらえるように、CloudShell で流すだけで一連のセットアップが完了する PowerShell スクリプト を用意しています。 ⑤アカウント連携チェックツール(Lambda関数) プロジェクト担当者が④の スクリプト を実行するタイミングは任意です。そこで②のアカウント管理台帳や SOC アカウントの Security Hub などからデータを取得し、セットアップが完了していないアカウントがあるかどうか Lambda 関数で定期的にチェック、結果を SOC 担当のチャットに通知しています。 ⑥検出結果配信ツール (ECSバッチアプリ) プロジェクトアカウントに対して定期的に Security Hub の検出結果を Excel レポートとして配信するバッチアプリを ECS Fargate で実行します。 詳細は 以前のブログ でご説明しています。 ⑦脅威検知通知ツール(Lambda関数) クラウド アカウントに対する緊急度の高い脅威(今まさに侵入されている可能性がある、など)が発生した場合、⑥の検出結果のレポート配信のタイミングに関わらず、なるべく早い段階でプロジェクト担当者に通知されるべきです。Security Hub の連携を行うと、メンバーアカウントの GuardDuty アラートも SOC アカウントにニアリアルタイムで連携されるため、 EventBridge と Lambda 関数によるイベントドリブンで緊急度の高い GuardDuty アラートを SOC 担当のチャットに通知します。 SOC 担当はアラートの内容を確認し、プロジェクト担当者に連絡を取ります。 ⑧アカウント連携解除ツール( PowerShell スクリプト ) プロジェクトアカウントを SOC サービスから解除する場合は、 SOC アカウントの CloudShell で実行することで連携を解除できる PowerShell スクリプト を利用します。プロジェクトアカウント側の設定(Security Hub の無効化)などはプロジェクト側の作業に委ねています。 ⑨セキュリティコン トロール の対応方針、 セキュリティ設定ガイド( Excel , PowerPoint ) Security Hub のアラートに対する理解は、プロジェクトによってまちまちです。プロジェクト側のアラートへの対応をサポートするため、 SOC 担当で Security Hub のコン トロール を調査し、対応方針やノウハウの資料をプロジェクト向けに公開しています。 まとめ ISID 社内で Security Hub の利用をサポートするためのツール群(資料、 スクリプト 含む)をざっとご紹介しました。 ただし今の形がベストだと思っているわけではなく、運用に必要なツールやその仕組みは、今後の SOC の成長に応じて変わってくるだろうなと思っています。 私たちは同じチームで働いてくれる仲間を大募集しています!たくさんのご応募をお待ちしています。 セキュリティエンジニア(セキュリティ設計) 執筆: @kou.kinyo 、レビュー: 寺山 輝 (@terayama.akira) ( Shodo で執筆されました )
アバター
ISID X(クロス) イノベーション 本部 の三浦です。 筆者の関わっている案件では、コンテナ利用、 AWS Fargate利用を進めております。 今日は、 AWS Fargateのコストを下げるための取り組み例についてご紹介します。 目次 目次 AWS Fargate Spotとは? terraformのコードでAWS Fargate Spot対応 稼働率の例 まとめ AWS Fargate Spotとは? Fargate Spotは、EC2スポット インスタンス と同じように、 AWS 内で未使用の クラウド リソースを活用することでオンデマンド料金と比べて最大70%の割引価格で一時的に利用できる インスタンス です。 Fargate Spotが空きキャパシティを確保できるかぎり、ユーザーは指定したタスクを起動できます。 AWS にキャパシティが必要になったとき、Fargate Spotで稼働するタスクは2分前の通知とともに中断されます。 そのため、本番で使おうとすると様々な検討が必要ですが、中断しても問題にならないような小規模の開発環境、技術検証環境ならデメリットを最小化して使うことができます。 AWS Fargate の料金 Amazon ECS 向け Fargate Spot の料金 AWS Fargate の料金 より terraformのコードで AWS Fargate Spot対応 terraformでecs serviceを定義している場合、resource " aws _ecs_service"に下記の赤枠のコードを追加するだけで、Fargate Spot対応が完了します。本案件では、複数コンテナを使用しておらず1コンテナを立ち上げる仕様です。 中断が問題にならないフェイズ(インフラ担当者が構築中、極一部の開発者のみが使用する段階)ではFargate Spotで起動し、本稼働のフェイズではFargateに切り替えるといったことをしています。 なお、Fargate Spotの仕様としては、weightにて通常のFargateとFargate Spotの比率をコン トロール できます。例えば、7割をfargateにし3割をspotにするとspotが落ちるような状況下でも影響を限定できます。 また1コンテナしか存在しないような場合でも、weightを1:0 ⇒0:1のように切り替えることにより、非常に小さな修正でFargate SpotとFargate を切り替えられます。 (青枠の部分は キャパシティー プロバイダーの設定とコンフリクトするため、削除が必要です) 稼働率 の例 このようにコストの削減に効果的で、かつ、対応も簡単なFargate Spotですが、あまり頻繁に立ち上がらないとつらいものがあります。 ということで、Fargate Spotが実際に、どのくらいの頻度で稼働し続けるのか、再起動はどれくらい発生するかという実例をご紹介いたします。 下記は日本リージョンで小さいコンテナを20コンテナ、1週間起動したときのコストです。 コスト エクスプローラ ーでみると、ほぼ全ての時間で安定して起動しており、特定の曜日、時間帯に大規模に中断しつづけるということはありませんでした( アベイラビリティ ーゾーン IDはapne1-az4を使用しました)。 一方、ある程度の頻度でタスクの終了は発生しており、ログを確認したところ、20コンテナで48hで42回再起動していました。1コンテナ当たり、1日一回程度の頻度になります。なお、再起動は特定の時間帯に明らかに偏っているということはありませんでした。 ということで、ある程度の再起動とは引き換えになりますが、大幅なコスト削減が可能となります。 まとめ ということで、本格的なSpotの利用となると採用が難しいところもあるとは思いますが、まず、構築時、技術検証時、プライベート環境等からFargate Spotを使ってみるというのはいかがでしょうか? 使ってみると、意外と中断は気にならずいいかんじでコストが抑制できるかもしれません。 私たちは同じチームで働いてくれる仲間を探しています。 クラウド アーキテクトの業務に興味がある方のご応募をお待ちしています。 クラウドアーキテクト 執筆: @miura.toshihiko 、レビュー: @yamashita.tsuyoshi ( Shodo で執筆されました )
アバター
ISID X(クロス) イノベーション 本部 の三浦です。 筆者の関わっている案件では、コンテナ利用、 AWS Fargate利用を進めております。 今日は、 AWS Fargateのコストを下げるための取り組み例についてご紹介します。 目次 目次 AWS Fargate Spotとは? terraformのコードでAWS Fargate Spot対応 稼働率の例 まとめ AWS Fargate Spotとは? Fargate Spotは、EC2スポット インスタンス と同じように、 AWS 内で未使用の クラウド リソースを活用することでオンデマンド料金と比べて最大70%の割引価格で一時的に利用できる インスタンス です。 Fargate Spotが空きキャパシティを確保できるかぎり、ユーザーは指定したタスクを起動できます。 AWS にキャパシティが必要になったとき、Fargate Spotで稼働するタスクは2分前の通知とともに中断されます。 そのため、本番で使おうとすると様々な検討が必要ですが、中断しても問題にならないような小規模の開発環境、技術検証環境ならデメリットを最小化して使うことができます。 AWS Fargate の料金 Amazon ECS 向け Fargate Spot の料金 AWS Fargate の料金 より terraformのコードで AWS Fargate Spot対応 terraformでecs serviceを定義している場合、resource " aws _ecs_service"に下記の赤枠のコードを追加するだけで、Fargate Spot対応が完了します。本案件では、複数コンテナを使用しておらず1コンテナを立ち上げる仕様です。 中断が問題にならないフェイズ(インフラ担当者が構築中、極一部の開発者のみが使用する段階)ではFargate Spotで起動し、本稼働のフェイズではFargateに切り替えるといったことをしています。 なお、Fargate Spotの仕様としては、weightにて通常のFargateとFargate Spotの比率をコン トロール できます。例えば、7割をfargateにし3割をspotにするとspotが落ちるような状況下でも影響を限定できます。 また1コンテナしか存在しないような場合でも、weightを1:0 ⇒0:1のように切り替えることにより、非常に小さな修正でFargate SpotとFargate を切り替えられます。 (青枠の部分は キャパシティー プロバイダーの設定とコンフリクトするため、削除が必要です) 稼働率 の例 このようにコストの削減に効果的で、かつ、対応も簡単なFargate Spotですが、あまり頻繁に立ち上がらないとつらいものがあります。 ということで、Fargate Spotが実際に、どのくらいの頻度で稼働し続けるのか、再起動はどれくらい発生するかという実例をご紹介いたします。 下記は日本リージョンで小さいコンテナを20コンテナ、1週間起動したときのコストです。 コスト エクスプローラ ーでみると、ほぼ全ての時間で安定して起動しており、特定の曜日、時間帯に大規模に中断しつづけるということはありませんでした( アベイラビリティ ーゾーン IDはapne1-az4を使用しました)。 一方、ある程度の頻度でタスクの終了は発生しており、ログを確認したところ、20コンテナで48hで42回再起動していました。1コンテナ当たり、1日一回程度の頻度になります。なお、再起動は特定の時間帯に明らかに偏っているということはありませんでした。 ということで、ある程度の再起動とは引き換えになりますが、大幅なコスト削減が可能となります。 まとめ ということで、本格的なSpotの利用となると採用が難しいところもあるとは思いますが、まず、構築時、技術検証時、プライベート環境等からFargate Spotを使ってみるというのはいかがでしょうか? 使ってみると、意外と中断は気にならずいいかんじでコストが抑制できるかもしれません。 私たちは同じチームで働いてくれる仲間を探しています。 クラウド アーキテクトの業務に興味がある方のご応募をお待ちしています。 クラウドアーキテクト 執筆: @miura.toshihiko 、レビュー: @yamashita.tsuyoshi ( Shodo で執筆されました )
アバター
こんにちは。X(クロス) イノベーション 本部 ソフトウェアデザインセンター の山下です。 入社したのが2021年の10月なのでそろそろ1年と半年くらいが経過しました。 今日は入社までの経緯や、仕事の雰囲気などについてお話出来ればと思います。 会社の雰囲気 自分が受けている印象では、ISIDは会社の雰囲気として意思決定が素早く意見が通りやすいという印象があります。 結果、会議の時間がとても短く大変驚いています。 コロナ禍での入社だったので、基本的にリモートでしか業務に参加していないのですが、皆さん意見をちゃんと言ってくれます。また自分が提案した意見についても真剣に議論して貰えます。 やっている業務内容 担当している業務ですが、社内の開発に関する改善活動全般という感じです。 例えば、CI/CDの普及活動であったり、 GitHub のCodespacesなどについての検証活動といったことをやっています。 成果の一部はテックブログとして公開したりしていますね。 また、新しい技術が出てきたらそれをキャッチアップするために試してみるといったこともやっています。 入社の経緯 前職では、家電メーカーの研究部門でソフトウェア開発の生産性に関する研究、社内施策の実施などの業務に携わっていました。例えば、ソフトウェア リポジトリ のマイニング、CI/CD環境の整備などです。 前職の業務もやりがいがあるものでした。しかし、会社規模が大きい会社ということもあり段々とマネジメントに関する業務が増え、スキルなどもマネジメント系の能力を伸ばしていくことが期待されるようになってきました。 もう少し技術に触れている時間を増やせる会社を探していたところISIDを知人に紹介されて、自分も良さそうと思って転職を決めました。 入社前に不安だったこと、実際 入社前に不安に感じていたものは以下のような物でした。 技術力の不足 業務知識や業界の常識の不足 プライベートの時間の確保 それぞれについて不安と実際どうだったかについて見ていきます。 技術力の不足 前職では研究職で、ISIDに入社すると技術職になります。これは似ている面はありますが違うものであると自分は思っています。例えば、製品開発を最初から最後まで全部やり切るといった経験は研究職だとなかなか経験できません。 なので、実際の開発で役に立つような知識を自分が持っているか不安でした。 特に入社前から技術をリードしていく立場として、メンバに技術を教える立場であることを期待されていました。上手くやれるのか不安を持っていました。結論から言うと、これは杞憂だったようです。 技術的な側面については、教える場面では高度な技術の知識が必要というよりは基本的な知識があれば十分でした。例えば計算機に関する基本的な知識、 Linux の操作に関する一通りの知識などです。なので今のところ自分の技術の能力が不足していて困ったという場面には遭遇していません。 また、ISIDでは知らない技術がある場合にはその知識を獲得するための期間や勉強する場が準備される体制になっています。知識不足で業務に支障が出るということは発生しなさそうです。 業務知識や業界の常識の不足 前職でSIのような仕事と全く無関係ということはなかったのですが、いざ当事者となると業界の ドメイン 知識が十分であるかというとかなり疑問でした。入社して1年経ちますが、過去の経験が生きている場面もありますが、やはり分からない話もあります。その場合、社内で質問すれば教えてもらえるので大変助かっています。皆さまありがとうございます。 逆に前職の経験に引っ張られてしまう場面も多く、まだまだ勉強することは多いなと感じています。 プライベートの時間の確保 前職では業務時間がやや長くなる傾向にありました。プライベートの時間に影響が出るほどではなかったのですが、ISIDに入社してからはプライベートの時間が大幅に取れるようになりました。前職と比較して会議の時間が減り、効率的な働き方が可能になったからと自分では分析しています。 また、前職では8時間と設定されていた標準の労働時間がISIDでは7時間となっており、純粋に1時間ほどプライベートの時間を確保しやすくなりました。 前職と働き方で変わった事 業務上の目標は前職で一番変化した点は意思決定が早くなり、作業時間の確保が比較的容易になったので 技術的な調査、検証といった時間を確保できるようになったのが一番の変化だと考えています。 前職では会社の規模が大きかったため、利害関係者が多くなる傾向にありました。 結果として資料作成や会議が多くなり実際の技術に使える時間は限られていました。 ISIDに入社してからは、利害関係者も少なく意思決定が早いことから調整の為の会議やそれに向けた資料の作成 といった時間が減り、技術的な調査、検証の時間が増えています。 一方で、意思決定や利害関係者が少なく、自分の判断が会社に影響を与える比率が大きくなりました、そのため責任を感じる場面も増えたなという印象があります。気を引き締めて仕事に臨まないといけない場面は増えたかもしれません。 これは、自分としてはやりたいことをやるまでの手数が減ったので良かったかなと考えています。意思決定の早さについては1年半経った今でも驚かされる事が多いので、まだまだ慣れが必要だなと考えています。 最後に 今回は、ISIDに入社して1年半経った自分の入社前の不安や働き方の変化について簡単にですが振り返ってみました。 もし、ISIDにご興味がある方は是非応募してみてください。 私たちは同じチームで働いてくれる仲間を探しています。今回のエントリで紹介したような仕事に興味のある方、ご応募お待ちしています。 ソリューションアーキテクト 執筆: @yamashita.tsuyoshi 、レビュー: @handa.kenta ( Shodo で執筆されました )
アバター
こんにちは。X(クロス) イノベーション 本部 ソフトウェアデザインセンター の山下です。 入社したのが2021年の10月なのでそろそろ1年と半年くらいが経過しました。 今日は入社までの経緯や、仕事の雰囲気などについてお話出来ればと思います。 会社の雰囲気 自分が受けている印象では、ISIDは会社の雰囲気として意思決定が素早く意見が通りやすいという印象があります。 結果、会議の時間がとても短く大変驚いています。 コロナ禍での入社だったので、基本的にリモートでしか業務に参加していないのですが、皆さん意見をちゃんと言ってくれます。また自分が提案した意見についても真剣に議論して貰えます。 やっている業務内容 担当している業務ですが、社内の開発に関する改善活動全般という感じです。 例えば、CI/CDの普及活動であったり、 GitHub のCodespacesなどについての検証活動といったことをやっています。 成果の一部はテックブログとして公開したりしていますね。 また、新しい技術が出てきたらそれをキャッチアップするために試してみるといったこともやっています。 入社の経緯 前職では、家電メーカーの研究部門でソフトウェア開発の生産性に関する研究、社内施策の実施などの業務に携わっていました。例えば、ソフトウェア リポジトリ のマイニング、CI/CD環境の整備などです。 前職の業務もやりがいがあるものでした。しかし、会社規模が大きい会社ということもあり段々とマネジメントに関する業務が増え、スキルなどもマネジメント系の能力を伸ばしていくことが期待されるようになってきました。 もう少し技術に触れている時間を増やせる会社を探していたところISIDを知人に紹介されて、自分も良さそうと思って転職を決めました。 入社前に不安だったこと、実際 入社前に不安に感じていたものは以下のような物でした。 技術力の不足 業務知識や業界の常識の不足 プライベートの時間の確保 それぞれについて不安と実際どうだったかについて見ていきます。 技術力の不足 前職では研究職で、ISIDに入社すると技術職になります。これは似ている面はありますが違うものであると自分は思っています。例えば、製品開発を最初から最後まで全部やり切るといった経験は研究職だとなかなか経験できません。 なので、実際の開発で役に立つような知識を自分が持っているか不安でした。 特に入社前から技術をリードしていく立場として、メンバに技術を教える立場であることを期待されていました。上手くやれるのか不安を持っていました。結論から言うと、これは杞憂だったようです。 技術的な側面については、教える場面では高度な技術の知識が必要というよりは基本的な知識があれば十分でした。例えば計算機に関する基本的な知識、 Linux の操作に関する一通りの知識などです。なので今のところ自分の技術の能力が不足していて困ったという場面には遭遇していません。 また、ISIDでは知らない技術がある場合にはその知識を獲得するための期間や勉強する場が準備される体制になっています。知識不足で業務に支障が出るということは発生しなさそうです。 業務知識や業界の常識の不足 前職でSIのような仕事と全く無関係ということはなかったのですが、いざ当事者となると業界の ドメイン 知識が十分であるかというとかなり疑問でした。入社して1年経ちますが、過去の経験が生きている場面もありますが、やはり分からない話もあります。その場合、社内で質問すれば教えてもらえるので大変助かっています。皆さまありがとうございます。 逆に前職の経験に引っ張られてしまう場面も多く、まだまだ勉強することは多いなと感じています。 プライベートの時間の確保 前職では業務時間がやや長くなる傾向にありました。プライベートの時間に影響が出るほどではなかったのですが、ISIDに入社してからはプライベートの時間が大幅に取れるようになりました。前職と比較して会議の時間が減り、効率的な働き方が可能になったからと自分では分析しています。 また、前職では8時間と設定されていた標準の労働時間がISIDでは7時間となっており、純粋に1時間ほどプライベートの時間を確保しやすくなりました。 前職と働き方で変わった事 業務上の目標は前職で一番変化した点は意思決定が早くなり、作業時間の確保が比較的容易になったので 技術的な調査、検証といった時間を確保できるようになったのが一番の変化だと考えています。 前職では会社の規模が大きかったため、利害関係者が多くなる傾向にありました。 結果として資料作成や会議が多くなり実際の技術に使える時間は限られていました。 ISIDに入社してからは、利害関係者も少なく意思決定が早いことから調整の為の会議やそれに向けた資料の作成 といった時間が減り、技術的な調査、検証の時間が増えています。 一方で、意思決定や利害関係者が少なく、自分の判断が会社に影響を与える比率が大きくなりました、そのため責任を感じる場面も増えたなという印象があります。気を引き締めて仕事に臨まないといけない場面は増えたかもしれません。 これは、自分としてはやりたいことをやるまでの手数が減ったので良かったかなと考えています。意思決定の早さについては1年半経った今でも驚かされる事が多いので、まだまだ慣れが必要だなと考えています。 最後に 今回は、ISIDに入社して1年半経った自分の入社前の不安や働き方の変化について簡単にですが振り返ってみました。 もし、ISIDにご興味がある方は是非応募してみてください。 私たちは同じチームで働いてくれる仲間を探しています。今回のエントリで紹介したような仕事に興味のある方、ご応募お待ちしています。 ソリューションアーキテクト 執筆: @yamashita.tsuyoshi 、レビュー: @handa.kenta ( Shodo で執筆されました )
アバター