OpenXR开发实战项目之VR Beginner

一、框架视图


二、关键代码

XRExclusiveSocketInteractor


using UnityEngine;

using UnityEngine.XR.Interaction.Toolkit;

/// <summary>

/// Subclass of the classic Socket Interactor from the Interaction toolkit that will only accept object with the right

/// SocketTarget

/// </summary>

public class XRExclusiveSocketInteractor : XRSocketInteractor

{

    public string AcceptedType;

    public override bool CanSelect(XRBaseInteractable interactable)

    {

        SocketTarget socketTarget = interactable.GetComponent<SocketTarget>();

        if (socketTarget == null)

            return false;

        return base.CanSelect(interactable) && (socketTarget.SocketType == AcceptedType);

    }

    public override bool CanHover(XRBaseInteractable interactable)

    {

        return CanSelect(interactable);

    }

}

CauldronContent


using System;

using System.Collections;

using System.Collections.Generic;

using UnityEngine;

using UnityEngine.Events;

using UnityEngine.VFX;

using Random = UnityEngine.Random;

/// <summary>

/// Handle all operations related to dropping thing in the cauldron and brewing things.

/// </summary>

public class CauldronContent : MonoBehaviour

{

    [System.Serializable]

    public class Recipe

    {

        public string name;

        public string[] ingredients;

        public int temperature;

        public int rotation;

    }

    [System.Serializable]

    public class BrewEvent : UnityEvent<Recipe> { };



    public Recipe[] Recipes;

    public int TemperatureIncrement;

    [Header("Effects")]

    public GameObject SplashEffect;

    public Animator CauldronAnimator;



    private VisualEffect splashVFX;

    public VisualEffect brewEffect;

    /// <summary>

    /// Will be called when the cauldron finish brewing, with the recipe as parameters or null if no recipe match.

    /// </summary>

    public BrewEvent OnBrew;

    [Header("Audio")]

    public AudioSource AmbientSoundSource;

    public AudioSource BrewingSoundSource;

    public AudioClip[] SplashClips;



    bool m_CanBrew = false;



    List<string> m_CurrentIngredientsIn = new List<string>();

    int m_Temperature = 0;

    int m_Rotation = -1;

    float m_StartingVolume;



    private CauldronEffects m_CauldronEffect;

    private void Start()

    {

        m_CauldronEffect = GetComponent<CauldronEffects>();

        splashVFX = SplashEffect.GetComponent<VisualEffect>();

        m_StartingVolume = AmbientSoundSource.volume;

        AmbientSoundSource.volume = m_StartingVolume * 0.2f;

    }

    void OnTriggerEnter(Collider other)

    {

        CauldronIngredient ingredient = other.attachedRigidbody.GetComponentInChildren<CauldronIngredient>();

        Vector3 contactPosition = other.attachedRigidbody.gameObject.transform.position;

        contactPosition.y = gameObject.transform.position.y;

        SplashEffect.transform.position = contactPosition;



        SFXPlayer.Instance.PlaySFX(SplashClips[Random.Range(0, SplashClips.Length)], contactPosition, new SFXPlayer.PlayParameters()

        {

            Pitch = Random.Range(0.8f, 1.2f),

            SourceID = 17624,

            Volume = 1.0f

        }, 0.2f, true);

        splashVFX.Play();

        RespawnableObject respawnableObject = ingredient;

        if (ingredient != null)

        {

            m_CurrentIngredientsIn.Add(ingredient.IngredientType);

        }

        else

        {

            //added an object that is not an ingredient, it will make automatically fail any recipe

            m_CurrentIngredientsIn.Add("INVALID");

            respawnableObject = other.attachedRigidbody.GetComponentInChildren<RespawnableObject>();

        }

        if (respawnableObject != null)

        {

            respawnableObject.Respawn();

        }

        else

        {

            Destroy(other.attachedRigidbody.gameObject, 0.5f);

        }

    }

    public void ChangeTemperature(int step)

    {

        m_Temperature = TemperatureIncrement * step;

        m_CauldronEffect.SetBubbleIntensity(step);

    }

    public void ChangeRotation(int step)

    {

        m_Rotation = step - 1;

        m_CauldronEffect.SetRotationSpeed(m_Rotation);

    }

    public void Brew()

    {

        if(!m_CanBrew)

            return;

        brewEffect.SendEvent("StartLongSpawn");

        CauldronAnimator.SetTrigger("Brew");



        Recipe recipeBewed = null;

        foreach (Recipe recipe in Recipes)

        {

            if(recipe.temperature != m_Temperature || recipe.rotation != m_Rotation)

                continue;

            List<string> copyOfIngredient = new List<string>(m_CurrentIngredientsIn);

            int ingredientCount = 0;

            foreach (var ing in recipe.ingredients)

            {

                if (copyOfIngredient.Contains(ing))

                {

                    ingredientCount += 1;

                    copyOfIngredient.Remove(ing);

                }

            }

            if (ingredientCount == recipe.ingredients.Length)

            {

                recipeBewed = recipe;

                break;

            }

        }

        ResetCauldron();

        StartCoroutine(WaitForBrewCoroutine(recipeBewed));

    }

