徳島ゲーム開発ごっこ 技術ブログ

ゲームを作るために役に立ったり立たなかったりする技術を学んでいきます!

【Unity】僕もPhotonを使いたい #14 射撃攻撃をしてみる

 Photonを使って乱入型ゲームを作っています。
 街に対して破壊攻撃を行う敵をプレイヤー達の手で守っていこうというゲームを目指しています。
 敵を迎撃する必要がある以上、プレイヤーの攻撃処理を組み込む必要があります。
 今回はプレイヤーの射撃攻撃部分を作っていこうと思っています。 

f:id:urahimono:20161001220427j:plain


この記事にはUnity5.4.1f1及びPUN1.76を使用しています。

射撃攻撃の弾はPhotonViewを使うべきなのかな

 さっそく射撃の処理をスクリプトに記述していきましょう。
 とりあえず、一定時間ごとに自動で射撃を行う形で作成していきます。
 射撃用の弾のアセットには、手近なところに本のアセットがあったのでそれを使います。

 さて弾の生成はどのようにすればよいのでしょうか。
 弾のGameObjectにPhotonViewを追加し、PhotonNetwork.InstantiateSceneObject()を使うことで各プレイヤーに弾の生成の同期を取ることが出来ると思います。
www.urablog.xyz

 ですが、果たして弾のGameObjectにPhotonViewは必要なのでしょうか。
 弾の動きが等速直線運動ならば、PhotonTransformViewなどを使って位置の同期を取らずとも、発射タイミングと位置と方向さえ同じならば、各プレイヤーのゲーム内で移動処理を行えばほぼほぼ同じ弾の挙動になるはずです。
 もちろんホーミングをさせたりするのであれば話は変わってきますが……。

 めったやたらにいろいろなオブジェクトをPhotonViewを使って管理するのは処理負荷の点であまりよくないと思われます。
 これはPhotonのドキュメントにも書かれており、以下のリンクの制限の項目にこのように書かれています。
https://doc.photonengine.com/ja/pun/current/reference/pun-and-undoc.photonengine.com

 ほとんどのゲームは、(キャラクターにつき1か2...そしてそれが通常そうです。) 1プレイヤー当たり若干のビューID以上を決して必要としないことに留意することは 重要です。
 さらに必要な場合は、何か間違っているかもしれません!
 PhotonViewとIDを全ての弾丸に割り当てるのは、極めて非効率的です。
 その代わりに、プレイヤーまたは武器のPhotonViewを経由して、あなたが発砲した弾丸を追跡してください。

 日本語としてちと怪しいですが、全ての弾にPhotonViewをアタッチするのは、あまりよくなさそうです。

 そのため弾のGameObjectにはPhotoViewはアタッチせずに、発射タイミングをRPC()で同期させ、各プレイヤーのゲーム内にてGameObject.Instantiate()を使って射撃を同期させましょう。

RPC()を使って射撃を同期させてみよう

 プレイヤーキャラクターを制御するコンポーネント、PlayerCharacterControllerに射撃を行う処理を記述してみましょう。
 PlayerCharacterControllerのアタッチされたGameObjectは、各プレイヤーがPhotonNetwork.Instantiate()を使って生成します。そのためPhotonViewを所持しています。
 一定の時間ごとに、PhotonView.RPC()を用いて、各プレイヤーに射撃を行うことを通知します。

 このRPC()を呼ぶ際に、どんな情報を渡せばいいでしょうか。

 まず発射座標を通知する必要があります。
 キャラクターの座標はPhotonTransformViewを使って同期を取っています。
 現在のキャラクターの座標から発射させる場合、座標が通信の都合上ずれてしまう可能性が高いです。
 RPC()の引数で発射座標を渡してあげた方が、各ゲーム間のずれが少なくなります。

 そして発射方向です。
 方向は座標以上に大切です。
 角度が少しずれるだけで着弾地点が大幅にずれてしまいます。
 発射角度もRPC()の引数で渡してあげましょう。

f:id:urahimono:20161004081328g:plain

 RPC()でシリアライズ可能な構造体として、Vector3Quaternionも登録さていますので、transform.positiontransform.rotationを渡してあげれば、座標と方向を各プレイヤーのゲームに渡すことができます。

 ここで今回作成しているゲームのスクリーンショットをご覧ください。

