Building a flexible drag and drop hacking system with Scriptable Objects.

The Mission of this devlog: To provide players with the power to hack various objects in the game world, unleashing a multitude of actions, from disabling cameras to causing explosions. In short: Causing Mayhem. So, let’s get right into it.

Drawing Inspiration from Watch Dogs

Watch Dogs, a game that allows players to hack into an interconnected city, served as my primary muse. I was captivated by the idea of giving players the ability to manipulate their surroundings and devised a plan to bring this level of interactivity into my own project. Also I am very inspired by its clean and functional UI.

Conceptualizing the System

My system revolves around a few core components:

1. HackableSO (Scriptable Object): At the heart of my system lies the HackableSO, a Scriptable Object representing objects in the game world that can be hacked. Each HackableSO carries critical information such as descriptions, sprites, battery costs (if applicable), and most importantly, a list of HackableActionSOs.

2. HackableActionSO (Scriptable Object): These Scriptable Objects hold the logic for actions that can be executed on a HackableSO. Actions like “Disable Camera” or “Trigger Explosion” are defined as HackableActionSOs. They encapsulate the execution logic for their respective actions.

3. Drag-and-Drop Interface: My system makes it possible to include as many interactions to one hackable as I want. Just to select a HackableSO and attach HackableActionSOs to it. Done.

4. Runtime Execution: When a player decides to hack a HackableSO, all the associated HackableActionSOs spring into action, executing their designated functions.

Code Examples

Here’s a sneak peek into the code that brings my dynamic hacking system to life. I’ll focus on the two key Parts: HackableSO and HackableActionSO. These just get called via a hacking ability like that: hackableSO.Execute();

C#
// HackableSO.cs
using System.Collections.Generic;
using UnityEngine;

[CreateAssetMenu(fileName = "Hackable", menuName = "Scriptable Objects/Hackable", order = 0)]
public class HackableSO : ScriptableObject
{
    // ... (other variables)

    public List<HackableActionSO> hackableActions;
    List<HackableActionSO> instances = new List<HackableActionSO>();

    public void Initialize(Transform position, GameObject gameObject, ObjectHackable objectHackable)
    {
        // Initialization logic for HackableSO
        // Instantiate HackableActionSOs and add them to the 'instances' list
    }

    public void Execute(Transform position, GameObject gameObject, ObjectHackable objectHackable)
    {
        // Execution logic for HackableSO
        // Loop through 'instances' and execute associated HackableActionSOs
    }

    public void Deinitialize(Transform position, GameObject gameObject, ObjectHackable objectHackable)
    {
        // Deinitialization logic for HackableSO
    }
}

In this code snippet, HackableSO is responsible for initializing and executing HackableActions. It maintains a list of instances of HackableActionSOs to ensure that each action can be executed independently.

C#
// HackableActionSO.cs
using UnityEngine;

public class HackableActionSO : ScriptableObject
{
    public virtual void Initialize(Transform senderTransform, GameObject senderGO, ObjectHackable senderObjectHackable)
    {
        // Initialization logic for the action
    }

    public virtual void Execute(Transform senderTransform, GameObject senderGO, ObjectHackable senderObjectHackable)
    {
        // Execution logic for the action
    }

    public virtual void Deinitialize(Transform senderTransform, GameObject senderGO, ObjectHackable senderObjectHackable)
    {
        // Deinitialization logic for the action
    }
}

Meanwhile, HackableActionSO serves as the base class for all hackable actions, providing methods for initialization, execution, and deinitialization, making it easy for me to create custom actions.

Example for a HackableSO Configuration

Here is an example for that Hackable Action that just instantiates Objects like particle effects and sound.

C#
public class InstantiatePrefabAndSound : HackableActionSO
{
    public bool stopAfterSeconds;
    public float secondsToStop;
    public List<GameObject> ObjectsToInstantiate;
    public List<AudioClip> audioClips;
    
