徳島ゲーム開発ごっこ 技術ブログ

ゲームを作るために役に立ったり立たなかったりする技術を学んでいきます!

【Unity】WebAPIを呼んで天気予報を取得してみよう

 ……やれやれビショビショだ。
 出かける時は雨が降っていなかったのに、帰りには土砂降りだよ。
 傘を持っていけばよかったなぁ……。
 天気予報はちゃんと見ておくべきだったね。

 というわけで今回はWeb上で提供されているAPIを呼んで、天気予報を取得することにチャレンジしてみますよ。
 ちなみにただ単に、天気予報が知りたいだけの人は、テレビやネットニュースを見ればいいと思うな。


この記事にはUnity2017.4.1f1を使用しています。

天気予報情報を取得したい

 天気予報情報はどこから取得できるのだろうか。
 いろいろ調べてみたけど「お天気Webサービス」がWebAPIを提供してくれているようだ。
 これはありがたい。

weather.livedoor.com

 早速使ってみよう。
 ……と言いたいところだけど、このWebAPIを呼ぶには各都市のIDを知らなくてはいけないようだ。
 な、なんなんだ、都市のIDって。
 とにかく都市のIDを取得してみようか。

都市情報を取得する

 お天気Webサービスでは都市情報が定義されているようだ。
 都市のIDはこの都市情報の中に含まれているようだね。

livedoor

 この情報をスクリプト上で取得する必要がありそうだ。
 インターネット上の情報の取得処理にはUnityWebRequestを使って作成してみよう。

docs.unity3d.com
docs.unity3d.com

WeatherController.cs

using UnityEngine;
using UnityEngine.UI;
using System.Collections;
using System.Collections.Generic;
using System.Linq;

public class WeatherController : MonoBehaviour
{
    private IEnumerator CallWebRequest( string i_requestURL, System.Action<string> i_onFinished )
    {
        if( i_onFinished == null )
        {
            Debug.LogErrorFormat( "コールバックが指定されてないぜ。これじゃ結果がわからないぜ。" );
            yield break;
        }

        if( string.IsNullOrEmpty( i_requestURL ) )
        {
            Debug.LogErrorFormat( "URLぐらい指定してくれよ……。" );
            i_onFinished( null );
            yield break;
        }

        var request = UnityEngine.Networking.UnityWebRequest.Get( i_requestURL );
        yield return request.SendWebRequest();

        // ドキュメントなどでは"isError"を使って判定しているようだけど、
        // "isNetworkError"に名前が変わったようなので、こっちを使ってくれよな!
        if( request.isHttpError || request.isNetworkError )
        {
            Debug.LogErrorFormat( "残念ながらエラーが発生しちまったらしい。詳細を添付しておく。{0}", request.error );
            i_onFinished( null );
            yield break;
        }

        i_onFinished( request.downloadHandler.text );
    }

} // class WeatherController

 こんな感じだろうか。
 ではこの関数を使用して、都市情報を取得してみよう。
 ちなみに都市情報はXML形式で取得できるんだ。
 XML形式の扱いについては、以前調査したのでその情報を元にデシリアライズしようっと。

www.urablog.xyz

CityData.cs(デシリアライズ先のクラス情報)

public class CityData
{
    public CityData( string i_id, string i_name )
    {
        ID      = i_id;
        Name    = i_name;
    }

    public string ID
    {
        get;
        private set;
    }

    public string Name
    {
        get;
        private set;
    }
}

WeatherController.cs

using UnityEngine;
using UnityEngine.UI;
using System.Collections;
using System.Collections.Generic;
using System.Linq;

public class WeatherController : MonoBehaviour
{
    private static readonly string DEFINITION_TABLE_URL = "http://weather.livedoor.com/forecast/rss/primary_area.xml";
    private static readonly System.Xml.Linq.XName CITY_TITLE_ATTRIBUTE  = System.Xml.Linq.XName.Get( "title" );
    private static readonly System.Xml.Linq.XName CITY_ID_ATTRIBUTE     = System.Xml.Linq.XName.Get( "id" );

    private CityData[] m_cities = null;

    private void Start()
    {
        StartCoroutine( Initialize() );
    }


    #region Initialize

    private IEnumerator Initialize()
    {
        CityData[] cities = null;
        {
            System.Action<CityData[]> onFinished = ( i_cities ) =>
            {
                cities = i_cities;
            };
            yield return GetCityTableProcess( onFinished );
        }

        if( cities == null || cities.Length == 0 )
        {
            Debug.LogErrorFormat( "都市情報取ってこれんかったわ。フォーマットでも変わったのかなぁ。" );
            yield break;
        }

        m_cities = cities;
    }

    private IEnumerator GetCityTableProcess( System.Action<CityData[]> i_onFinished )
    {
        if( i_onFinished == null )
        {
            Debug.LogErrorFormat( "コールバックが指定されてないぜ。これじゃ結果がわからないぜ。" );
            yield break;
        }

        string xmlText = null;
        {
            System.Action<string> onFinished = ( i_text ) =>
            {
                xmlText = i_text;
            };
            yield return CallWebRequest( DEFINITION_TABLE_URL, onFinished );
        }

        if( string.IsNullOrEmpty( xmlText ) )
        {
            Debug.LogErrorFormat( "1次細分区定義表の取得に失敗だ!" );
            i_onFinished( null );
            yield break;
        }

        Debug.LogFormat( "1次細分区定義表(XML):\n{0}", xmlText );
        CityData[] cities = ParseCityList( xmlText );

        i_onFinished( cities );
    }

    private CityData[] ParseCityList( string i_xmlText )
    {

        var cities = new List<CityData>();

        try
        {
            System.Xml.Linq.XDocument xml = System.Xml.Linq.XDocument.Parse( i_xmlText );
            System.Xml.Linq.XElement root = xml.Root;

            // cityタグに都市情報があるはずだ!
            // フォーマットが変わったら、この方法じゃ無理になるかもしれないかもね。
            var cityElements    = root.Descendants( "city" );
            if( cityElements != null )
            {
                foreach( var element in cityElements )
                {
                    var titleAttribute  = element.Attribute( CITY_TITLE_ATTRIBUTE );
                    var idAttribute     = element.Attribute( CITY_ID_ATTRIBUTE );
                    if( titleAttribute == null || idAttribute == null )
                    {
                        continue;
                    }

                    cities.Add( new CityData( idAttribute.Value, titleAttribute.Value ) );
                }
            }
        }
        catch( System.Exception i_exception )
        {
            Debug.LogErrorFormat( "うーむ、このXML情報は読み込めなかったらしい。エラーの詳細を添付しておくよ。{0}", i_exception );
        }

        return cities.ToArray();
    }

    #endregion // Initialize

} // class WeatherController

f:id:urahimono:20180430160442p:plain

 ちゃんと取得できたみたいだぞ!
 ただログに表示するだけでは味気ないので、UIとして画面に表示しようかな。
 UGUIのドロップダウンメニューを使おう。

@code03
f:id:urahimono:20180430160453p:plain

 うんうん、これで知りたい天気予報の都市を指定できるようになったぞ!

天気予報情報を取得する

 都市のIDの取得に成功した。
 これで天気予報のWebAPIが呼べるようになったぞ。
 さっそく呼んでみよう。

 WebAPIを呼ぶのには、先ほど作ったCallWebRequest()を使おう。
 ちなみに天気予報の情報はJson**形式で取得できるようだ。
 それならばJsonUtility.FromJson()を使ってパパっとデシリアライズしてしまおう。

docs.unity3d.com

デシリアライズ先のクラス情報。
WeatherData.cs

using UnityEngine;

[System.Serializable]
public class WeatherData
{
    [SerializeField]
    public ForecastData[] forecasts = null;


