Reactive Skies

Real-Time Weather Integration & Cloud Services Playground

Project Overview

Reactive Skies began as a class lab project focused on API integration and cloud services, but has since evolved into my personal experimental playground for testing cutting-edge Unity features. The project demonstrates real-time weather data integration using the OpenWeatherMap API, user authentication through PlayFab, and optimized data caching strategies. It's become my go-to sandbox for exploring new APIs, testing backend services, and prototyping network features before implementing them in larger projects.

Technical Highlights

Real-Time Weather API

OpenWeatherMap integration with dynamic skybox switching

PlayFab Authentication

Email and device-based login with persistent sessions

Smart Caching System

Local storage optimization for images and data

XML Parsing

Robust API response handling with error management

XML Parsing

Robust API response handling with error management

Dynamic Lighting

Time-based lighting adjustments from sunrise/sunset data

Dynamic Lighting

Time-based lighting adjustments from sunrise/sunset data

Screenshot Utility

Timestamped screenshot capture system

Code Showcase

Dynamic Weather System with API Integration

The WeatherManager pulls live weather data from OpenWeatherMap's API, parses XML responses, and dynamically adjusts the game's skybox and lighting based on real-world conditions. The system calculates dawn/dusk periods using actual sunrise/sunset times and applies appropriate atmospheric effects. This was my first deep dive into REST API integration!

WeatherManager.cs - API Integration & Dynamic Environment

using System;
using System.Collections;
using System. Xml.Linq;
using UnityEngine;
using UnityEngine.Networking;

public class WeatherManager : MonoBehaviour
{
    public string location = "Orlando,us";
    public Material clearDawnSkyboxMaterial;
    public Material clearDaySkyboxMaterial;
    public Material clearDuskSkyboxMaterial;
    public Material clearNightSkyboxMaterial;
    public Material rainSkyboxMaterial;
    public Material snowSkyboxMaterial;
    public Light directionalLight;
    public float dawnDuskMargin;

    private enum TimeOfDay { Dawn, Day, Dusk, Night }
    private TimeOfDay currentTimeOfDay;
    private string xmlApi => "http://api.openweathermap.org/data/2.5/weather?q=" + 
                             location + "&mode=xml&appid=dc349e3935101abb942e111db21c2985";

    // Generic API call wrapper with error handling
    private IEnumerator CallAPI(string url, Action callback)
    {
        using (UnityWebRequest request = UnityWebRequest.Get(url))
        {
            yield return request.SendWebRequest();
            if (request.result == UnityWebRequest.Result.ConnectionError)
            {
                Debug.LogError($"network problem: {request.error}");
            }
            else if (request.result == UnityWebRequest. Result. ProtocolError)
            {
                Debug.LogError($"response error: {request.responseCode}");
            }
            else
            {
                callback(XDocument.Parse(request.downloadHandler.text));
            }
        }
    }

    public IEnumerator GetWeatherXML(Action callback)
    {
        return CallAPI(xmlApi, callback);
    }

    void Start() 
    {
        directionalLight.color = Color.white;
        directionalLight.intensity = 1. 0f;
        RenderSettings.skybox = clearDaySkyboxMaterial;
        StartCoroutine(GetWeatherXML(OnXMLDataLoaded));
    }

