Close

Victor Rosas

VR & Multiplayer Programmer

4+ Years Experience | Unity & C# | VR & Multiplayer

Download CV

About Me

I'm Victor Rosas, a Gameplay Programmer with 4+ years of professional experience specializing in Unity and C#. My focus is multiplayer systems, gameplay mechanics, and complex interactive simulations — from award-winning VR industrial applications to co-op action games.

I have a proven track record of shipping award-winning titles, developing 300+ production scripts across large-scale codebases, and architecting real-time networked systems using client-server architecture. I'm passionate about clean, performant code and continuously push the limits of what gameplay can feel like.

When I'm not coding, I explore new game mechanics, stay sharp on industry trends, and find inspiration in the worlds around me.

4+ Years Experience
300+ Scripts Written
10+ Projects Shipped
1 Industry Award

Experience

Dataverso

Programmer

  • Architected and implemented a real-time multiplayer system using client-server network architecture, enabling seamless low-latency multi-user collaboration — directly comparable to online multiplayer game networking.
  • Designed and built a robust save/load system ensuring reliable, structured persistence of complex application state across sessions — directly applicable to open-world game state management and level streaming.
  • Developed complex gameplay-adjacent tools including a pattern recognition engine and algorithmic simulation systems, demonstrating large-scale, performance-conscious C# development.
  • Implemented cross-platform data export/import supporting CSV, OBJ, and FBX formats, ensuring compatibility and smooth data transfer across diverse platform targets.

Minverso

Gameplay Programmer / Lead

  • Shipped 'FLSMIDTH', an award-winning real-time interactive 3D application recognized with Best Development Award at Expomin and Perumin 2023 — delivering physics-driven player interactions, animations, and a responsive game-like environment to live audiences.
  • Developed 300+ modular Unity C# scripts to implement, upgrade, and maintain gameplay features across 4 major projects, with a strong focus on system efficiency and reusability across a large codebase.
  • Designed advanced player interaction and simulation systems using Unity physics, animation controllers, and VR principles — ensuring immersive, responsive and performant experiences.
  • Led full development lifecycle across 6 total projects in collaboration with designers and content creators, consistently delivering on tight deadlines.
  • Applied cross-platform development best practices throughout the Unity pipeline, ensuring consistent performance across target platforms.

Sudaka Games

Gameplay Programmer Intern

  • Designed and fixed 9 game levels to enhance player experience, applying level design and critical thinking.
  • Developed 20+ C# scripts including AI and randomization features, reducing bugs by 50%.
  • Improved UI placement and design by 40%, creating a more seamless player experience.
  • Contributed to 50%+ of new game content and features in close collaboration with the team.

Education

Universidad Andrés Bello

March 2019 – December 2022

Bachelor of Science in Game Design

Gained hands-on experience across individual and team game projects using state-of-the-art equipment and industry-experienced faculty. Built a strong portfolio spanning game programming, level design, and systems architecture. Developed a deep foundation in game development pipelines and collaborative production workflows.

Projects

Portonazo

Portonazo

Unity C# Co-op Beat-em-up

A 3rd-person beat-em-up for 2 players set in a gang-warfare revenge story. Rise through the ranks, battle enemy gangs across their territories, and uncover who murdered your older brother. Weapons, items, and teamwork are key.

View Project
El Desafio del Mago

El Desafío del Mago

Unity C# Puzzle Platformer

Navigate the magician's absurd and escalating challenge across increasingly difficult levels. A quirky world full of comedic elements wrapped around a clever puzzle-platformer experience.

View Project
Donny and Dinna

Donny & Dinna

Unity C# Co-op Puzzle

A cooperative puzzle game for two players who must work together to solve oxygen-recovery challenges. Communication and coordination are everything.

View Project
View More Projects
Mateo una Aventura Jurasica

Mateo: Una Aventura Jurásica

Unity C# Adventure Puzzle

A time-traveling child must collect ancient stones to return to the present. Help the dinosaurs and solve puzzles using the ancient statue to find your way home.

View Project

Code Samples

Things I actually built — and why they ended up the way they did

LagCompensatedShooter.cs
C# · Unity Netcode for GameObjects NetworkBehaviour · ServerRpc · Ring Buffer

Hit reg was broken early on — players swore they were on target but the server kept saying miss. Root cause: the server runs the raycast 80–200ms after the client fires, by which point everyone's already moved. This records hitbox transforms server-side at 20Hz in a linked list per player, then rewinds each one back to the client's fire timestamp before casting. Capped at 500ms of lookback — any longer and it starts to feel unfair going the other way.

using System.Collections.Generic;
using Unity.Netcode;
using UnityEngine;

public class LagCompensatedShooter : NetworkBehaviour
{
    [SerializeField] private Transform muzzle;
    [SerializeField] private float     range           = 25f;
    [SerializeField] private LayerMask hitMask;
    [SerializeField] private int       historyCapacity = 32;
    [SerializeField] private float     snapshotRate    = 20f;

    private struct HitboxFrame
    {
        public Vector3    Position;
        public Quaternion Rotation;
        public float      Timestamp;
    }

    private readonly Dictionary<ulong, LinkedList<HitboxFrame>> _history = new();
    private float _nextSnapshot;

    private void Update()
    {
        if (IsServer && Time.time >= _nextSnapshot)
        {
            _nextSnapshot = Time.time + 1f / snapshotRate;
            RecordAllHitboxes();
        }

        if (IsOwner && Input.GetButtonDown("Fire1"))
            FireServerRpc(muzzle.position, muzzle.forward, Time.time);
    }

    private void RecordAllHitboxes()
    {
        foreach (var client in NetworkManager.Singleton.ConnectedClientsList)
        {
            ulong id = client.ClientId;
            if (!_history.ContainsKey(id))
                _history[id] = new LinkedList<HitboxFrame>();

            Transform t = client.PlayerObject?
                .GetComponentInChildren<Collider>()?.transform;
            if (t == null) continue;

            _history[id].AddFirst(new HitboxFrame
            {
                Position  = t.position,
                Rotation  = t.rotation,
                Timestamp = Time.time
            });

            while (_history[id].Count > historyCapacity)
                _history[id].RemoveLast();
        }
    }

    [ServerRpc]
    private void FireServerRpc(Vector3 origin, Vector3 dir, float clientTs)
    {
        // rewind every hitbox to where it was when the client pulled the trigger
        var saved = new Dictionary<ulong, (Vector3 pos, Quaternion rot, Transform t)>();

        foreach (var pair in _history)
        {
            var frame = ClosestFrame(pair.Value, clientTs);
            if (frame == null) continue;

            var t = NetworkManager.Singleton.ConnectedClients[pair.Key]
                        .PlayerObject?.GetComponentInChildren<Collider>()?.transform;
            if (t == null) continue;

            saved[pair.Key] = (t.position, t.rotation, t);
            t.SetPositionAndRotation(frame.Value.Position, frame.Value.Rotation);
        }

        bool hit = Physics.Raycast(origin, dir, out RaycastHit info, range, hitMask);

        if (hit && info.collider.TryGetComponent(out IDamageable dmg))
            dmg.TakeDamage(25f, OwnerClientId);

        // restore everyone to the present before next frame
        foreach (var s in saved.Values)
            s.t.SetPositionAndRotation(s.pos, s.rot);
    }

    private static HitboxFrame? ClosestFrame(LinkedList<HitboxFrame> frames, float ts)
    {
        HitboxFrame? best     = null;
        float        bestDiff = float.MaxValue;

        foreach (var f in frames)
        {
            float diff = Mathf.Abs(f.Timestamp - ts);
            if (diff < bestDiff) { best = f; bestDiff = diff; }
            if (f.Timestamp < ts - 0.5f) break;   // don't look back more than 500ms
        }
        return best;
    }
}
ProjectileSimulator.cs
C# · Unity Job System + Burst Compiler IJobParallelFor · NativeArray · Swap-Remove

Had a crowd shooter level with 200+ simultaneous projectiles and was burning 2ms per frame just on movement updates — that's most of a 16ms budget gone right there. Moved all position/velocity/lifetime math into a Burst-compiled IJobParallelFor so it runs in parallel on worker threads. Physics.CheckSphere stays on the main thread after Complete() since Unity's physics API isn't thread-safe. Dead projectiles get swap-removed to keep the array packed. Went from ~2.4ms down to ~0.18ms for 512 active projectiles.

using Unity.Burst;
using Unity.Collections;
using Unity.Jobs;
using Unity.Mathematics;
using UnityEngine;

public class ProjectileSimulator : MonoBehaviour
{
    [SerializeField] private float gravity        = -18f;
    [SerializeField] private float drag           = 0.015f;
    [SerializeField] private int   maxProjectiles = 512;

    private NativeArray<float3> _positions;
    private NativeArray<float3> _velocities;
    private NativeArray<float>  _lifetimes;
    private NativeArray<bool>   _alive;
    private int                 _count;
    private JobHandle           _handle;

    [BurstCompile]
    struct SimulateJob : IJobParallelFor
    {
        public float               DeltaTime;
        public float               Gravity;
        public float               Drag;
        public NativeArray<float3> Positions;
        public NativeArray<float3> Velocities;
        public NativeArray<float>  Lifetimes;
        public NativeArray<bool>   Alive;