    [System.Serializable]
    public class ForecastData
    {
        [SerializeField]
        public string telop = null;
        [SerializeField]
        public string date  = null;
        [SerializeField]
        public TemperatureData temperature = null;
    }

    [System.Serializable]
    public class TemperatureData
    {
        [SerializeField]
        public TemperatureDetailData min = null;
        [SerializeField]
        public TemperatureDetailData max = null;
    }

    [System.Serializable]
    public class TemperatureDetailData
    {
        [SerializeField]
        public string celsius = null;
    }
}

WeatherController.cs

using UnityEngine;
using UnityEngine.UI;
using System.Collections;
using System.Collections.Generic;
using System.Linq;

public class WeatherController : MonoBehaviour
{
    private static readonly string API_URL              = "http://weather.livedoor.com/forecast/webservice/json/v1";
    private static readonly string API_CITY_PARAMETER   = "city";


    #region GetWeather

    public void OnGetWeather()
    {
        // UI上で取得ボタンを押したら呼ばれるぜ!

        if( m_cities == null || m_cities.Length == 0 )
        {
            Debug.LogErrorFormat( "都市情報を取得できてないから、天気取得なんて無理無理!" );
            return;
        }

        if( m_dropdownUI == null ||
            !m_dropdownUI.gameObject.activeInHierarchy )
        {
            Debug.LogErrorFormat( "都市のUI表示されてなくね?" );
            return;
        }

        string selectedCityName = m_dropdownUI.captionText.text;
        if( string.IsNullOrEmpty( selectedCityName ) )
        {
            Debug.LogErrorFormat( "どこの都市を選択したか、わかんないんだけど……。" );
            return;
        }

        CityData selectedCity = m_cities.FirstOrDefault( value => value.Name == selectedCityName );
        if( selectedCity == null )
        {
            Debug.LogErrorFormat( "知らない都市なんだが……。この都市知ってる? {0}", selectedCityName );
            return;
        }

        StartCoroutine( GetWeatherProcess( selectedCity.ID ) );
    }

    private IEnumerator GetWeatherProcess( string i_cityID )
    {
        if( string.IsNullOrEmpty( i_cityID ) )
        {
            Debug.LogErrorFormat( "ちゃんと都市IDを入れてくれよ!" );
            yield break;
        }

        string requestURL   = string.Format( "{0}?{1}={2}", API_URL, API_CITY_PARAMETER, i_cityID );
        string jsonText     = null;
        {
            System.Action<string> onFinished = ( i_text ) =>
            {
                jsonText    = i_text;
            };
            yield return CallWebRequest( requestURL, onFinished );
        }

        if( string.IsNullOrEmpty( jsonText ) )
        {
            Debug.LogErrorFormat( "天気情報の取得に失敗だ!" );
            yield break;
        }


        Debug.LogFormat( "天気情報(Json):\n{0}", jsonText );

        var weatherData = JsonUtility.FromJson<WeatherData>( jsonText );
    }

    #endregion // GetWeather

} // class WeatherController

f:id:urahimono:20180430160504p:plain

 データ取得に成功だ!
 あとはこの情報を画面に表示してしまおう。
 まずはUIモデル用のコンポーネントを作成してっと。

WeatherUI.cs

using UnityEngine;
using UnityEngine.UI;

public class WeatherUI : MonoBehaviour
{
    [SerializeField]
    private Text m_date = null;
    [SerializeField]
    private Text m_weather = null;
    [SerializeField]
    private Text m_temperatureMin = null;
    [SerializeField]
    private Text m_temperatureMax = null;

    public string Data
    {
        set
        {
            if( m_date != null )
            {
                m_date.text = value;
            }
        }
    }

    public string Weather
    {
        set
        {
            if( m_weather != null )
            {
                m_weather.text = value;
            }
        }
    }

    public string TemperatureMin
    {
        set
        {
            if( m_temperatureMin != null )
            {
                if( string.IsNullOrEmpty( value ) )
                {
                    m_temperatureMin.text = string.Format( "最低温度:---" );
                    return;
                }
                m_temperatureMin.text = string.Format( "最低温度:{0}℃", value );
            }
        }
    }