    IEnumerator WaitForBrewCoroutine(Recipe recipe)

    {

        BrewingSoundSource.Play();

        AmbientSoundSource.volume = m_StartingVolume * 0.2f;

        m_CanBrew = false;

        yield return new WaitForSeconds(3.0f);

        brewEffect.SendEvent("EndLongSpawn");

        CauldronAnimator.SetTrigger("Open");

        BrewingSoundSource.Stop();



        OnBrew.Invoke(recipe);

        m_CanBrew = true;

        AmbientSoundSource.volume = m_StartingVolume;

    }

    void ResetCauldron()

    {

        m_CurrentIngredientsIn.Clear();       

    }

    public void Open()

    {

        CauldronAnimator.SetTrigger("Open");

        m_CanBrew = true;

        AmbientSoundSource.volume = m_StartingVolume;

    }

}

EndingSequence


using System.Collections;

using System.Collections.Generic;

using UnityEngine;

using UnityEngine.SceneManagement;

using UnityEngine.Serialization;

public class EndingSequence : MonoBehaviour

{

    [FormerlySerializedAs("meshRenderer")]

    public MeshRenderer MeshRenderer;



    MaterialPropertyBlock m_Block;

    Color m_ColorSphere;

    // Start is called before the first frame update

    void Start()

    {

        m_ColorSphere = new Color(1,1,1,0);

        m_Block = new MaterialPropertyBlock();

        m_Block.SetColor("_BaseColor", m_ColorSphere);

        MeshRenderer.SetPropertyBlock(m_Block);

    }

    public void StartSequence()

    {

        StartCoroutine(FadeOut());

    }

    IEnumerator FadeOut()

    {

        float alpha = m_ColorSphere.a;

        const float length = 4.0f;

        for (float t = 0.0f; t < length; t += Time.deltaTime)

        {

            Color newColor = new Color(1, 1, 1, Mathf.Lerp(alpha, 1, t / length));

            MeshRenderer.GetPropertyBlock(m_Block);

            m_Block.SetColor("_BaseColor", newColor);

            MeshRenderer.SetPropertyBlock(m_Block);

            yield return null;

        }

        SceneManager.LoadScene(SceneManager.GetActiveScene().buildIndex);

    }

}

Hourglass


using System.Collections;

using System.Collections.Generic;

using UnityEngine;

using UnityEngine.Serialization;

public class Hourglass : MonoBehaviour

{

    [FormerlySerializedAs("hourglassTop")]

    public GameObject HourglassTop;

    [FormerlySerializedAs("hourglassBottom")]

    public GameObject HourglassBottom;

    [FormerlySerializedAs("timerFill")]

    public float TimerFill = 1.0f;

    [FormerlySerializedAs("particleSand")]

    public ParticleSystem ParticleSand;

    Material m_TopMat;

    Material m_BotMat;

    // Start is called before the first frame update

    void Start()

    {

        m_TopMat = HourglassTop.GetComponent<MeshRenderer>().material;

        m_BotMat = HourglassBottom.GetComponent<MeshRenderer>().material;

    }

    // Update is called once per frame

    void Update()

    {

        if (Vector3.Dot(transform.up, Vector3.down) > 0.8 && TimerFill > 0)

        {

            TimerFill -= 0.1f * Time.deltaTime;

            ParticleSand.Play();

        }

        else if (Vector3.Dot(transform.up, Vector3.down) < -0.8 && TimerFill < 1)

        {

            TimerFill += 0.1f * Time.deltaTime;

            ParticleSand.Play();

        } else

        {

            ParticleSand.Stop();

        }

        m_TopMat.SetFloat("TimerFill", TimerFill);

        m_BotMat.SetFloat("TimerFill", TimerFill);

    }

}

MagicBallProjectile


using System.Collections;

using System.Collections.Generic;

using UnityEngine;

public class MagicBallProjectile : ProjectileBase

{

    Rigidbody m_Rigidbody;

    float m_LaunchTime = 0;

    bool m_Launched = false;

    ObjectLauncher m_Launcher;

    void Awake()

    {

        m_Rigidbody = GetComponent<Rigidbody>();

        m_Rigidbody.useGravity = false;

    }

    void Update()

    {

        if (m_Launched)

        {

            m_LaunchTime += Time.deltaTime;

            if (m_LaunchTime > 10.0f)

            {

                //trigger a collision if we reached 10s without one, to force recycle the projectile, it's now too far

                OnCollisionEnter(null);

            }

        }

    }

    public override void Launched(Vector3 direction, ObjectLauncher launcher)

    {

        //as they are pooled, they could have been already used and have previous velocity

        m_Rigidbody.velocity = Vector3.zero;

        m_Rigidbody.AddForce(direction * 200.0f);

        m_Launcher = launcher;

        m_Launched = true;

        m_LaunchTime = 0.0f;

    }

    void OnCollisionEnter(Collision other)

    {

        gameObject.SetActive(false);

        m_Launcher.ReturnProjectile(this);

        m_Launched = false;

    }

}

MagicTractorBeam


using System.Collections;

using System.Collections.Generic;

using System.Linq;

using UnityEngine;