    public void OnXMLDataLoaded(XDocument data)
    {
        Debug.Log(data);

        if (data. Root. Element("city") != null)
        {
            Debug.Log("Successfully retrieved weather data for " + 
                     data.Root.Element("city").Attribute("name").Value);
        }
        
        DateTime now = DateTime.Now;

        // Parse sunrise/sunset times from API
        XElement sun = data.Root.Element("city").Element("sun");
        DateTime sunrise = DateTime.Parse(sun. Attribute("rise").Value).ToLocalTime();
        DateTime sunset = DateTime.Parse(sun.Attribute("set").Value).ToLocalTime();

        // Determine time of day with margin for dawn/dusk
        if (now < sunrise. AddMinutes(-dawnDuskMargin) || now > sunset.AddMinutes(dawnDuskMargin))
        {
            Debug.Log("It's night time.");
            directionalLight.intensity = 1.3f;
            directionalLight.color = new Color32(55, 95, 135, 255);
            currentTimeOfDay = TimeOfDay. Night;
        }
        else if (now >= sunrise.AddMinutes(-dawnDuskMargin) && now <= sunrise.AddMinutes(dawnDuskMargin))
        {
            Debug.Log("It's dawn.");
            directionalLight.intensity = 1.2f;
            directionalLight. color = new Color32(255, 222, 50, 255);
            currentTimeOfDay = TimeOfDay.Dawn;
        }
        else if (now >= sunset.AddMinutes(-dawnDuskMargin) && now <= sunset.AddMinutes(dawnDuskMargin))
        {
            Debug.Log("It's dusk.");
            directionalLight.intensity = 1.6f;
            directionalLight. color = new Color32(254, 180, 32, 255);
            currentTimeOfDay = TimeOfDay. Dusk;
        }
        else
        {
            Debug. Log("It's daytime.");
            directionalLight. intensity = 1.25f;
            directionalLight. color = new Color32(255, 255, 224, 255);
            currentTimeOfDay = TimeOfDay.Day;
        }

        // Apply appropriate skybox based on time of day
        switch (currentTimeOfDay)
        {
            case TimeOfDay. Dawn:
                RenderSettings. skybox = clearDawnSkyboxMaterial;
                break;
            case TimeOfDay.Day:
                RenderSettings.skybox = clearDaySkyboxMaterial;
                break;
            case TimeOfDay.Dusk:
                RenderSettings. skybox = clearDuskSkyboxMaterial;
                break;
            case TimeOfDay.Night:
                RenderSettings.skybox = clearNightSkyboxMaterial;
                break;
        }

        // Check for precipitation and override skybox
        XElement weather = data.Root.Element("precipitation");
        if (weather != null)
        {
            switch (weather.Attribute("mode").Value)
            {
                case "rain":
                    Debug.Log("It's raining!");
                    RenderSettings.skybox = rainSkyboxMaterial;
                    break;
                case "snow":
                    Debug.Log("It's snowing!");
                    RenderSettings.skybox = snowSkyboxMaterial;
                    break;
                default:
                    Debug.Log("No precipitation.");
                    break;
            }
        }
    }
}
                        

PlayFab Authentication System

Implemented a flexible authentication system supporting multiple login methods (email, device ID) through the Strategy pattern. The system includes persistent login sessions, automatic re-authentication, and secure credential storage. This was my introduction to backend-as-a-service (BaaS) platforms!

ILogin.cs - Authentication Interface

using PlayFab.ClientModels;
using PlayFab;

// Strategy pattern interface for multiple login methods
public interface ILogin
{
    void Login(System.Action onSuccess, System.Action onError);
}
                        
PlayFabManager.cs - Authentication Manager

using PlayFab. ClientModels;
using PlayFab;
using UnityEngine;

public class PlayFabManager : MonoBehaviour
{
    private LoginManager loginManager;
    private string savedEmailKey = "SavedEmail";
    private string userEmail;
    
    private void Start()
    {
        loginManager = new LoginManager();
        
        // Auto-login if credentials are saved
        if (PlayerPrefs.HasKey(savedEmailKey))
        {
            string savedEmail = PlayerPrefs.GetString(savedEmailKey);
            EmailLoginButtonClicked(savedEmail, "SavedPassword");
        }
    }
    
    public void EmailLoginButtonClicked(string email, string password)
    {
        userEmail = email;
        loginManager.SetLoginMethod(new EmailLogin(email, password));
        loginManager.Login(OnLoginSuccess, OnLoginFailure);
    }
    
    public void OnLoginSuccess(LoginResult result)
    {
        Debug. Log("Login successful!");
        
        // Persist email for future auto-login
        if (! string.IsNullOrEmpty(userEmail))
            PlayerPrefs.SetString(savedEmailKey, userEmail);
            
        LoadPlayerData(result.PlayFabId);
    }
    
    public void OnLoginFailure(PlayFabError error)
    {
        Debug.LogError("Login failed: " + error.ErrorMessage);
    }
    
    private void LoadPlayerData(string playFabId)
    {
        var request = new GetUserDataRequest
        {
            PlayFabId = playFabId
        };
        PlayFabClientAPI.GetUserData(request, OnDataSuccess, OnDataFailure);
    }
    
    private void OnDataSuccess(GetUserDataResult result)
    {
        // Load Player
        Debug.Log("Player data loaded successfully.");
    }
    
    private void OnDataFailure(PlayFabError error)
    {
        Debug.LogError("Failed to load player data: " + error.ErrorMessage);
    }
}
                        
EmailLogin.cs - Email Authentication Strategy

using PlayFab.ClientModels;
using PlayFab;

public class EmailLogin : ILogin
{
    public string Email { get; private set; }
    public string Password { get; private set; }

    public EmailLogin(string email, string password)
    {
        Email = email;
        Password = password;
    }

    public void Login(System.Action onSuccess, System.Action onError)
    {
        var request = new LoginWithEmailAddressRequest
        {
            Email = Email,
            Password = Password
        };
        PlayFabClientAPI.LoginWithEmailAddress(request, onSuccess, onError);
    }
}
                        
DeviceLogin.cs - Device ID Authentication Strategy

