Merry Fragmas 3.0: UI, Graphics, and Animations

確認済のバージョン: 5.5

-

難易度: 中級

In this multi-day session we will be building a Networked Multiplayer First Person Shooter game from the ground up. Session number two adds our game UI as well and better character models and networked animations. In the final session we will be adding player information, a lobby, and more game features. Spread holiday fear!

Merry Fragmas 3.0: UI, Graphics, and Animations

中級 Multiplayer Networking

GunPositionSync

Code snippet

using UnityEngine;
using UnityEngine.Networking;

public class GunPositionSync : NetworkBehaviour
{
    [SerializeField] Transform cameraTransform;
    [SerializeField] Transform handMount;
    [SerializeField] Transform gunPivot;
    [SerializeField] Transform rightHandHold;
    [SerializeField] Transform leftHandHold;
    [SerializeField] float threshold = 10f;
    [SerializeField] float smoothing = 5f;

    [SyncVar] float pitch;
    Vector3 lastOffset;
    float lastSyncedPitch;
    Animator anim;

    void Start()
    {
        anim = GetComponent<Animator> ();

        if (isLocalPlayer)
            gunPivot.parent = cameraTransform;
        else
            lastOffset = handMount.position - transform.position;
    }

    void Update()
    {
        if (isLocalPlayer) 
        {
            pitch = cameraTransform.localRotation.eulerAngles.x;
            if (Mathf.Abs (lastSyncedPitch - pitch) >= threshold) 
            {
                CmdUpdatePitch (pitch);
                lastSyncedPitch = pitch;
            }
        } 
        else 
        {
            Quaternion newRotation = Quaternion.Euler (pitch, 0f, 0f);

            Vector3 currentOffset = handMount.position - transform.position;
            gunPivot.localPosition += currentOffset - lastOffset;
            lastOffset = currentOffset;

            gunPivot.localRotation = Quaternion.Lerp (gunPivot.localRotation, 
                newRotation, Time.deltaTime * smoothing);
        }
    }

    [Command]
    void CmdUpdatePitch(float newPitch)
    {
        pitch = newPitch;
    }

    void OnAnimatorIK()
    {
        if (!anim)
            return;

        anim.SetIKPositionWeight (AvatarIKGoal.RightHand, 1f);
        anim.SetIKRotationWeight (AvatarIKGoal.RightHand, 1f);
        anim.SetIKPosition (AvatarIKGoal.RightHand, rightHandHold.position);
        anim.SetIKRotation (AvatarIKGoal.RightHand, rightHandHold.rotation);

        anim.SetIKPositionWeight (AvatarIKGoal.LeftHand, 1f);
        anim.SetIKRotationWeight (AvatarIKGoal.LeftHand, 1f);
        anim.SetIKPosition (AvatarIKGoal.LeftHand, leftHandHold.position);
        anim.SetIKRotation (AvatarIKGoal.LeftHand, leftHandHold.rotation);
    }
}

Player

Code snippet

using UnityEngine;
using UnityEngine.Networking;
using UnityEngine.Events;

[System.Serializable]
public class ToggleEvent : UnityEvent<bool>{}

public class Player : NetworkBehaviour 
{
    [SerializeField] ToggleEvent onToggleShared;
    [SerializeField] ToggleEvent onToggleLocal;
    [SerializeField] ToggleEvent onToggleRemote;
    [SerializeField] float respawnTime = 5f;

    GameObject mainCamera;
    NetworkAnimator anim;

    void Start()
    {
        anim = GetComponent<NetworkAnimator> ();
        mainCamera = Camera.main.gameObject;

        EnablePlayer ();
    }

    void Update()
    {
        if (!isLocalPlayer)
            return;

        anim.animator.SetFloat ("Speed", Input.GetAxis ("Vertical"));
        anim.animator.SetFloat ("Strafe", Input.GetAxis ("Horizontal"));
    }

    void DisablePlayer()
    {
        if (isLocalPlayer) 
        {
            PlayerCanvas.canvas.HideReticule ();
            mainCamera.SetActive (true);
        }

        onToggleShared.Invoke (false);

        if (isLocalPlayer)
            onToggleLocal.Invoke (false);
        else
            onToggleRemote.Invoke (false);
    }

    void EnablePlayer()
    {
        if (isLocalPlayer) 
        {
            PlayerCanvas.canvas.Initialize ();
            mainCamera.SetActive (false);
        }

        onToggleShared.Invoke (true);

        if (isLocalPlayer)
            onToggleLocal.Invoke (true);
        else
            onToggleRemote.Invoke (true);
    }

    public void Die()
    {
        if (isLocalPlayer) 
        {
            PlayerCanvas.canvas.WriteGameStatusText ("You Died!");
            PlayerCanvas.canvas.PlayDeathAudio ();

            anim.SetTrigger ("Died");
        }

        DisablePlayer ();

        Invoke ("Respawn", respawnTime);
    }

