Mirror Example - Pong

3 minute read



Pong Example

Assets > Mirror > Example > Pong > Scenes > Scene

고전 게임 Pong을 유니티 엔진으로 구현한 게임이다.

W, S (UpArrow, DownArrow) 로 움직인다.


Hierarchy

hierarchy


  • NetworkManager

    Network Manager를 상속한 Network Manager Pong 스크립트를 가지고 있다.

    Pong은 2인용 게임이기 때문에 Max Connections 를 2로 해준다.

    Player Prefab에는 플레이어인 Racket 프리팹이 등록되어있고 Registered Spawnable Prefabs에는 공 Ball 프리팹이 등록되어있다.

  • Table

    게임의 맵이다.

  • RacketSpawnLeft/RacketSpawnRight

    플레이어 Spawn 지점이다.

게임을 플레이하면 플레이어(Racket)이 생성된다.

hierarchy


Network Manager Pong Script

NetworkManager를 상속받은 스크립트로 게임의 전반적인 네트워크 동작을 구현한다.

플레이어의 스폰지점인 leftRacketSpawn, rightRacketSpawn 을 참조하여 플레이어를 Spawn한다.

그리고 게임을 플레이하는데 중요한 Ball 오브젝트의 생성도 처리한다.

using UnityEngine;

/*
	Documentation: https://mirror-networking.gitbook.io/docs/components/network-manager
	API Reference: https://mirror-networking.com/docs/api/Mirror.NetworkManager.html
*/

namespace Mirror.Examples.Pong
{
    // Custom NetworkManager that simply assigns the correct racket positions when
    // spawning players. The built in RoundRobin spawn method wouldn't work after
    // someone reconnects (both players would be on the same side).
    [AddComponentMenu("")]
    public class NetworkManagerPong : NetworkManager
    {
        public Transform leftRacketSpawn;
        public Transform rightRacketSpawn;
        GameObject ball;

OnServerAddPlayer는 서버에 클라이언트가 추가될 때마다 호출된다.

클라이언트가 참가를하게되면 해당 플레이어를 생성한다. 생성 위치는 left, right Spawn지점으로 처음 입장시 leftRacketSpawn으로 생성한다.

NetworkServer.AddPlayerForConnection은 AddPlayer 메시지를 서버에 보낸 후 서버에서 호출하여 연결 플레이어를 추가한다.

연결을 위한 플레이어가 추가될 때 해당 클라이언트는 자동으로 Ready 상태가 된다. 플레이어 오브젝트는 자동으로 Spawn되기 때문에 NetworkServer.Spawn을 호출할 필요가 없다.

만약 해당 playerControllerId의 플레이어가 이미 존재한다면 이 동작은 실패하게된다. 따라서 플레이어를 교체하는 작업으로 사용할 수 없다. (이 경우 replace 사용)

        public override void OnServerAddPlayer(NetworkConnectionToClient conn)
        {
            // add player at correct spawn position
            Transform start = numPlayers == 0 ? leftRacketSpawn : rightRacketSpawn;
            GameObject player = Instantiate(playerPrefab, start.position, start.rotation);
            NetworkServer.AddPlayerForConnection(conn, player);


플레이어가 2명이 되면 ball을 네트워크 상에서 생성한다.

            // spawn ball if two players
            if (numPlayers == 2)
            {
                ball = Instantiate(spawnPrefabs.Find(prefab => prefab.name == "Ball"));
                NetworkServer.Spawn(ball);
            }
        }


OnServerDisconnect는 서버의 연결이 해제되면 호출된다.

ball을 파괴하고 base.OnServerDisconnect를 호출한다.

        public override void OnServerDisconnect(NetworkConnectionToClient conn)
        {
            // destroy ball
            if (ball != null)
                NetworkServer.Destroy(ball);

            // call base functionality (actually destroys the player)
            base.OnServerDisconnect(conn);
        }
    }
}


Player Script

NetworkBehaviour를 상속한다.

플레이어의 움직임을 관리한다.

using UnityEngine;

namespace Mirror.Examples.Pong
{
    public class Player : NetworkBehaviour
    {
        public float speed = 30;
        public Rigidbody2D rigidbody2d;

FixedUpdate에서 로컬 플레이어를 검사하여 움직임을 처리한다. rigidbody2d의 velocity값을 조절하는 방식으로 움직인다.

        // need to use FixedUpdate for rigidbody
        void FixedUpdate()
        {
            // only let the local player control the racket.
            // don't control other player's rackets
            if (isLocalPlayer)
                rigidbody2d.velocity = new Vector2(0, Input.GetAxisRaw("Vertical")) * speed * Time.fixedDeltaTime;
        }
    }
}


Ball Script

NetworkBehaviour를 상속한다.

ball이 생성되었을 때 그리고 플레이어와 충돌을 처리한다.

using UnityEngine;

namespace Mirror.Examples.Pong
{
    public class Ball : NetworkBehaviour
    {
        public float speed = 30;
        public Rigidbody2D rigidbody2d;

OnStartServer가 호출되면 base.OnStartServer를 호출하고 rigidbody2d.simulated 옵션을 true로 한다.

Simulated true

  • 시뮬레이션을 통해 Rigidbody2D 이동(중력 및 물리력 적용)한다.

  • 부착된 모든 Collider2D는 계속해서 새로운 접촉점이 생성되고 지속적으로 접촉점을 검사해야한다.

  • 부착된 모든 Joint2D를 Simulated하고 Rigidbody2D부착을 강요한다.

  • 리지드바디 2D, 충돌기 2D 및 조인트 2D의 모든 내부 물리학 물체가 메모리에 남아 있음

Simulated false

  • 강체 2D는 시뮬레이션에 의해 움직이지 않는다(중력 및 물리력은 적용되지 않음)

  • 강체 2D는 새로운 접점을 만들지 않으며, 부착된 모든 충돌체 2D 접점은 파괴된다.

  • 부착된 조인트 2D는 시뮬레이션되지 않으며 부착된 강체 2Ds를 구속하지 않는다.

  • 리지드바디 2D, 충돌기 2D 및 조인트 2D의 모든 내부 물리학 물체는 메모리에 남아 있다.

        public override void OnStartServer()
        {
            base.OnStartServer();

            // only simulate ball physics on server
            rigidbody2d.simulated = true;

            // Serve the ball from left player
            rigidbody2d.velocity = Vector2.right * speed;
        }

        float HitFactor(Vector2 ballPos, Vector2 racketPos, float racketHeight)
        {
            // ascii art:
            // ||  1 <- at the top of the racket
            // ||
            // ||  0 <- at the middle of the racket
            // ||
            // || -1 <- at the bottom of the racket
            return (ballPos.y - racketPos.y) / racketHeight;
        }

        // only call this on server
        [ServerCallback]
        void OnCollisionEnter2D(Collision2D col)
        {
            // Note: 'col' holds the collision information. If the
            // Ball collided with a racket, then:
            //   col.gameObject is the racket
            //   col.transform.position is the racket's position
            //   col.collider is the racket's collider

            // did we hit a racket? then we need to calculate the hit factor
            if (col.transform.GetComponent<Player>())
            {
                // Calculate y direction via hit Factor
                float y = HitFactor(transform.position,
                                    col.transform.position,
                                    col.collider.bounds.size.y);

                // Calculate x direction via opposite collision
                float x = col.relativeVelocity.x > 0 ? 1 : -1;

                // Calculate direction, make length=1 via .normalized
                Vector2 dir = new Vector2(x, y).normalized;

                // Set Velocity with dir * speed
                rigidbody2d.velocity = dir * speed;
            }
        }
    }
}