using UnityEngine.XR.Interaction.Toolkit;

/// <summary>

/// Utility script that can pull toward the hand/controller the currently selected object through a Raycast.

/// </summary>

public class MagicTractorBeam : MonoBehaviour

{

    public LineRenderer BeamRenderer;

    public float TargetDistance = 0.05f;

    public Vector3 LocalRayAxis = new Vector3(1,0,0);

    public bool IsEnabled => BeamRenderer.gameObject.activeSelf;

    public bool IsTracting => m_TractingObject;



    XRDirectInteractor m_DirectInteractor;



    Rigidbody m_HighlightedRigidbody;

    SelectionOutline m_CurrentSelectionOutline = null;

    bool m_TractingObject;

    RaycastHit[] m_HitCache = new RaycastHit[16];



    void Start()

    {

        m_DirectInteractor = GetComponentInChildren<XRDirectInteractor>();

    }



    void Update()

    {

        if(m_CurrentSelectionOutline != null)

            m_CurrentSelectionOutline.RemoveHighlight();



        if(m_TractingObject)

            Tracting();

        else if (IsEnabled)

        {

            Vector3 worldAxis = m_DirectInteractor.transform.TransformDirection(LocalRayAxis);

            m_HighlightedRigidbody = null;

            int mask = ~0;

            mask &= ~(1 << LayerMask.NameToLayer("Hands"));

            int count = Physics.SphereCastNonAlloc(m_DirectInteractor.transform.position, 0.2f, worldAxis, m_HitCache, 15.0f, mask, QueryTriggerInteraction.Ignore);

            if (count != 0)

            {

                float closestDistance = float.MaxValue;

                float closestDistanceGrabbable = float.MaxValue;

                XRGrabInteractable closestGrababble = null;

                for (int i = 0; i < count; ++i)

                {

                    if (closestDistance > m_HitCache[i].distance)

                        closestDistance = m_HitCache[i].distance;

                    if (m_HitCache[i].rigidbody == null)

                        continue;

                    var grabbable = m_HitCache[i].rigidbody.GetComponentInChildren<XRGrabInteractable>();

                    if (grabbable != null && m_HitCache[i].distance < closestDistanceGrabbable)

                    {

                        closestDistanceGrabbable = m_HitCache[i].distance;

                        closestGrababble = grabbable;

                    }

                }

                BeamRenderer.SetPosition(1, LocalRayAxis * closestDistance);

                if (closestGrababble != null)

                {

                    var filter = closestGrababble.GetComponentInChildren<MeshFilter>();

                    if (filter != null)

                    {

                        m_HighlightedRigidbody = closestGrababble.GetComponent<Rigidbody>();

                        var outline = m_HighlightedRigidbody.GetComponentInChildren<SelectionOutline>();

                        if (outline != null)

                        {

                            m_CurrentSelectionOutline = outline;

                            m_CurrentSelectionOutline.Highlight();

                        }

                    }

                }

            }

        }

    }

    void Tracting()

    {

        Vector3 target = m_DirectInteractor.transform.position - m_DirectInteractor.transform.right * TargetDistance;

        Vector3 toTarget = target - m_HighlightedRigidbody.transform.position;



        if(toTarget.sqrMagnitude > 1.0f)

            toTarget.Normalize();



        m_HighlightedRigidbody.velocity = Vector3.zero;

        m_HighlightedRigidbody.AddForce(toTarget * 2.0f, ForceMode.VelocityChange);

    }

    public void StartTracting()

    {

        if (m_HighlightedRigidbody != null && m_DirectInteractor)

        {

            if (m_HighlightedRigidbody.TryGetComponent(out StationaryOnPull stationary))

            {

                m_HighlightedRigidbody.isKinematic = false;               

            }

            m_TractingObject = true;

        }

    }

    public void StopTracting()

    {

        if (m_HighlightedRigidbody != null)

        {

            if (!m_HighlightedRigidbody.isKinematic && (m_HighlightedRigidbody.TryGetComponent(out StationaryOnPull stationary)))

            {

                m_HighlightedRigidbody.isKinematic = true;

            }

            m_TractingObject = false;

            m_HighlightedRigidbody.velocity = Vector3.zero;

            m_HighlightedRigidbody = null;

        }



    }

    public void EnableBeam()

    {

        //can't enable beam if we have an interactor currently holding something

        if(m_DirectInteractor != null && m_DirectInteractor.isSelectActive)

            return;



        BeamRenderer.gameObject.SetActive(true);

    }

    public void DisableBeam()

    {

        BeamRenderer.gameObject.SetActive(false);

    }

}

MasterController


using System;

using System.Collections.Generic;

using UnityEngine;

using UnityEngine.SpatialTracking;

using UnityEngine.XR;

using UnityEngine.XR.Interaction.Toolkit;

/// <summary>

/// Master script that will handle reading some input on the controller and trigger special events like Teleport or

/// activating the MagicTractorBeam

/// </summary>

public class MasterController : MonoBehaviour

{

    static MasterController s_Instance = null;

    public static MasterController Instance => s_Instance;

    public XRRig Rig => m_Rig;

    [Header("Setup")]

