おれんじりりぃぶろぐ

きっと何者にもなれないエンジニアのブログ

UnityにおけるAwaitAsyncを使った非同期実装について②

概略

前回は、最新Unityにおけるマルチスレッドプログラミングの方法についての紹介をしました。 今回はUnityでTask、awaitを使ったマルチスレッドを行う時に起きる問題について取り上げます。

Unityにおけるメインスレッド制約

using System.Threading;
using System.Threading.Tasks;
using UnityEngine;
using UnityEngine.UI;

public class AsyncSampleScene : MonoBehaviour
{

    [SerializeField] private Text text;

    private async void Start()
    {
        Debug.Log($"ID {Thread.CurrentThread.ManagedThreadId}, Start Start()");
        await TestAsyncMethod();
        Debug.Log($"ID {Thread.CurrentThread.ManagedThreadId}, End Start()");
    }

    private async Task TestAsyncMethod()
    {
        await Task.Run(() =>
        {
            Thread.Sleep(1000);
            text.text = "1";  
        });
    }
}

上記のコードを実行すると以下のエラーが出ます。

get_isActiveAndEnabled can only be called from the main thread.
Constructors and field initializers will be executed from the loading thread when loading a scene.
Don't use this function in the constructor or field initializers, instead move initialization code to the Awake or Start function.

これはメインスレッド以外からTextにアクセスを行っているため、メインスレッドで実行してくださいというエラー内容になります。 MonoBehaviourを継承しているクラスの使用についてはメインスレッドからアクセスしなければならないという制約があるためです。 では、この問題にはどのような対処方法が考えられるでしょうか?

対処方法

これに対する対処には以下の方法が考えられます。 まずは、stringを返す方法です。

using System.Threading;
using System.Threading.Tasks;
using UnityEngine;
using UnityEngine.UI;

public class AsyncSampleScene : MonoBehaviour
{

    [SerializeField] private Text text;

    private async void Start()
    {
        Debug.Log($"ID {Thread.CurrentThread.ManagedThreadId}, Start Start()");
        text.text = await TestAsyncMethod();
        Debug.Log($"ID {Thread.CurrentThread.ManagedThreadId}, End Start()");
    }

    private async Task<string> TestAsyncMethod()
    {
        var text = string.Empty;
        await Task.Run(() =>
        {
            Thread.Sleep(1000);
            text = "1";  
            return text;
        });
        return text;
    }
}

2つ目がコルーチンを使う方法です。 そして、3つ目があるスレッドから特定のスレッド(メインスレッド)にアクセスする方法になります。

あるスレッドからメインスレッドへのアクセス方法

using System.Threading;
using System.Threading.Tasks;
using UnityEngine;
using UnityEngine.UI;

public class AsyncSampleScene : MonoBehaviour
{

    [SerializeField] private Text text;

    private async void Start()
    {
        Debug.Log($"ID {Thread.CurrentThread.ManagedThreadId}, Start Start()");
        await TestAsyncMethod();
        Debug.Log($"ID {Thread.CurrentThread.ManagedThreadId}, End Start()");
    }

    private async Task TestAsyncMethod()
    {
        var context = SynchronizationContext.Current;

        await Task.Run(() =>
        {
            context.Post((state) =>
            {
                text.text = "1";
            }, null);

            Thread.Sleep(1000);

            context.Post((state) =>
            {
                text.text = "2";
            }, null);

            Thread.Sleep(1000);

            context.Post((state) =>
            {
                text.text = "3";
            }, null);

            Thread.Sleep(1000);
        });
    }
}

SynchronizationContext.Currentは現在の同期コンテキストを取得できるプロパティになり、すなわちメインスレッドを取得することができます。 androidjava層からUnityのメインスレッドのコンテキストを取得して操作を行うイメージに近いと思います。

参考

gasを使ったお手軽WebAPI実装

概要

超お手軽にWebAPIを作る方法です。 GASを使って、POSTしてスプレッドシートにデータを保存してそれをGETする方法です。 40秒でAPI作りなと言われた時の対応法です。

実演

POSTするとspreadsheetに保存してくれます。

curl -X POST -d "timestamp=1523790159&content=hoge" https://script.google.com/macros/s/xxxx/exec

スクリーンショット 2018-04-15 22.43.41.png (80.4 kB)

GETするとjsonを返してくれます。

curl -L -X GET https://script.google.com/macros/s/xxxx/exec
{"timestamp":"1523790159","content":"hoge"}

手順

「新規作成」-> 「スプレッドシート」を選択します。 スクリーンショット 2018-04-15 22.45.01.png (79.8 kB)

「ツール」->「スクリプトエディタ」でgasを起動します。 スクリーンショット 2018-04-15 22.46.02.png (103.3 kB)

以下のソースコードを実装します。

// データ更新
function doPost(e) {
  // パラメータのパース
  var timestamp = e.parameters.timestamp;
  var content = e.parameters.content;

  var sheet = SpreadsheetApp.getActive().getSheetByName('シート1');
  var sheetData  = sheet.getDataRange().getValues();
  sheetData.push([timestamp, content]);
  // シートへの書き込み、getRange(開始行、開始列、行数、列数)
  sheet.getRange(1,1,sheetData.length,2).setValues(sheetData);
}

// データ取得
function doGet(e) {  
  var sheet = SpreadsheetApp.getActive().getSheetByName('シート1');
  var sheetData = sheet.getDataRange().getValues();

// 最終行を取得する
  var timestamp = String(sheet.getRange(sheet.getLastRow(),1,1,1).getValue());
  var content = sheet.getRange(sheet.getLastRow(),2,1,1).getValue();

  var result = {'timestamp': timestamp, 'content': content};

  return ContentService.createTextOutput(JSON.stringify(result)).setMimeType(ContentService.MimeType.JSON);
}

POSTリクエストが来た場合にはdoPost()がGETリクエストが来た場合にはdoGet()が呼ばれます。

JSONでのPOST

jsonでpostしたい場合は以下のようにします。

function doPost(e) {
  var json = e.postData.getDataAsString();
  var data = JSON.parse(json);
  
  var timestamp = data.timestamp
  var content = data.content

  var sheet = SpreadsheetApp.getActive().getSheetByName('シート1');
  var sheetData  = sheet.getDataRange().getValues();
  sheetData.push([timestamp, content]);
  // シートへの書き込み、getRange(開始行、開始列、行数、列数)
  sheet.getRange(1,1,sheetData.length,2).setValues(sheetData);
}

アクセスできるように公開する

[公開] -> 「ウェブアプリケーションとして導入」を選択します。 スクリーンショット 2018-04-16 0.23.24.png (53.4 kB)

以下のように設定して導入をクリックします。 スクリーンショット 2018-04-16 1.04.35.png (77.3 kB)

アプリケーションにアクセスできるユーザーは「全員(匿名ユーザーを含む)」にする必要があります。会社などの組織アカウントでは、制限が書けられていてこの選択肢がないこともあります。その場合は個人アカウントを使ってください。

アクセス先のURLが表示されるのでメモします。

注意

スクリプトの更新を行った時には、ウェブアプリケーションの導入を再度行う必要があります。(面倒くさい)その場合でもURLは変わらないようです。

おまけ

Unityからでも問題なく使えます。

using UnityEngine;
using System.Collections;
using UnityEngine.Networking;
 
public class APITest : MonoBehaviour {
    void Start() {
        StartCoroutine(GetText());
    }
 
    IEnumerator GetText() {
        UnityWebRequest www = UnityWebRequest.Get("https://script.google.com/macros/s/xxxx/exec");
        yield return www.SendWebRequest();
 
        if(www.isNetworkError || www.isHttpError) {
            Debug.Log(www.error);
        }
        else {
            // 結果をテキストとして表示します
            Debug.Log(www.downloadHandler.text);
        }
    }
}
{"timestamp":"1523790159","content":"hoge"}
UnityEngine.Debug:Log(Object)

参考

UnityにおけるAwaitAsyncを使った非同期実装について①

概略

Unity2018.1から正式に.Net4.7、C#6への対応がなされます。 C#5からは、非同期的な実装をまるで同期的に記述できるasync/awaitというキーワードが導入されました。今回はasync/awaitを使ったUnityにおけるマルチスレッドプログラミングについて解説を行います。

非同期実装を行う必要性とは(・・?)

同期実装とは、プログラムを記述通り順番に実行していきます。対して、非同期実装では記述順とは異なる順序での実行がなされます。どうして非同期に処理を行う必要があるのでしょうか。

スクリーンショット 2018-04-13 20.13.05.png (69.3 kB)

この図は同期処理を表したものです。HeavyMethod()の実行中は、メインスレッドを専有してしまいUIが固まる、イベントが受けられないなどといった状況に陥ってしまいます。 非同期処理の必要性は、メインスレッドの専有を避けることにあります。

非同期実装を行う方法

  • Unityコルーチン
  • async/await(C#5以降)

Unityでは、非同期な処理を行えるようにコルーチンの仕組みが用意されています。

スクリーンショット 2018-04-13 20.09.49.png (65.8 kB)

コルーチンでは、各処理を高速に切り替え一つの処理がメインスレッドを専有しない仕組みになっています。しかし、全ての処理をメインスレッドで行っているため処理限界に達することがあります。 重たい処理に関してはメインスレッドではなく別スレッドで実行し処理が終わったらメインスレッドに戻ることが求められます。

スクリーンショット 2018-04-13 20.32.30.png (73.6 kB)

C#5以前でもThreadを扱うクラスがありマルチスレッドな実装を行うことは可能でした。しかし、低レベルなAPIレベルでしか提供がなされておらずやや扱いにくい印象でした。C#5以降ではこれを解消する直感的かつ簡潔にまるで同期処理のように非同期処理が記述できるasync/awaitの仕組みがで導入されました。

Taskの説明

マルチスレッドな非同期処理を見ていく前にTaskの説明を行います。 Taskはある一連の処理をまとめた一つの単位になります。

var task = Task.Run(() =>
{
    Debug.Log("HelloWorld");
    Debug.Log("HelloWorld");
    Debug.Log("HelloWorld");
});

これはデバッグログを3回出力するという一つのTaskになります。 Task.Runは、Taskのファクトリメソッドになります。 Taskでの処理は自動的にスレッドプールになります。

マルチスレッドの実装

さて、Taskについての理解ができました。 このTask単位でマルチスレッドに行う方法を見ていきます。

シンプルなマルチスレッド実装

using System.Threading;
using System.Threading.Tasks;
using UnityEngine;

public class AsyncSampleScene : MonoBehaviour {

    private async void Start()
    {
        Debug.Log($"ID {Thread.CurrentThread.ManagedThreadId}, Start Start()");
        await TestAsyncMethod();
        Debug.Log($"ID {Thread.CurrentThread.ManagedThreadId}, End Start()");
    }

    private Task TestAsyncMethod()
    {
        return Task.Run(() => {
            Debug.Log($"ID {Thread.CurrentThread.ManagedThreadId}, [Async] 1");
            Thread.Sleep(1000); 
            Debug.Log($"ID {Thread.CurrentThread.ManagedThreadId}, [Async] 2");
            Thread.Sleep(1000);
            Debug.Log($"ID {Thread.CurrentThread.ManagedThreadId}, [Async] 3");
            Thread.Sleep(1000);
        });
    }
}

結果

ID 1, Start Start() //メインスレッド
ID 86, [Async] 1 // 別スレッド
ID 86, [Async] 2 // 別スレッド
ID 86, [Async] 3 // 別スレッド
ID 1, End Start() // メインスレッド

(ここでのIDはスレッドごとに割り当てられる一意のIDです)

メインスレッドでログを1回出力し、別のスレッドで処理を実行し、最後にまたメインスレッドに戻ってログを出力しています。 処理を別スレッドに逃すことができ、その完了をハンドリングしてメインスレッドに戻ることができました。 ポイントは、awaitとTask.Runです。 実行関数の前にawaitをつけるとTestAsyncMethod()の完了を待って、処理が終わったら後続処理をメインスレッドで続行してくれます。awaitを使ったメソッドではシグネチャにasyncをつけることになっています。 Task.RunはTaskのファクトリーメソッドであり、この中のTaskを別スレッドで実行してくれます。

返り値がある場合は以下のようになります。

using System.Threading;
using System.Threading.Tasks;
using UnityEngine;

public class AsyncSampleScene : MonoBehaviour
{
    private async void Start()
    {
        Debug.Log($"ID {Thread.CurrentThread.ManagedThreadId}, Start Start()");
        int j = await TestAsyncMethod();
        Debug.Log($"ID {Thread.CurrentThread.ManagedThreadId}, {j}");
    }

    private Task<int> TestAsyncMethod()
    {
        return Task.Run<int>(() =>
        {
            int i = 0;
            Debug.Log($"ID {Thread.CurrentThread.ManagedThreadId}, [Async] 1");
            i++;
            Thread.Sleep(1000); // 擬似的な重たい処理
            Debug.Log($"ID {Thread.CurrentThread.ManagedThreadId}, [Async] 2");
            i++;
            Thread.Sleep(1000);
            Debug.Log($"ID {Thread.CurrentThread.ManagedThreadId}, [Async] 3");
            i++;
            Thread.Sleep(1000);
            return i;
        });
    }
}

結果

ID 1, Start Start()
ID 116, [Async] 1
ID 116, [Async] 2
ID 116, [Async] 3
ID 1, End Start()

Task.Wait()の罠

次に示すのは、デットロックを引き起こすヤバイコードです。 (UnityEditorで実行すると応答なくなって強制終了するしかなくなります)

using System.Threading;
using System.Threading.Tasks;
using UnityEngine;

public class AsyncSampleScene : MonoBehaviour {

    private void Start()
    {
        Debug.Log($"ID {Thread.CurrentThread.ManagedThreadId}, Start Start()");
        Task t = KusoAsyncMethod();
        t.Wait(); // tのTaskが終わるまで待つ
        Debug.Log($"ID {Thread.CurrentThread.ManagedThreadId}, End Start()");
    }

    private Task KusoAsyncMethod()
    {
        return Task.Run(() => {
            Debug.Log($"ID {Thread.CurrentThread.ManagedThreadId}, [Async] 1");
            Thread.Sleep(1000);
            Debug.Log($"ID {Thread.CurrentThread.ManagedThreadId}, [Async] 2");
            Thread.Sleep(1000);
            Debug.Log($"ID {Thread.CurrentThread.ManagedThreadId}, [Async] 3");
            Thread.Sleep(1000);
        });
    }
}

なぜこのようなことになってしまうのでしょうか。

スクリーンショット 2018-04-14 0.06.22.png (111.2 kB)

図にすると上図のような状態に陥ってしまいます。 一方で、awaitを使うと下図になり挙動が異なることが分かります。

スクリーンショット 2018-04-14 0.27.05.png (105.1 kB)

まとめ

  • Task、async/awaitを使うことでマルチスレッドな非同期処理を直感的に記述することができる
  • Taskクラスを使うとスレッドプールを自動で行ってくれる
  • 別スレッドで発生した例外もキャッチすることができる
  • Task.Wait()またはTask.Result()をデットロックに陥る危険性がある

参考

gasを使って特定のGoogleドライブ以下のファイル更新を通知する

概要

特定のGoogleドライブ以下のファイル更新を検知してCWで通知する方法です。

なぜgasなのか(・・?)

何と言ってもスクリプトを実行するサーバーを用意しなくて済むからです。chatworkへの通知も非常にお手軽に実装することが可能です。 Googleドライブとの親和性ももちろん高く、簡単な実装で実現が可能です。

手順

ドライブの「新規作成」->「その他」->「Goggle Apps Script」を選択します。 スクリーンショット 2018-03-15 2.30.09.png (142.2 kB)

「リソース」->「ライブラリ」からChatworkClientを導入します。 スクリーンショット 2018-03-15 2.07.11.png (63.6 kB)

ライブラリを追加のところに「M6TcEyniCs1xb3sdXFF_FhI-MNonZQ_sT」を入力します。 * https://github.com/cw-shibuya/chatwork-client-gas

いい感じにスクリプトを書きます。

var Token = "xxxxxx";  // chatwork bot
var RoomId = xxxxx; // CWの部屋ID

//対象とするGoogleDrive親フォルダのID
// IDはURLのfolders/以下の英数字
var PARENT_FOLDER_ID = "xxxx";

// 24時間(ミリsec)
var SEARCH_TIME = 86400;

function myFunction() {
  var parentFolder = DriveApp.getFolderById(PARENT_FOLDER_ID);
  
  var dateobj = new Date();
  var nowUnixTime = Math.floor(dateobj.getTime() / 1000) ;
  var dayAgoUnixTime = nowUnixTime - SEARCH_TIME;
  var mes = "";
  
  mes += "[info][title](F)本日更新されたドキュメント一覧(F)[/title]";
  
  var allFiles = getAllFilesName(parentFolder);
  for(var i = 0; i < allFiles.length; i++){
    // ファイル更新のタイムスタンプ(unixtime)
    var fileTimeStamp = Math.floor(Date.parse(allFiles[i].getLastUpdated())/1000);
    
    if(fileTimeStamp > dayAgoUnixTime){
      mes += "* ";
      mes += allFiles[i].getName();
      mes += "\n";
      
      mes += "  * ";
      mes += allFiles[i].getUrl();
      mes += "\n";
      
    }
    
    //Logger.log(allFiles[i].getName());
  }

  mes += "[/info]";
  
  // CW通知
  var client = ChatWorkClient.factory({token: Token});
  client.sendMessage({room_id: RoomId, body: mes});
  
}

function getAllFilesName(parentFolder){
  var fileList = [];
  var files = parentFolder.getFiles();
  while(files.hasNext()){
    fileList.push(files.next());
  }
  
  var childFolders = parentFolder.getFolders();
  
  while(childFolders.hasNext()){
    var childFolder = childFolders.next();
    
    // 再帰的な呼び出し
    fileList = fileList.concat(getAllFilesName(childFolder));
  }
  return fileList;
}

getFiles()getFolders()では、そのフォルダに属したファイルやフォルダしか取れないため再帰的に取得を行う必要があります。

トリガーを好きな時間に設定します。 「編集」->「現在のプロジェクトのトリガー」->「新しいトリガーを追加」から設定します。

スクリーンショット 2018-03-15 2.17.24.png (55.7 kB)

通知が来ます スクリーンショット 2018-03-18 0.30.01.png (36.6 kB)

リファレンス

gasを使ったお手軽サーバー死活監視

概要

超お手軽にWebサーバーの死活監視をしてchatworkに通知する方法です。

なぜgasなのか(・・?)

何と言っても死活監視のスクリプトをcronするサーバーを用意しなくて済むからです。chatworkへの通知も非常にお手軽に実装することが可能です。 高度なことはできませんが、雑に死活監視するくらいであればgas環境でも十分です。

手順

ドライブの「新規作成」->「その他」->「Goggle Apps Script」を選択します。 スクリーンショット 2018-03-15 2.30.09.png (142.2 kB)

「リソース」->「ライブラリ」からChatworkClientを導入します。 スクリーンショット 2018-03-15 2.07.11.png (63.6 kB)

ライブラリを追加のところに「M6TcEyniCs1xb3sdXFF_FhI-MNonZQ_sT」を入力します。

いい感じにスクリプトを書きます。

var siteURL = "http://xxxxxxxx.com"; // チェックしたいサーバーのURL
var Token = "xxxx"; // chatworkのbot token
var RoomId = xxxx; // 通知したい部屋のID

function myFunction() {

  try {
    // URLをフェッチ - muteHttpExceptions:trueの場合、HTTPエラーの際に例外をスローしない
    var response = UrlFetchApp.fetch(siteURL, { muteHttpExceptions:true });
    // レスポンスコード
    var code = response.getResponseCode();

    // レスポンスコード 200をチェックする
    if(code == 200) {
        Logger.log("access OK");
        Logger.log("Response code: " + code);
    } else {
        var mes = "[info][title]死活監視[/title][To:xxxx]\nhogehogeが落ちています[/info]";
        Logger.log(mes);
        var client = ChatWorkClient.factory({token: Token});
        client.sendMessage({room_id: RoomId, body: mes});
    }
  } catch(err) {
    // catch : DNSエラーなどでURLをfetch出来ないとき
    var mes = "[info][title]死活監視[/title][To:xxxx]\nhogehogeにアクセスできません[/info]";
    Logger.log(mes);
    var client = ChatWorkClient.factory({token: Token});
    client.sendMessage({room_id: RoomId, body: mes});
  }
}

トリガーを設定します。 「編集」->「現在のプロジェクトのトリガー」->「新しいトリガーを追加」から設定します。 スクリーンショット 2018-03-15 2.17.24.png (55.7 kB)

あとは、サーバーが落ちるのを楽しみに待つだけです٩( ‘ω’ )و

Unity、Android、iOSのビルドトラブルシューティング

Unityを使っていて主にビルド周りでハマったことをメモとして残したいと思います。

Q. Unityはどこからダウンロードできますか?

  1. こちらからどうぞ

リリース

パッチリリース

Q. Unityのキャッシュサーバーが落ちると自力ビルドできない?

  1. そのようです。ビルド開始後cacheサーバーへの接続が成功し、ビルド終了後に接続を切るタイミングでサーバーが落ちているとビルド自体が失敗するようです。

Q. ローカルキャッシュサーバーが上手く動かないんだけど?

キャッシュサーバーの設定をlocalにするとどうやら上手くいかない場合があるようです。 その場合でもremoteにしてlocalhost:8126にしてあげれば上手くいくようです。

Q. UnityでAndroid SDK、NDK、Javaのバージョンはどこに設定すればいいですか?

  1. [Unity] -> [Preferences] -> [External Tools]から設定できます。

f:id:orange_lily27:20180505213323p:plain

たまにNDKのPath設定が吹っ飛ぶことがあるのですが謎です(´・_・`)

Q. ビルドしようとしたらandroidコマンドがないってエラーが出ます

  1. Android SDK 25.3以降だとandroidコマンドなくなってビルドできなくなっています。 低いバージョンをインストールするか、toolsフォルダだけ古いバージョン(25.2以下)に入れ替える必要があります。

Android SDK Offline: Android SDK Tools

f:id:orange_lily27:20180505215231p:plain

Q. Android NDKのバージョンはいくつにすればよいですか?

  1. UnityではNDKのバージョンはr10e固定です il2cppビルドしない場合はndkの設定は追加しなくても大丈夫です。

アーカイブはここから入手できます。

追記: Unity2017.2からはr13bにアップデートされたようです。

Q. AndroidのBuild-toolsやSDK Plattformはどうやってバージョンアップすればいいですか?

  1. コマンドからandroidと打つ、またはAndroidStudio → Tools → Android → SDK ManagerからAndroid SDK Managerを起動してできます

f:id:orange_lily27:20180505213502p:plain

Q. HighSierraでUnityプロジェクトが開けません

HighSierraからディスクのフォーマットがAPFSとなった影響です。対応方法などは以下を参照してください。

『横井軍平ゲーム館』を読んでの感想

www.amazon.co.jp

3行まとめ

  • 任天堂の発展に寄与した横井軍平氏のインタビュー本であり、任天堂や横井さんに興味がある人にオススメ
  • すぐにゲームが作れるスキルや発想力は特に身につかない
  • コイズム

感想

本書は任天堂の発展になくてはならない人であった横井軍平氏のインタビュー本です。横井氏は任天堂の一社員としてラブテスターゲームウォッチゲームボーイなどを世に送り出してきたその人です。アナログゲームとデジタルゲーム両方の分野で成功を収めており、遊びの本質を制していた人と言っても良い人です。その横井氏を著者である牧野武文氏が亡くなる直前にインタービューしたものを元に構成、解説を付け加えて1997年に発売された単行本の文庫化です。

インタビュー中に特に難しい質疑応答はなく、誰でも楽しく横井氏の昔話を聞くような感覚で本書を読むことができます。しかし、その中には横井氏の根本的な考え方が散りばめられており、読者は横井さんの哲学であるヨコイズムを透けて見ることになります。ただし、透けて見ることが本書の主題なので、具体的なアイデアが湧いてくる方法やゲーム・おもちゃの作り方を学ぶことはあまりできません。そのような目的で本書を手に取ることはオススメできません。

wikipedia横井軍平について調べるとヨコイズムついて短く解説してあるが、あれは本質ではないと私は考えています。そしてこの本を読むことによってヨコイズムの一端を垣間見ることはできますが、受け取り方は人それぞれ変わってくると思います。それだけヨコイズムが示唆に富み多様性を秘めているということでしょう。

ちなみに私の思うヨコイズムの本質とは、「アイデア」と「技術」と「実現」のバランスを図ること、そして、人を制して遊びを制するということです。アイデアだけでもそれは絵に書いた餅だし、技術だけでも楽しさは提供できないただのガラクタになってしまいます。 横井氏の有名な言葉として「枯れた技術の水平思考」があります。文字通りに受け取れば他分野で成熟した技術をエンターテイメントとして組み込むという意味になります。そういう意味ももちろんあるでしょうが、そうではなくCPU性能がどうこう、カラーだ、高画質だと単なる技術一辺倒で押し通すのではなく「アイデア」と「技術」を最適に組み合わせることが「遊び」の本質である、その時の技術とは必ずしも最先端ではないということを言っているのではないかと感じました。

そして、ヨコイズムに欠かしてはならないのは「実現」です。いくらアイデアと技術があってもお金と人がいなければ何もできないということです。そのあたりのバランス感覚の良さが横井氏の閃き力や技術力よりも秀でた一番の才能であることがよく分かります。 さらに「遊び」を制するためには、面白い遊びを作り出すだけではまだ足りなくて、その「遊び」が人々の生活にどのように溶けこんでいくかここまで先回りして予想できるようなビジョンを打ち立てないといけないということが本書からは見て取ることができました。

これらのことは、それはそれは至極当たり前のことなんだけど、そのことを根本的に理解し、実現していくことの難しさとその困難に立ち向かう光明、これこそがヨコイズムであると私は強く思っています。

「アイデアを持って策謀を成す」人 これが本書を読み終わった時の私の横井さんに対する印象でした。 作る側、遊ぶ側エンターテイメントに少しでも携わる者としてはぜひ読んでおきたい一冊です。

『ノンデザイナーズ・デザインブック』を読んでの感想

ノンデザイナーズ・デザインブックを読んだ読書感想文。

概要 ~Hello Design World~

本書はタイトル通りデザイナーではないが洒落乙なプレゼンテーションやwebページをなどを作成したい人向けの本です。私がコードだけで全てを語ることができるハイパーエンジニアならばよいのですが、残念ながらそうではありません。自分の行った成果を人に正しく伝える手段を身につけなければいけません。この本は情報をデザインし、相手に伝わるアウトプットに昇華させるテクニックを紹介してくれています。

大きくは前後半の2部構成になっています。前半はデザインの4つの大原則「近接」「整列」「コントラスト」「反復」と「色」についての知識紹介です。さらに、前半最後では4つの原則を豊富なサンプルを交えてトレーニングしてくれます。

以下に紹介されている4つの原則について簡単にまとめてみました。

  • 「近接」とは、関連ある要素を近くに配置しまとめ、逆に関連しない項目同士を離すことで情報を組織化することです。

  • 「整列」とは、デザインの組織化に加え一体感を演出し情報を整理する効果があります。もう1つの目的としてユーザの視点を誘導し情報の組織化を行います。安定感を求め何も考えずに中央揃えにしてしまうことがあるがこれは良くありません。中央揃えは角張った静粛な場面など目的のある場合を除いて使わないようにすべきです。左揃えや右揃えを使用したほうが洗練したデザインをつくり上げることが可能です。また、基本的には1ページには1種類の方向へ揃えるべきです(例えば、右揃えと中央揃えを併用してはいけない)。

  • 「反復」とは、デザインにリズムを生み出し、そのコンテンツに一貫性を与えてくれます。小見出しなど既に反復している要素を目立たせることで情報に一体感を演出してくれます。

  • コントラスト」とは、実現方法はフォントの選択、色、形、サイズ、空白などあらゆる方法があります。大事なことは2つの要素の違いをはっきりくっきりさせるさせることで、微妙な違いは意味がありません。決して、臆病になってはいけないのです。

後半は「フォント」の重要性と使い方にページを割いています。残念なのは欧文フォント向けに書かれており、日本語フォントにはそのまま当てはめれない部分があることです。別に、日本語フォントの知識について資料を読む必要性がありそうです。

実践 ~ただの履歴書には興味ありません~

「ただの履歴書には興味ありません。この中に優秀なエンジニアがいたら弊社のところに来なさい。以上」

将来、人材エージェント、ジョン・スミスさんからこのような転職の誘いを受けるかもしれません。今回、そんな来る日に備え自分の履歴書を作成してみました。

before

(注意:この履歴書の内容はフィクションです)

希望職種はエンジニアなので、これで十分でしょうか(?_?)う〜ん、どうも私の優秀さが伝わっているとは思えません。ひと目にも触れることはないでしょう(´・_・`)
このことから履歴書においても最低限の身だしなみは大切なことがよく分かります。それでは、この『ノンデザイナーズ・デザインブック』に紹介されているテクニックを実際に使ってみましょう。

after

主な変更点

  • 中央揃え → 右揃えにする
  • 「学歴」や「志望動機」などの小見出しは左揃えにすることによってコントラストを出した
  • オレンジの水平線を小見出しの下に挿れることによって反復とグループ化を図った
  • afterの左側には大きく名前バーを設置しアピール
  • 「履歴書」や「名前」など書かなくても分かる情報をafterでは削除
  • フォントの種類を1種類から2種類へ
  • フォントを「MSP ゴシック」から「ヒラギノ角ゴ Pro」「ヒラギノ明朝 Pro」へ変更
  • afterでは小見出しや左側の名前バーにはサンセリフな「ヒラギノ角ゴ Pro」を、本文にはセリフ体である「ヒラギノ明朝 Pro」を使用しコントラストを出した
  • afterの名前バーと小見出しには同じフォント使用しているが、それぞれw6, w3を使用しwightを変えている
  • afterの左側の茶畑の緑と水平線のオレンジ色は補色関係である

なんということでしょう。素人目にもだいぶデザインが改善されたことを感じることができます。これならばジョン・スミスさんもひと目見る気になるかもしれません。今の私は中身が伴っていないので落とされるでしょうが、見てもらえるだけでも一歩前進です。

もちろん、まだまだこの履歴書には改善や工夫の余地は残されていると思います。しかし、私はエンジニアだからデザインなんか関係ないというダメ意識は少なくとも改革することができました。

まとめ ~さよなら絶望デザイン~

この本の冒頭にジョシアツリーの話が出てきます。確かにそこに存在はしていたがまったく意識することがなかったので認識出来なかったジョシアの木を図鑑を見て名前を認識することによってはじめて見えるようになったというお話です。「知識」が足されることで新たな世界を見ることが出来るようになったのです。このように、この本ではデザインに対してまったく知識のなかった私に新たな視点を与え新たな世界を見せてくれました。

私はデザイナーとは、無数のフォントや色を巧みに駆使し素敵なイラストやキャラクターを散りばめ、私が作った糞ダサなwebページも一瞬にして絶対アクセスしたくなるようになる、そんなことができる人のことだと思っていました。まるで魔法使いです。人は自分の領分以外の理解が及ばない分野になると魔法のような感覚に陥ることがあるかもしれません。しかし、その正体は魔法なのではなく小さな知識やテクニックを駆使した努力の結晶だったです。

今回、この本を読みデザインの世界を少しだけですが覗くことによって魔法などではない努力の積み重ねを理解することができ、少しだけですが真似ごとができるようになりました。