DEV Community

Cover image for 🌦️ WeatherAPI | Introducing real-time weather inside a game.
Davi Massini
Davi Massini

Posted on

🌦️ WeatherAPI | Introducing real-time weather inside a game.

🌦️ WeatherAPI

Hello, guys, all good? I've had this post in my head for a while and I still don't know exactly which direction I'm going to take with it. I wouldn't like it to be just a sample of what I worked on in this personal project, and at the same time, I think a detailed tutorial would be huge. So I'll try to merge the two and copy some of what I've already done in the README.md on GitHub, but enough of the rambling!

To begin with, I'll try to contextualize what happened to get us here. I was on my way to the gym when I passed by the living room and a friend was watching a video of Escape from Tarkov. He immediately commented on the game's feature of raining whenever it starts to rain in Tarkov. This gave me a spark and I remember my first response being: "I can do that!"

That said, what we're going to see below is a case of over-engineering. Even though it works, all of this is very focused on what I had on hand and I just wanted to implement something similar to what was reported.

Since I've put so much text here, I think it's fair - and to try to keep your attention - to show a demo of what we're going to build.

WeatherAPI - Demo do projeto

πŸ‘£ Getting Started

With the idea planted, and already internalized that I would use spring-boot to create our API, I needed to decide on which engine it would be built. I had been studying Unreal Engine for some time, but because I had more know-how in Unity and because I found this AMAZING weather system, I opted for our last alternative.

I just needed to find a reliable and free source of weather data that was suitable for our problem. This is where WeatherAPI comes in - I have to be honest, I don't remember if I came up with the name before or after finding this solution, but it turns out that both are the same. Since this is just a portfolio, I didn't think it was too bad to keep it.

πŸ€” Understanding requirements

My first step was to start a 2D project in Unity Hub, import the mentioned asset in the paragraph above, and open the sample scene (located within scenes). If you're not sure how to do this, I suggest following these steps.

After playing with the particle settings for some many minutes, I went to understand what triggered each of them. Looking at our objects within the Hierarchy tab, we can identify exactly where the sliders are, and through the Inspector tab, the respective trigger.

Objects and trigger of the selected object.

As this is the Slider component, it is known that its control properties are:

  • minValue - The minimum value the slider can reach;
  • maxValue - The maximum value the slider can reach;
  • value - The current value of the slider.

Observing the image above, we can identify that the minimum and maximum values are 0 and 1, respectively. With that, it is defined that my API must have the response in this format.

Regarding the engine, for now, we just need to understand which fields will be consumed. Since I won't make any changes to the asset for this presentation, I will follow exactly what has already been developed.

  • Master - Overall intensity, which we will always set to 1;
  • Rain - Intensity of rain, which will be delivered by the API;
  • Wind - Intensity of wind, which will be delivered by the API;
  • Fog - Intensity of fog, which will be delivered by the API.
  • Lightning - Intensity of lightning, which will be delivered by the API.

🍬 Consuming API (Yum, yum)

Before providing our service, let's consume the WeatherAPI and understand which data we will handle. It's very simple: just access the website and create an account; after these steps, you will notice that you have been granted 14 days of the pro version for free, but even though there are no charges, the free version will meet 100000% of the demands.

On the dashboard, there will be some usage information and also the link to the documentation. Right away, we have the call for "Current weather," which is given by the address http://api.weatherapi.com/v1/current.json, and the mandatory parameters are key (your generated key) and q (the chosen city). Below is an example call, where I used Postman as a CLI.

Example of API call.

Why is this important? Because we need to know which attributes we will have to map later on. This way, this entire response will become classes within our project.

Observing this JSON, we have some cool and important information, such as:

  • condition - Object to demonstrate weather condition.
  • wind_kph - Wind speed in km/h.

Let's give more attention to "condition," because it has the code property that, according to the documentation, shows the weather condition. In the documentation, there is a JSON of all possible responses for this data. Keep this information, as it will be extremely important later on.

🍝 Let's get our hands dirty

Remember when I talked about spring-boot? Well, to make our lives easier we will use spring initializr. If you want to use my configurations, they are shown below (pay attention to the dependency requirement).

Spring Configuration

Just download the generated code and import it into your IDE of choice (I am using the Community version of IntelliJ). Remember to configure Java with the appropriate version. The next steps will require us to get our hands dirty, so it's already getting interesting!

1. Creating the response

