🌦️ WeatherAPI | Colocando o clima em tempo real dentro de um jogo.
🌦️ WeatherAPI
Olá, tchurma, tudo certo? Eu tô com esse post na cabeça tem um tempo e mesmo assim não sei exatamente qual rumo eu vou tomar com ele, não gostaria que fosse apenas uma amostra do que trabalhei - nessa brincadeira, cof cof - nesse projeto pessoal e ao mesmo tempo acho que um tutorial detalhado seria algo enorme. Então vou tentar mesclar os dois e copiar um pouco do que já fiz no README.md do GitHub, mas agora chega de enrolação!
De começo, vou tentar contextualizar o que aconteceu para chegarmos aqui. Estava indo para à academia quando passei pela sala e um amigo estava assistindo um vídeo de Escape from Tarkov, imediatamente ele comentou sobre a feature do jogo de chover sempre que começa a chover em Tarkov. Isso me deu um estalo e eu lembro de minha primeira resposta ser: "Posso fazer isso!".
Dito isso, o que iremos visualizar abaixo se trata de um caso de over-engineering. Mesmo funcionando, isso tudo é bem focado no que eu tinha em mãos e eu apenas gostaria de implementar algo semelhante ao relatado.
Já que botei tanto texto aqui, acho justo - e pra tentar prender a atenção de vocês - mostrar uma demo do que iremos montar.
👣 Primeiros passos
Com a ideia plantada, e já internalizado que iria utilizar spring-boot para criar nossa API, precisava decidir em qual engine seria feito. Estava estudando Unreal Engine já há algum tempo, mas por ter um know-how maior na Unity e por ter achado esse MARAVILHOSO sistema climático, optei pela nossa última alternativa.
Faltava somente achar uma fonte confiável e gratuita de climatempo condizente com as necessidades do nosso problema. É aqui que entra a WeatherAPI - tenho que ser sincero eu não lembro se eu defini o nome antes ou depois de achar essa solução, mas acabou que ambos são iguais. Como se trata apenas de um portfólio, não achei de todo ruim manter.
🤔 Entendendo necessidades
Meu primeiro passo foi iniciar um projeto 2D no Unity Hub, importar o asset mencionado no parágrafo acima e abrir a cena de exemplo (fica dentro de scenes). Caso tenha alguma dúvida de como fazer isso, sugiro seguir esses passos.
Após brincar por alguns muitos minutos com as configuração das párticulas, fui entender o que acionava cada uma delas. Olhando para nossos objetos dentro da aba Hierarchy podemos identificar exatamente onde estão os sliders, e pela aba Inspector o trigger perspectivo.
Por se tratar do componente Slider, é conhecido que suas propriedades de controle são:
- minValue - Valor mínimo que o slider pode chegar;
- maxValue - Valor máximo que o slider pode chegar;
- value - Valor atual do slider.
Observando a imagem acima conseguimos identificar que o valor mínimo e máximo são 0 (zero) e 1 (um), respectivamente. Com isso, está definido que minha API deve ter o response nesse molde.
Relacionado a engine, por agora, basta apenas entendermos quais campos serão consumidos. Como não irei fazer alterações no asset para essa apresentação, vou seguir a risca o que já foi desenvolvido.
- Master - Intensidade geral, iremos definir ela como 1 sempre;
- Rain - Intesidade da chuva, será entregue pela API;
- Wind - Intesidade do vento, será entregue pela API;
- Fog - Intesidade da neblina, será entregue pela API.
- Lightning - Intesidade do raio, será entregue pela API.
🍬 Consumindo API (Nham, nham)
Antes de fazer o nosso serviço, vamos consumir a WeatherAPI e entender quais dados iremos tratar. Para isso é muito simples: basta acessar o site e criar uma conta; após esses passos você irá perceber que ganhou 14 dias gratuítos da versão pro, porém, além de não haver nenhuma cobrança a versão grátis irá suprir em 100000% as demandas.
No dashboard terão algumas informações de uso e também o link para a documentação. De cara, temos a chamada para "Current weather" que se dá pelo endereço http://api.weatherapi.com/v1/current.json e os parâmetros obrigatórios são key (sua chave gerada) e q (a cidade escolhida). Abaixo um exemplo de chamada, onde utilizei o Postman como CLI.
Por que isso é importante? Pois precisamos saber quais serão os atribuitos que teremos que mapear posteriormente. Dessa forma, esse response inteiro irá virar classes dentro do nosso projeto.
Observando esse JSON temos algumas informações legais e importantes, como por exemplo:
- condition - Objeto para demonstrar a condição do tempo.
- wind_kph - Velocidade do tempo em km/h.
Vamos dar uma atenção maior ao "condition", isso porquê nele temos a propriedade code que, pela documentação, demonstra a condição climática. Ainda na doc, temos um JSON de todos as possíveis respostas para esse dado. Guarde essas informações, mais a frente serão de extrema importância.
🍝 Mão na massa
Lembra que eu falei do spring-boot? Pois bem, para facilitar nossas vidas iremos utilizar o spring initializr. Caso queira fidelizar com as minhas configurações, são as mostradas abaixo (se atente a necessidade da dependência).
Basta baixar o que foi gerado e importar na sua IDE de escolha (estou utilizando a versão Community do IntelliJ). Lembre de configurar o Java com a versão adequada. Os próximos passos já irão nos fazer meter a mão na massa, então já começa a ficar interessante!
1. Criando o response
Após o primeiro build (é automático, assim que você importa), podemos começar a brincar. Primeiro vou criar na "main.java.$artefato" uma pasta chamada "domain" e dentro dela uma outra chamada "response".
Em sequência, vou criar a classe (uma vez que já haviamos definido seus atributos no cabeçalho "🤔 Entendendo necessidades") pelo nome de "WeatherResponse" e vou preencher com as propriedades (todos serão do tipo "double") e seus respectivos getters/setters. Abaixo uma imagem, caso queira comparar.
2. Mapeando a API Externa
Pois bem, agora chegou uma hora meio chatinha... Iremos transformar aquela resposta da nossa chamada em quatro DTOs: ConditionDto, CurrentDto, LocationDto e WeatherDto. A tipagem de cada campo pode ser encontrada no guia de uso das chamadas, onde está "decimal" pode usar float/double tranquilamente. Para continuar utilizando um padrão de organização, criei a pasta "dto" dentro de "domain".
3. Criando o controller
Bom, já temos algo suficiente para montarmos nossa classe de controller. Fora da pasta "domain", criamos a pasta "controller" e dentro dela a classe WeatherController. Aqui iremos começar a utilizar algumas anotações do spring, e como eu havia comentado se eu passar por todos os pontos desse projeto acredito que ficará muiiito extenso (mais? 😂). Por isso, vou deixar o link para o código e explicar um pouco.
Basicamente, estamos:
- instanciando qual é a URL que queremos chamar;
- mapeando o path /weather para ser acionada a nossa API;
- deixando "city" e "apiKey" como parâmetros obrigatórios;
- recebendo o response e transformando para o objeto;
- retornando o método do service onde passamos nosso objeto por parâmetro.
É provável que você esteja vendo uma mensagem de erro, dizendo não existir esse tal "WeatherService" e isso está condizente com a realidade - afinal ainda não o criamos. Com isso, vamos passar para o próximo passo e criar nossa classe onde a mágia será feita! Antes de mais nada, mais uma fotenha.
4. Olha o serviiiiiiçooooo
Okay, hora de realmente codar algo! No mesmo nível da "domain" e "controller" vamos criar a nossa "service", e posteriormente criaremos a classe "WeatherService". Assim como anteriormente, vou copiar os códigos e resumir seus funcionamentos.
getWeather() Nossa função principal, nela estamos setando todos os campos de nosso response e retornando o mesmo. Caso você adicione isso a seu código agora diversas linhas irão dar erro, isso é devido a ausência das outras funções.
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;
}
windKphToScale() Eu pulei uma linha, sim... É porquê todos que tem "code" no início eu vou explicar de uma vez só, então melhor deixar pro final já que vamos gastar um pouquinho mais de esforço neles.
Precisamos transformar o dado recebido em partes de 1000 (mil), isso porquê nosso slider tem seu value indo de 0 (zero) a 1 (um) com 3 (três) casas decimais. Eu fiz uma pequena pesquisa e notei que ventos de 61km/h são praticamente um limite para "vento forte" - escala 7 de Beaufort - que é até onde vai nossas partículas no projeto. Daí foi só dividir um valor pelo outro e orientar para pegar apenas as 3 primeiras casas decimais.
Dever de casa: o que aconteceria se recebessemos um valor maior de 61? Deveria ter algo para fazer o handle disso?
private static double windKphToScale(double windKph) {
int maxWindKph = 61;
double windInScale = windKph / maxWindKph;
String removeWindDecimalPlaces = String.format("%.3g", windInScale).replace(",", ".");
return Double.parseDouble(removeWindDecimalPlaces);
}
codeToRain() && codeToFog() Ainda tá faltando um, eu sei! É que explicando a lógica desses, o outro vai ser super simples de entender e inclusive faremos ele de uma outra maneira. Lembra que eu pedi pra deixar na mente a informação do objeto "condition" guardada? Pois então, iremos utilizar ele agora! Além dessa parte da API não há nada que nos indique as características do clima, portanto aquele JSON mostrando todas as respostas possíveis será utilizado para contornarmos esse problema.
Pedi pro ChatGPT ler esse mesmo texto e transforma-lo em uma tabela onde os separasse de tempo limpo até mega chuvoso. Tive um resultado legal, poderia ter refinado ainda mais mas achei que já seria o suficiente.
Criei uma nova pasta dentro de "domain" chamada "enums" e a classe "WeatherConditionEnum", separei em 6 (seis) diferentes enunciados e assim ficou o resultado.
public enum WeatherConditionEnum {
CLEAR,
LIGHT_RAIN,
MODERATE_RAIN,
HEAVY_RAIN,
THUNDER_RAIN,
UNDEFINED
}
Mais uma vez, dentro de "domain" criei a pasta chamada "utils" e a classe "WeatherGroup" - onde serão mapeados nossos enums acima. Além do mapeamento, iremos fazer um método para retornar o enum específico do "code" passado. Devido ao tamanho máximo do texto, tive que retirar o trecho do código mas você encontra o mesmo clicando aqui.
E ambos os passos serão repetidos em relação ao "fog", utils e enum.
Com tudo isso criado, podemos agora ir para nossas funções finalmente. O que iremos fazer aqui é um switch onde passaremos por parâmetro o "code" do "condition", e para retornar um "case" chamaremos nosso método de dentro da classe de agrupamento onde cada um dos casos terá um valor entre 0 (zero) e 1 (um) a ser retornado. Ficando assim:
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;
};
}
private static double codeToFog(int code) {
return switch (groupFogCondition(code)) {
case UNDEFINED -> 0.0;
case MIST -> 0.5;
case FOG -> 1;
};
}
E pronto, assim temos nossos métodos funcionando perfeitamente!
codeToLightning()
Como eu disse, explicando os de cima esse aqui fica super fácil. Usar enum e map ajuda muito a deixar o projeto escalável, mas e se você tivesse pouquissimas coisas a serem mapeadas e não quisesse ter todo esse trabalho? É o que iremos ver a seguir. Só há dois codes que resultam em tempestades de raios, e por isso vamos seta-los hardcoded.
private static double codeToLightning(int code) {
return switch (code) {
case 1273 -> 0.5;
case 1276 -> 1;
default -> 0.0;
};
}
Definimos os dois casos onde haverá um número maior que 0 (zero) a ser retornado e caso o código passado não faça parte de um dos dois, ele irá cair no "default".
5. Teste 1, 2, 3 - Teste 1, 2, 3
Chegou uma hora maravilhosa! A hora onde: tudo desmorona; a casa cai; o filho chora e a mãe não vê; o último pedaço de pão que tu achou largado na cozinha cai no chão e ainda com a manteiga pra baixo;;;;;;
Brincadeira! Chegou a hora de você ver as glórias de seu esforço. Vamos rodar nosso projeto, e para isso é bem simples. Basta você localizar a aba "Gradle" dentro de sua IDE, e então seguir o caminho Tasks -> application -> bootRun; ou simplesmente abrir a janela de comandos e rodar o comando "gradle bootRun".
O projeto deve rodar rapidinho e você verá o terminal igual o meu abaixo, ou parcialmente igual dependendo da IDE.
Agora você pode ir no seu API Client favorito e chamar por http://localhost:8080/weather passando os parâmetros "city" e "apiKey", para confirmar que está tudo lindo! Se tudo der certo, você deve estar vendo uma resposta parecida com essa.
🚶♂️ Finalizando a caminhada
Com toda a nossa API finalizada, precisamos apenas fazer a comunicação com o Unity. Para isso, vamos voltar em nossa aba "Hierarchy" dentro do programa e criar um novo objeto vazio - vou chamar de CallWeatherAPI. Nesse objeto iremos adicionar um componente script (eu escolhi o nome APIHelper, mas fique a vontade para mudar), onde o mesmo acionará nossa chamada.
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);
}
}
}
- instanciamos o nome da cidade e chave como públicas, para poder ter visão pela engine;
- criamos o método de consumo da API;
- caso dê sucesso será impresso no console o response;
- caso dê erro será impresso a razão do erro;
Note que atualmente não temos nenhuma utilidade para a resposta que recebemos, portanto é necessário agora trabalhar em cima dela. Para isso iremos criar uma classe com os atributos que recebemos (essa classe será exatamente igual a nossa WeatherResponse, porém, dentro da Unity). O arquivo C# criado na engine deve se parecer com o código abaixo.
using System;
[Serializable]
public class WeatherAPI_Response {
public float rain;
public float wind;
public float fog;
public float lightning;
}
Agora só temos que fazer alguns ajustes pequenos, começaremos mudando todos os "onValueChange" de nossos Sliders para "Editor and Runtime";
Depois vamos ajustar algumas coisinhas em nossa classe APIHelper, como:
- receber os componentes de Slider;
- instanciar os componentes buscando os mesmos;
- melhorar nosso método de chamada para transformar o JSON para classe;
- criar um método para mudar os valores dos Sliders de acordo com o que recebemos da API.
Novamente pelo limite de tamanho eu tive que remover o trecho de código final, porém, você encontra o mesmo clicando aqui.
🎉 Pode estourar o Champagne
Se você chegou até aqui, eu não estiver maluco, e as máquinas não tiverem dominado tudo fazendo com que as linguagens de programação não mais funcionem, é bem provável que você tenha conseguido fazer algo muito massa hoje! E eu nem tô falando desse projeto, eu tô falando de você ter mantido a atenção no texto, isso aqui ficou bem mais longo do que eu esperava mas acho que conseguimos passar por todos os pontos, pelo menos, de uma forma "ok".
Esse foi o resultado obtido seguindo todas as migalhas em nosso caminho... Mas talvez você tenha notado que esteja meio cru, certo?! Que tal melhorar esse projeto? Aqui vão algumas ideias.
- dia/noite;
- neve (que inclusive já tá incluso nesse asset);
- pesquisa em tempo real de cidade;
- mostrar quantos graus está fazendo;
- chamar a API de tempos em tempos para atualizar o clima.
Inclusive algumas dessas ideias eu mesmo implementei numa demonstração, caso você queira brincar um pouco é só clicar aqui e todo o código do backend você encontra no meu GitHub. Inclusive, você sabe como foi que eu fiz pra colocar uma compilação Unity e minha API na internet de graça? Acho que tá aí um bom próximo conteúdo a ser postado aqui...
No mais, agradeço a todos pela atenção e qualquer problema/dúvida/sugestão sou todo ouvidos.
🗺️ Agradecimentos
- NASA's Goddard Space Flight Center - Goddard Media Studios
- Weather System - Pixel Weather Particles
Cara, achei um máximo a sua ideia. pretendo utilizá-la algum dia.
Cara, adorei a sua ideia!!!
Sabe o que seria show? Aproveitar que você está monitorando o clima em "tempo real" e que a moçada (e garotada) está curtindo seu game para gerar ALERTAS CLIMÁTICOS.
Já pensou que máximo, o garoto receber um alerta de uma tempestade severa enquanto joga? Poderia receber orientações sobre segurança e, de repente, salvar a família toda...
Eu fiz isso em um projeto antigo (que já está fora do ar mas ainda tem a landing page: https://piuui.com
Mas não era um game... acho sua ideia imensamente superior à minha.
Boaa man! Essa foi uma ótima ideia, dá uma sensação de realismo no jogo né
Bom demais! Eu fiz uma aplicação em .NET utilizando essa mesma lógica alguns anos atrás, a diferença que ela mudava o meu wallpaper de acordo com clima/horário. Uma pena que eu não conhecia o git naquela época!
Caralho Davi que foda, parabéns pelo projeto mesmo, já dei uma star lá no repo e vou dar uma olhada mais detalhada depois...
Me deu até umas ideias para usar a api para customizar o meu ambiente baseado no clima da cidade que estou kkkk
Idéia maluca: usar essa lógica pra deixar as mudanças climáticas muito concretas.
Se tiver algum site com o histórico do clima, daria pra fazer algum jogo que se passa em duas épocas diferentes (p. ex. hoje e a 50 anos atrás), e deixar o jogador experimentar as diferenças climáticas.
Excelente post, sempre quis saber como fazer isso.
muito bom
Sensacional!