This has been a bit of a long time coming challenge for me, if you look back at previous questions for Component based spells you are likely to find my old questions, given that they are 1 + 2 years old respectively and I'm on attempt three, I thought I'd try again hopefully with more knowledge!
I am attempting to build a ScriptableObject based spell system, where each spell is made up of five components, Spell_Cast, Spell_Aim, Spell_Visual, Spell_Motion, Spell_Behaviour and a bunch of information on the spell itself (Cast, Cost, Max Targets, Base Spell Power, Range, Radius) etc.
Currently I am trying to get bare bones examples working.
For my proof of concept spells, I have picked "Mage Hand" and "Mage Light"
Mage Hand:
- Spell Cast : Channel (Casts while button is held)
- Spell Aim : Raycast (Target Object)
- Spell Visual : A Prefab model of a hand.
- Spell Motion : Projectile (Hand will fire from player to target)
- Spell Behaviour: This parents the target object to the Mage Hand Effect Instance, Disables Gravity, Freezes Rotation and Holds it in-front of the player while the button is held.
Mage Light:
- Spell Cast : Instant (Casts instantly)
- Spell Aim : Raycast (Target Object)
- Spell Visual : A Prefab model of a particle system and light.
- Spell Motion : Projectile (Light will fire from player to target)
- Spell Behaviour: Most of this spell takes place on the Visual Prefab itself, the spell just fires it towards the target, and in PostEffect sets a self destruct timer based on spell power. The prefab handles 'sticking/unsticking' itself to things and destroying itself with monobehaviours.
Both these spells work but I am feeling quite unsure about the spell behaviours, currently there are 1 behaviour scriptableobject per spell.
These are not generalised, I realise I could rework the Behaviour class so it just holds a list of actions to run on the target whatever it may be, but I don't understand how I could make a spell like Mage Hand work using only general actions and the fact it would need to do some actions just once (What if I added an effect that reduces the weight of held item?), but do the actual 'holding' of the object every frame while the button is held.
Take for example my 'Mage Hand' behaviour, there are a lot of sub behaviours (Holding T lets you slightly adjust the hand, or Press ScrollWheel throws the object which i'm not happy with but I went with 'just get something working' philosophy)
I am storing all of the spell state data on SpellCastData and any action I use too often in a behaviour gets moved to the base Spell_Behaviour class so I'm not repeating myself.
Below (first) is component I use to store and cast spells (Entity_Cast_Comp), second is the behaviour for 'Mage Hand'
So, all of that out of the way my question is: How do I generalise a spell such as Mage Hand that I could then turn my Spell Behaviour objects into a collection of generalised actions?
Thanks for reading this far!
public class Entity_Cast_Comp : MonoBehaviour
{
[HideInInspector]
public GameObject parent;
[Header("Available Spells")]
public List<Spell> spells = new List<Spell>();
[Header("Debug information")]
public bool debug = false;
//events
[HideInInspector]
public UnityEvent OnSpellsChanged;
private void Start()
{
parent = this.gameObject;
}
public void Cast(Spell spell, KeyCode button)
{
SpellCastData data = new SpellCastData();
data.button = button;
data.caster = parent;
//TODO : This seems like a bad idea
data.castLocation = parent.transform.Find("CastPointRight");
//check the spell isnt already casting
if (data.spellRunning == false && GameObject.FindObjectOfType<UI_Controller>().UIOpen() == false)
{
data.spellRunning = true;
data.SetState(SpellStates.AIMING);
StartCoroutine(DoCast(spell, data));
}
}
public void AddSpell(Spell spell)
{
if (!spells.Contains(spell))
{
spells.Add(spell);
OnSpellsChanged.Invoke();
}
}
public void RemoveSpell(Spell spell)
{
if (spells.Contains(spell))
{
spells.Remove(spell);
OnSpellsChanged.Invoke();
}
}
private IEnumerator DoCast(Spell spell, SpellCastData data)
{
while (data.spellRunning)
{
data.UpdateButtonState();
switch (data.state)
{
case SpellStates.AIMING:
yield return spell.spellAim.Aim(spell, parent, data);
break;
case SpellStates.CASTING:
yield return spell.spellCast.Cast(spell, parent, data);
yield return spell.spellAim.DrawAim(spell, parent, data);
foreach (var target in data.targets)
{
if (!data.effectInstances.ContainsKey(target))
{
//TODO : F
data.effectInstances[target] = spell.spellVisual.SpawnEffect(data.castLocation.position, parent.transform.rotation);
data.targetsHit.Add(target, false);
}
}
foreach (var target in data.targets)
{
while (data.targetsHit[target] == false)
{
yield return spell.spellMotion.Move(spell, parent, data, target);
}
}
foreach (var target in data.targets)
{
if (data.preEffectDone.ContainsKey(target) == false)
{
yield return spell.spellBehaviour.PreEffect(spell, parent, data, target);
data.preEffectDone.Add(target, true);
}
yield return spell.spellBehaviour.Effect(spell, parent, data, target);
}
break;
case SpellStates.CANCELLING:
data.spellRunning = false;
data.Reset();
yield break;
case SpellStates.FINISHED:
foreach (var target in data.targets)
{
yield return spell.spellBehaviour.PostEffect(spell, parent, data, target);
}
data.Reset();
yield break;
}
yield return null;
}
yield break;
}
}
This is the spell behaviour for 'Mage Hand'
[CreateAssetMenu(menuName = scriptableObjectPath + componentPath + "TeleHold")]
public class Spell_Effect_TelekineticHold : Spell_Behaviour
{
public float speed = 5f;
public float moveSpeed = 3.5f;
public float xOffsetMin = 0f;
public float xOffsetMax = 2f;
public float yOffsetMin = 0f;
public float yOffsetMax = 2f;
public float zOffsetMin = -0.5f;
public float zOffsetMax = 1f;
private Transform pivotPoint;
private float zOffset = 0;
private float xOffset = 0;
private float yOffset = 0;
private Vector3 targetOffset = new Vector3();
public float smoothTime = 0.3F;
private Vector3 targetOriginalRotation;
private Vector3 velocity = Vector3.zero;
private Quaternion quaternion;
public override IEnumerator Effect(Spell _parent, GameObject caster, SpellCastData data, GameObject target)
{
//hold target in place as long as casting and target exists
if (target)
{
//TODO : Extra action keys shouldnt be hard coded
if (Input.GetKey(KeyCode.T))
{
xOffset += Input.GetAxisRaw("Mouse X");
yOffset += Input.mouseScrollDelta.y * 0.25f;
}
else
{
zOffset += Input.mouseScrollDelta.y * 0.25f;
}
//TODO : Extra action keys shouldnt be hard coded
//and this probably doesnt want to work like this?
if (Input.GetKey(KeyCode.Mouse2))
{
yield return PostEffect(_parent, caster, data, target);
AddForce(target, data);
data.spellRunning = false;
yield break;
}
xOffset = Mathf.Clamp(xOffset, xOffsetMin, xOffsetMax);
yOffset = Mathf.Clamp(yOffset, yOffsetMin, yOffsetMax);
zOffset = Mathf.Clamp(zOffset, zOffsetMin, zOffsetMax);
int targetIndex = data.targets.FindIndex(x => x == target);
//TODO : Improve multi hand stacking
targetOffset = new Vector3(xOffset + 1.4f * targetIndex, yOffset, 0);
if (data.effectInstances[target] != null)
{
Widget targetWidget = target.GetComponent<Widget>();
Vector3 targetPosition = (pivotPoint.position + targetOffset) + (pivotPoint.forward * zOffset * 5f);
if (targetWidget)
{
targetPosition.y = targetPosition.y + targetWidget.carryTargetPositionOffset.y;
target.transform.localPosition = Vector3.zero + targetWidget.carryTargetPositionOffset;
}
else
{
target.transform.localPosition = Vector3.zero;
}
data.effectInstances[target].transform.DOMove(targetPosition, smoothTime);
data.effectInstances[target].transform.rotation = data.caster.transform.rotation;
}
yield return null;
}
else
{
yield break;
}
yield break;
}
public override IEnumerator PreEffect(Spell _parent, GameObject caster, SpellCastData data, GameObject target)
{
//Debug.Log("pre effect");
if (target)
{
SetSpellDataPreviousParent(data, target);
pivotPoint = caster.transform.Find("MageHandRight").transform;
ParentToEffectInstance(target, data);
targetOriginalRotation = target.transform.eulerAngles;
CopyPhysicsSettings(target);
DisablePhysics(target);
}
yield break;
}
public override IEnumerator PostEffect(Spell _parent, GameObject caster, SpellCastData data, GameObject target)
{
// Debug.Log("post effect");
if (target)
{
target.transform.eulerAngles = targetOriginalRotation;
RemoveParentToEffectInstance(target, data);
RestorePhysicsSettings(target);
ResetVariables();
DeleteEffectInstance(data, target);
}
yield break;
}
private void ResetVariables()
{
zOffset = 0;
xOffset = 0;
yOffset = 0;
}
}
Here's a picture of a how it currently looks, as perhaps I havent explained myself well enough, I have a system in place working, but I consider it to be far too brittle to be worth going ahead with, as simple spells like 'Fireball' etc would easy enough to implement but I have no idea how I would implement Mage Hand In a very generic way.