    void Respawn()
    {
        if (isLocalPlayer) 
        {
            Transform spawn = NetworkManager.singleton.GetStartPosition ();
            transform.position = spawn.position;
            transform.rotation = spawn.rotation;

            anim.SetTrigger ("Restart");
        }

        EnablePlayer ();
    }
}

PlayerCanvas

Code snippet

using UnityEngine;
using UnityEngine.UI;

public class PlayerCanvas : MonoBehaviour 
{
    public static PlayerCanvas canvas;

    [Header("Component References")]
    [SerializeField] Image reticule;
    [SerializeField] UIFader damageImage;
    [SerializeField] Text gameStatusText;
    [SerializeField] Text healthValue;
    [SerializeField] Text killsValue;
    [SerializeField] Text logText;
    [SerializeField] AudioSource deathAudio;

    //Ensure there is only one PlayerCanvas
    void Awake()
    {
        if (canvas == null)
            canvas = this;
        else if (canvas != this)
            Destroy (gameObject);
    }

    //Find all of our resources
    void Reset()
    {
        reticule = GameObject.Find ("Reticule").GetComponent<Image> ();
        damageImage = GameObject.Find ("DamagedFlash").GetComponent<UIFader> ();
        gameStatusText = GameObject.Find ("GameStatusText").GetComponent<Text> ();
        healthValue = GameObject.Find ("HealthValue").GetComponent<Text> ();
        killsValue = GameObject.Find ("KillsValue").GetComponent<Text> ();
        logText = GameObject.Find ("LogText").GetComponent<Text> ();
        deathAudio = GameObject.Find ("DeathAudio").GetComponent<AudioSource> ();
    }

    public void Initialize()
    {
        reticule.enabled = true;
        gameStatusText.text = "";
    }

    public void HideReticule()
    {
        reticule.enabled = false;
    }

    public void FlashDamageEffect()
    {
        damageImage.Flash ();
    }

    public void PlayDeathAudio()
    {
        if (!deathAudio.isPlaying)
            deathAudio.Play ();
    }

    public void SetKills(int amount)
    {
        killsValue.text = amount.ToString ();
    }

    public void SetHealth(int amount)
    {
        healthValue.text = amount.ToString ();
    }

    public void WriteGameStatusText(string text)
    {
        gameStatusText.text = text;
    }

    public void WriteLogText(string text, float duration)
    {
        CancelInvoke ();
        logText.text = text;
        Invoke ("ClearLogText", duration);
    }

    void ClearLogText()
    {
        logText.text = "";
    }
}

PlayerHealth

Code snippet

using UnityEngine;
using UnityEngine.Networking;

public class PlayerHealth : NetworkBehaviour 
{
    [SerializeField] int maxHealth = 3;

    [SyncVar (hook = "OnHealthChanged")] int health;

    Player player;


    void Awake()
    {
        player = GetComponent<Player> ();
    }

    [ServerCallback]
    void OnEnable()
    {
        health = maxHealth;
    }

    [Server]
    public bool TakeDamage()
    {
        bool died = false;

        if (health <= 0)
            return died;

        health--;
        died = health <= 0;

        RpcTakeDamage (died);

        return died;
    }

    [ClientRpc]
    void RpcTakeDamage(bool died)
    {
        if (isLocalPlayer)
            PlayerCanvas.canvas.FlashDamageEffect ();

        if (died)
            player.Die ();
    }

    void OnHealthChanged(int value)
    {
        health = value;
        if (isLocalPlayer)
            PlayerCanvas.canvas.SetHealth (value);
    }
}

PlayerShooting

Code snippet

using UnityEngine;
using UnityEngine.Networking;

public class PlayerShooting : NetworkBehaviour 
{
    [SerializeField] float shotCooldown = .3f;
    [SerializeField] Transform firePosition;
    [SerializeField] ShotEffectsManager shotEffects;

    [SyncVar (hook = "OnScoreChanged")] int score;

    float ellapsedTime;
    bool canShoot;

    void Start()
    {
        shotEffects.Initialize ();

        if (isLocalPlayer)
            canShoot = true;
    }

    [ServerCallback]
    void OnEnable()
    {
        score = 0;
    }

    void Update()
    {
        if (!canShoot)
            return;

        ellapsedTime += Time.deltaTime;

        if (Input.GetButtonDown ("Fire1") && ellapsedTime > shotCooldown) 
        {
            ellapsedTime = 0f;
            CmdFireShot (firePosition.position, firePosition.forward);
        }
    }

