C# / Unity Tip: Embedding quotes within a string

We all know how it goes. As soon as we are adding a bit of juice to our strings, it get’s messy. As I was building a modal for my UI System and writing down some placeholder texts, my OCD began to tingle.

C#
string message = $"Do you want to leave this level? \n Loot collected: {coinpercent} %  <sprite=\"uiicons\" name=\"coin\">";

I dont know why, but the standard “escape with backslash” approach is somewhat unreadable for me. One method would be to shift the problem and use string interpolation like that example below, but that also makes it a bit more complicated.

C#
string spriteTag = "<sprite=\"uiicons\" name=\"coin\">";
string title = $"Do you want to leave this level? \n Loot collected: {coinpercent} % {spriteTag}";

Precede with @ and use double quotes

A quick google search later (ChatGPT failed) I found another approach, that I was unaware of.

If you precede the string with the “@” character, you can use a double set of quotation marks to indicate a single set of quotation marks within a string. If you prefix the string with the “@” character, you can use a set of double quotation marks to specify a single set of quotation marks within a string. And it makes it more readable. Have a look:

C#
//Standard Method
string message = $"Do you want to leave this level? \n Loot collected: {coinpercent} %  <sprite=\"uiicons\" name=\"coin\">";

//String Interpolation
string spriteTag = "<sprite=\"uiicons\" name=\"coin\">";
string title = $"Do you want to leave this level? \n Loot collected: {coinpercent} % {spriteTag}";

//@Method
string message = $@"Do you want to leave this level? 
Loot collected: {coinpercent} % <sprite=""uiicons"" name=""coin"">";

One Caveat however: The @ character in C# treats the string as a verbatim string literal, which means it includes all whitespace and line breaks exactly as they are written. This includes the indentation at the start of the line. So if you are in Code and use indentation, it gets shifted.

But nevertheless. Cool to learn some different approaches.

C# / Unity Tip: Get the index from end of an array or list

So, I was in the midst of developing my game when I wanted to retrieve the penultimate (I looked it up, it exists) member of my pathfinding array to determine if the point before the ULTIMATE target is reachable. So, naturally, I used the old self-referencing line to get the work done and created a static method to do the job.

C#
        ...PathExtensions.GetPositionOnPath(path, path.vectorPath.Count -2)...        
        
        /// Method to retrieve the index from the end
        
        public static Vector3 GetPositionOnPath(Path path, int i)
        {
            return path.vectorPath[i];
        }

But wait! Something in the deeper parts of my brain itched. There was some other way to achieve this much more elegantly. And yes, I was right! Again!

INTRODUCING THE ^_____^

After a quick Google search, I found the answer. With C# 8.0 and later, you can indeed use the caret symbol (^) to index arrays, which is known as the “index from end” feature. Nice! It is now much more readable and easier to maintain.

C#
        ...PathExtensions.GetPositionOnPath(path,^2)...
        
        /// Much simpler method to retrieve the index from the end
        
        public static Vector3 GetPositionOnPath(Path path, Index i)
        {
            return path.vectorPath[i];
        }

Some examples:

C#
        string[] colors = { "red", "blue", "green", "yellow", "orange", "purple", "pink", "brown", "black", "white" };

        // Accessing the last element of the array:
        Debug.Log(colors[^1]);
        // Output: "white"

        // Accessing the second to last element of the array:
        Debug.Log(colors[^2]);
        // Output: "black"

        // Accessing the third element of the array:
        Debug.Log(colors[^3]);
        // Output: "brown"

        // Accessing the first element of the array:
        Debug.Log(colors[^10]);
        // Output: "red"

Hope that helps! Bye.

Official documentation:

https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/operators/member-access-operators#index-from-end-operator-

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: Refactoring Interactive Screens

Since the beginning of my project, I have placed great emphasis on ensuring that the in-game screens are interactive and have a credible interface that fits the scenario. Sometimes with more success, sometimes with less. I had three different ways to interact with in-game desktops:

  1. Transmitting UI events via Raycast and translating Collider coordinates into Canvas coordinates.
  2. Translating mouse position to relative cursor graphic position within the Canvas. Raycasting from the cursor graphic to the contents.
  3. Activating a fullscreen UI upon interaction with the PC.

As I prioritized immersion, it was initially essential to me that when using the interfaces, the environment and the monitor were visible. While it looked cool and somewhat worked, it only worked partially. Sometimes clicks were not registered, double clicks even more often, and dragging elements was a different story altogether. Stacked Canvases? Forget it. They didn’t even exist for my raycasts. My self-hacked option number 2 actually worked the best. So well that it was the standard for most of my project’s development time. The problem was that it was so dependent on the cursor that, depending on the cursor’s position on the Canvas, it could only reach a certain height or width. Since you can’t directly control and set the mouse cursor to zero in Unity, this issue was relatively common. Also. The different Unity Event Systems for UI controls still give me a headache. Draghandler? Doubleclickhandler? Handlerhandler? This was just trial and error all the day.