    public bool DisableSetupForDebug = false;

    public Transform StartingPosition;

    public GameObject TeleporterParent;



    [Header("Reference")]

    public XRRayInteractor RightTeleportInteractor;

    public XRRayInteractor LeftTeleportInteractor;

    public XRDirectInteractor RightDirectInteractor;

    public XRDirectInteractor LeftDirectInteractor;

    public MagicTractorBeam RightTractorBeam;

    public MagicTractorBeam LeftTractorBeam;



    XRRig m_Rig;



    InputDevice m_LeftInputDevice;

    InputDevice m_RightInputDevice;

    XRInteractorLineVisual m_RightLineVisual;

    XRInteractorLineVisual m_LeftLineVisual;

    HandPrefab m_RightHandPrefab;

    HandPrefab m_LeftHandPrefab;



    XRReleaseController m_RightController;

    XRReleaseController m_LeftController;

    bool m_PreviousRightClicked = false;

    bool m_PreviousLeftClicked = false;

    bool m_LastFrameRightEnable = false;

    bool m_LastFrameLeftEnable = false;

    LayerMask m_OriginalRightMask;

    LayerMask m_OriginalLeftMask;



    List<XRBaseInteractable> m_InteractableCache = new List<XRBaseInteractable>(16);

    void Awake()

    {

        s_Instance = this;

        m_Rig = GetComponent<XRRig>();



    }

    void OnEnable()

    {

        InputDevices.deviceConnected += RegisterDevices;

    }

    void OnDisable()

    {

        InputDevices.deviceConnected -= RegisterDevices;

    }

    void Start()

    {

        m_RightLineVisual = RightTeleportInteractor.GetComponent<XRInteractorLineVisual>();

        m_RightLineVisual.enabled = false;

        m_LeftLineVisual = LeftTeleportInteractor.GetComponent<XRInteractorLineVisual>();

        m_LeftLineVisual.enabled = false;

        m_RightController = RightTeleportInteractor.GetComponent<XRReleaseController>();

        m_LeftController = LeftTeleportInteractor.GetComponent<XRReleaseController>();

        m_OriginalRightMask = RightTeleportInteractor.interactionLayerMask;

        m_OriginalLeftMask = LeftTeleportInteractor.interactionLayerMask;



        if (!DisableSetupForDebug)

        {

            transform.position = StartingPosition.position;

            transform.rotation = StartingPosition.rotation;



            if(TeleporterParent != null)

                TeleporterParent.SetActive(false);

        }



        InputDeviceCharacteristics leftTrackedControllerFilter = InputDeviceCharacteristics.HeldInHand | InputDeviceCharacteristics.Left;

        List<InputDevice> foundControllers = new List<InputDevice>();



        InputDevices.GetDevicesWithCharacteristics(leftTrackedControllerFilter, foundControllers);

        if (foundControllers.Count > 0)

            m_LeftInputDevice = foundControllers[0];





        InputDeviceCharacteristics rightTrackedControllerFilter = InputDeviceCharacteristics.HeldInHand | InputDeviceCharacteristics.Right;

        InputDevices.GetDevicesWithCharacteristics(rightTrackedControllerFilter, foundControllers);

        if (foundControllers.Count > 0)

            m_RightInputDevice = foundControllers[0];

        //if (m_Rig.TrackingOriginMode != TrackingOriginModeFlags.Floor)

        //    m_Rig.cameraYOffset = 1.8f;

    }

    void RegisterDevices(InputDevice connectedDevice)

    {

        if (connectedDevice.isValid)

        {

            if ((connectedDevice.characteristics & InputDeviceCharacteristics.HeldInHand) == InputDeviceCharacteristics.HeldInHand)

            {

                if ((connectedDevice.characteristics & InputDeviceCharacteristics.Left) == InputDeviceCharacteristics.Left)

                {

                    m_LeftInputDevice = connectedDevice;

                }

                else if ((connectedDevice.characteristics & InputDeviceCharacteristics.Right) == InputDeviceCharacteristics.Right)

                {

                    m_RightInputDevice = connectedDevice;

                }

            }

        }

    }



    void Update()

    {

        if(Input.GetKeyDown(KeyCode.Escape))

            Application.Quit();



        RightTeleportUpdate();

        LeftTeleportUpdate();

    }

    void RightTeleportUpdate()

    {

        Vector2 axisInput;

        m_RightInputDevice.TryGetFeatureValue(CommonUsages.primary2DAxis, out axisInput);



        m_RightLineVisual.enabled = axisInput.y > 0.5f;



      //  RightTeleportInteractor.InteractionLayerMask = m_LastFrameRightEnable ? m_OriginalRightMask : new LayerMask();



        if (axisInput.y <= 0.5f && m_PreviousRightClicked)

        {

            m_RightController.Select();

        }



        if (axisInput.y <= -0.5f)

        {

            if(!RightTractorBeam.IsTracting)

                RightTractorBeam.StartTracting();

        }

        else if(RightTractorBeam.IsTracting)

        {

            RightTractorBeam.StopTracting();

        }

        //if the right animator is null, we try to get it. It's not the best performance wise but no other way as setup

        //of the model by the Interaction Toolkit is done on the first update.

        if (m_RightHandPrefab == null)

        {

            m_RightHandPrefab = RightDirectInteractor.GetComponentInChildren<HandPrefab>();

        }

        m_PreviousRightClicked = axisInput.y > 0.5f;

        if (m_RightHandPrefab != null)

        {

            m_RightHandPrefab.Animator.SetBool("Pointing", m_PreviousRightClicked);

        }

        m_LastFrameRightEnable = m_RightLineVisual.enabled;

    }

