tomo_manaのブログ

tomo_manaのブログ

Unityで一からゲームを作る方法を紹介しています。

Unity学習#34-2 EventSystemとInputSystemUIInputModule (Unity 2019.4.4f1)

第12回でInputSystemについて、第13回でEventSystemについて、それぞれ調べました。複数シーンでゲームを構成する場合は、もう少し深掘りが必要なことが分かったので、再度いくつか疑問に思っていたことについて調べました。


全体として知りたいのは、EventSystemからPlayerInput経由でメッセージを受け取るまでの流れです。

今回は、EventSystem と InputSystem 用のコンポーネント InputSystemUIInputModule との関係について。

特に、(1) 複数のシーンでゲームが構成されている場合、EventSystemはシーン毎に置いても正しく動作するのか。また、(2) InputManager から InputSystem に変える時に StandaloneInputModule から InputSystemUIInputModule に置き換えましたが、この二つはどんな繋がりがあるのか。EventSystemはそもそも何をしていて、InputSysmteUIInputModule は何をしているのか。この2つについて調べました。

動機は、シーンが複数ある場合に、EventSystem をどのように配置したら良いかを知りたかったからでした。

結論

こんな感じでした。

(1) EventSystem は、シーン毎に一つずつあっても正常に動作します排他制御)。
(2) EventSystem は StandaloneInputModule と InputSystemUIInputModule の共通ベースクラス BaseInputModule を参照しています。また、InputSystemUIInputModule は、入力イベントの「状態管理」しています。EventSystem は BaseInputModule の排他制御をしています。


以下は EventSystem と InputSystemUIInputModule(BaseInputModule) の関係を表した図です。

f:id:tomo_mana:20210622003533j:plain
イメージ

EventSystem

まず、EventSystem が何をしているか。
EventSystem は、InputManager または InputSystem による入力監視処理が、一つだけ呼び出されるようにしています(排他制御)。また、EventSystem は現在選択されているゲームオブジェクト(SelectedGameObject)を保持する箱でもあり、選択が変更されたときにゲームオブジェクトに変更を伝えます(ISelectHandler、IDeselectHandler)。