    public override void Initialize(Transform sendertransform, GameObject senderGo, ObjectHackable senderobjectHackable)
    {
        // Additional initialization logic can be added here
    }

    public override void Execute(Transform sendertransform, GameObject senderGo, ObjectHackable senderobjectHackable)
    { 
        foreach (var gameObject in ObjectsToInstantiate)
        {
            var go = Instantiate(gameObject, sendertransform.position, Quaternion.identity);
            go.transform.parent = sendertransform;
            go.SetActive(true);
            if (stopAfterSeconds) Destroy(go, secondsToStop);
        }
        
        foreach (var audioClip in audioClips)
        {
            // Play sound once
            AudioSource audio = senderGo.AddComponent<AudioSource>();
            audio.clip = audioClip;
            audio.spatialBlend = 1;
            audio.Play();
            if (stopAfterSeconds) Destroy(audio, secondsToStop);
        }
    }
    
    public override void Deinitialize(Transform sendertransform, GameObject senderGo, ObjectHackable senderobjectHackable)
    {
        // Additional deinitialization logic can be added here
    }
}

Devlog: Litte box of joy

So, I’ve spent the last few days sorting through the mess in my project and rolling back to an earlier state from my trusty git repo. Gotta admit, the motivation’s been taking a hit with all the rewiring and development work looming ahead. But you know what? Today was a different story. Managed to wrap up all those loose ends and finally jumped back into the development phase where the real fun is. Feels awesome to just let loose and work on some simple, quirky mechanics again.

I was just thinking about my Reddit thread asking for ideas about indoor stealth mechanics. Instantly, my brain went, “the Box!” Seriously, the simplest and coolest idea. Spent about an hour or so putting it all together: rough animations, particle effects, an Ability class, and the interaction event that kicks it all off. And guess what? It’s like the perfect little chunk of joy.

Found the fun!

Devlog: Crouching Ability

After my rant about relying too much on premade Assets, I’m making progress on my new custom character controller. The first and maybe most important Part of stealth locomotion is the Crouching/Sneaking ability. So I opened up Etras Starter Assets, inherited the base ability class and wrote my solution from scratch.

WebGL Demo

Features

  • The crouching ability can be triggered by toggling or holding
  • An “over-the-shoulder” cinemachine camera gets activated, as soon as the view gets narrower
  • The player will only stand up if there is enough headroom, to reduce clipping
  • The component will detect if there is a space in walking direction where crouching is possible
    • Optional Layermask detection
    • Enables crouching automatically
    • Checks if the space is between crouching height and standing height to minimize unwanted detections
C#
    private bool DetectCrouchableObstacle()
    {
        if(m_IsCrouching) return false;
        var rayCastOrigin = autoCrouchRaycastOrigin.position;
        var rayCastOriginlocalPosition = autoCrouchRaycastOrigin.localPosition;
        
        var isAboveStandingHeight = Physics.Raycast(rayCastOrigin, Vector3.up, defaultStandingHeight - rayCastOriginlocalPosition.y, autoCrouchDetectionMask);
        var isAboveCrouchHeight = Physics.Raycast(rayCastOrigin, Vector3.up, crouchHeight - rayCastOriginlocalPosition.y, autoCrouchDetectionMask);
        return isAboveStandingHeight && !isAboveCrouchHeight;
    }

Things to improve

  • Enable Rootmotion
  • Fine tuning of the camera behavior – Update existing camera instead of adding a new one.

Challenges

  • Unity Character Controller behaviour
    • When switching back to standing height too early, the player collided with the walls and got catapulted in walking direction
    • I tried it with lerping from crouch to stand first, but that led to different issues
    • Fix: Check for collisions on the whole character radius with boxcast instead of raycast
C#
    private bool CanStandUp()
    {
        return !Physics.BoxCast(transform.position, characterController.bounds.extents, Vector3.up, transform.rotation, defaultStandingHeight, obstacleMask);
    }

Main Take aways

I set myself the challenge when writing the component and for future components that it must be immediately understandable even to outsiders. That was kind of really fun. Also: