Master the skies in a high-speed aerial gauntlet.
Wingman is an arcade-style air racing game set in a vibrant archipelago. Race through canyons, ruins, and windmills across challenging tracks, testing your precision, speed, and flight skills. Easy to pick up but hard to master, Wingman offers fun for both casual and competitive players.
Wingman is an air racing game that challenges players to master their flying skills across a vibrant archipelago. Players navigate through varied landscapes filled with environmental obstacles like canyons and windmills while striving to complete time trials and races. With an emphasis on precision and speed, the game encourages replayability as players aim to beat personal bests and climb the leaderboards.
As a gameplay programmer on Wingman, I was responsible for implementing key systems that enhanced both the functionality and player experience. My work focused on the checkpoint system, timer functionalities, and UI integration, ensuring smooth and engaging gameplay. Here's a breakdown of my contributions:
This project strengthened my skills in system design, event-driven programming, and UI development. It also taught me valuable lessons in teamwork, communication, and managing complex game logic under tight deadlines.
Download the files below to explore my personal contributions during the development of Wingman.
View key scripts I worked on in Wingman.
using System;
using System.Collections;
using TMPro;
using UnityEngine;
public class Timer : MonoBehaviour {
public event EventHandler OnTimeElapsedChanged;
public class OnTimeElapsedChangedEventArgs : EventArgs {
public float TimeElapsed;
}
public float TimeElapsed => _timeElapsed;
private float _timeElapsed;
private float _lastCheckpointTime = 0;
private int _respawnsSinceCheckpoint = 0;
[SerializeField] private int _penaltyForRespawn = 3;
[SerializeField] private GameObject _penaltyTimeObject;
[SerializeField] private TMP_Text _penaltyText;
private bool _isPenaltyDisplayed = false;
private bool _isRunning = false;
GameRecorder gameRecorder;
PlayBackPlayer playbackPlayer;
private void Start() {
gameRecorder = GameRecorder.instance;
playbackPlayer = FindObjectOfType();
}
private void Update() {
if (!_isRunning) return;
_timeElapsed += Time.deltaTime;
OnTimeElapsedChanged?.Invoke(this, new OnTimeElapsedChangedEventArgs { TimeElapsed = _timeElapsed });
}
public void StartTimer() {
_isRunning = true;
gameRecorder.StartRecording();
playbackPlayer.StartPlayback();
}
public void PauseTimer() {
_isRunning = false;
gameRecorder.StopRecording();
playbackPlayer.StopPlayback();
}
public void ResetTimer() {
_timeElapsed = 0f;
OnTimeElapsedChanged?.Invoke(this, new OnTimeElapsedChangedEventArgs { TimeElapsed = _timeElapsed });
}
public void RestartTimer() {
ResetTimer();
StartTimer();
}
}
using UnityEngine;
using TMPro;
public class GameTimerUI : MonoBehaviour {
[SerializeField] private TextMeshProUGUI _timerText;
private Timer _gameTimer;
private GameManager _gameManager;
float _endTime = 45f;
private void Start() {
_gameManager = FindObjectOfType();
_gameTimer = GameManager.Instance.GameTimer;
_gameTimer.OnTimeElapsedChanged += GameTimer_OnTimeElapsedChanged;
}
private void OnDestroy() {
_gameTimer.OnTimeElapsedChanged -= GameTimer_OnTimeElapsedChanged;
}
private void GameTimer_OnTimeElapsedChanged(object sender, Timer.OnTimeElapsedChangedEventArgs e) {
int minutes = Mathf.FloorToInt(e.TimeElapsed / 60);
int seconds = Mathf.FloorToInt(e.TimeElapsed % 60);
int milliseconds = Mathf.FloorToInt((e.TimeElapsed * 1000) % 1000);
if (minutes * 60 + seconds >= _endTime) {
_timerText.color = Color.red;
}
_timerText.text = string.Format("{0:00}:{1:00}:{2:00}", minutes, seconds, milliseconds / 10);
}
}
using System.Collections;
using TMPro;
using UnityEngine;
public class CountdownController : MonoBehaviour {
[SerializeField] private int _countdownTime;
[SerializeField] private TextMeshProUGUI _countdownDisplay;
private PlaneMovement _planeMovement;
private PlayerInput _playerInput;
private Timer _timer;
private void Start() {
_playerInput = FindObjectOfType();
_planeMovement = FindObjectOfType();
_timer = FindObjectOfType();
StartCoroutine(CountdownToStart());
}
IEnumerator CountdownToStart() {
yield return new WaitForSeconds(0.1f);
while (_countdownTime >= 1) {
_countdownDisplay.text = _countdownTime == 1 ? "GO!" : _countdownTime.ToString();
if (_countdownTime == 1) AfterCountdown();
else BeforeCountdown();
_countdownTime--;
yield return new WaitForSeconds(1f);
}
_countdownDisplay.gameObject.SetActive(false);
}
private void BeforeCountdown() {
_timer.PauseTimer();
_playerInput.DisableInput();
_planeMovement.DisableMovement();
}
private void AfterCountdown() {
_playerInput.EnableInput();
_planeMovement.EnableMovement();
_timer.StartTimer();
}
}
using System;
using UnityEngine;
[Serializable]
public class Checkpoint : MonoBehaviour {
[SerializeField] private Material _defaultMaterial;
[SerializeField] private Material _highlightMaterial;
[SerializeField] private Material _goalMaterial;
[SerializeField] private GameObject _flagCanvas;
[SerializeField] private LODGroup _lodGroup;
public enum CheckpointType { Start, Default, Highlight, End }
private CheckpointType _currentType = CheckpointType.Default;
public void SetType(CheckpointType type) {
_currentType = type;
switch (type) {
case CheckpointType.Start:
case CheckpointType.Default: UpdateMaterial(_defaultMaterial); break;
case CheckpointType.Highlight: UpdateMaterial(_highlightMaterial); break;
case CheckpointType.End: UpdateMaterial(_goalMaterial); ShowFlag(); break;
}
}
private void UpdateMaterial(Material targetMaterial) {
foreach (LOD lod in _lodGroup.GetLODs()) {
foreach (Renderer lodRenderer in lod.renderers) {
lodRenderer.sharedMaterial = targetMaterial;
}
}
}
private void ShowFlag() {
_flagCanvas.SetActive(true);
}
}
using System;
using System.Collections.Generic;
using UnityEngine;
public class CheckpointManager : MonoBehaviour {
public event EventHandler OnPlayerFinished;
[SerializeField] private Checkpoint _startCheckpoint;
[SerializeField] private Checkpoint _endCheckpoint;
[SerializeField] private List _checkpoints;
private int _currentCheckpointIndex = -1;
private void Start() {
EnableCheckpoints();
}
private void OnTriggerEnter(Collider other) {
Checkpoint collidedCheckpoint = other.GetComponentInParent();
if (collidedCheckpoint == null) return;
if (collidedCheckpoint == GetNextCheckpoint()) {
_currentCheckpointIndex++;
ShowNextCheckpoints();
}
if (collidedCheckpoint == _endCheckpoint && AllCheckpointsVisited()) {
OnPlayerFinished?.Invoke(this, EventArgs.Empty);
}
}
private bool AllCheckpointsVisited() {
return _currentCheckpointIndex == _checkpoints.Count - 1;
}
private void EnableCheckpoints() {
_startCheckpoint.gameObject.SetActive(true);
_endCheckpoint.gameObject.SetActive(true);
foreach (var checkpoint in _checkpoints) checkpoint.gameObject.SetActive(true);
}
private Checkpoint GetNextCheckpoint() {
return _currentCheckpointIndex == _checkpoints.Count - 1 ? _endCheckpoint : _checkpoints[_currentCheckpointIndex + 1];
}
private void ShowNextCheckpoints() {
if (_currentCheckpointIndex >= 0) _checkpoints[_currentCheckpointIndex].gameObject.SetActive(false);
if (_currentCheckpointIndex + 1 < _checkpoints.Count) _checkpoints[_currentCheckpointIndex + 1].gameObject.SetActive(true);
}
}
using TMPro;
using UnityEngine;
public class CheckpointUI : MonoBehaviour {
[SerializeField] private TextMeshProUGUI checkpointText;
private CheckpointManager checkpointManager;
private void Start() {
checkpointManager = FindObjectOfType();
}
private void Update() {
if (checkpointManager != null) {
checkpointText.text = $"{checkpointManager.GetCheckpointsLeft()} / {checkpointManager.GetTotalCheckpoints()}";
}
}
}
using System;
using UnityEngine;
public class GameManager : MonoBehaviour {
public static GameManager Instance;
public Timer GameTimer => _gameTimer;
private Timer _gameTimer;
private void Awake() {
if (Instance == null) Instance = this;
else Destroy(gameObject);
_gameTimer = FindObjectOfType();
}
private void Start() {
_gameTimer.StartTimer();
}
public void EndGame() {
_gameTimer.PauseTimer();
Debug.Log("Game Over");
}
}
Check out the game and my contributions!