EventSystem同士の連携(リスト化・排他制御

同一ゲーム内に複数のEventSystem があった場合、現在有効な EventSystem 以外は動かないようになっています。現在有効な EventSystem は、シングルトンである current です。(EventSystem.current)

f:id:tomo_mana:20210619225547p:plain
EventSystem管理(シングルトン構造)

この current と、現在アクセス可能な EventSystem たちを管理するシングルトンのリスト m_EventSystems があります。この EventSystemのリスト は、EventSystem コンポーネントを持つゲームオブジェクトがアクティブになった時に登録され、非アクティブになった時に登録解除されます。先述の current は、このリストの先頭にいる EventSystem です。

f:id:tomo_mana:20210619225635p:plain
EventSystemへの登録と解除、有効化

この リスト は private なので、EventSystem の外から リスト を直接書き変えることはできません。その代わりに、シーンが切り替わった時など、EventSystemを切り替える場合は、current にこれから使いたい EventSystem をセットします。これで、current にセットした EventSystem は、このリストの先頭になります。

InputManager と InputSystem の互換性

従来の入力システムである InputManager と、新しい入力システムである InputSystem 間の互換性は、両者の上位クラス BaseInputModule を用意することで吸収しています。

f:id:tomo_mana:20210619231232p:plain
EventSystemとBaseInputModule

このBaseInputModuleを継承したクラスは、入力が確定した時に、IEventSystemHandler を持つゲームオブジェクトにメッセージを伝える役割を果たします。また、入力が確定したことを調べるため、入力状態を管理(トレース)しています。例えばボタンを押した/離した、ドラッグした/離した、などです。
尚、InputSystemUIInputModule は、Navigation と呼ばれる仕組みを一緒に管理しています。これは経路探索などで使用できるようですが、まだ調べていないので、以下のリンクを参考に貼っておきます。
【Unity】Navigationを使って経路探索させる - Qiita

EventSystem と InputSystemUIInputModule の関係

EventSystem の役目

EventSystem は、InputSystemUIInputModule にとって、タイマーのような役目を果たします。

f:id:tomo_mana:20210619233914p:plain
EventSystemからのトリガ(Update)

EventSystem は、それ自身がコンポーネントのため、MonoBehavior(UIBehavior) を継承しています。MonoBehavior を継承するクラスは、UnityシステムからのUpdate()イベントを受信します。EventSystem は、このUpdate() のタイミング毎に、現在有効なInputSystemUIInputModule に、入力状態の確認(Process())を依頼します。

InputSystemUIInputModule 自身もコンポーネント(BaseInputModule が UIBehaviorを継承)のため、Update() を受け取れそうなものですが、EventSystem からのキックを待って動くようにできています。これは EventSystem側 で、BaseInputModule を排他制御させたいためと思われます。

EventSystem -> InputSystemUIInputModule

尚、EventSystem が InputSystemUIInputModule を一方的に参照している(親子)関係です。(が、InputSystemUIInputModule も、EventSystem にメッセージを投げることがあります)

f:id:tomo_mana:20210619233238p:plain
EventSystemとInputSystemUIInputModule


EventSystem から見て、現在有効なInputSystemUIInputModule は、リスト(m_CurrentInputModule)に保存されています。これはシングルトンではなく、各 EventSystemコンポーネント が持っています。

f:id:tomo_mana:20210619232916p:plain
EventSystem内のBaseInputModule群

EventSystem は、自分と同じゲームオブジェクト内の全ての InputSystemUIInputModule を認識します。ただし、複数の InputSystemUIInputModule があっても、有効な InputSystemUIInputModule は一つだけです。

ちなみに、このリストは GetComponent() で取得してくるだけで、登録順の入れ替えなどは特にしていないようですので、できれば EventSystem と同一ゲームオブジェクト内に BaseInputModule は一つだけ存在しているのが無難です。


これまでの内容を繋げると、冒頭の図になります。

f:id:tomo_mana:20210622003533j:plain


これで、複数シーンが起動していても、有効なEventSystems は常に一つであることが担保されています。また、有効な InputSystemUIInputModule も一つだけ、ということになります。

EventSystem <- InputSystemUIInputModule

EventSystem が InputSystemUIInputModule を一方的に参照している(親子)関係ですが、InputSystemUIInputModule も、EventSystem にメッセージを投げることがあります。それは、InputSystemUIInputModule が、ゲームオブジェクトの選択状態が変わったことを察知した時です。

InputSystemUIInputModule は、EventSystem.SetSelectedGameObject() を利用して、EventSystem にゲームオブジェクトの選択状態の変更を伝えます。その意味で、EventSystem と InputSystemUIInputModule は連携して動きます。


(以上)

Unity学習#34 複数シーン構成でのイベントシステムの切り替え (Unity 2019.4.4f1)

戦闘シーンの実装にあたって、イベントシステムはこれまでと同じ作り方でよいか迷ったので、先にゲームを複数シーンで構成する時の、イベントシステムの切り替えについて調べました。

複数シーンでのイベントシステムの切り替え

EventSystem を切り替えるには、EventSystem.current に、現在選択されているシーンの EventSystem を代入します

通常は、加算シーンで立ち上げておいて、アクティブなシーンを切り替えるタイミングで、EventSystem を切り替えます。この時、Camera(Transform系) と Canvas(RectTransform系)も併せて無効化します。また、EventSystem と連動して動く PlayerInput も無効化します。

コード

以下は、EventSystem、Camera、Canvas、PlayerInput がいずれも階層の一番上(Root)に属している場合のコードです。

static void ActivateSceneObjects(Scene scene)
{
    ForceSceneActivateState(scene, true);
}
static void DeactivateSceneObjects(Scene scene)
{
    ForceSceneActivateState(scene, false);
}
static void ForceSceneActivateState(Scene scene, bool state)
{
    GameObject[] gameObjects = scene.GetRootGameObjects();
    
    // カメラ
    foreach( GameObject g in gameObjects ){
        if( g.GetComponent<Camera>() != null ){
            g.SetActive(state);
            break;
        }
    }

    // イベントシステム
    foreach( GameObject g in gameObjects ){
        if( g.GetComponent<EventSystem>() != null ){
            g.SetActive(state);
            if( state == true ){
                // イベントシステムの切り替え
                EventSystem.current = g.GetComponent<EventSystem>();
            }
            break;
        }
    }

    // プレイヤーインプット
    foreach( GameObject g in gameObjects ){
        if( g.GetComponent<PlayerInput>() != null ){
            g.SetActive(state);
            break;
        }
    }

    // キャンバス
    foreach( GameObject g in gameObjects ){
        if( g.GetComponent<Canvas>() != null ){
            g.SetActive(state);
            break;
        }
    }
}

関連記事

EventSystem と InputSystemUIInputModule の関係

※作成中

EventSystem、InputSystemUIInputModule と PlayerInput

※作成中

InputUser と InputDevice/Control

※作成中

(以上)

Unity学習#34-1 戦闘メニューの構成

戦闘メニューを作る前に、戦闘メニューの拡張性について考えます。

基本的な構成

一般的な戦闘システムは、「誰が」「何を」「誰に」を繰り返すと仮定します。

f:id:tomo_mana:20210505180721p:plain
戦闘画面の遷移

(備考)
「にげる」は、ここではパーティ全体に適用。
「まほう」は「MP」など、体力の限り使用できる手段、また「やどや」で回復する。「アイテム」使ったらなくなる手段を想定。
※行動を決定する「オートバトル」は、本来人が決める戦略・行動をプログラムが代行しているという意味で、以下に示す「省略」や「ショートカット」には含まれない。


手順の拡張性から見ていきます。

手順数

先ほどの図が、行動決定までの入力数(アクション数)が最大(煩雑)と仮定します。実際は、ほとんどのゲームで、このアクション数を減らすため、いろんな工夫がされると考えます(ユーザが退屈しないため)。この工夫は、大きく分けて「省略」「ショートカット」がありそうです。(「オートバトル」は省略、ショートカットに含まない)

手順の省略

キャラ選択の省略(順序固定)

ターン制バトルの多くは、パーティーの並び順に行動を決定することで、キャラ選択を省略しています。これはドラゴンクエストや初期のファイナルファンタジーシリーズをはじめとして、よく見られる省略です。

f:id:tomo_mana:20210505233906p:plain
戦闘メニュー遷移(キャラ選択省略)
たたかうの省略(武器とアイテムへの回数制限)

武器とアイテムの区別を無くし、回数制限(耐久)を持たせると、「たたかう」が省略できます。これは初期の魔界塔士サガ(1,2)やファイヤーエンブレムなどに見られました。

f:id:tomo_mana:20210505233723p:plain
戦闘メニュー遷移(たたかう省略)
まほうとアイテムの同一視(まほうとアイテムへの回数制限)

「武器」「アイテム」に回数制限を採用したシステムに、さらに「まほう」まで回数式にすると、まほうとアイテムの選択画面そのものが省略できます。これは初期の魔界同士サガ(1、2)に見られました。また、ローグシリーズのように、魔法書や杖などのアイテムが魔法代わりになる(魔法に回数制限を持たせている)場合も同じ傾向があります。ただしローグシリーズの場合は、「叩く」か「振る」かで効果が違うなど、かなり細かい動作ができるようになっていました。

f:id:tomo_mana:20210505233832p:plain
戦闘メニュー遷移(まほう・アイテム同一視)
かくにんの省略

全プレイヤーの行動の最後の「かくにん」は、ターン制バトルで、パーティー全員の行動を承認する最後のステップですが、元々、ほとんどのターン制バトルでは「かくにん」が無かったと思います。最後のメンバーの行動が決定した時点で、入力は確定したと扱われることがほとんどです。
最後に行動の確認(または承認)を行うのは、オートバトルを採用したものなど、「デフォルト行動」「前回と同じ行動」(行動入力そのものの省略)が採用されたシステムで必要になります。最近プレイしたゲームではオクトパストラベラーで採用されていました。

リスト選択のショートカット

次にリスト選択のショートカット(マルチボタン)を見ていきます。

ファミコンからスーパーファミコンになり、さらにPlayStationへと発達する中で、マルチボタンが主流になりました。またパソコン系ゲームではキーボードが、最近ではスマホタブレット端末もマルチボタンがデフォルトになっています。

いくつかのショートカットキーが必要なゲームの場合(通常は反射神経を求められるゲームですが)、戦闘中の選択時間を短縮するために、リスト選択の代わりにショートカットキーが割り当てられています。

幾つかのショートカットのパターンを見ていきます。

キャラ選択ボタン(後述する速さの仕組みと関連)

リアルタイム性を追求したバトルで、かつ複数人を一人で扱うような場合、行動可能な時間に到達したキャラクターが複数人になった時に、キャラクターを特定のボタンで切り替えできます。ファミコン世代では、このシステムの先駆けになったファイナルファンタジーIV聖剣伝説をイメージされる方も多いのではと思います。妖怪ウォッチなどもこのシステムかと思います。

f:id:tomo_mana:20210505234856p:plain
キャラ切替ショートカット(タブ切り替え)

ターン制バトルにおいても、前述の「かくてい」を持つゲームでは、行動を変更する必要があるキャラだけをタップして、行動変更できるようにしているものもあります。

f:id:tomo_mana:20210505235118p:plain
キャラ切替ショートカット(アイコンタップ)
まほう・アイテムボタン(銃・属性などの駆け引き要因)

よりリアルタイム性を追求した、より反射神経が求められるようなゲームでは、まほうやアイテムの数を「スロット」として制限した上で、各スロットにキーを割り当てることで、リストの選択時間を省略するシステムを搭載したものもあります。これは銃撃戦などをモチーフにしたゲームで使われた手法なのではと思います。一般的なシューティング、縦・横スクロール、アクションRPGも、これに類するものと見ることができます。真っ先に思い出されるのは初代ゼルダの伝説でしょうか。

f:id:tomo_mana:20210505235304p:plain
アイテム選択ショートカット(スロット式)
ターゲット選択ルール(自動照準)

タクティカル系の流れを組むゲームや、アクションRPG系、MMORPG/FPS系ゲームなどでは、キャラクターの向きやターゲットとの位置関係などで、自動的に相手を選択することでターゲット選択が省略されることがあります。これも銃撃戦などを想定したもので、アーケード系ゲームや一部のシューティングゲーム系の流れを汲んでいるのではと思います。


次に、速さの概念を見ていきます。

速さの概念と描画処理

速さの概念には、いくつかのパターンがありそうです。

ターン制(1回行動)

ターン制では、速さはプレイヤーと敵の行動順を決定します。ターン制では、プレイヤーが時間をかけてキャラクター同士の協調動作(戦略・戦術)を練れるように、全てのプレイヤーの行動を一度に決めます。また、敵も同じように協調動作を仕掛けてきます。ターン制では、「速さ」は行動の順番のみに影響します。特別なオプションがない限りは、1ターンに行動できる回数は1回です。

少し表示処理についても考えてみます。ターン制では、行動決定とアニメーションなどの結果表示は状態として分離できます

f:id:tomo_mana:20210510223017p:plain
処理フロー(ターン制)

ターン制(カウントアップ式)

ターン制の中でも、速さをカウンターとして扱うものもあります。全員が速さに関わる順番待ちカウンタを持ち、カウントがゼロになったら行動できます。敵が主人公の倍の速さなら、主人公が1回攻撃する間に、敵は2回攻撃します。ローグシリーズやタクティクスオウガなどに見られました。

表示処理について考えます。プレイヤー入力は一人ずつになりますが、カウントアップ、行動決定、アニメーションなどの結果表示は状態として分離できます

f:id:tomo_mana:20210510223057p:plain
処理フロー(カウントアップ式)

時間になったら行動

先ほどのターン制(行動回数を規定)を、物理時間と関連付けたシステムについて考えます。行動を決める間も時間が流れるシステムでは、速さは先ほどのカウンタと同じように扱われますが、カウントアップはプレイヤーに必要な思考時間を考慮して、ゆっくりカウントされます。また、行動を決定している間も、敵の番が来たら、敵は最小思考時間で(涙)攻撃してきます。

こちらの表示処理は、少し複雑です。カウントアップ、行動決定、アニメーション処理などの結果表示は並列で処理されます。

f:id:tomo_mana:20210510223345p:plain
処理フロー(時間が来たら行動)

ファイナルファンタジーシリーズのように、「にげる」がショートカット化されることもあります。

アクションRPG/FPS

さらに行動回数を多く、人間の待ち時間を極限まで短くすると、アクションRPG/FPSになります。ターンの概念は無く、ボタンを押すたびに行動できます。常に行動決定が必要で忙しいので、大概はメインキャラ以外はオートになります。

アクション系では、速さは以下の3つの概念を持つものと思います。
(1) 移動速度(足の速さ)
(2) アクションから攻撃がヒットするまでの時間(ボタンを押してから攻撃が敵にヒットするまでの時間)
(3) リロード時間(ボタンを押してから次のボタン押下が有効になるまでの待ち)

表示処理は、行動決定とアニメーションなどの結果表示は完全に並列で処理されます。リストがスロット化されて、キャラの向きとアイテムの射程範囲で自動的にターゲットが決まる場合、ターゲット設定が省略されます。

f:id:tomo_mana:20210510223129p:plain
処理フロー(FPS


以下は、画面構成の一環として、視点(人称)について考えます。

視点

最近は2Dと3Dの区別がほとんど無くなっていますが、2Dの頃の一般的な手法として、一人称三人称がありました。一人称の一部は、プレイヤーの背中が映るものもあります。

一人称

f:id:tomo_mana:20210510233521p:plain
一人称

三人称

f:id:tomo_mana:20210510233540p:plain
三人称

最近は2D調のキャラも3Dでモデリングされているため、カメラワークでの切り替えで一人称と三人称(場合によって二人称や敵から見た一人称まで)を自由に切り替えることができます。ただ個人で作る場合は、3Dだとハードルが高いため、2Dでゲームを作るのが今でも一般的かもしれません。


(以上)

Unity学習#26-6 ScrollRect (Unity 2019.4.4f1)

#26 の実装にあたって、少しLayoutについて調べた内容を備忘のためにまとめます。今回は、ScrollRect で ILayoutGroup、ILayoutElement がどのように活用されているのかを調べました。

概要

(1) ScrollRect は ILayoutGroup だけで成り立っている(SetLayoutHorizontal/Vertical() をイベントトリガとして活用)
(2) Image と Mask はセットで動作する
(3) ScrollRect は Content も操作する(ILayoutGroupの更新時とScrollbar操作時)


ScrollViewとLayout

最初に、ScrollView での ILayoutGroup、ILayoutElement の実装について。

一般的なレイアウトの構成

HorizontalまたはVerticalLayoutGroupでは、ILayoutGroupとILayoutElementは以下のような関係でした。

f:id:tomo_mana:20210210231956p:plain
Layoutテンプレート

ScrollViewの構成

ScrollViewでは、以下のようになっています。

f:id:tomo_mana:20210503003618p:plain
ScrollView における Layout

ScrollView の ILayoutGroup は ScrollRect です。ScrollRectは子ゲームオブジェクトのILayoutElementの情報を参照しません。

(1) ScrollViewでは、ImageがILayoutElementを持っているものの、Imageが無くてもスクロール機能が使用できます。そのため、ScrollRect は ILayoutGroup だけを使ったレイアウトであると分かります。(なお、ScrollRect は ILayoutElement も実装していますが、ILayoutElement としては何もしていない)

(2) また、ScrollViewは基本的にViewportにImageとMaskをセットで持っています。ImageはILayoutElementですが、ScrollRect は Image の preferred width/height を参照しません

(3) ScrollRectは孫階層であるContentへの参照を持っています。ScrollRectはILayoutGroupのイベントであるSetLayoutHorizontal()/Vertical() と、Scrollbar 用のイベントである OnDrag()/OnBeginDrag()/OnScroll() で Content の Anchor情報を参照します(表示位置を変更するため)


ScrollRect:Viewport は ILayoutElement としてでなく RectTransform として直接参照されている。Content も直接参照されている。

f:id:tomo_mana:20210503225250p:plain
ScrollView - ScrollRect

ScrollView, Viewport, Content:ScrollView が RectTransform を操作していることがInspectorで確認できるのはViewportだけ。Content も実際は操作されている。

f:id:tomo_mana:20210503225307p:plain
RectTransform - ScrollView, Viewport, Content

(1) ScrollView と ILayoutGroup

ScrollRect は ILayoutElement と ILayoutGroup を実装しているので、CalcLayoutInputHorizontal()/Vertical() と SetLayoutHorizontal()/Vertical() を受け取ります。以前の記事で、Vertical/HorizontalLayoutGroup では、ILayoutGroup は自分のmin/preferred/flexible width/height 情報を使わないことが分かっています。

ScrollRectは、この2つのメッセージを受けた時にどうしているか。

CalcLayoutInputHorizontal/Vertical

何もしません。

public virtual void CalculateLayoutInputHorizontal() {}
public virtual void CalculateLayoutInputVertical() {}

SetLayoutHorizontal/Vertical

Viewport:anchorMin/Max, sizeDelta, anchordPosition を更新します。
Contentのサイズを使ってViewportの大きさを変更する(Scrollbarの表示/非表示)

public virtual void SetLayoutHorizontal()
{
    if (m_HSliderExpand || m_VSliderExpand)
    {
        LayoutRebuilder.ForceRebuildLayoutImmediate(content);
        m_ViewBounds = new Bounds(viewRect.rect.center, viewRect.rect.size);
    }
    if (m_VSliderExpand && vScrollingNeeded)
    {
        viewRect.sizeDelta = new Vector2(-(m_VSliderWidth + m_VerticalScrollbarSpacing), viewRect.sizeDelta.y);
       
        LayoutRebuilder.ForceRebuildLayoutImmediate(content);
        m_ViewBounds = new Bounds(viewRect.rect.center, viewRect.rect.size);
    }
    if (m_HSliderExpand && hScrollingNeeded)
    {
        viewRect.sizeDelta = new Vector2(viewRect.sizeDelta.x, -(m_HSliderHeight + m_HorizontalScrollbarSpacing));
        m_ViewBounds = new Bounds(viewRect.rect.center, viewRect.rect.size);
    }
    if (m_VSliderExpand && vScrollingNeeded && viewRect.sizeDelta.x == 0 && viewRect.sizeDelta.y < 0)
    {
        viewRect.sizeDelta = new Vector2(-(m_VSliderWidth + m_VerticalScrollbarSpacing), viewRect.sizeDelta.y);
    }
}
public virtual void SetLayoutVertical()
{
    UpdateScrollbarLayout();
    m_ViewBounds = new Bounds(viewRect.rect.center, viewRect.rect.size);
}

代入された m_ViewBounds は NormalizedPosition(スクロール位置)を決める時に使われます。

ここから、ScrollView は ILayoutGroup しか使っていないことと、ILayoutGroup の SetLayoutHorizontal()/Vertical() を Viewport の大きさを変更するために使っていることが分かります。

(2) Mask と Image (Viewport)

Mask処理は、今回知りたかったLayoutGroup/Elementと少し異なるため、ここでは触れませんが、MaskはImageとセットで動作します(Mask、Imageのどちらかが欠けても、クリッピング処理は正常に働かない)。

クリッピング処理はどうやって実現しているか気になるので、またいつか調べようと思いますが、今のところmaskとimageはセットで初めて動作して、どちらかが無いとマスクが実現できないことだけ分かりました。

(3) Content の更新

Scrollview のもう一つの謎は Content(RectTransform) への直接参照でした。Content は、レイアウト変更(ScrollViewの大きさが変更された時)と、スクロールバーが操作されたときの両方でアンカー情報が更新されます。調べてから振り返ってみると、確かにそういった使われ方をされるだろう、と思いました(調べるまでもなかった?)

レイアウト変更時

Content が ILayoutGroup を持っていた場合のために、LayoutRebuilder に Content を渡します。

public virtual void SetLayoutHorizontal()
{
    LayoutRebuilder.ForceRebuildLayoutImmediate(content);
    if (m_VSliderExpand && vScrollingNeeded)
    {
        LayoutRebuilder.ForceRebuildLayoutImmediate(content);
    }
}

スクロールバーが操作された時

ざっくりですが、OnDrag()/OnBeginDrag() と OnScroll() それぞれで、Content の Anchor を参照します。

public virtual void OnScroll(PointerEventData data)
{
    Vector2 position = m_Content.anchoredPosition;
    if (m_MovementType == MovementType.Clamped)
        position += CalculateOffset(position - m_Content.anchoredPosition);
}
public virtual void OnBeginDrag(PointerEventData eventData)
{
    m_ContentStartPosition = m_Content.anchoredPosition;
}
public virtual void OnDrag(PointerEventData eventData)
{
    Vector2 offset = CalculateOffset(position - m_Content.anchoredPosition);
}

参照された Anchor を使って、SetContentAnchordPosition()、SetNormalizedPosition() で Content の Anchor と 座標を更新しています。

protected virtual void SetContentAnchoredPosition(Vector2 position)
{
    if (!m_Horizontal)
        position.x = m_Content.anchoredPosition.x;
    if (!m_Vertical)
        position.y = m_Content.anchoredPosition.y;

    if (position != m_Content.anchoredPosition)
    {
        m_Content.anchoredPosition = position;
    }
}
protected virtual void SetNormalizedPosition(float value, int axis)
{
    float hiddenLength = m_ContentBounds.size[axis] - m_ViewBounds.size[axis];
    float newLocalPosition = m_Content.localPosition[axis] + contentBoundsMinPosition - m_ContentBounds.min[axis];
    Vector3 localPosition = m_Content.localPosition;
    if (Mathf.Abs(localPosition[axis] - newLocalPosition) > 0.01f)
    {
        m_Content.localPosition = localPosition;
    }
}

(以上)