After the first build (it's automatic, as soon as you import), we can start playing around. First, I'll create a folder called "domain" inside "main.java.$artifact" and inside it, another one called "response".

Next, I'll create the class (since we had already defined its attributes in the "πŸ€” Understanding requirements" section) with the name "WeatherResponse" and I'll fill it with properties (all of them will be of type "double") and their respective getters/setters. Below is an image, in case you want to compare.

Project with newly created response.

2. Mapping the External API

Well, now comes a somewhat tricky time... We will transform that response from our call into four DTOs: ConditionDto, CurrentDto, LocationDto and WeatherDto. The type of each field can be found in the call usage guide, where "decimal" can use float/double safely. To continue using an organization pattern, I created the "dto" folder inside "domain".

JSON transformed into DTOs

3. Creating the controller

Well, now we have something sufficient to build our controller class. Outside the "domain" folder, we create the "controller" folder and inside it the WeatherController class. Here we will start using some spring annotations, and as I mentioned if I go through all the points of this project, I believe it will be very long (more? πŸ˜‚). So, I'll leave the link to the code and explain a little.

Basically, we are:

  • instantiating the URL we want to call;
  • mapping the path /weather to trigger our API;
  • setting "city" and "apiKey" as mandatory parameters;
  • receiving the response and transforming it into an object;
  • returning the method of the service where we pass our object as a parameter.

It is likely that you are seeing an error message saying that such "WeatherService" does not exist and this is consistent with reality - after all, we have not created it yet. With that, let's move on to the next step and create our class where the "magic" will happen! But before that, one more photo.

WeatherService Error

4. Look at the service

Okay, time to actually code something! At the same level as "domain" and "controller" we will create our "service", and later we will create the "WeatherService" class. Just like before, I will copy the code and summarize its functionalities.

getWeather()
Our main function, in which we are setting all the fields of our response and returning it. If you add this to your code now, several lines will give an error, due to the absence of the other functions.

public static WeatherResponse getWeather(WeatherDto weatherDto) throws ParseException {
    CurrentDto currentDto = weatherDto.getCurrent();
    LocationDto locationDto = weatherDto.getLocation();

    WeatherResponse response = new WeatherResponse();
    response.setRain(codeToRain(currentDto.getCondition().getCode()));
    response.setWind(windKphToScale(currentDto.getWind_kph()));
    response.setFog(codeToFog(currentDto.getCondition().getCode()));
    response.setLightning(codeToLightning(currentDto.getCondition().getCode()));

    return response;
}
Enter fullscreen mode Exit fullscreen mode

windKphToScale()
I skipped a line, yes... It's because I'm going to explain all the ones that start with "code" at once, so it's better to leave them for last since we will spend a little more effort on them.

We need to transform the received data into parts of 1000 (thousand), because our slider has its value going from 0 (zero) to 1 (one) with 3 (three) decimal places. I did a little research and noticed that winds of 61km/h are practically a limit for "strong wind" - Beaufort scale 7 - which is where our particles in the project go up to. So all we had to do was divide one value by the other and orient it to only take the first 3 decimal places.

Homework: What would happen if we received a value greater than 61? Should there be something to handle that?

private static double windKphToScale(double windKph) {
    int maxWindKph = 61;
    double windInScale = windKph / maxWindKph;
    String removeWindDecimalPlaces = String.format("%.3g", windInScale).replace(",", ".");

    return Double.parseDouble(removeWindDecimalPlaces);
}
Enter fullscreen mode Exit fullscreen mode

codeToRain() && codeToFog()
Still missing one, I know! It's just that by explaining the logic of these, the other will be super easy to understand and we will even do it in a different way. Remember that I asked you to keep the information of the "condition" object stored in mind? Well, we will use it now! Besides this part of the API, there is nothing that indicates the characteristics of the weather, so that JSON showing all possible responses will be used to overcome this problem.

I asked ChatGPT to read this same text and turn it into a table where it separated from clear weather to super rainy. I got a nice result, I could have refined it even more, but I thought it was already enough.

ChatGTP

I created a new folder within "domain" called "enums" and the class "WeatherConditionEnum". I separated it into 6 different statements and this is the result.

public enum WeatherConditionEnum {
    CLEAR,
    LIGHT_RAIN,
    MODERATE_RAIN,
    HEAVY_RAIN,
    THUNDER_RAIN,
    UNDEFINED
}
Enter fullscreen mode Exit fullscreen mode

Once again, I created a new folder within "domain" called "enums" and the class "WeatherConditionEnum". I separated it into 6 different statements and this is the result.

In addition, within the "domain" folder, I created the folder "utils" and the class "WeatherGroup" - where our enums above will be mapped. In addition to the mapping, we will create a method to return the specific enum of the passed "code". Due to the maximum text size, I had to remove the code snippet but you can find it by clicking on this link.

Both steps will be repeated in relation to "fog", utils and enum.

With all this created, we can now move on to our functions. What we will do here is a switch where we will pass the "code" of the "condition" as a parameter, and to return a "case", we will call our method from within the grouping class where each case will have a value between 0 and 1 to be returned. It will look like this:

private static double codeToRain(int code) {
    return switch (groupWeatherCondition(code)) {
        case CLEAR, UNDEFINED -> 0.0;
        case LIGHT_RAIN -> 0.3;
        case MODERATE_RAIN -> 0.5;
        case HEAVY_RAIN -> 0.85;
        case THUNDER_RAIN -> 1;
    };
}
Enter fullscreen mode Exit fullscreen mode
private static double codeToFog(int code) {
    return switch (groupFogCondition(code)) {
        case UNDEFINED -> 0.0;
        case MIST -> 0.5;
        case FOG -> 1;
    };
}
Enter fullscreen mode Exit fullscreen mode

And there we have it, our methods are now working perfectly!

codeToLightning()

As I said, explaining the ones above, this one becomes super easy. Using enum and map helps a lot to make the project scalable, but what if you had very few things to be mapped and didn't want to have all this work? That's what we'll see next. There are only two codes that result in thunderstorms, and that's why we'll set them hard coded.

private static double codeToLightning(int code) {
    return switch (code) {
        case 1273 -> 0.5;
        case 1276 -> 1;
        default -> 0.0;
    };
}
Enter fullscreen mode Exit fullscreen mode

We defined the two cases where a number greater than 0 (zero) will be returned, and in case the code passed is not one of the two, it will fall into the "default".

5. Test 1, 2, 3 - Test 1, 2, 3

It's a wonderful time! The time when: everything falls apart; the house collapses; the child cries and the mother doesn't see; the last piece of bread you found lying in the kitchen falls to the ground and even with the butter down;;;;;;

Great news! It's time to see the glory of your efforts. Let's run our project, and it's very simple to do so. Just locate the "Gradle" tab within your IDE, and then follow the path Tasks -> application -> bootRun; or simply open the command window and run the "gradle bootRun" command.

bootRun task

The project should run quickly and you'll see the terminal like mine below, or partially like it depending on the IDE.

bootRun console

Now you can go to your favorite API client and call http://localhost:8080/weather passing the parameters "city" and "apiKey", to confirm that everything is working! If everything goes well, you should be seeing a response similar to this.

bootRun Response

πŸšΆβ€β™‚οΈ Finishing the walk

With our API fully completed, we only need to communicate with Unity. For that, let's go back to our "Hierarchy" tab within the program and create a new empty object - I'll call it CallWeatherAPI. In this object we will add a script component (I chose the name APIHelper, but feel free to change it), which will trigger our call.

using System.Net.Http;
using UnityEngine;

public class APIHelper : MonoBehaviour {

    [Header("Request")]
    public string cityName;
    public string ownKey;

    private void Start() {
        GetData(cityName, ownKey);
    }

    async void GetData(string city, string apiKey) {
        string API_URL = "http://localhost:8080/weather?city=" + city + "&apiKey=" + apiKey;
        using (var httpClient = new HttpClient()) {
            var response = await httpClient.GetAsync(API_URL);
            if (response.IsSuccessStatusCode)
                Debug.Log(await response.Content.ReadAsStringAsync());
            else
                Debug.Log(response.ReasonPhrase);
        }
    }
}
Enter fullscreen mode Exit fullscreen mode
  • we instantiate the city name and key as public, so we can have visibility in the engine;
  • we create the API consumption method;
  • if successful, the response will be printed in the console;
  • if an error occurs, the reason for the error will be printed.

Note that currently we have no use for the response we receive, so we need to work on it. To do this, we will create a class with the attributes we receive (this class will be exactly like our WeatherResponse, but within Unity). The C# file created in the engine should look like the code below.

using System;

[Serializable]
public class WeatherAPI_Response {
    public float rain;
    public float wind;
    public float fog;
    public float lightning;
}
Enter fullscreen mode Exit fullscreen mode

Now we just need to make a few small adjustments, starting by changing all "onValueChange" of our Sliders to "Editor and Runtime";

Unity Hud

Then we will adjust a few things in our APIHelper class, such as:

  • receiving the Slider components;
  • instantiating the components by retrieving them;
  • improving our call method to transform JSON into a class;
  • creating a method to change the values of the Sliders according to what we receive from the API.

Again, due to size limitations, I had to remove the final code snippet, but you can find it by clicking here.

πŸŽ‰ Pop the Champagne!

If you've made it this far, and I'm not crazy, and machines haven't taken over everything, making programming languages no longer work, it's likely that you've accomplished something pretty awesome today! And I'm not even talking about this project, I'm talking about you keeping your attention on the text, which turned out to be much longer than I expected, but I think we managed to go through all the points, at least in an "ok" way.

Final result.

This was the result obtained by following all the breadcrumbs in our path... But maybe you noticed that it's a bit raw, right?! How about improving this project? Here are some ideas:

  • Day/night;
  • Snow (which is already included in this asset);
  • Real-time city search;
  • Show how many degrees it is;
  • Call the API from time to time to update the weather.

I even implemented some of these ideas in a demonstration, so if you want to play around a bit, just click here, and you can find all the backend code on my GitHub. By the way, do you know how I managed to put a Unity build and my API on the internet for free? I think that's a good next topic to post here...

Anyway, thank you all for your attention, and if you have any problems, doubts or suggestions, I'm all ears.

πŸ—ΊοΈ Acknowledgments

Top comments (0)