    void LeftTeleportUpdate()

    {

        Vector2 axisInput;

        m_LeftInputDevice.TryGetFeatureValue(CommonUsages.primary2DAxis, out axisInput);



        m_LeftLineVisual.enabled = axisInput.y > 0.5f;



      // LeftTeleportInteractor.InteractionLayerMask = m_LastFrameLeftEnable ? m_OriginalLeftMask : new LayerMask();



        if (axisInput.y <= 0.5f && m_PreviousLeftClicked)

        {

            m_LeftController.Select();

        }



        if (axisInput.y <= -0.5f)

        {

            if(!LeftTractorBeam.IsTracting)

                LeftTractorBeam.StartTracting();

        }

        else if(LeftTractorBeam.IsTracting)

        {

            LeftTractorBeam.StopTracting();

        }



        //if the left animator is null, we try to get it. It's not the best performance wise but no other way as setup

        //of the model by the Interaction Toolkit is done on the first update.

        if (m_LeftHandPrefab == null)

        {

            m_LeftHandPrefab = LeftDirectInteractor.GetComponentInChildren<HandPrefab>();

        }

        m_PreviousLeftClicked = axisInput.y > 0.5f;



        if (m_LeftHandPrefab != null)

            m_LeftHandPrefab.Animator.SetBool("Pointing", m_PreviousLeftClicked);



        m_LastFrameLeftEnable = m_LeftLineVisual.enabled;

    }

}

ObjectLauncher


using System.Collections.Generic;

using UnityEngine;

public class ObjectLauncher : MonoBehaviour

{

    public Transform SpawnPoint;

    public ProjectileBase ObjectToSpawn;

    public bool IsAutoSpawn = false;

    public float LaunchRate = 0.5f;

    public AudioClip LaunchingClip;



    float m_LastLaunch = 0.0f;

    Queue<ProjectileBase> m_ProjectilesPool = new Queue<ProjectileBase>();

    void Awake()

    {

        enabled = false;

        for (int i = 0; i < 32; ++i)

        {

            var newObj = Instantiate(ObjectToSpawn, SpawnPoint.position, SpawnPoint.rotation);

            newObj.gameObject.SetActive(false);

            m_ProjectilesPool.Enqueue(newObj);

        }

    }

    public void Activated()

    {

        //if this is auto spawn regularly, we enable the script so the update is called.

        if (IsAutoSpawn)

        {

            enabled = true;

            m_LastLaunch = LaunchRate;

        }

        Launch();

    }

    public void Deactivated()

    {

        enabled = false;

    }

    void Update()

    {

        if (m_LastLaunch > 0.0f)

        {

            m_LastLaunch -= Time.deltaTime;

            if (m_LastLaunch <= 0.0f)

            {

                Launch();

                m_LastLaunch = LaunchRate;

            }

        }

    }

    void Launch()

    {

        var p = m_ProjectilesPool.Dequeue();

        p.gameObject.SetActive(true);

        p.transform.position = SpawnPoint.position;

        p.Launched(SpawnPoint.transform.forward, this);



        SFXPlayer.Instance.PlaySFX(LaunchingClip, SpawnPoint.position, new SFXPlayer.PlayParameters()

        {

            Pitch = Random.Range(0.9f, 1.2f),

            Volume = 1.0f,

            SourceID = -999

        });

    }



    public void ReturnProjectile(ProjectileBase proj)

    {

        m_ProjectilesPool.Enqueue(proj);

    }

}

public abstract class ProjectileBase : MonoBehaviour

{

    public abstract void Launched(Vector3 direction, ObjectLauncher launcher);

}

ObjectSpawner


using System;

using System.Collections;

using System.Collections.Generic;

using UnityEngine;

using UnityEngine.VFX;

/// <summary>

/// Will spawn the given prefab, either when the Spawn function is called or if a MinInstance is setup, until that

/// amount exist in the scene. If Spawn is called when there is already MaxInstance number of object in the scene,

/// it will destroy the oldest one to make room for the newest.

/// </summary>

public class ObjectSpawner : MonoBehaviour

{

    public GameObject Prefab;

    public VisualEffect SpawnEffect;

    public Transform SpawnPoint;

    public int MaxInstances = 2;

    public int MinInstance = 0;



    List<GameObject> m_Instances = new List<GameObject>();

    private void Start()

    {

        Prefab = null;

    }

    public void Spawn()

    {

        var newInst = Instantiate(Prefab, SpawnPoint.position, SpawnPoint.rotation);

        if (m_Instances.Count >= MaxInstances)

        {

            Destroy(m_Instances[0]);

            m_Instances.RemoveAt(0);

        }



        SpawnEffect.SendEvent("SingleBurst");

        m_Instances.Add(newInst);

    }