f:id:urahimono:20161004080930p:plain

 トップビューの見下ろしたゲーム画面になっています。
 そのためゲーム中ではY座標は使用せず、キャラクターのY軸座標は固定で、方向もY軸を回転軸にした方向のみです。
 通信で情報を渡す以上、渡すデータ量は少ないに越したことはありません。
 transform.positiontransform.rotationを直接渡すのではなく、座標はXZ軸の値のみを、方向もY軸の回転値のみを渡すことで軽量化させましょう。

using UnityEngine;
using System.Collections;

public class PlayerCharacterController : MonoBehaviour
{
    [SerializeField]
    private float   m_speed             = 3.0f;
    [SerializeField]
    private float   m_reloadTime        = 1.0f;
    [SerializeField]
    private string  m_bulletAsset       = null;

    private PhotonView  m_photonView    = null;

    void Awake()
    {
        m_photonView = GetComponent<PhotonView>();
    }

    void Start()
    {
        if( m_photonView.isMine )
        {
            StartCoroutine( ShootProcess( m_reloadTime ) );
        }
    }

    void Update()
    {
        if( !m_photonView.isMine )
        {
            return;
        }

        Move();
    }

    private void Move()
    {
        float horizontal = Input.GetAxis( "Horizontal" );
        float vertical = Input.GetAxis( "Vertical" );

        if( Mathf.Abs( horizontal ) < Mathf.Epsilon &&
            Mathf.Abs( vertical ) < Mathf.Epsilon )
        {
            return;
        }

        var dir = new Vector3( horizontal, 0.0f, vertical );
        transform.forward = dir.normalized;
        transform.position += dir * m_speed * Time.deltaTime;
    }

    private IEnumerator ShootProcess( float i_time )
    {
        var wait = new WaitForSeconds( i_time );

        // 一定時間ごとに自動で弾を発射する.
        while( true )
        {
            yield return wait;

            // RPCで送る情報量を少なくするため、座標値にはxzのみ、回転値はy軸の回転角度のみ渡す.
            Vector2 posXZ = new Vector2( transform.position.x, transform.position.z );
            float angle = transform.eulerAngles.y;

            m_photonView.RPC( "Shoot", PhotonTargets.AllViaServer, posXZ, angle );
        }
    }

    [PunRPC]
    private void Shoot( Vector2 i_pos, float i_angle )
    {
        if( string.IsNullOrEmpty( m_bulletAsset ) )
        {
            Debug.LogWarning( "missing bullet asset!" );
            return;
        }

        Vector3 pos = new Vector3( i_pos.x, 0.0f, i_pos.y );
        Quaternion rot = Quaternion.Euler( 0.0f, i_angle, 0.0f );
        GameObject.Instantiate( Resources.Load( m_bulletAsset ), pos, rot );
    }

} // class PlayerCharacterController

f:id:urahimono:20161004080955g:plain

 生成する弾のGameObjectに移動処理用のコンポーネントをアタッチしていないため、ただ本を地面に置いているみたいになっていますが、座標と方向はあってそうです。

 それでは弾(本)の移動用スクリプトを作成しましょう。
 これはRigidbodyを使って移動させる簡単なものです。

[RequireComponent( typeof( Rigidbody ) )]
public class BookBullet : MonoBehaviour
{
    [SerializeField]
    float m_moveForce   = 0.0f;
    public float MoveForce
    {
        get
        {
            return m_moveForce;
        }
        set
        {
            m_moveForce = value;
        }
    }

    [SerializeField]
    private float m_destroyedTime   = 3.0f;

    private Rigidbody   m_rigidbody = null;


    void Awake()
    {
        m_rigidbody = GetComponent<Rigidbody>();
    }

    void Start()
    {
        m_rigidbody.AddForce( transform.forward * MoveForce );
        
        // 一定時間経過したら、オブジェクトを破棄する.
        GameObject.Destroy( gameObject, m_destroyedTime );
    }

    void OnTriggerEnter( Collider i_other )
    {
        GameObject.Destroy( gameObject );
    }
} // class BookBullet

f:id:urahimono:20161004081024g:plain

 自分とは別のプレイヤーの弾も、いい感じ発射されています。
 とりあえず弾の発射はOKそうです。
 次に当たり判定の処理を記述していきましょう。

僕の僕は当ててもいいけど、君の僕は当てては駄目だ

 では射撃対象用の敵コンポーネントを作成しましょう。
 つくりとしては、弾のコンポーネントを持つGameObjectに当たるとダメージを受けるというものです。
 ダメージはとりあえず1で固定させておきましょう。
 敵のオブジェクトはPhotonNetwork.InstantiateSceneObject()で生成し、ダメージを受けたらRPC()で同期を取ります。

