itch.io is community of indie game creators and players

Devlogs

In-game PC breakdown

Host Matter
A downloadable game for Windows

Some people dropped into my Discord and were wondering how I made the in-game PC. When I was making it, I also tried looking online for examples of how these things are usually done, but I couldn’t really find anything for a simple PC setup like the one in Host Matter. So I figured I’d provide some code and setup examples to show how I built it. It’s probably far from perfect and definitely not the most optimal approach, but this was the best I could come up with at the time. Here how it looks in game:

I’ll go through it piece by piece to show how everything is assembled.

First, we need to create a canvas that will be used for the PC UI. This is a canvas with its render mode set to Screen Space – Camera, and it has its own camera in the scene attached to it.

Then we need to create a Render Texture and set this Camera's output to the render texture we created. Projection is set to orthographic so that image is rendered to the texture with no perspective distortion.

Then we create a Material and set Base map to the render texture we created.

Create a default Unity Quad mesh and attach this Material to the quad. So now we are rendering our canvas onto the quad. 

So each tab has premade UGUI content that i disable by default and then toggle via code when user interacts with tab UI elements.

On Canvas i have attached a Mono Behavior that handles the logic for switching between our content.

There quite a decent amount of code that handles the content of the PC, i don't think it makes sense putting it here, but basically there are methods for toggling Canvas elements on and off, loading data for in game messages and stuff like that, if you are interested in some specifics of this code, you can message me on discord and i will share. So in short its things like this:

Important note here, is that onclick even will be invoked from the ScreenCursorController script which i provided below.

I guess i will just drop whole code for this, it has everything documented by comments, so should be pretty self explanatory. But agin if you have any questions about it, let me know.