        public void Execute(int i)
        {
            if (!Alive[i]) return;

            Lifetimes[i] -= DeltaTime;
            if (Lifetimes[i] <= 0f) { Alive[i] = false; return; }

            float3 vel = Velocities[i];
            vel.y     += Gravity * DeltaTime;
            vel        = vel * (1f - Drag * DeltaTime);

            Positions[i]  += vel * DeltaTime;
            Velocities[i]  = vel;
        }
    }

    private void Awake()
    {
        _positions  = new NativeArray<float3>(maxProjectiles, Allocator.Persistent);
        _velocities = new NativeArray<float3>(maxProjectiles, Allocator.Persistent);
        _lifetimes  = new NativeArray<float>(maxProjectiles,  Allocator.Persistent);
        _alive      = new NativeArray<bool>(maxProjectiles,   Allocator.Persistent);
    }

    private void Update()
    {
        _handle.Complete();

        // collision + cleanup on main thread (can't do Physics calls inside jobs)
        for (int i = 0; i < _count; )
        {
            if (!_alive[i] || Physics.CheckSphere(
                    (Vector3)_positions[i], 0.12f,
                    LayerMask.GetMask("Enemy", "World")))
            {
                OnDied((Vector3)_positions[i]);

                // swap-remove: pull last element into this slot
                _count--;
                _positions[i]  = _positions[_count];
                _velocities[i] = _velocities[_count];
                _lifetimes[i]  = _lifetimes[_count];
                _alive[i]      = _alive[_count];
            }
            else i++;
        }

        _handle = new SimulateJob
        {
            DeltaTime  = Time.deltaTime,
            Gravity    = gravity,
            Drag       = drag,
            Positions  = _positions,
            Velocities = _velocities,
            Lifetimes  = _lifetimes,
            Alive      = _alive,
        }.Schedule(_count, 64);
    }

    public void Spawn(Vector3 pos, Vector3 vel, float life)
    {
        if (_count >= maxProjectiles) return;

        _handle.Complete();   // can't write to NativeArrays while job is running
        _positions[_count]  = pos;
        _velocities[_count] = vel;
        _lifetimes[_count]  = life;
        _alive[_count]      = true;
        _count++;
    }

    private void OnDied(Vector3 pos) { /* trigger pooled VFX here */ }

    private void OnDestroy()
    {
        _handle.Complete();
        _positions.Dispose();
        _velocities.Dispose();
        _lifetimes.Dispose();
        _alive.Dispose();
    }
}
VRPhysicsHand.cs
C# · Unity XR Interaction Toolkit Rigidbody · InputDevice · Haptics · Velocity Ring Buffer

Kinematic hands clip through geometry. AddForce-based hands feel floaty and lag a frame behind. This drives the Rigidbody by setting velocity directly toward a target transform each FixedUpdate — tight tracking with real physics collisions. The velocity ring buffer exists because grabbing a single-frame velocity at release gives garbage throw results if the player lets go at the top of their swing (that frame is nearly zero). Averaging 8 frames fixed throw detection immediately.

using System.Collections.Generic;
using UnityEngine;
using UnityEngine.XR;

[RequireComponent(typeof(Rigidbody))]
public class VRPhysicsHand : MonoBehaviour
{
    [SerializeField] private Transform target;
    [SerializeField] private float     posStrength     = 30f;
    [SerializeField] private float     rotStrength     = 20f;
    [SerializeField] private float     hapticThreshold = 2.5f;
    [SerializeField] private XRNode    hand            = XRNode.RightHand;

    private Rigidbody               _rb;
    private InputDevice             _device;
    private readonly Queue<Vector3> _linHistory = new();
    private readonly Queue<Vector3> _angHistory = new();
    private const int               HistorySize = 8;

    private void Awake() => _rb = GetComponent<Rigidbody>();

    private void OnEnable()
    {
        InputDevices.deviceConnected    += OnConnected;
        InputDevices.deviceDisconnected += OnDisconnected;
        TryAcquireDevice();
    }

    private void OnDisable()
    {
        InputDevices.deviceConnected    -= OnConnected;
        InputDevices.deviceDisconnected -= OnDisconnected;
    }

    private void FixedUpdate()
    {
        // position: drive velocity toward target each physics step
        Vector3 delta    = target.position - transform.position;
        _rb.velocity     = delta * posStrength;

        // rotation: convert delta quaternion to angular velocity
        Quaternion dRot  = target.rotation * Quaternion.Inverse(transform.rotation);
        dRot.ToAngleAxis(out float angle, out Vector3 axis);
        if (!float.IsInfinity(axis.x))
            _rb.angularVelocity = axis * (angle * Mathf.Deg2Rad * rotStrength);

        Enqueue(_linHistory, _rb.velocity);
        Enqueue(_angHistory, _rb.angularVelocity);
    }

