Skip to content

Instantly share code, notes, and snippets.

@TigerHix
Created July 17, 2023 15:24
Show Gist options
  • Save TigerHix/18e9f20152c0cfac38fd5528c7af16b6 to your computer and use it in GitHub Desktop.
Save TigerHix/18e9f20152c0cfac38fd5528c7af16b6 to your computer and use it in GitHub Desktop.
using System;
using Cysharp.Threading.Tasks;
using UnityEngine;
using Warudo.Core;
using Warudo.Core.Attributes;
using Warudo.Core.Graphs;
using Warudo.Core.Utils;
using Warudo.Plugins.Core.Assets;
using Warudo.Plugins.Core.Assets.Character;
using Warudo.Plugins.Interactions.Mixins;
using Object = UnityEngine.Object;
namespace Warudo.Plugins.Interactions.Nodes {
[NodeType(Id = "07594cda-42b6-48ee-ac27-e6700ac8c251", Title = "THROW_PROP_AT_CHARACTER", Category = "CATEGORY_INTERACTIONS")]
public class ThrowPropAtCharacterNode : Node {
[DataInput]
[Label("PROP_SOURCE")]
[PreviewGallery]
[AutoCompleteResource("Prop")]
public string PropSource;
[DataInput]
[Label("IMPACT_PARTICLE_SOURCE")]
[PreviewGallery]
[AutoCompleteResource("Particle")]
public string ImpactParticleSource;
[DataInput]
[Label("LAUNCH_SOUND_SOURCE")]
[AutoCompleteResource("Sound")]
public string LaunchSoundSource;
[DataInput]
[Label("IMPACT_SOUND_SOURCE")]
[AutoCompleteResource("Sound")]
public string ImpactSoundSource;
[DataInput]
[Label("TARGET_CHARACTER")]
public CharacterAsset Character;
[Mixin]
public FromTo FromTo;
[DataInput]
[Label("SCALE")]
[FloatSlider(0.1f, 3f)]
public float Scale = 1f;
[DataInput]
[FloatSlider(1f, 100f)]
[Label("MASS")]
public float Mass = 25f;
[DataInput]
[FloatSlider(1f, 10f)]
[Label("SPEED")]
public float Speed = 5f;
[DataInput]
[Label("ALIVE_TIME")]
[Description("ALIVE_TIME_DESCRIPTION")]
public float AliveTime = 5f;
[DataInput]
[Label("IMPACT_PARTICLE_SCALE")]
[FloatSlider(0.1f, 3f)]
public float ImpactParticleScale = 0.25f;
[DataInput]
[Label("SOUND_VOLUME")]
[FloatSlider(0f, 1f)]
public float SoundVolume = 0.1f;
private GameObject impactParticle;
private AudioClip launchSound;
private AudioClip impactSound;
private float particleAliveTime;
protected override void OnCreate() {
base.OnCreate();
FromTo.CharacterGetter = () => Character;
Watch(nameof(ImpactParticleSource), () => {
if (impactParticle != null) {
Object.DestroyImmediate(impactParticle);
impactParticle = null;
}
if (ImpactParticleSource != null) {
impactParticle = Context.ResourceManager.ResolveResourceUri<GameObject>(ImpactParticleSource);
impactParticle.SetActive(false);
particleAliveTime = 0f;
foreach (var particleSystem in impactParticle.GetComponentsInChildren<ParticleSystem>()) {
particleAliveTime = Mathf.Max(particleAliveTime, particleSystem.main.duration);
}
particleAliveTime += 5f;
}
});
Watch(nameof(LaunchSoundSource), () => {
launchSound = null;
if (LaunchSoundSource != null) {
launchSound = Context.ResourceManager.ResolveResourceUri<AudioClip>(LaunchSoundSource);
}
});
Watch(nameof(ImpactSoundSource), () => {
impactSound = null;
if (ImpactSoundSource != null) {
impactSound = Context.ResourceManager.ResolveResourceUri<AudioClip>(ImpactSoundSource);
}
});
}
public override void Destroy() {
base.Destroy();
if (impactParticle != null) {
Object.DestroyImmediate(impactParticle);
impactParticle = null;
}
launchSound = null;
impactSound = null;
}
[FlowInput]
public Continuation Enter() {
if (PropSource.IsNullOrWhiteSpace() || Character.IsNullOrInactiveOrDisabled()) {
return Exit;
}
Throw();
return Exit;
}
public async void Throw() {
var gameObject = Context.ResourceManager.ResolveResourceUri<GameObject>(PropSource);
if (gameObject == null) {
throw new Exception("Failed to load resource: " + PropSource);
}
gameObject.transform.localScale *= Scale;
gameObject.transform.SetLayerRecursively(LayerMask.NameToLayer("Prop"));
// TODO: This is necessary or not?
if (Application.isEditor) {
// Wait for shaders to compile
gameObject.SetActive(false);
await UniTask.DelayFrame(5);
gameObject.SetActive(true);
}
Character.DisableTemporaryRagdollTime = Time.realtimeSinceStartup + AliveTime;
var rigidbody = gameObject.GetComponent<Rigidbody>();
if (rigidbody == null) {
rigidbody = gameObject.AddComponent<Rigidbody>();
}
rigidbody.mass = Mass;
var startPos = FromTo.FromPosition;
var endPos = FromTo.ToPosition;
gameObject.transform.position = startPos;
var behavior = gameObject.AddComponent<ThrownPropBehavior>();
behavior.Parent = this;
// Calculate the displacement vector and its horizontal distance
var deltaPosition = endPos - startPos;
var t = 1 / Speed;
// Calculate the horizontal and vertical distances
float horizontalDistance = new Vector2(deltaPosition.x, deltaPosition.z).magnitude;
float verticalDistance = deltaPosition.y;
// Calculate the initial horizontal and vertical velocities
float initialHorizontalVelocity = horizontalDistance / t;
float gravity = Physics.gravity.y;
float initialVerticalVelocity = verticalDistance / t + 0.5f * Mathf.Abs(gravity) * t;
// Calculate the initial velocity vector
Vector3 initialVelocity = new Vector3(deltaPosition.x, 0, deltaPosition.z).normalized * initialHorizontalVelocity;
initialVelocity.y = initialVerticalVelocity;
// Calculate the force vector
Vector3 force = Mass * initialVelocity;
// Apply the force to the rigidbody
rigidbody.AddForce(force, ForceMode.Impulse);
behavior.PlayLaunchSound();
Object.Destroy(gameObject, AliveTime);
}
[FlowOutput]
public Continuation Exit;
[FlowOutput]
[Label("ON_COLLIDE")]
public Continuation OnCollide;
class ThrownPropBehavior : MonoBehaviour {
public ThrowPropAtCharacterNode Parent { get; set; }
public bool Collided { get; set; }
private AudioSource audioSource;
public void PlayLaunchSound() {
if (Parent.launchSound != null) {
if (audioSource == null) audioSource = gameObject.AddComponent<AudioSource>();
audioSource.clip = Parent.launchSound;
audioSource.volume = Parent.SoundVolume;
audioSource.Play();
}
}
void OnCollisionEnter(Collision collision) {
if (Collided || Parent.Character.IsNullOrInactiveOrDisabled() || !collision.collider.transform.IsChildOf(Parent.Character.PuppetMaster.transform)) return;
Collided = true;
Parent.InvokeFlow(nameof(Parent.OnCollide));
if (Parent.impactParticle != null) {
var particle = Instantiate(Parent.impactParticle, collision.contacts[0].point, Parent.impactParticle.transform.rotation);
particle.SetActive(true);
particle.transform.localScale *= Parent.ImpactParticleScale;
particle.GetComponents<PSSizeControl>().ForEach(it => it.scaleNumber = Parent.ImpactParticleScale);
Destroy(particle, Parent.particleAliveTime);
}
if (Parent.impactSound != null) {
if (audioSource == null) audioSource = gameObject.AddComponent<AudioSource>();
audioSource.clip = Parent.impactSound;
audioSource.volume = Parent.SoundVolume;
audioSource.Play();
Destroy(audioSource, audioSource.clip.length);
}
}
}
}
}
@TigerHix
Copy link
Author

FromTo (source code here) is a mixin which is basically a reusable component across entity types (i.e. asset, node, plugin).

You can define data inputs in a mixin just like doing it in a node. The data inputs in a mixin will be registered at their parent, in this case, the ThrowPropAtCharacterNode, which is why you can see the data inputs defined in the mixin on the node in editor. A mixin also has its own OnCreate, OnDestroy lifecycle methods (and OnUpdate etc. for a BehavioralMixin), and they are invoked when their parents go through these lifecycle events.

The reason of using a mixin is to avoid duplicating code. For example, LaunchLiquidAtCharacterNode also uses the FromTo mixin as it requires data inputs from the user regarding the start and end position of the liquid. By simply writing [Mixin] public FromTo FromTo;, the data inputs in the mixin can be "imported" along with any independent, reusable logic.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment