Galdr: Forgotten Songs

Category

Game

Status

Completed

Duration

7 Weeks

Team Size

13 Members

Role

Gameplay & Systems Programmer

Engine

Unity

Languages

C#

This game was a 7-week project made in Unity. My role was being both

systems programmer and gameplay programmer on this project,

responsible for implementing a system for handling all the interactables that

the player can interact with, which includes items and puzzle mechanics.

private void Update() {
    if (this.interactablesInRange.Count > 0) {
        GameObject latestGameObject = this.interactablesInRange[^1];
        bool activeGameObject = true;

        if (this.hasActivatedOrderedButtons) {
            this.ShowInteractPrompt(InteractionType.BUTTON);
            this.hasActivatedOrderedButtons = false;
        }

        if (this.hasDeactivatedOrderedButtons) {
            Interactable latestAvailableInteractable = this.GetLastInteractableBySkippingType(
                typeof(Puzzle_ActivateButton));
            if (latestAvailableInteractable != null) {
                this.ShowInteractPrompt(latestAvailableInteractable);
            } else {  InteractPrompt.Instance.HideInteractPrompt();
            }
            this.hasDeactivatedOrderedButtons = false;
        }

        if (this.hasActivatedOneOrderedButton) {
            activeGameObject = false;
            this.hasActivatedOneOrderedButton = false;
        }

        if (this.hasActivatedClickButton) {
            activeGameObject = false;
            InteractPrompt.Instance.HideInteractPrompt();
            this.hasActivatedClickButton = false;
        }

        if (this.hasPickedUp) {
            this.hasPickedUp = false;
            activeGameObject = false;
            this.toBeDestroyed = true;
        }

        if (latestGameObject == null) {
            activeGameObject = false;
        }

        if (!activeGameObject) {
            this.interactablesInRange.RemoveAt(this.interactablesInRange.Count-1);
            if (this.toBeDestroyed) {
                Destroy(latestGameObject);
            }

            if (this.interactablesInRange.Count != 0) {
                latestGameObject = this.interactablesInRange[^1];

                if (latestGameObject != null) {
                    Interactable latestInteractable = this.GetLastInteractable();
                    if (latestInteractable.GetType() == typeof(Puzzle_ActivateButton) &&
                            ((Puzzle_ActivateButton)latestInteractable).
                            GetButtonInteractionType() == InteractionType.BUTTON &&
                            ((Puzzle_ActivateButton)latestInteractable).IsButtonActive()) {
                        this.ShowInteractPrompt(InteractionType.BUTTON);
                    } else if (latestInteractable.GetType() == typeof(Puzzle_ClickSendPuzzleEvent) &&
                            ((Puzzle_ClickSendPuzzleEvent)latestInteractable).
                            GetButtonInteractionType() == InteractionType.BUTTON &&
                            !((Puzzle_ClickSendPuzzleEvent)latestInteractable).IsButtonActive()) {
                        this.ShowInteractPrompt(InteractionType.BUTTON);
                    } else if (latestInteractable.GetInteractionType() is InteractionType type && (
                            type == InteractionType.VERBAL ||
                            type == InteractionType.ITEM ||
                            type == InteractionType.SPELL_ATTRIBUTE)) {
                        this.ShowInteractPrompt(type);
                    }
                }
            } else { InteractPrompt.Instance.HideInteractPrompt(); }
        }
    }
}

private void OnTriggerEnter(Collider other) {
    if (other.gameObject.TryGetComponent(out Interactable interactable)) {
        bool activeInteractable = true;

        if (interactable.GetType() == typeof(Puzzle_ClickSendPuzzleEvent)) {
            if (!((Puzzle_ClickSendPuzzleEvent)interactable).IsButtonActive()) {
                this.interactablesInRange.Add(other.gameObject);
            } else {
                activeInteractable = false;
            }
        } else {
            this.interactablesInRange.Add(other.gameObject);
        }

        if (activeInteractable) {
            this.ShowInteractPrompt(interactable);
        }
    }
}

private void OnTriggerExit(Collider other) {
    if (other.gameObject.TryGetComponent(out Interactable interactable)) {
        this.interactablesInRange.Remove(other.gameObject);

        if (this.interactablesInRange.Count != 0) {
            Interactable newInteractableInRange = this.GetLastInteractable();
            this.ShowInteractPrompt(newInteractableInRange);
        } else { InteractPrompt.Instance.HideInteractPrompt

Collision: Interactable Handling

Whenever the player is able to reach

an interactable, it is placed in a list.

The system then determines

whether the player can actually

interact with the interactable (f.e.

if a puzzle mechanism has already

been activated, then the interactable

is removed from the list) and is then

presented in the UI with an icon, or

not. The system also makes sure

that the player always prioritizes the

latest interactable that the player has

found and is still reachable.

Interaction: Player Input

Handles the latest interactable

that the player has detected and

wants to interact with. Data from

a ScriptableObject that

represents a specific interactable

is returned, in case the player

object needs to handle that data

and change something in the

game (opening the journal,

picking up an item, activate a

puzzle mechanism etc).

public void OnInteract(InputAction.CallbackContext obj) {
    if (isPlayingMusic || Player.Instance.isOutOfBody) return;

    Interactable interactable = interactZone.GetLastInteractable();
    if (interactable != null) {

        SO_InteractableData result = interactable.Interact();

        if (result != null) {
            switch (interactable) {
                case InteractableArtifact artifact:
                    Inventory.TryCollectItem(artifact);
                    artifact.GetGameObject().GetComponent<Collider>().enabled = false;
                    this.interactZone.PickedUpInteractable();
                    JournalManager.Instance.OpenJournalToPage(0);
                    break;
                case InteractableSignPost signPost:
                    break;
                case InteractableNPC npc:
                    Inventory.TryUnlockNpc(npc); 
                    break;
                case InteractableTransition transition:
                    break;
                case InteractableDoor door:
                    break;
                case InteractableSpellAttribute spellAttribute:
                    Inventory.TryCollectItem(spellAttribute);
                    spellAttribute.GetGameObject().GetComponent<Collider>().enabled = false;
                    this.interactZone.PickedUpInteractable();
                    JournalManager.Instance.OpenJournalToPage(2);
                    break;
                case Puzzle_ActivateButton puzzleActivateButton:
                    break;
                case Puzzle_ClickSendPuzzleEvent clickPuzzleEvent:
                    if (clickPuzzleEvent.IsButtonActive()) {
                        this.interactZone.ActivatedClickButton();
                    }
                    break;
                default: // If nothing matches
                    Debug.Log(interactable.GetType().Name);
                    break;
            }
        } else {
            if (interactable.GetType() == typeof(Puzzle_ClickSendPuzzleEvent)) {
                if (((Puzzle_ClickSendPuzzleEvent)interactable).IsButtonActive()) {
                    this.interactZone.ActivatedClickButton();
                }
            }
        }
    } else {
        Debug.Log("No interactable found"

© 2026 Sebastian Valck. All rights reserved.

Sebastian's Portfolio