    private static void Enqueue(Queue<Vector3> q, Vector3 v)
    {
        q.Enqueue(v);
        if (q.Count > HistorySize) q.Dequeue();
    }

    // call these from the grab/release handler instead of reading rb.velocity directly
    public Vector3 GetThrowVelocity() => Average(_linHistory);
    public Vector3 GetThrowAngular()  => Average(_angHistory);

    private static Vector3 Average(Queue<Vector3> q)
    {
        if (q.Count == 0) return Vector3.zero;
        Vector3 sum = Vector3.zero;
        foreach (var v in q) sum += v;
        return sum / q.Count;
    }

    private void OnCollisionEnter(Collision col)
    {
        float impulse = col.impulse.magnitude;
        if (impulse < hapticThreshold || !_device.isValid) return;

        float strength = Mathf.Clamp01(impulse / 12f);
        float duration = Mathf.Lerp(0.05f, 0.3f, strength);
        _device.SendHapticImpulse(0, strength, duration);
    }

    private void TryAcquireDevice()
    {
        var devices = new List<InputDevice>();
        var flags   = InputDeviceCharacteristics.Controller |
                      (hand == XRNode.RightHand
                          ? InputDeviceCharacteristics.Right
                          : InputDeviceCharacteristics.Left);
        InputDevices.GetDevicesWithCharacteristics(flags, devices);
        if (devices.Count > 0) _device = devices[0];
    }

    private void OnConnected(InputDevice d)
    {
        if (d.characteristics.HasFlag(InputDeviceCharacteristics.Controller))
            TryAcquireDevice();
    }

    private void OnDisconnected(InputDevice d)
    {
        if (_device == d) _device = default;
    }
}
SaveSystem.cs
C# · Async/Await · Thread Safety SemaphoreSlim · CancellationToken · Atomic Write

File.WriteAllText on the main thread was causing 40–80ms hitches when saves got large. Moved writes to a background thread via Task.Run, wrapped in a SemaphoreSlim so two saves can't race each other. CancellationTokenSource means if the player saves twice fast, the first write gets cancelled instead of both landing out of order. Atomic write via a .tmp rename prevents a corrupted save if the game crashes mid-write — the rename only happens after the full write completes.

using System;
using System.IO;
using System.Threading;
using System.Threading.Tasks;
using UnityEngine;

public sealed class SaveSystem
{
    private static readonly Lazy<SaveSystem> _lazy =
        new Lazy<SaveSystem>(() => new SaveSystem());

    public static SaveSystem Instance => _lazy.Value;

    private readonly SemaphoreSlim       _lock = new SemaphoreSlim(1, 1);
    private          CancellationTokenSource _cts;

    private SaveSystem() { }

    public async Task SaveAsync<T>(string slot, T data) where T : class
    {
        // cancel any in-flight save for this slot — last write wins
        _cts?.Cancel();
        _cts?.Dispose();
        _cts = new CancellationTokenSource();
        var token = _cts.Token;

        await _lock.WaitAsync(token);
        try
        {
            token.ThrowIfCancellationRequested();
            string json = JsonUtility.ToJson(data, prettyPrint: false);
            await WriteAtomicAsync(SlotPath(slot), json, token);
        }
        catch (OperationCanceledException)
        {
            Debug.Log($"[SaveSystem] '{slot}' write superseded by a newer save.");
        }
        finally
        {
            _lock.Release();
        }
    }

    public async Task<T> LoadAsync<T>(string slot) where T : class, new()
    {
        string path = SlotPath(slot);
        if (!File.Exists(path)) return new T();

        await _lock.WaitAsync();
        try
        {
            string raw = await Task.Run(() => File.ReadAllText(path));
            return JsonUtility.FromJson<T>(raw) ?? new T();
        }
        finally
        {
            _lock.Release();
        }
    }

    private static async Task WriteAtomicAsync(
        string path, string json, CancellationToken ct)
    {
        string tmp = path + ".tmp";
        await Task.Run(() => File.WriteAllText(tmp, json), ct);
        ct.ThrowIfCancellationRequested();
        File.Move(tmp, path, overwrite: true);   // atomic on same filesystem
    }

    private static string SlotPath(string slot) =>
        Path.Combine(Application.persistentDataPath, slot + ".json");
}

Skills

Languages

  • C#
  • C++

Game Engines

  • Unity
  • Unreal Engine
  • Blueprints (UE)

Specialties

  • Multiplayer / Netcode
  • VR Development
  • Gameplay Systems
  • Level Design
  • Game Balancing
  • AI Programming

Tools

  • Blender
  • Adobe Photoshop
  • Adobe Illustrator
  • Git

Soft Skills

  • Teamwork
  • Adaptability
  • Quick Learner
  • Leadership

Get in Touch