1
\$\begingroup\$

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.

enter image description here

\$\endgroup\$
0
\$\begingroup\$

You could use a main "Spell" C# object with all parameters (Cost, etc.) as public fields, so you can change them in the editor, and also setup public fields for Visual, Motions... Those last fields should be of type SpellVisual, SpellMotion... (I will talk about them later). If some fields have some finite options (like active/passive spell for example) you can use simple drop-down lists, or have the main "Spell" abstract and use child classes for "ActiveSpell", "PassiveSpell", etc.

For "SpellVisual" you can setup a main abstract C# object, and then child of this object like for example a "particle circle" visual effect, an "3d model rotating" effect, etc. Like the first object, expose all specific parameters as public fields to be changed in the inspector, like particle config, duration, etc. And do the same for others objects type like "SpellMotion" and others you need...

Then, when all C# objects are set-up, you just have to create prefabs which uses the classes defined earlier, and all specific spell stuff are then configured in the inspector. (Just in case you realize you need as many "SpellXXX" prefabs as "Spells" prefabs, it may be better to move all stuff directly in the "Spell" class)

As a rule of thumbs, you should ask if something is game-theme related. If no => C#, if yes => Prefab/Object config.

For example, never create a FireAreaDamageEffect class, but treat all types of damage as a list, and then create a AreaDamageEffect with a list of damage types, which should be global to the whole game.


Here are the main advantage of this solution:

  • If you are working as a team on this project, spells can be created and edited by a non-developer person, like a character designer.
  • If you want players to extend your game with mods, they don't need access to the source code
  • For testing, you don't need to recompile or restart game to change parameters, including visual ones (can be changed directly in unity inspector during playtest)

The only disadvantage I can think of right now is that you need more work at first to setup the system, but you will gain time from it even if working alone on the project.

| improve this answer | |
\$\endgroup\$
  • \$\begingroup\$ Hi! Thanks for your reply, What you are describing is basically what I have already, minus the Spell_Visual class improvements, 'Ill add a picture of editor for what I currently have, as I said in my main question, I am struggling in how I would break down Mage Hand into a bunch of single actions that replicate what I have currently. Then I could generalise the entire system better but the mage hand spell is key to interacting with things in the game. \$\endgroup\$ – Pheonix2105 May 30 at 17:39

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.