using PlayFab. ClientModels;
using PlayFab;
using UnityEngine. iOS;

public class DeviceLogin :  ILogin
{
    private string deviceID;
    
    public DeviceLogin()
    {
        deviceID = Device.vendorIdentifier;
    }
    
    public void Login(System.Action onSuccess, System.Action onError)
    {
        var request = new LoginWithCustomIDRequest
        {
            CustomId = deviceID,
            CreateAccount = true  // Auto-create account on first login
        };
        PlayFabClientAPI.LoginWithCustomID(request, onSuccess, onError);
    }
}
                        

Smart Image Caching System

Developed an optimized image loading system that caches downloaded images locally to reduce bandwidth and improve load times. The system downloads images from URLs on first access, stores them persistently, and retrieves from cache on subsequent loads. Includes cache management for clearing old data.

BillboardImagePicker.cs - Cached Image Loading

using System;
using System.Collections;
using System.IO;
using UnityEngine;
using UnityEngine. Networking;

public class BillboardImagePicker : MonoBehaviour
{
    public string billboardID;
    public string[] imageUrls;

    private string localPath;
    private Renderer billboardRenderer;

    void Start()
    {
        billboardRenderer = GetComponent();
        int randomIndex = UnityEngine.Random.Range(0, imageUrls.Length);
        Debug.Log("Selected image URL: " + imageUrls[randomIndex]);
        
        // Generate cache path with unique identifier
        localPath = Application.persistentDataPath + "/" + billboardID + "_" + randomIndex + ".png";
        StartCoroutine(LoadImageFromURL(imageUrls[randomIndex], ApplyTexture));
    }

    private IEnumerator LoadImageFromURL(string url, Action callback)
    {
        byte[] imageData;
        
        // Check if image is already cached
        if (! File.Exists(localPath))
        {
            // Download and cache image
            UnityWebRequest request = UnityWebRequestTexture.GetTexture(url);
            yield return request. SendWebRequest();
            Debug.Log("Downloading image from URL: " + url);
            
            imageData = DownloadHandlerTexture.GetContent(request).EncodeToPNG();
            File.WriteAllBytes(localPath, imageData);
        }
        else
        {
            // Load from cache
            Debug.Log("Loading image from cache: " + localPath);
            imageData = File.ReadAllBytes(localPath);
        }
        
        // Convert byte array to texture
        Texture2D texture = new Texture2D(2, 2);
        texture.LoadImage(imageData);
        callback(texture);
    }

    private void ApplyTexture(Texture2D texture)
    {
        billboardRenderer.material. mainTexture = texture;
    }

    public void ClearCachedImage()
    {
        // Clean up all cached images for this billboard
        for (int i = 0; i < imageUrls.Length; i++)
        {
            string path = Application.persistentDataPath + "/" + billboardID + "_" + i + ".png";
            if (File.Exists(path))
            {
                File.Delete(path);
                Debug.Log("Deleted cached image at: " + path);
            }
        }
    }
}
                        

Screenshot Capture Utility

Simple but effective screenshot tool with timestamped filenames. Perfect for capturing different weather conditions during testing and creating portfolio content!

ScreenshotTool.cs - Timestamped Screenshot System

using System;
using UnityEngine;

public class ScreenshotTool : MonoBehaviour
{
    public void TakeScreenshot()
    {
        // Generate timestamped filename
        string timestamp = DateTime.Now.ToString("yyyyMMdd_HHmmss");
        string filename = $"Screenshot_{timestamp}.png";
        
        ScreenCapture.CaptureScreenshot(filename);
        Debug.Log($"Screenshot saved as {filename}");
    }
}
                        

Learning & Experimentation

API Integration

What I Learned: RESTful API consumption, XML parsing, asynchronous request handling, and error management. OpenWeatherMap taught me how to work with real-time external data sources.

Cloud Services

What I Learned: Backend-as-a-Service platforms, user authentication flows, session management, and cloud data storage. PlayFab became my testing ground for understanding player accounts and persistent data.

Performance Optimization

What I Learned: Local caching strategies, reducing network calls, efficient file I/O, and data persistence. These techniques significantly improved app responsiveness.

Dynamic Environments

What I Learned: Runtime material swapping, procedural skybox switching, dynamic lighting adjustments, and creating reactive game worlds that respond to real-world data.

Developer Reflection

Reactive Skies started as a simple class assignment but quickly became my favorite experimentation space. Every time I want to test a new API, try out a cloud service, or prototype a network feature, I come back to this project. It's taught me that some of the best learning happens when you turn assignments into playgrounds, where mistakes are cheap and creativity flows freely. The weather integration was particularly satisfying; seeing Unity's skybox change to match the actual weather outside my window felt like magic!