using UnityEngine;
using UnityEngine.UI;
using UnityEngine.EventSystems;
using System.Collections.Generic;
public class ScreenCursorController : MonoBehaviour
{
    [Header("Screen Setup")]
    [SerializeField] private Canvas uiCanvas;
    [SerializeField] private GameObject displayQuad;
    [SerializeField] private RenderTexture pcScreenRenderTexture;
    [SerializeField] private Canvas inGameUICanvas; // contains crosshair and other UI elements that we need to disable when in screen mode
    [Header("Cursor Setup")]
    [SerializeField] private GameObject cursorSprite;
    [SerializeField] private Vector2 cursorOffset = Vector2.zero;
    [Header("Interaction Settings")]
    [SerializeField] private bool hideSystemCursor = true;
    [Header("Audio")]
    [SerializeField] private SoundID clickSound1 = SoundID.MouseClick1;
    [SerializeField] private SoundID clickSound2 = SoundID.MouseClick2;
    private CameraManager cameraManager;
    private Camera currentCamera;
    private bool isScreenMode = false;
    private bool wasPaused = false;
    private Vector2 screenBounds;
    public bool IsScreenMode => isScreenMode;
    private void Start()
    {
        cameraManager = GameManager.Instance.cameraManager;
        if (uiCanvas == null || inGameUICanvas == null)
        {
            Debug.LogError("ScreenCursorController: UI Canvas is not assigned!");
            return;
        }
        if (displayQuad == null)
        {
            Debug.LogError("ScreenCursorController: Display Quad is not assigned!");
            return;
        }
        CalculateScreenBounds();
    }
    private void CalculateScreenBounds()
    {
        RectTransform canvasRect = uiCanvas.transform as RectTransform;
        screenBounds = canvasRect.rect.size;
    }
    private void Update()
    {
        if (!isScreenMode) return;
        // Check if game is paused
        bool isCurrentlyPaused = GameManager.Instance != null && GameManager.Instance.currentState == GameManager.GameState.Paused;
        // If we just entered pause state, disable cursor control
        if (isCurrentlyPaused && !wasPaused)
        {
            if (cursorSprite != null)
                cursorSprite.SetActive(false);
        }
        // If we just exited pause state, re-enable cursor control
        else if (!isCurrentlyPaused && wasPaused)
        {
            if (cursorSprite != null)
                cursorSprite.SetActive(true);
            // Re-establish screen mode cursor state
            Cursor.lockState = CursorLockMode.None;
            Cursor.visible = false;
        }
        wasPaused = isCurrentlyPaused;
        // Don't update cursor if game is paused
        if (isCurrentlyPaused) return;
        Camera previousCamera = currentCamera;
        if (cameraManager != null)
        {
            currentCamera = cameraManager.CurrentActiveCamera;
        }
        if (currentCamera == null) return;
        UpdateCursorPosition();
        if (Input.GetMouseButtonDown(0))
        {
            CheckUIElementUnderCursor();
        }
    }
    private Vector2 GetUIScreenPoint()
    {
        if (currentCamera == null) return Vector2.zero;
        Vector3 mousePosition = Input.mousePosition;
        // Scale mouse position to match render texture resolution
        if (pcScreenRenderTexture != null)
        {
            mousePosition = new Vector3(
                mousePosition.x * pcScreenRenderTexture.width / Screen.width,
                mousePosition.y * pcScreenRenderTexture.height / Screen.height,
                mousePosition.z
            );
        }
        // For Screen Space - Camera mode, we need to use the canvas's camera
        Camera canvasCamera = uiCanvas.worldCamera;
        if (canvasCamera == null)
        {
            Debug.LogWarning("Canvas has no camera assigned!");
            return Vector2.zero;
        }
        // Convert screen position to canvas space
        if (RectTransformUtility.ScreenPointToLocalPointInRectangle(
                uiCanvas.transform as RectTransform,
                mousePosition,
                canvasCamera,
                out Vector2 localPoint))
        {
            // For Screen Space - Camera, the local coordinates are relative to the canvas center
            // So we clamp to half the canvas size in each direction
            float halfWidth = screenBounds.x * 0.5f;
            float halfHeight = screenBounds.y * 0.5f;
            localPoint.x = Mathf.Clamp(localPoint.x, -halfWidth, halfWidth);
            localPoint.y = Mathf.Clamp(localPoint.y, -halfHeight, halfHeight);
            return localPoint;
        }
        else
        {
            Debug.LogWarning("Failed to convert screen point to local point");
        }
        return Vector2.zero;
    }
    private void UpdateCursorPosition()
    {
        if (cursorSprite == null) return;
        Vector2 uiScreenPoint = GetUIScreenPoint();
        Vector2 clampedPosition = uiScreenPoint + cursorOffset;
        SetCursorPosition(clampedPosition);
    }
    private void SetCursorPosition(Vector2 screenPosition)
    {
        if (cursorSprite == null) return;
        RectTransform cursorRect = cursorSprite.GetComponent<RectTransform>();
        if (cursorRect != null)
        {
            cursorRect.anchoredPosition = screenPosition;
        }
    }
    public void EnterScreenMode()
    {
        if (isScreenMode) return;
        isScreenMode = true;
        wasPaused = false;
        // Only hide cursor if game is not paused
        if (GameManager.Instance == null || GameManager.Instance.currentState != GameManager.GameState.Paused)
        {
            Cursor.visible = false;
        }
        Cursor.lockState = CursorLockMode.None;
        inGameUICanvas.gameObject.SetActive(false);
    }
    public void ExitScreenMode()
    {
        if (!isScreenMode) return;
        isScreenMode = false;
        wasPaused = false;
        // Reset cursor state
        Cursor.lockState = CursorLockMode.Locked;
        Cursor.visible = false;
        inGameUICanvas.gameObject.SetActive(true);
    }
    public void CheckUIElementUnderCursor()
    {
        if (cursorSprite == null || uiCanvas == null) return;
        // Use the mouse position directly, but scale it to match the render texture resolution
        Vector3 mousePosition = Input.mousePosition;
        // Scale mouse position to match render texture resolution (same as GetUIScreenPoint)
        if (pcScreenRenderTexture != null)
        {
            mousePosition = new Vector3(
                mousePosition.x * pcScreenRenderTexture.width / Screen.width,
                mousePosition.y * pcScreenRenderTexture.height / Screen.height,
                mousePosition.z
            );
        }
        PointerEventData pointerData = new PointerEventData(EventSystem.current)
        {
            position = mousePosition
        };
        GraphicRaycaster raycaster = uiCanvas.GetComponent<GraphicRaycaster>();
        List<RaycastResult> results = new List<RaycastResult>();
        raycaster.Raycast(pointerData, results);
        int randomIndex = Random.Range(0, 2);
        SoundID clickSound = randomIndex == 0 ? clickSound1 : clickSound2;
        GameManager.Instance.audioManager.PlaySoundAtPosition(clickSound, displayQuad.transform.position);
        foreach (var result in results)
        {
            ExecuteEvents.Execute(result.gameObject, pointerData, ExecuteEvents.pointerClickHandler);
        }
    }
    private void OnDestroy()
    {
        Cursor.lockState = CursorLockMode.None;
    }
}

I think this is pretty much it, there also parts of the setup that handle interaction and switcing of the camera that looks at the PC, but i don't think its in the scope of this post.

Download Host Matter
Leave a comment