    void Update()

    {

        for (int i = 0; i < m_Instances.Count; ++i)

        {

            //if the object was destroyed, remove from the list

            if (m_Instances[i] == null)

            {

                m_Instances.RemoveAt(i);

                i--;

            }

        }

        if (Prefab != null)

        {

            while (m_Instances.Count < MinInstance)

            {

                Spawn();

            }

        }
    }
}

Potion

using System;

using System.Collections;

using System.Collections.Generic;

using UnityEngine;

using UnityEngine.Serialization;

using UnityEngine.VFX;

using Random = UnityEngine.Random;
public class Potion : MonoBehaviour{

    static int NextFreeUniqueId = 3000;



    public string PotionType = "Default";

    public GameObject plugObj;

    public ParticleSystem particleSystemLiquid;

    public ParticleSystem particleSystemSplash;

    public float fillAmount = 0.8f;

    public GameObject popVFX;

    [FormerlySerializedAs("meshRenderer")]

    public MeshRenderer MeshRenderer;

    [FormerlySerializedAs("smashedObject")]

    public GameObject SmashedObject;



    [Header("Audio")]

    public AudioClip PouringClip;

    public AudioClip[] PoppingPlugAudioClip;

    public AudioClip[] BreakingAudioClips;



    bool m_PlugIn = true;

    Rigidbody m_PlugRb;

    MaterialPropertyBlock m_MaterialPropertyBlock;

    Rigidbody m_RbPotion;

    int m_UniqueId;

    AudioSource m_AudioSource;

    bool m_Breakable;

    float m_StartingFillAmount;

    void OnEnable()

    {

        particleSystemLiquid.Stop();



        if(particleSystemSplash)

            particleSystemSplash.Stop();



        m_MaterialPropertyBlock = new MaterialPropertyBlock();

        m_MaterialPropertyBlock.SetFloat("LiquidFill", fillAmount);

        MeshRenderer.SetPropertyBlock(m_MaterialPropertyBlock);

        m_PlugRb = plugObj.GetComponent<Rigidbody>();

        popVFX.SetActive(false);

        m_RbPotion = GetComponent<Rigidbody>();

        m_StartingFillAmount = fillAmount;

        m_Breakable = true;

    }

    void Start()

    {

        m_AudioSource = SFXPlayer.Instance.GetNewSource();

        m_AudioSource.gameObject.transform.SetParent(particleSystemLiquid.transform, false);

        m_AudioSource.gameObject.SetActive(true);

        m_AudioSource.clip = PouringClip;

        m_AudioSource.maxDistance = 2.0f;

        m_AudioSource.minDistance = 0.2f;

        m_AudioSource.loop = true;

        m_UniqueId = NextFreeUniqueId++;

    }

    void OnDestroy()

    {

        Destroy(m_AudioSource.gameObject);

    }

    // Update is called once per frame

    void Update()

    {

        if (Vector3.Dot(transform.up, Vector3.down) > 0 && fillAmount > 0 && m_PlugIn == false)

        {

            if (particleSystemLiquid.isStopped)

            {

                particleSystemLiquid.Play();

                m_AudioSource.Play();

            }



            fillAmount -= 0.1f * Time.deltaTime;

            float fillRatio = fillAmount / m_StartingFillAmount;

            m_AudioSource.pitch = Mathf.Lerp(1.0f, 1.4f, 1.0f - fillRatio);

            RaycastHit hit;

            if (Physics.Raycast(particleSystemLiquid.transform.position, Vector3.down, out hit, 50.0f, ~0, QueryTriggerInteraction.Collide))

            {

                PotionReceiver receiver = hit.collider.GetComponent<PotionReceiver>();

                if (receiver != null)

                {

                    receiver.ReceivePotion(PotionType);

                }

            }

        }

        else

        {

            particleSystemLiquid.Stop();

            m_AudioSource.Stop();

        }

        MeshRenderer.GetPropertyBlock(m_MaterialPropertyBlock);

        m_MaterialPropertyBlock.SetFloat("LiquidFill", fillAmount);

        MeshRenderer.SetPropertyBlock(m_MaterialPropertyBlock);

    }

    public void ToggleBreakable(bool breakable)

    {

        m_Breakable = breakable;

    }

    public void PlugOff()

    {

        if (m_PlugIn)

        {

            m_PlugIn = false;

            m_PlugRb.transform.SetParent(null);

            m_PlugRb.isKinematic = false;

            m_PlugRb.AddRelativeForce(new Vector3(0, 0, 120));

            popVFX.SetActive(true);

            m_PlugIn = false;

            plugObj.transform.parent = null;

            SFXPlayer.Instance.PlaySFX(PoppingPlugAudioClip[Random.Range(0, PoppingPlugAudioClip.Length)], m_PlugRb.transform.position, new SFXPlayer.PlayParameters()

            {

                Pitch = Random.Range(0.9f, 1.1f),

                Volume = 1.0f,

                SourceID = -99

            });

        }



    }

    private void OnCollisionEnter(Collision collision)