    public string TemperatureMax
    {
        set
        {
            if( m_temperatureMax != null )
            {
                if( string.IsNullOrEmpty( value ) )
                {
                    m_temperatureMax.text = string.Format( "最高温度:---" );
                    return;
                }
                m_temperatureMax.text = string.Format( "最高温度:{0}℃", value );
            }
        }
    }


} // class WeatherUI

 あとはこのコンポーネントに、先ほど取得した天気予報の情報を設定すれば完成だ。

WeatherController.cs

using UnityEngine;
using UnityEngine.UI;
using System.Collections;
using System.Collections.Generic;
using System.Linq;

public class WeatherController : MonoBehaviour
{

    [SerializeField]
    private WeatherUI[] m_weatherUIs = null;

    private IEnumerator GetWeatherProcess( string i_cityID )
    {
        if( string.IsNullOrEmpty( i_cityID ) )
        {
            Debug.LogErrorFormat( "ちゃんと都市IDを入れてくれよ!" );
            yield break;
        }

        string requestURL   = string.Format( "{0}?{1}={2}", API_URL, API_CITY_PARAMETER, i_cityID );
        string jsonText     = null;
        {
            System.Action<string> onFinished = ( i_text ) =>
            {
                jsonText    = i_text;
            };
            yield return CallWebRequest( requestURL, onFinished );
        }

        if( string.IsNullOrEmpty( jsonText ) )
        {
            Debug.LogErrorFormat( "天気情報の取得に失敗だ!" );
            yield break;
        }


        Debug.LogFormat( "天気情報(Json):\n{0}", jsonText );

        var weatherData = JsonUtility.FromJson<WeatherData>( jsonText );
        SetWeatherUI( weatherData );
    }

    private void SetWeatherUI( WeatherData i_weatherData )
    {
        if( i_weatherData == null )
        {
            Debug.LogErrorFormat( "天気情報が空のようだぜ!" );
            return;
        }

        var forecasts = i_weatherData.forecasts;
        if( forecasts == null || forecasts.Length == 0 )
        {
            Debug.LogErrorFormat( "天気情報が空のようだぜ!" );
            return;
        }

        if( m_weatherUIs == null || m_weatherUIs.Length == 0 )
        {
            Debug.LogErrorFormat( "天気情報を表示するUIの準備が出来ていないようだね" );
            return;
        }

        // 日付毎のUIの数より天気情報の日付数の方が多い場合は、UI分だけ表示。
        // 逆に日付毎のUIの数の方が多い場合は、天気情報分だけ表示。
        for( int i = 0; i < m_weatherUIs.Length; ++i )
        {
            var ui = m_weatherUIs[ i ];
            if( ui == null )
            {
                continue;
            }

            if( forecasts.Length <= i )
            {
                ui.gameObject.SetActive( false );
                continue;
            }

            ui.gameObject.SetActive( true );
            var forecast = forecasts[ i ];

            // 温度は入っていないこともあるみたいなので、一応チェックしておこう。
            ui.Data             = forecast.date;
            ui.Weather          = forecast.telop;
            ui.TemperatureMin   = forecast.temperature.min != null ? forecast.temperature.min.celsius : null;
            ui.TemperatureMax   = forecast.temperature.max != null ? forecast.temperature.max.celsius : null;
        }

    }
} // class WeatherController

f:id:urahimono:20180430160520p:plain

 やった。これで天気予報が取得できた!
 これで雨に濡れる心配もなさそうだ。

 というわけで記事もできたことだし、散歩がてらに出かけようか。
 先ほどの天気予報では、名古屋は晴れのようだけど、大阪はどうか見てなかったな。
 雲行きはそんなに良くなさそうだけど。
 ……ま、いっか。
 今はまだ雨は降ってないし、手ぶらで出かけるか。

【Unity】WebAPIを呼んで天気予報を取得してみよう