public class Enemy : MonoBehaviour
{
    private PhotonView  m_photonView    = null;

    private int m_life  = 100;
    public int Life
    {
        get
        {
            return m_life;
        }
        set
        {
            m_life = value;
        }
    }

    void Awake()
    {
        m_photonView    = GetComponent<PhotonView>();
    }

    void OnTriggerEnter( Collider i_other )
    {
        var bullet = i_other.gameObject.GetComponent<BookBullet>();
        if( bullet != null )
        {
            m_photonView.RPC( "Damage", PhotonTargets.AllViaServer, 1 );
        }
    }

    [PunRPC]
    private void Damage( int i_damage )
    {
        m_life  -= i_damage;
    }
} // class Enemy

f:id:urahimono:20161004081111g:plain

 これはおかしい!
 敵に攻撃しているのはPlayer1のキャラクターだけなのに、敵の体力が2ずつ減っていっています。
 スクリプトをみても、Damage()関数に渡しているのは紛れもなく1です。

 これはPlayer1,2の両方のゲーム内で敵にダメージを与えているからです。
 Player1のゲーム内のPlayer1の攻撃と、Player2のゲーム内のPlayer1の攻撃の両方が有効になってしまっているのです。
 これが3人のプレイヤーで遊んでいた場合は、Player1のゲーム内のPlayer1の攻撃と、Player2のゲーム内のPlayer1の攻撃と、Player3のゲーム内のPlayer1の攻撃が有効になってしまいます。
 これが4人のプレイヤーで遊んでいた場合は、Player1のゲーム内のPlayer1の攻撃と、Player2のゲーム内のPlayer1の攻撃と、Player3のゲーム内のPlayer1の攻撃と、Player4のゲーム内のPlayer1の攻撃が有効になってしまいます。
 これが5人のプレイヤーだと……きっと大変なことになります・

 PhotonViewがアタッチされているならばisMineで判定することできるのですが、今回はPhotonViewがないため、isMineのようなものを自作する必要があります。

public class BookBullet : MonoBehaviour
{
    public bool IsMine
    {
        get;
        private set;
    }

    public void Initialize( bool i_isMine )
    {
        IsMine  = i_isMine;
    }
} // class BookBullet

 RPC()を呼ぶ際にPlayerIDを渡して、自分自身のPlayerIDと比べてisMine値を設定しましょう。

public class PlayerCharacterController : MonoBehaviour
{
    private IEnumerator ShootProcess( float i_time )
    {
        var wait    = new WaitForSeconds( i_time );

        while( true )
        {
            yield return wait;

            // RPCで送る情報量を少なくするため、座標値にはxzのみ、回転値はy軸の回転角度のみ渡す.
            Vector2 posXZ   = new Vector2( transform.position.x, transform.position.z );
            float   angle   = transform.eulerAngles.y;

            m_photonView.RPC( "Shoot", PhotonTargets.AllViaServer, posXZ, angle, PhotonNetwork.player.ID );
        }
    }

    [PunRPC]
    private void Shoot( Vector2 i_pos, float i_angle, int i_playerID )
    {
        if( string.IsNullOrEmpty( m_bulletAsset ) )
        {
            Debug.LogWarning( "missing bullet asset!" );
            return;
        }

        Vector3     pos = new Vector3( i_pos.x, 0.0f, i_pos.y );
        Quaternion  rot = Quaternion.Euler( 0.0f, i_angle, 0.0f );

        var obj     = GameObject.Instantiate( Resources.Load( m_bulletAsset ), pos, rot ) as GameObject;
        var bullet  = obj.GetComponent< BookBullet >();
        bullet.Initialize( i_playerID == PhotonNetwork.player.ID );
    }
} // public class PlayerCharacterController
public class Enemy : MonoBehaviour
{
    void OnTriggerEnter( Collider i_other )
    {
        var bullet = i_other.gameObject.GetComponent<BookBullet>();
        if( bullet != null && bullet.IsMine )
        {
            m_photonView.RPC( "Damage", PhotonTargets.AllViaServer, 1 );
        }
    }
} // public class Enemy

f:id:urahimono:20161004081234g:plain

 これで射撃をしているプレイヤーのみが攻撃できるようになりました。

 おとなしくPhotonViewを使ったほうがいいような気がしてきました……。

 次回 www.urablog.xyz

 前回 www.urablog.xyz