Stick to the MVP

So, I reflected on what it means to build a prototype. It should work, and the polish comes later. If I really wanted to deliver something robust, I had to rewrite this core component. So, I did just that. After hours of research, along with many people facing the same issues as me, I came across several valuable insights (https://forum.unity.com/threads/handling-pointer-events-on-render-texture.1158272/#post-7432208) and tutorials (https://www.youtube.com/watch?v=fXsdK2umVmM) that helped me a lot, but there always seemed to be some crucial thing that didn’t quite work.

I’ve been waiting for a sale on Dream OS, but somehow, it hasn’t happened. So, I watched the videos by Michsky, the publisher of Dream OS, and there it was, a completely different approach. Apparently, they simply open a Screen Space Canvas, cleverly disguised through a well-directed camera movement. Looked easy enough. So, I opened Unity, placed the computer preset, and started experimenting. Within two hours, I had a completely robust solution up and running.

My simple Solution

It’s not perfect yet, but it’s a good start!
  • Put a Canvas in Screen Space Camera-Mode and Render a Render Texture
  • Put a Canvas in the Game on a Display Object with the same Aspect Ratio
    • Inside is a Raw Image which displays the Render Texture and fill the Canvas
    • This ensures that it’s the same size and lighting (unlit) as the Full Screen Overlay Canvas
      • You can also use a normal Plane and use an Unlit Material with the Render Texture as Base Texture, but I didn’t quite get it right.
  • Put a Cinemachine Virtual Camera with a custom Blend in Front of the In-World-Canvas and fit it in
  • The Player Interaction with the object does the following things
    • Deactivate The Player Controller
    • Activate the Cinemachine Virtual Camera to initiate the Blending “animation”
    • After a given time (here 1,5 seconds) put the Screen Spaces Camera Mode to Screen Space Overlay
  • Check if the player is in visible range to enable or disable the render texture camera canvas to save performance. I just checked with a trigger volume. Works great.
  • Done!

The immersion might not be as stunning as with the world space solutions, but the UI here is native, so all functions work flawlessly. I think I’ll keep it for now. Additional benefit: I dont have to implement animations for the character.

To really show off the depressing office flair, I want to go for a 90s Windwos aesthetic. Luckily, there’s a cool asset called 90’s Desktop UI that I can use as a base. Together with the aesthetics of the Chicago95 icon set, I’m sure I’ll conjure up some excellent desktops. I’m looking forward to it.

Limitations: The Blend will only work in a 16:9 aspect ratio. I’m not a Unity UI Expert yet.

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:

Starting from Scratch – A Retrospective

I’ve found myself in this situation again. This is just a rant about myself. What was initially a beautiful project, designed to be a lean prototype, has once more spiraled out of control. That means dealing with dozens of assets all intricately connected, expensive plugins, overloaded integrations, and a web of dependencies. Coupled with a switch from URP to Build-In Render Pipeline mid project due to compatibility reasons. The project is fine per se. I have no big problems and it still is maintainable. But it feels bloated with redundant files and piles of stuff I will never use again.

So, here we go again, launching attempt number three – a fresh start. My goal is to bring my vision to life without getting bogged down in micromanagement. That’s why I’ve decided to kick things off with my first devlog. Hi there!

Introduction

I’m Tobias, and this marks the very first devlog of my journey as a solo developer. Over two decades in the game industry have taught me many things… not programming, though. Instead, I’ve learned how to navigate office dynamics and human interactions in that environment. You might be thinking, “Office? Do you mean the room where I keep my gaming rig?” Nope. I’m referring to a place where a diverse group of people must coexist and where 90% of problems are dumped on just 1% of the employees – the IT department. This is where our protagonist, Peter Struggle, comes into play.

Game Concept

Meet Peter, the lone IT employee in a company plagued by mountains of daily main and secondary tasks. His mission: to minimize open tasks by the end of the day. However, every interaction with a fellow employee adds a new task to his list. Players face a choice: take the risky but quick path, potentially piling up tasks, or opt for stealth and diversion to keep interactions, and subsequently tasks, to a minimum. When Peter has zero tasks left, he gains bonuses that can be used to enhance his skills and tools. The gameplay draws inspiration primarily from games like Splinter Cell and Hitman, incorporating casual graphics and humor to fit the scope of a solo developer, mixed with popcultural references to IT Crowd, The Office, Severance, Mr. Robot and Silicon Valley.

Third Person Controller

Ah, the allure of Feature Creep strikes once more. But I started out on the right foot with the versatile, community driven and beautifuly barebones “Etra’s Starter Assets” controller – a free gem that I can’t recommend enough. It’s a straightforward and remarkably efficient controller, complete with some useful extras.

So, I started integrating Peters abilities – crouching, shadow detection, sneaking, interaction UI, and even an extended interaction component. No issues there. Integrating Rewired? Smooth sailing. Implementing Final IK? Mission accomplished. Even integrating movement via behavior trees was a breeze. The true power lies in the asset’s well-structured simplicity. No mysterious scriptables or confusing dependencies. If you need component X, simply add it wherever you want. Need to disable an ability? Just call .enabled=false. Need an extension? Use the event system. The Unity way.

The temptation of the Unity Summer Sale – The sunk cost fallacy strikes back

But then the summer sale came. After buying and trying out “Invector,” I thought I was through with character controllers. I have a strong aversion to bloated assets that weigh down entire projects. However, the highly praised Opsive Third Person Controller caught my eye during the Unity Summer Sale. What could possibly go wrong? After all, it’s the best-selling asset around, and the Opsive Behavior Tree is a seriously awesome asset. So, I took the plunge and made the purchase, despite some lingering doubts – even after watching this video:

“It feels bloated, but also like something is missing.” I cannot agree more.

So, after spending days of reading documentation, watching tutorial videos, and trying to piece everything together, I found myself filled with buyer’s remorse. I was saddened by the fact that I had abandoned my beautiful custom controller for this massive monolith of an asset and lost the speed and traction, that I started with. Integrating it with other packages, even official integration packages, became a real headache. I was overwhelmed. I felt frustrated spending hours on simple tasks like incorporating my sound manager system or getting a feel for the locomotion and camera movement. In the end, I used the old approach, placing function on top without utilizing the built-in functions of the character controller. Unfortunately, it felt like adding a fifth wheel to a car – an awkward and unfitting addition. I always have the feeling that it could break any second.

This is just an example for a general problem I have with some premade assets. The Opsive Character Controller is a very good asset, the developer is awesome and the support is stellar. But in a fast iterating prototyping phase, it just slows me down. Others may find the “completeness” of the asset comforting, but for me its ballast. I just want to have a blank slate that I can hook some components on or remove them, without breaking everything. But this is something I learned the hard way. Keep it specialized. There is no “one size fits all” solution in game dev.

What I definitely will use the Opsive Character Controller for in my prototyping phase is the awesome integration with the Behavior Designer or AI Navigation in general. Its just flawless and setup in under a minute. And thanks to the extended animation system, it just looks good from start.

I have already prepared some Behavior Trees and Character Sheets that I can reuse 1:1. What would a stealth game be without “Wait, who’s there? Oh, it was just the wind”. Only that in my game it will sound more like “Hey Peter! I have a problem with my PC. Can you help me?”

Modular Asset Packs

As the owner of quite a few modular asset packs from Synty and co., I am naturally often tempted to load everything I have directly into the project. That’s obvious. You want to “quickly and easily” click together a few levels. I noticed that using pre-made graphics tempted me to focus on too many details already. Therefore, I decided to work only with prototypes from now until the basic systems are finished. This should help me to concentrate on the essential issues of the development. I will go back to a basic tool for prototyping and level design: Greyboxing with Probuilder. Simple and easy. This way, questions of believability should only arise later, when it comes to the final level design. For now, it’s about “finding the fun”.

The Office Scene I tried to fit in all my mechanics made of prebuild modular Assets.

I found it challenging to find a balance in level design that emphasizes stealth mechanics while providing a believable environment. Normal office buildings consist of long corridors and individual offices with little open space. Many of the stealth game mechanics I want to incorporate require the player to have a good overview and not be constantly surprised by enemies running out of offices or the like. Using premade assets led me to “work around” the mechanics to find solutions for the assets I have. I think that is a big game design mistake I made. But maybe I will go in further detail in a later post about stealth mechanics.

New Beginnings – Have you tried to turn it off and on again?

And so, the first entry of Struggle Peter’s devlog begins with “Embracing the Awkward.” Back to the drawing board. Starting a new project. Salvaging the old one. And before putting in assets, really thinking about “Do I really need this in my folder?”, “Does it serve my vision?”. I’m more motivated than ever to bring my Project to life. It’s a learning project. Not a big one. All the problems I encountered, I encountered countless times before. Maybe this time I learned my lesson. Maybe. Man, I love this.


Notion is a big help to stay organized.

My Plan for the future of the project

  • Stay agile at all costs
    • Stick to the MVP
    • Delivering fast working increments
    • Let people test
    • Reduce Micromanagement
    • Have a playable version ready in one month
      • Post iterations on itch.io
  • Managing expectations
    • I started this project to learn how to develop and release a simple game
    • Even with my experience, I’m still a beginner and nobody expects my first real game to be perfect
  • Keep track of and report the progress, even If I am the only one who will ever read it (this blog)
  • Always try to clean up the mess after a long coding session.
    • Organize so that it is also quickly understandable and readable by others. Even if it is the future self. He kinda forgets everything all the time.
  • Keep Notion as my Game Design Document and Vision Board, its excellent for that
    • Mark MVP Features to keep organized
  • Don‘t buy assets until it‘s clear where the project is going – ignore FOMO
    • Understanding and integrating new and unknown assets take a lot of time. I dont have a lot of time.
  • Assets that I feel comfortable with and will add to the project to boost productivity