    [Command]
    void CmdFireShot(Vector3 origin, Vector3 direction)
    {
        RaycastHit hit;

        Ray ray = new Ray (origin, direction);
        Debug.DrawRay (ray.origin, ray.direction * 3f, Color.red, 1f);

        bool result = Physics.Raycast (ray, out hit, 50f);

        if (result) 
        {
            PlayerHealth enemy = hit.transform.GetComponent<PlayerHealth> ();

            if (enemy != null) 
            {
                bool wasKillShot = enemy.TakeDamage ();

                if (wasKillShot)
                    score++;
            }
        }

        RpcProcessShotEffects (result, hit.point);
    }

    [ClientRpc]
    void RpcProcessShotEffects(bool playImpact, Vector3 point)
    {
        shotEffects.PlayShotEffects ();

        if (playImpact)
            shotEffects.PlayImpactEffect (point);
    }

    void OnScoreChanged(int value)
    {
        score = value;
        if (isLocalPlayer)
            PlayerCanvas.canvas.SetKills (value);
    }
}

RendererToggler

Code snippet

using UnityEngine;

public class RendererToggler : MonoBehaviour 
{
    [SerializeField] float turnOnDelay = .1f;
    [SerializeField] float turnOffDelay = 3.5f;
    [SerializeField] bool enabledOnLoad = false;

    Renderer[] renderers;

    void Awake () 
    {
        renderers = GetComponentsInChildren<Renderer> (true); 

        if (enabledOnLoad)
            EnableRenderers ();
        else
            DisableRenderers ();
    }

    //Method used by our Unity events to show and hide the player
    public void ToggleRenderersDelayed(bool isOn)
    {
        if (isOn)
            Invoke ("EnableRenderers", turnOnDelay);
        else
            Invoke ("DisableRenderers", turnOffDelay);
    }

    public void EnableRenderers()
    {
        for (int i = 0; i < renderers.Length; i++) 
        {
            renderers [i].enabled = true;
        }
    }

    public void DisableRenderers()
    {
        for (int i = 0; i < renderers.Length; i++) 
        {
            renderers [i].enabled = false;
        }
    }

    //Will be used to change the color of the players for different options
    public void ChangeColor(Color newColor)
    {
        for (int i = 0; i < renderers.Length; i++) 
        {
            renderers [i].material.color = newColor;
        }
    }
}

UIFader

Code snippet

using UnityEngine;
using System.Collections;

// This class is used to fade in and out groups of UI
// elements.  It contains a variety of functions for
// fading in different ways.
[RequireComponent(typeof(CanvasGroup))]
public class UIFader : MonoBehaviour
{
    [SerializeField] float fadeSpeed = 1f;              // The amount the alpha of the UI elements changes per second.
    [SerializeField] CanvasGroup groupToFade;           // All the groups of UI elements that will fade in and out.
    [SerializeField] bool startVisible;                 // Should the UI elements be visible to start?
    [SerializeField] bool startWithFade;                // Should the UI elements begin fading with they start up? Fading can either be in or out (opposite of their starting alpha)

    bool visible;           // Whether the UI elements are currently visible.


    void Reset()
    {
        //Attempt to grab the CanvasGroup on this object
        groupToFade = GetComponent<CanvasGroup>();
    }

    void Start()
    {
        //If the object should start visible, set it to be visible. Otherwise, set it invisible
        if (startVisible)
            SetVisible ();
        else
            SetInvisible ();

        //If there shouldn't be any initial fade, leave this method
        if (!startWithFade)
            return;

        //If the object is currently visible, fade out. Otherwise fade in
        if (visible)
            StartFadeOut ();
        else
            StartFadeIn ();
    }

    //Publicly accessible methods for fading in or fading out without needing to start a 
    //coroutine. These are needed in order for UI events (like buttons) to start a fade in
    //or out.
    public void StartFadeIn()
    {
        StartCoroutine (FadeIn ());
    }

    public void StartFadeOut()
    {
        StartCoroutine (FadeOut ());
    }

    // These functions are used if fades are required to be instant.
    public void SetVisible ()
    {
        groupToFade.alpha = 1f;
        visible = true;
    }


    public void SetInvisible ()
    {
        groupToFade.alpha = 0f;
        visible = false;
    }

    IEnumerator FadeIn()
    {
        // Fading needs to continue until the group is completely faded in
        while (groupToFade.alpha < 1f) 
        {
            //Increase the alpha
            groupToFade.alpha += fadeSpeed * Time.deltaTime;
            //Wait a frame
            yield return null;
        }

        // Since everthing has faded in now, it is visible.
        visible = true;
    }

    IEnumerator FadeOut ()
    {
        while (groupToFade.alpha > 0f) 
        {
            groupToFade.alpha -= fadeSpeed * Time.deltaTime;

            yield return null;
        }

        visible = false;
    }

    public void Flash()
    {
        StartCoroutine (ProcessFlash ());
    }

    IEnumerator ProcessFlash()
    {
        yield return StartCoroutine (FadeIn ());
        yield return StartCoroutine (FadeOut ());
    }
}