    {

        if (m_Breakable && m_RbPotion.velocity.magnitude > 1.35)

        {

            if (m_PlugIn)

            {

                m_PlugRb.isKinematic = false;

                plugObj.transform.parent = null;

                Collider c;

                if (plugObj.TryGetComponent(out c))

                    c.enabled = true;



                Destroy(plugObj, 4.0f);

            }

            foreach (Transform child in transform)

            {

                child.gameObject.SetActive(false);

            }

            if (particleSystemSplash != null)

            {     

                particleSystemSplash.gameObject.SetActive(true);

                if (fillAmount > 0)

                {

                    particleSystemSplash.Play();

                }

            }

            SmashedObject.SetActive(true);

            SFXPlayer.Instance.PlaySFX(BreakingAudioClips[Random.Range(0, BreakingAudioClips.Length)], transform.position, new SFXPlayer.PlayParameters()

            {

                Pitch = Random.Range(0.8f, 1.2f),

                SourceID = m_UniqueId,

                Volume = 1.0f

            });

            Rigidbody[] rbs = SmashedObject.GetComponentsInChildren<Rigidbody>();

            foreach (Rigidbody rb in rbs)

            {

                rb.AddExplosionForce(100.0f, SmashedObject.transform.position, 2.0f, 15.0F);

            }

            Destroy(SmashedObject, 4.0f);

            Destroy(this);

        }     

    }

}

Radio


using System.Collections;

using System.Collections.Generic;

using UnityEngine;

public class Radio : MonoBehaviour

{

    public AudioSource MusicSource;

    public AudioSource NoiseSource;

    public AudioClip[] MusicClips;

    float m_TuningRatio = 0.0f;

    float m_VolumeRatio = 0.0f;



    void Start()

    {

        Tune();

    }

    public void VolumeChanged(DialInteractable dial)

    {

        float ratio = dial.CurrentAngle / dial.RotationAngleMaximum;

        m_VolumeRatio = ratio;



        Tune();

    }

    public void TuningChanged(DialInteractable dial)

    {

        //off

        if (dial.CurrentAngle < 0.01f)

        {

            MusicSource.Stop();

            NoiseSource.Stop();

        }

        else if(!MusicSource.isPlaying)

        {

            MusicSource.Play();

            NoiseSource.Play();

        }



        if(MusicClips.Length == 0)

            return;

        float ratio = dial.CurrentAngle / dial.RotationAngleMaximum;

        float stepSize = dial.RotationAngleMaximum / MusicClips.Length;

        float stepRatio = dial.CurrentAngle / stepSize;

        int closest = Mathf.RoundToInt(stepRatio);

        float dist = Mathf.Abs(closest - stepRatio) / 0.5f;

        if (closest == 0)

            dist = 1.0f;

        else

        {

            AudioClip c = MusicClips[closest - 1];

            if (c != MusicSource.clip)

            {

                int sample = MusicSource.timeSamples;

                MusicSource.clip = c;

                MusicSource.timeSamples = sample;

            }

        }

        m_TuningRatio = 1.0f - dist;

        Tune();

    }

    void Tune()

    {

        MusicSource.volume = m_TuningRatio * m_VolumeRatio;

        NoiseSource.volume = (1.0f - m_TuningRatio) * m_VolumeRatio;

    }

}

RespawnableObject


using System;

using System.Collections;

using System.Collections.Generic;

using UnityEngine;

/// <summary>

/// This will save the starting position/rotation of an object, and will move it back there when Respawn is called.

/// Useful for object that should never get destroyed when thrown in fire or the cauldron, like the ingredients.

/// </summary>

public class RespawnableObject : MonoBehaviour

{

    Vector3 m_StartingPosition;

    Quaternion m_StartingRotation;

    Rigidbody m_Rigidbody;



    void Start()

    {

        m_Rigidbody = GetComponent<Rigidbody>();

        if (m_Rigidbody == null)

        {

            m_StartingPosition = transform.position;

            m_StartingRotation = transform.rotation;

        }

        else

        {

            m_StartingPosition = m_Rigidbody.position;

            m_StartingRotation = m_Rigidbody.rotation;

        }

    }

    public void Respawn()

    {

        if (m_Rigidbody == null)

        {

            transform.position = m_StartingPosition;

            transform.rotation = m_StartingRotation;

        }

        else

        {

            m_Rigidbody.velocity = Vector3.zero;

            m_Rigidbody.angularVelocity = Vector3.zero;

            m_Rigidbody.position = m_StartingPosition;

            m_Rigidbody.rotation = m_StartingRotation;

        }

    }

}

WatchScript


using System;

using System.Collections;

using System.Collections.Generic;

using System.Linq;

using UnityEngine;

using UnityEngine.Events;

using UnityEngine.UI;

public class WatchScript : MonoBehaviour

{



    //by making a script subclass that, the wrist will pick them up at start time and will notify them it's been loaded

    //this will allow them to add button to the UI that are scene specific.

    public abstract class IUIHook : MonoBehaviour

    {

        public abstract void GetHook(WatchScript watch);

    }



    public float LoadingTime = 2.0f;

    public Slider LoadingSlider;

    [Header("UI")]

