参考記事
o8queさんの記事
公式チュートリアル
基本的にこれに書いてあることをGoogle翻訳にかけて調べました。
画像付きなのでなんとなーくでも読めるかも。
できたもの
書いたコード(一部抜粋)
Assets/QuantumUser/Simulation/ の下にスクリプトを作らないといけないみたい。
データ指向なので、UnityのECSみたいにエンティティ、コンポーネント、システムという概念に考えを分けて作るみたい(激ムズ!)
コンポーネント
DSL(.qtnファイル)を書いて定義
component UltraPlayer
{
AssetRef<UltraPlayerConfig> PlayerConfig;
TimeCounter BulletCoolTimeTimeCounter;
}
component UltraPlayer
{
AssetRef<UltraPlayerConfig> PlayerConfig;
TimeCounter BulletCoolTimeTimeCounter;
}
input
{
button Right;
button Left;
button Up;
button Down;
button Fire;
}
component Bullet
{
FP LifeTime;
EntityRef Owner;
}
singleton component AllPlayersManager
{
list<EntityRef> AllPlayers;
}
システム
プレイヤー生成
using UnityEngine.Scripting;
namespace Quantum.QuantumUser.Simulation.UltraQuantumTest
{
[Preserve]
public unsafe class PlayerSpawnSystem : SystemSignalsOnly, ISignalOnPlayerAdded
{
public void OnPlayerAdded(Frame f, PlayerRef player, bool firstTime)
{
var data = f.GetPlayerData(player);
var asset = f.FindAsset(data.PlayerAvatar);
var entity = f.Create(asset);
if (!f.Unsafe.TryGetPointer<PlayerLink>(entity, out var playerLink))
return;
playerLink->PlayerRef = player;
var v = f.GetOrAddSingleton<AllPlayersManager>();
if (!f.TryResolveList(v.AllPlayers, out var list))
return;
list.Add(entity);
}
}
}
f.GetOrAddSingleton<>()とかでシングルトンを使える。
シングルトンの詳しい使い方はこのページに書いてある。
また、Photon QuantumでもListやDictionaryを使えるけど、ちょっとクセがある。
詳しくはここに。
とりあえず、初期化時にAllocate、破棄時にFree、使用時にResolveってやらないとダメみたい。
AllPlayerManagerコンポーネントの初期化と破棄処理
using UnityEngine.Scripting;
namespace Quantum.QuantumUser.Simulation.UltraQuantumTest
{
[Preserve]
public unsafe class HandleAllPlayers : SystemSignalsOnly,
ISignalOnComponentAdded<AllPlayersManager>,
ISignalOnComponentRemoved<AllPlayersManager>
{
public void OnAdded(Frame f, EntityRef entity, AllPlayersManager* component)
{
component->AllPlayers = f.AllocateList<EntityRef>();
}
public void OnRemoved(Frame f, EntityRef entity, AllPlayersManager* component)
{
f.FreeList(component->AllPlayers);
component->AllPlayers = default;
}
}
}
プレイヤー移動
using Photon.Deterministic;
using UnityEngine.Scripting;
namespace Quantum.QuantumUser.Simulation.UltraQuantumTest
{
[Preserve]
public unsafe class PlayerMoveSystem : SystemMainThreadFilter<PlayerMoveSystem.Filter>
{
public struct Filter
{
public EntityRef EntityRef;
public PlayerLink* PlayerLink;
public PhysicsBody2D* PhysicsBody2D;
public UltraPlayer* UltraPlayer;
}
public override void Update(Frame f, ref Filter filter)
{
var config = f.FindAsset(filter.UltraPlayer->PlayerConfig);
var input = f.GetPlayerInput(filter.PlayerLink->PlayerRef);
//Move
var v = new FPVector2();
if (input->Right)
v.X = 1;
if (input->Left)
v.X = -1;
if (input->Up)
v.Y = 1;
if (input->Down)
v.Y = -1;
filter.PhysicsBody2D->Velocity = v * config.playerSpeed;
}
}
}
SystemMainThreadFilter< Filter>を継承することで、Entity全ての中からFilter内のコンポーネントを持つEntityだけを抽出して、そいつらにUpdateをかけてるみたい。
上記コードだと、PlayerLink,PhysicsBody2D,UltraPlayerを全て持ってるEntityだけにUpdateをかける。(EntityRefはデフォで持ってる。はず…)
PlayerLinkを持っていてもPhysicsBody2Dを持っていなかったら呼ばない。
inputはDSLで定義したものが使われる。
弾発射
using System;
using System.Linq;
using Photon.Deterministic;
using Quantum.Core;
using UnityEngine;
using UnityEngine.Scripting;
namespace Quantum.QuantumUser.Simulation.UltraQuantumTest
{
[Preserve]
public unsafe class PlayerShootSystem : SystemMainThreadFilter<PlayerShootSystem.Filter>
{
public struct Filter
{
public EntityRef EntityRef;
public PlayerLink* PlayerLink;
public UltraPlayer* UltraPlayer;
public Transform2D* Transform2D;
}
public override void Update(Frame f, ref Filter filter)
{
var input = f.GetPlayerInput(filter.PlayerLink->PlayerRef);
var counter = filter.UltraPlayer->BulletCoolTimeTimeCounter;
var config = f.FindAsset(filter.UltraPlayer->PlayerConfig);
counter.CountUp(f);
//Fire
if (input->Fire && counter.CurrentTime >= FP._0_20)
{
var bullet = f.Create(config.bulletPrototype);
if (!f.Unsafe.TryGetPointer<Transform2D>(bullet, out var transform2D))
return;
transform2D->Teleport(f, CalcPos(f, filter));
if (!f.Unsafe.TryGetPointer<PhysicsBody2D>(bullet, out var physicsBody2D))
return;
physicsBody2D->Velocity = CalcDir(f, filter) * 5;
counter.Reset();
}
filter.UltraPlayer->BulletCoolTimeTimeCounter = counter;
}
private static FPVector2 CalcPos(FrameBase f, Filter filter)
{
return filter.Transform2D->Position + CalcDir(f, filter);
}
private static FPVector2 CalcDir(FrameBase f, Filter filter)
{
if (!f.Unsafe.TryGetPointerSingleton<AllPlayersManager>(out var allPlayersManager))
throw new Exception();
if (!f.TryResolveList(allPlayersManager->AllPlayers, out var list))
throw new Exception();
var player = filter.EntityRef;
var otherPlayer = list.FirstOrDefault(value => value != player);
if (otherPlayer == default)
{
return FPVector2.Right;
}
if (!f.Unsafe.TryGetPointer<Transform2D>(otherPlayer, out var target))
throw new Exception();
var dir = target->Position - filter.Transform2D->Position;
return dir.Normalized;
}
}
}
他のプレイヤーに向かって弾を飛ばす。
f.Create()でEntityを作成して、f.Unsafe.TryGetPointer<>()で作ったエンティティから好きなコンポーネントを取得してる。
UnityのInstantiateとGetComponent<>()の代わりだと思う。
コンポーネントの値の取得だけならf.TryGet<>()もある。
なんでGetじゃなくてGetPointerにしているのかっていうと、PhotonQuantumのコンポーネントは参照型でなく値型なので、コピーが渡されるだけ。後でSetをしないといけないのがちょっとめんどい。
あと、公式チュートリアルでポインターの方を使っていたので…
感想
ラグをほとんど感じなかった。
Photon Fusionとかの状態同期方式でありがちな相手プレイヤーがピクピクするやつをダサいと思っていたので、それが理論上起こらないのですごく魅力的。
でも難しい!!!
そもそもデータ指向プログラミングが難しい!!!
今まで培ってきたデザインパターンだったり設計が全然通用しない。
UniTask、UniRx、VContainerのUnity三種の神器を使って戦えない。Rxに関しては、一応Quantum側で色々用意されてるみたいだけど。
UniTaskやコルーチンを使えないので、「ちょっと待つ」の処理を良い感じに書けない。
公式チュートリアルやサンプルプロジェクトでもUpdate関数内でDeltaTimeをインクリメントするという原始的な方法を使っていた。まじかー…
VContainerなどを使えないので、いろんなとこから参照したいものは全てシングルトンにしちゃうのかな?
他に、例えばスキルが無限に増えるゲームを作るときに、自分はISkillみたいなインターフェースを作って継承させていろんなスキルを作っていたのだけど、データ指向だとそうもいかなそう。解決策は今のところ思いつかず。
とにかくデータ指向が難しい!!!!!