    public Canvas RootCanvas;

    public Transform ButtonParentTransform;

    public Button ButtonPrefab;

    public Toggle TogglePrefab;



    [Header("Event")]

    public UnityEvent OnLoaded;

    public UnityEvent OnUnloaded;

    public GameObject UILineRenderer;



    bool m_Loading = false;

    float m_LoadingTimer;

    void Start()

    {

        LoadingSlider.gameObject.SetActive(false);

        var hooks = FindObjectsOfType<IUIHook>();

        foreach (var h in hooks)

        {

            h.GetHook(this);

        }



        RootCanvas.worldCamera = Camera.main;

    }

    // Update is called once per frame

    void Update()

    {

        if (m_Loading)

        {

            m_LoadingTimer += Time.deltaTime;

            LoadingSlider.value = Mathf.Clamp01(m_LoadingTimer / LoadingTime);

            if (m_LoadingTimer >= LoadingTime)

            {

                OnLoaded.Invoke();

                UILineRenderer.SetActive(true);

                LoadingSlider.gameObject.SetActive(false);

                m_Loading = false;

            }

        }

    }

    public void LookedAt()

    {

        m_Loading = true;

        m_LoadingTimer = 0.0f;

        LoadingSlider.value = 0.0f;

        LoadingSlider.gameObject.SetActive(true);

    }

    public void LookedAway()

    {

        m_Loading = false;

        OnUnloaded.Invoke();

        LoadingSlider.gameObject.SetActive(false);

        UILineRenderer.SetActive(false);

    }

    public void AddButton(string name, UnityAction clickedEvent)

    {

        var newButton = Instantiate(ButtonPrefab, ButtonParentTransform);

        var text = newButton.GetComponentInChildren<Text>();



        RecursiveLayerChange(newButton.transform, ButtonParentTransform.gameObject.layer);

        text.text = name;

        newButton.onClick.AddListener(clickedEvent);

    }

    public void AddToggle(string name, UnityAction<bool> checkedEvent)

    {

        var newToggle = Instantiate(TogglePrefab, ButtonParentTransform);

        var text = newToggle.GetComponentInChildren<Text>();



        RecursiveLayerChange(newToggle.transform, ButtonParentTransform.gameObject.layer);

        text.text = name;



        newToggle.onValueChanged.AddListener(checkedEvent);

    }

    void RecursiveLayerChange(Transform root, int layer)

    {

        foreach (Transform t in root)

        {

            RecursiveLayerChange(t, layer);

        }

        root.gameObject.layer = layer;

    }

}

WitchHouseUIHook


using System.Collections;

using System.Collections.Generic;

using UnityEngine;

using UnityEngine.SceneManagement;

/// <summary>

/// This will be picked up automatically by the wrist watch when it get spawn in the scene by the Interaction toolkit

/// and setup the buttons and the linked events on the canvas

/// </summary>

public class WitchHouseUIHook : WatchScript.IUIHook

{

    public GameObject LeftUILineRenderer;

    public GameObject RightUILineRenderer;



    public override void GetHook(WatchScript watch)

    {

        watch.AddButton("Reset", () => { SceneManager.LoadScene(SceneManager.GetActiveScene().buildIndex); });

        watch.AddButton("Unlock Teleporters", () => {MasterController.Instance.TeleporterParent.SetActive(true);});

        watch.AddToggle("Closed Caption", (state) => { CCManager.Instance.gameObject.SetActive(state); });

        LeftUILineRenderer.SetActive(false);

        RightUILineRenderer.SetActive(false);

        watch.UILineRenderer = LeftUILineRenderer;

    }

}

WoodenLogBehaviour


using System.Collections;

using System.Collections.Generic;

using UnityEngine;

using UnityEngine.Serialization;

public class WoodenLogBehaviour : MonoBehaviour

{

    [FormerlySerializedAs("meshRenderer")]

    public MeshRenderer MeshRenderer;



    MaterialPropertyBlock m_Block;

    float m_BurnAmount = 0.0f;

    bool m_Burn  = false;



    // Start is called before the first frame update

    void Start()

    {

        m_Block = new MaterialPropertyBlock();

        m_Block.SetFloat("BurnAmount", m_BurnAmount);

        MeshRenderer.SetPropertyBlock(m_Block);

    }

    // Update is called once per frame

    void Update()

    {

        if (m_Burn && m_BurnAmount < 1.0f)

        {

            m_BurnAmount += 0.2f * Time.deltaTime;

        }

        MeshRenderer.GetPropertyBlock(m_Block);

        m_Block.SetFloat("BurnAmount", m_BurnAmount);

        MeshRenderer.SetPropertyBlock(m_Block);

    }

    private void OnTriggerEnter(Collider other)

    {

        if (other.gameObject.tag == "Ignite")

        {

            m_Burn = true;

        }     

    }

}

三、效果展示

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,098评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,213评论 2 380
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 149,960评论 0 336
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,519评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,512评论 5 364
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,533评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,914评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,574评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,804评论 1 296
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,563评论 2 319
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,644评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,350评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,933评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,908评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,146评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,847评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,361评论 2 342

推荐阅读更多精彩内容