mizdra's blog

ぽよぐらみんぐ

日向縁さんの誕生日をお祝いしてゲームを作った話

はじめに

この記事はゆゆ式 Advent Calendar 2018 9日目の記事です.

adventar.org

今回は日向縁さんの誕生日をお祝いしてゲームを作ったので, その紹介をします.

日向縁さんと目

皆さんは「日向縁」というキャラクターを聞いて初めに何を連想するでしょうか. ゆるふわ, お姫様, よく笑う, などなど. 人によって様々なイメージが浮かぶと思います. ちなみに僕は「1の目」を最初に連想しました. そう, 日向縁さんといえば作中でよく数字の「1」のような目をすることで知られています. 原作者の三上小又氏のツイートなどで「1の目」をネタにされていることで印象深い方も多いでしょう.

また, 日向縁さんの誕生日が11月11日であることに因んでこんなイラストも描かれています.

ゆかりスロット

かくいう僕も日向縁さんの誕生日をお祝いしてこんなゲームを作ってみました.

yukari-slot.mizdra.net

日向縁さんの目を揃えるスロットゲームです. 任意のタイミングで「とめる!」ボタンを押すと任意の数字が目として揃っていきます. 「11」が揃えば完成です.

またスロットの結果をシェアしたいというご期待に答えて, シェアボタンを用意してあります. 皆さんのお気に入りの縁さんをシェアできる最高便利機能です.

こんな感じで画像付きでツイートできる.

難易度調整に少し拘っていて, 目で追える速度で回っているけど中々狙って止められない程度になっています. 何度も挑戦して是非完成させてみて下さい.

開発時のちょっとした小ネタ

こちらはスロットを停止させた時に止めた目の取得に失敗して, 片目が undefined になった「undefinedゆかり」です.

f:id:mizdra:20181209232800p:plain:w400

そしてこちらはレイヤの重ね合わせの順番をミスった結果誕生した「ゆかりモンスター」です. 怪談とかに出てきそう.

f:id:mizdra:20181209233124p:plain:w400

おわりに

以上が ゆゆ式 Advent Calendar 2018 9日目「日向縁さんの誕生日をお祝いしてゲームを作った話」となります. いかがでしたでしょうか. ただ「11」を揃えて完成させるのを狙うだけでなく, 色んな目を揃えて日向縁さんの表情を楽しむのも良いかもしれません. 皆さんも是非ゆかりスロットで遊んで貰えればと思います.

10日目は@ekme_brbrさんの担当です!

ポケモンGO プレイ体験記

この記事はPokémon RNG Advent Calendar 2018 一日目の記事です.

adventar.org

今年もPokémon RNG Advent Calendarの季節がやって来てしまいましたが, 皆さんはいかがお過ごしでしょうか. 僕はポケモン最新作を発売初日に購入して真っ先に 「知的財産の表記」を読み, 起動して主人公とライバルの名前を決めた所で満足して寝ました. 主人公の部屋からまだ脱出できていない引きこもり太郎です.

さて今年は初日に何を書くのか色々悩んだのですが, 例年1日目の記事はボリュームのあるものとなっていて (参考1, 参考2), 「Advent Calendarに記事を投稿したことがない人にとって敷居が高いと思われてしまうのではないか」との声が寄せられていました. そこで今回は「ポケモンGO プレイ体験記」というテーマでゆるふわな記事を書いていこうと思います.

ポケモンGOとの出会い

僕がポケモンGOを初めて知ったのは, ポケモンGOの正式サービス開始前のフィールドテスト(ベータテスト)に参加していた知人から話を聞いたのがきっかけでした. 実はその知人はIngressをそこそこやり込んでいるプレイヤーで, 旗から見ていて随分楽しそうにプレイしていたので, 以前から位置情報ゲーム自体には興味を持っていました.

Ingressは陣取り合戦のために船を出して遥々離島まで行ったり, 海外まで行ったりする, あまり活発でない僕にとって近づきにくいゲームという印象 (偏見です) がありましたが, ポケモンGOはアメリカでの先行リリースでも社会現象を引き起こす程多くの人に遊ばれていたようなので, 特に抵抗感もなく始められました. 僕が前から好きだった「ポケモン」*1がベースになっているという点も後押しになっていたりします.

最近のプレイの様子

基本は大学への通学中にプレイしているという感じです. 電車での移動中は速度超過によるペナルティでボックス整理程度しかできませんが, 電車を降りた後の徒歩での移動で毎日40分程度ポチポチしてます. とはいってもそこそこ人通りの多い場所を通って通学しているので, 安全を考慮して最近は「おでかけモード」をONにした状態でスリープモードにして歩くことが多いです. おでかけモード, 便利ですよね…

またイベントなどに時期に合わせて, 時々公園 (お台場とか) や駅 (新宿駅/錦糸町駅) に行って数時間じっくりプレイするといったこともしています. リリース初期の頃は大宮公園 (地元から比較的楽に行くことができた) に行くことが多く, 午前午後ぶっ通しで遊んで帰る, みたいなムーブをしていました. 大宮公園は大宮駅からそこそこ距離があるものの, 第一・第二・第三公園の3つの公園からなる広大な敷地を持ち, それぞれ異なるポケモンの巣が設定されていたので, お気に入りの場所でした.

大宮第三公園. めっちゃ良い感じに撮れていますが, これはたまたま本格的なカメラを使って写真を撮っている人が近くに居て, その構図を真似して撮ってみたら良い感じになっただけです.

めちゃめちゃに歩いた様子が映ってる.

ちょっとしたテクニック

10kmタマゴは普段孵化させずスタックしておいて, アメや星の砂のボーナスが掛かっているイベント時にまとめて孵化させると, アメや星の砂を効率的に稼げて便利です. 星の砂は孵化以外でも手に入れる方法がありますが, 10kmから出てくるポケモンのアメは中々手に入らないので, 星の砂よりもアメボーナスがある時に孵化させるのがオススメです.

アメ2倍デーに5つ10kmを孵化させて, プテラx1/ヒンバスx1/ヨーギラスx2/ラッキーx1が出てきた様子.

おわりに

以上が Pokémon RNG Advent Calendar 2018 1日目「ポケモンGO プレイ体験記」となります. いかがでしたでしょうか. 最新作でポケモンGOと連携できるらしいのですが, まだ主人公の部屋から脱出できていないので, その機能を体験できていません. 頑張って進めようと思います…

それでは今日からクリスマスまでの間, Pokémon RNG Advent Calendar 2018 を楽しんでいきましょう. 良いクリスマスを!

2日目は @oupo さんの担当です!

*1:最新作はまだ主人公の部屋から出てない程度にはやってませんが…

WebAssemblyを使って乱数調整ツールをWebに移植した話

tl;dr

  • C++のツールをWebAssemblyを使ってWebに移植した
  • WebAssemblyへコンパイルする言語としてRustを, JS-WebAssembly間のバインディングにwasm-bindgenを採用した
  • 乱数計算処理をWebAssemblyで実装することで, C++実装と比べて0.2〜0.7倍, JS実装と比べて1〜13倍の性能が出た

はじめに

枠だけ確保してずっと放置していた去年のAdvent Calendarの記事がようやく書けたと思ったら, もう少しで今年のAdvent Calendarがやってくる季節になってしまいました. この記事は Pokémon RNG Advent Calendar 2017 23日目の記事です.

adventar.org

皆さんは「乱数調整」という言葉を知っているでしょうか? 乱数調整とは簡単に言うと計算機によってゲームのランダムな事象を予測し, 狙った結果を引き起こす行為のことです. 例えば, 乱数調整を行うことで珍しいアイテムや強いキャラを狙って入手したり, 貴重なイベントを狙って発生させたりできるようになります.

https://www.mizdra.net/entry/2016/12/01/235954www.mizdra.net

この Pokemon RNG Advent Calendar はポケモンにおける乱数調整についての記事を書く Advent Calendar です. 今回はAdvent Calendarのネタとして7世代の孵化乱数*1に関するツールを作ったので, その紹介をします.

背景

計算機によってゲームのランダムな事象を予測するためには, 現在の乱数生成器の状態を特定することが不可欠です. 現在でいくつかの特定手法が確立されており, その内の1つに乱数生成器の初期化方法に注目して初期Seedを特定する方法があります. これは乱数生成器が32bitの初期Seedで初期化されていることを利用して, 32bit値の総当たりにより初期Seedを特定するものです.

これを利用した初期Seed特定ツールがおだんぽよ氏開発の search-tinymt-seed です. search-tinymt-seed では乱数生成器の初期化直後に生成された8つのタマゴの個体の性格を元に, 初期Seedを 2^32 通り総当たりで検索して初期Seedを特定します.

しかしこのツールはC++によって実装されており, 公式サイトで配布されているビルド済みバイナリが動かない場合はユーザが自力でビルドしなければなりません. そこで今回はWebAssemblyを使って search-tinymt-seed をWebへと移植し, ブラウザさえあれば誰でも簡単に利用できるようにしてみました.

ツールの紹介

以下のリンクからアクセスできます.

https://search-tinymt-seed-for-web.netlify.comsearch-tinymt-seed-for-web.netlify.com

https://github.com/mizdra/search-tinymt-seed-for-webgithub.com

乱数生成器の初期化直後に生成された8つのタマゴの個体の性格を入力すると, その条件にマッチする初期Seedを出力してくれます. また計算方式としてJavaScriptとより高速に計算が可能なWebAssemblyの2種類の方式が選べます. デフォルトの入力 [れいせい, きまぐれ, のんき, おっとり, すなお, おだやか, まじめ, てれや] では初期Seed候補として [0x0B76DDAF, 0x261C6F52] が出力されるようになっています.

技術面について

ツールのコアとなる乱数計算処理にWebAssemblyを使用することで, Webツールでありながら高速で, (WebAssemblyをサポートする) ブラウザがインストールされたあらゆる環境から利用できるというポータビリティを実現しています. PC版のChromeやSafari, Firefoxに加え, モバイルのChromeやSafariなどでも動くはずです *2.

WebAssemblyへとコンパイルできる言語には C/C++/Rust/AssemblyScript などがありますが, 今回はRustを採用しました. エコシステムの枯れ具合を考慮するとC/C++でも良いですが, 最近になってRust/WebAssembly周りのエコシステムが充実してきたため, 素振りがてらRustを使ってみることにしました.

www.mizdra.net

WebAssemblyを使って開発する際にまず問題になるのはJS-WebAssembly間でのデータの受け渡しです. 現在のWebAssemblyの仕様では整数やIEEE754準拠の浮動小数点数などの基本的な値しか直接JS-WebAssembly間で受け渡しできないため, 構造体や文字列といったデータを受け渡す際は一工夫必要です. こうしたデータバインディングをWebAssemblyの仕様として取り入れる Host Bindings という Proposal も出ていますが, 現在仕様策定中でまだまだブラウザで利用できるような状況ではありません. そこで今回は Host Bindings のポリフィル的な位置づけにあるwasm-bindgenを使用し, データバインディングを実現しています.

hacks.mozilla.org

例えばツールでは以下のようにRust側からJavaScript側でexportされた関数を呼び出して, 進捗の表示を実装しています.

// /src/worker/worker.ts

// `foundSeeds` の型として `number[]` の代わりに `Uint32Array` を使い,
// Rust側から見えるようにexportする
export function postProgressAction (foundSeeds: Uint32Array, seed: number) {
  // UI スレッドに進捗を送信
  postMessage({
    type: 'PROGRESS',
    payload: {
      foundSeeds: Array.from(foundSeeds),
      calculatingSeed: seed,
    },
  } as Progress)
}
// /src/wasm/lib.rs

// wasm_bindgen アトリビュートで wasm-bindgen による
// データバインディングを利用することを宣言し,
// 合せてimportしたいJavaScriptの関数のシグネチャと
// その関数が宣言されている場所 (JavaScriptのモジュール) を指定する
#[wasm_bindgen(module = "../worker/worker")]
extern {
    // `&[u32]` と `Uint32Array`, `u32` と `number` が
    // それぞれ wasm-bindgen によって対応付けられる
    fn postProgressAction(foundSeeds: &[u32], seed: u32);
}

#[wasm_bindgen]
pub fn search_tinymt_seed(natures: &[u32], has_shiny_charm: bool) -> Vec<u32> {
    let mut found_seeds: Vec<u32> = Vec::new();
    let param = tinymt32::Param {
      mat1: 0x8F7011EE,
      mat2: 0xFC78FF1F,
      tmat: 0x3793FDFF,
    };

    each_u32!(seed => {
        let mut rng = tinymt32::from_seed(param, seed);

        let found = natures
            .iter()
            .all(|&nature| nature == get_egg_nature(&mut rng, has_shiny_charm));


        if seed % 0x0100_0000 == 0 && seed != 0 {
            // 通常のRustの関数と同じように呼び出す
            postProgressAction(found_seeds.as_slice(), seed);
        }

        if found {
            found_seeds.push(seed);
            postProgressAction(found_seeds.as_slice(), seed);
        }
    });

    found_seeds
}

またツールでは初期Seed検索中にUIスレッドがブロックされるのを防ぐため, 初期Seed検索処理をWebWorker上で実行しています. Safariなどの一部のブラウザには一定期間UIスレッドがブロックされるとブロックの原因になっているスクリプトを停止する機構が備わっているため, 「どうせ計算ボタン1つしかないツールだしUIスレッドくらいブロックされても困らないでしょ」という気持ちでWebWorker無しで雑に実装すると, 計算が途中で停止してしまいます. WebWorkerの導入はその対策にもなっている訳です.

f:id:mizdra:20181016174516p:plain
スレッドと計算の流れを示したアーキテクチャの図

計算時間の比較

f:id:mizdra:20181016123648p:plain
計測時間の比較. MacOS/Android/iOSごとに違うデバイスで測定. 異なるデバイス間の値は単純に比較できないので注意.

移植元のC++実装はマルチスレッドで並列化されているため, JS/WebAssembly実装と同じシングルスレッド構成になるよう改変したものの結果を載せています. 環境やブラウザによりますがWebAssembly実装はC++実装と比べて0.2〜0.7倍, JS実装と比べて1〜13倍と, 十分ツールとしては実用的なレベルの性能が出ています. ちなみにこの計算時間はシングルスレッドでの計測値なので, マルチスレッドにより乱数計算処理を並列化すればさらなる性能の向上が見込めます.

困ったこと

Webpack で WebWorker 上から wasm を Dynamic Import できない

現状 worker-loader を使ってWebWorkerをバンドルしているプロジェクトにて, wasm-bindgen で生成されたスクリプトを WebWorker 上から Dynamic import すると実行時エラーになります.

github.com

例えば次のようなコードが実行時エラーになります.

// /app.js
import Worker from './worker.js'
const worker = new Worker()
worker.onmessage = (event) => {
  console.log(event.data) // expected: 3
}
// /worker.js
import('./lib.js').then((module) => {
  const result = module.add(1, 2)
  postMessage(result)
})
// /lib.rs
extern crate wasm_bindgen;
use wasm_bindgen::prelude::*;

#[wasm_bindgen]
pub fn add(a: i32, b: i32) -> i32 {
    a + b
}
// /webpack.config.js
const { resolve } = require('path')
const HtmlWebpackPlugin = require('html-webpack-plugin')

const rootPath = resolve(__dirname, '.')
const distPath = resolve(rootPath, './dist')

module.exports = {
  entry: {
    app: resolve(rootPath, './app.js'),
  },
  output: {
    path: distPath,
    filename: '[name].[hash].js',
    // WebWorkerの実行コンテキストでは `window` グローバルオブジェクトが無いので,
    // 代わりに `this` でグローバルオブジェクトを参照するようにする
    globalObject: 'this',
  },

  resolve: {
    extensions: ['.js', '.wasm'],
  },

  module: {
    rules: [
      { test: /worker\.js$/, loader: 'worker-loader' },
    ],
  },

  plugins: [
    new HtmlWebpackPlugin(),
  ],
}

この問題はWebpackのBootstrapコード *3 の一部がエントリポイントだけに埋め込まれてしまい, WebWorker 内には埋め込まれないことに起因しています. WebWorkerとそれ以外のモジュールの実行コンテキストは分かれているので, 片方だけにBootstrapコードが埋め込まれているともう片方からそのコードを参照することが出来ず, エラーとなるという訳です. WebpackのWebWorker/WebAssemblyサポートはまだまだ未熟なため, こうしたエッジケースに嵌まることが度々あります.

今回はIssueのコメントにもあるように, worker-loaderに頼らず webpack.config.js で WebWorker を明示的に別のエントリポイントとして分けることで問題を回避しました. こうすることで Webpack が WebWorker とそれ以外のモジュールの実行コンテキストが分かれていることを正しく認識し, WebWorker にも初期化コードを埋め込んでくれるようになります.

// /webpack.config.js
const { resolve } = require('path')
const webpack = require('webpack')
const HtmlWebpackPlugin = require('html-webpack-plugin')
const webpackMerge = require('webpack-merge')

const rootPath = resolve(__dirname, '.')
const distPath = resolve(rootPath, './dist')

const WORKER_PATH = '/worker.js' // ビルド後のworkerのパス

const baseConfig = {
  resolve: {
    extensions: ['.js', '.wasm'],
  },

  plugins: [
    new webpack.DefinePlugin({
      WORKER_PATH: JSON.stringify(WORKER_PATH),
    }),
  ],
}

// app用にentry/outputフィールドをカスタマイズした設定
const appConfig = webpackMerge(baseConfig, {
  entry: {
    app: resolve(rootPath, './app.js'),
  },
  output: {
    path: distPath,
    filename: '[name].[hash].js',
  },

  plugins: [
    new HtmlWebpackPlugin(),
  ],
})

// worker用にtarget/entry/outputフィールドをカスタマイズした設定
const workerConfig = webpackMerge(baseConfig, {
  target: 'webworker',
  entry: {
    'worker': resolve(rootPath, './worker.js'),
  },
  output: {
    path: distPath,
    filename: 'worker.js',
    // target フィールドで WebWorker 用に `globalObject` フィールドが
    // 自動で書き換えられているので, `globalObject` フィールドを手動で指定する必要はない
    // globalObject: 'this',
  },
})

module.exports = [appConfig, workerConfig]
// /app.js
const worker = new Worker(WORKER_PATH)
worker.onmessage = (event) => {
  console.log(event.data) // 3
}

おわりに

WebAssemblyを使ってC++の乱数調整ツールをWebに移植することで, ブラウザさえあれば誰でも簡単に利用できるポータビリティと, (ブラウザにより多少差はあるものの) 十分ツールとしては実用的なレベルの性能を実現することができました. 個人的にはもっと低い性能が出ると思っていたので, C++実装とそれほど大差のない性能が出た時は驚きました. WebAssemblyスゴイ.

また記事では触れていませんでしたが, 実はRustを使って型安全性/メモリ安全性のあるDXの良いWebアプリケーション開発環境の構築に挑戦する, というのも今回の開発で掲げたテーマの1つだったりします. リポジトリをよく見ると, ちゃっかりTypeScriptを導入していたり, JS/Rustの自動コンパイルやNetlifyによるpush駆動deployを実現していたりします. ですから今回の開発で小さいプロジェクトながらもそうした開発環境を構築できることが分かったのは大きな成果だったと思います. 今度はもっとガッツリRust/WebAssemblyを使って大きなものを作ってみたいですね. 最近は wasm-bindgen 以外にも js-sysweb-sys といったRustからJavaScript APIやWeb APIを直接利用できるcrateが登場しているので, 合せてそちらも触っていけると良さそうです.

*1:育て屋から貰えるタマゴの生成に関する乱数調整を対象とする分野のことです.

*2:ちなみに手元のEdge for Windowsでは動きませんでした

*3:参考: https://github.com/webpack/webpack/blob/a4e5f63d9d370b191bb9586570bc22bf2947e390/lib/MainTemplate.js

はてなサマーインターン2018に参加してクイズ大会で優勝してきた

はじめに

大学3回生の id:mizdra です. 普段はJavaScriptを触っていて, Webアプリケーションを作って遊んでいます.

8月から9月にかけて約1ヶ月間, はてなサマーインターン2018に参加してきました. この記事はその体験記となります.

developer.hatenastaff.com

f:id:mizdra:20180924133406j:plain:w400

応募

僕は普段からはてなブログやはてなブックマークを利用している「はてなユーザ」の一人で, はてなに愛着を持っています. また, Hatena Developer Blogに見られるはてなの技術に対する姿勢が大好な「はてなのファン」でもあります. はてなユーザ/はてなのファンとして, はてなのサービスに直接関わってみたい, はてなの社内の様子を直接見てみたい. そうしたことが動機で, インターンに応募しました. また, 大学の先輩方 (id:miki_bene, id:hogashi) が過去のはてなインターンに参加していて, とても楽しそうにしている様子を見ていたことも応募のきっかけの1つだったりします.

志望動機やポートフォリオを書いて応募すると, 選考面談を受けることになります. 面談ではCTOの id:motemen さんと共に何故はてなをインターンシップ先に選んだのか, 今までどんな技術を学び/扱ってきたかなどについて話しました. 自分が今までに作ったアプリやツールの話をしながら「良い〜」「良いですね〜」などと楽しくお話をさせて頂きました.

その後無事インターンに参加できることが決まったので, 新幹線で埼玉から京都へと向かいました.

新幹線車内で朝食を食べている時の様子です.

前半 (1週目〜2週目)

インターンの前半では技術やはてなに関する講義を受け, その講義で学んだことを活かして演習を解きます. メンターの補助を受けながら, はてなの中の人による講義を受けることができます. *1

特に, 初めの1週間はWebアプリケーションに関する講義があり, 演習を進めていくと日記サービスが完成するというカリキュラムになっています. 加えて2周目ではAWSから講師をお招きしてAWSハンズオンが開催され, 1週目で作った日記サービスを本番環境にデプロイします. 講義でWebサービスを支える技術を網羅的に学び, 学んだことを元に自分の力で実際に稼働するWebサービスを作っていく. このような体験が出来るのは, はてなインターンならではだと思います. 特に, 僕のように普段Webフロントエンドばかりやっていて, DBやWebサーバについてあまり学習する機会が無い, という人にとっては絶好の学習の機会です. はてなインターンの講義は, そうした「Webサービスを支える技術を網羅的に学びたいけど, まだ手が出せてない人」に是非オススメしたいと思える内容でした.

f:id:mizdra:20180924184608p:plain
GraphQLの講義を受けている時の様子です.

また, 今回のはてなインターンでは従来のPerlやScalaをベースとする講義資料が, Golangをベースとするものへと一新されています. 新しいものを触ることが好きな人にもオススメです.

後半 (3週目〜4週目)

配属

インターンの後半2週間は開発チームに配属され, 実際に稼働しているサービスの開発を行います. 僕は第一志望だったコンテンツプラットフォームコースに当たるマンガチームに配属されました. マンガチームではスマホ向けのマンガアプリや, Web向けのマンガビューワ「GigaViewer」などを開発をしています. 今回は一緒に配属された id:YaaMaa さんと共に, GigaViewerの開発を行いました.

GigaViewerについては, 以下の記事で紹介されているので是非読んでみて下さい *2.

developer.hatenastaff.com

開発の様子

マンガチームでは, 主に id:hitode909 さんや id:onk さんとメンタリングしながら開発しました. まずは実際にサービスを使ってみて, どんな機能があったら嬉しいかというユーザ視点の思いをチーム内で共有し, 次にその機能はどういうデザインであるべきか, 実現するにはどういう技術を用いれば良いのか, という議論をしていきます. デザインについては, デザイナーの id:swimy1113 さんと相談しながら決めていきます. その後, 本当に挙げられた技術で機能を実現できるかどうかを詳しく調査し, 実装へと移ります.

実装の過程では, 同じチームに配属された id:YaaMaa さんと2人で手分けしながら開発を進めました. 僕がのんびり開発していたら id:YaaMaa さんがシュッと仕上げていることが多々あり, ヒッとなっていました. すごかった.

ちなみに僕はWebフロントエンドのロジック周りを主に担当していて, id:Pasta-K さんや id:hitode909 さんに相談しながら開発していました. お2方とも的確なアドバイスをして下さり, 爆速で開発を進めることができました. ありがとうございました.

リリースした機能

3つの機能をリリースしました. 全てとなりのヤングジャンプでのみ利用可能です.

  1. 閲覧済み作品の新着エピソード更新通知機能
  2. 新着エピソードのレコメンド機能
    • ビューワの最後のページに更新通知で表示されているエピソードがレコメンドされる
  3. ビューワページのSPA化
    • ビューワページ間の遷移に限定して, シームレスな遷移ができる
    • 遷移速度の改善以外に, 全画面表示状態が遷移後も維持されるなどの効果もある

f:id:YaaMaa:20180910221611p:plainf:id:YaaMaa:20180910221909p:plain
リリースした機能のスクリーンショット*3

「閲覧済み作品の新着エピソード更新通知機能」は「作品の更新を知りたい」という僕の思いから実装されたものです. トップページにアクセスするだけで, 以前見た作品の最新話が出ているかどうか, ひと目で判断できます. 実装にあたっては, 会員未登録のユーザにも更新通知を出したいという難しい要件や, 利用したいAPIがブラウザで実装されていない問題など, 様々な困難に遭遇しましたが, 機能を制限するなど問題を回避できるよう方向転換して*4何とか実装することができました. とても思い入れのある機能です.

また, レコメンドの改善やSPA化は id:YaaMaa さんが提案したアイデアを元に実現されました. SPAの実装は, 技術的に非常に面白い課題でした. どちらもユーザの体験を改善する良い機能だと思います.

f:id:mizdra:20180924152908g:plain
機能をリリースすると, オフィスに転がってるくす玉でお祝いします.

マンガチームの世界観

マンガチームのスペースの中央には木魚が置いてあり, 叩くとチームメンバーがバッと集まってきます. 困ったらこの木魚を叩いて助けを呼びます. また, 木魚の横には id:Pasta-K さんのアクリルブロックが置いてあります. 何だかよく分からないですが, そういう世界が広がってます.

f:id:mizdra:20180924133555j:plain
Suzuriで買えます.

同じ階にはあの「飲み会IoTボタン」が置いてあります.

成果発表

インターン最終日には「ほたて」と呼ばれる「ットなスクをがけた」チームを社員が投票して投票し, 表彰する会があります. インターン生はインターンでの成果をここで発表し, 🏆表彰🏆されます *5.

cybozushiki.cybozu.co.jp

マンガチームは5チーム中, 2位という結果になりました. 準優勝めでたい 🎉🎉🎉. 優勝した id:gazimum さん, おめでとうございます. 次は負けません ⚔️.

f:id:mizdra:20180924133701j:plain

おわりに

今まで経験したことのない最高の夏を過ごすことができました. こんなにインターン生のことを考えているインターンは他には無いと思います. この記事を読んでいる皆さんも, 是非来年以降応募して最高の夏を過ごしましょう.

最後になりますが, インターンを通して本当に色々な方のお世話になりました. メンターや講師の方々, マンガチームの方々, その他インターンを支えて下さった方々, 本当にありがとうございました!

おまけ

白熱する優勝争いの様子です.

https://twitter.com/meteor_saan/status/1032952399256670208

※Mackerelチームのインターン生です.

※大規模チームのメンターです.

https://twitter.com/meteor_saan/status/1032982397539643392

※マンガチームのインターン生 (僕) です.

あわせて読みたい

e-ntyo.hatenablog.com

https://blog.meteors.me/entry/2018/09/08/112050blog.meteors.me

tomoyaf.hatenablog.com

yaamaa-memo.hatenablog.com

turtar-fms.hatenablog.com

noahorberg.hatenablog.com

guni1192.hatenablog.com

*1:僕の場合は僕の大学の先輩にあたる id:miki_bene さんがメンターで, とても丁寧に質問などに対応して下さいました. ありがとうございました 🙏.

*2:余談ですが, GigaViewerのことはインターンに行く前にこの記事を読んで知っていて, 「はてな社ではこんな最高便利なマンガビューワを作っていたのか〜」という感想を持っていました. まさかその開発に携われるとは思っていなかったので, 配属が決まった時は嬉しかったです.

*3:引用元: はてなインターン2018に参加した - 見返すかもしれないメモ

*4:個人的にはインターンシップ一番のハイライトでした.

*5:実は僕はインターンの最後の2日間体調を崩してしまい, 残念ながら成果発表会に参加することができませんでした. 色んな人にめちゃめちゃ心配されたり気遣いして頂いて, お陰さまで今は元気です. ありがとうございました 🙇

Chrome拡張機能でコールバック地獄を解決する

Chrome拡張機能の非同期APIはコールバックにより実装されています. 例えば, 拡張機能ごとに用意されるストレージからデータを取得する場合, ストレージへのアクセス中にJavaScriptの処理が中断されるのを防ぐため, 非同期APIが用意されています.

// background.js
chrome.storage.local.get(['admin'], (result1) => {
  chrome.storage.local.get([`user/${result1.admin}/name`], (result2) => {
    console.log(result2)
  })
})

このコールバックによる非同期APIはJavaScriptにおいては一般的な非同期APIの実装手法ですが, 「コールバック地獄」という問題を引き起こします. これを解決する手法としてES2015で追加されたPromiseとジェネレータ, もしくはES2017で追加されたasync/awaitを利用する手法が知られています.

numb86-tech.hatenablog.com

numb86-tech.hatenablog.com

susisu.hatenablog.com

しかしChrome拡張機能の非同期APIはコールバックにより実装されているため, Chrome拡張機能の中では直接的にはPromiseasync/awaitを利用することが出来ません. そのため, 手動で非同期APIをPromiseでWrapするか, 以下のようなWrapperライブラリを利用する必要があります.

この記事ではmozilla/webextension-polyfillを使ってコールバック地獄を解決する方法を紹介します.

mozilla/webextension-polyfillを使ってコールバック地獄を解決する

ブラウザ間で統一的なAPIで拡張機能を作成できるよう策定されたWeb Extension APIというものがあり, 現時点でMozilla FirefoxとMicrosoft Edgeでサポートされています. mozilla/webextension-polyfillはこのWeb Extension APIのChrome向けのPolyfillとなっています. Web Extension APIの非同期APIは全てPromiseで提供されているため, Chrome拡張機能でコールバック地獄を解決する方法として用いることができます.

mozilla/webextension-polyfillnpm.comからもインストールできますが, モジュールバンドラなどを挟む必要があるため, 少々手間です. これに変わるより手軽なものとして, unpkg.comから直接ライブラリをダウンロードし, manifest.jsonbackground.scriptsなどから読み込む方法があります.

$ cd extension-project
$ wget https://unpkg.com/webextension-polyfill/dist/browser-polyfill.min.js
// manifest.json
{
  // ...

  "background": {
    "scripts": [
      "browser-polyfill.min.js",
      "background.js"
    ]
  },

  "content_scripts": [{
    // ...
    "js": [
      "browser-polyfill.min.js",
      "content.js"
    ]
  }]
}

このようにPolyfillを読み込むことでbackground.jsなどからbrowserという名前空間からWeb Extension APIにアクセスできます.

// background.js
browser.storage.local.get(['admin'])
  .then((result) => {
    return browser.storage.local.get([`user/${result.admin}/name`])
  })
  .then((result) => {
    console.log(result)
  })

非同期APIはPromiseを返すので, 次のようにasync/awaitを利用したコードにも書き換えることもできます.

// background.js
(async () => {
  const result1 = await browser.storage.local.get(['admin'])
  const result2 = await browser.storage.local.get([`user/${result1.admin}/name`])
  console.log(result2)
})()

以上, Chrome拡張機能でコールバック地獄を回避するTIPSでした.

#NowPlaying for Google Play Music 拡張機能の v1.0.0 をリリースしました

はじめに

先日公開した Google Play Music 向けの #NowPlaying 拡張機能 がアップデートして v1.0.0 になりました 🎉🎉🎉

新機能

  • Firefox のサポート
  • ユーザカスタム設定をサポート

Firefox のサポート

要望のあったFirefoxのサポートを実装しました. Chromeと全く同じように曲の共有が行えるようになります. 以下のリンクからFirefoxにインストールできます.

これはFirefox 48にて採用された WebExtensions API を利用すること実現されました.

ユーザカスタム設定をサポート

SNSメッセージのテンプレートや付加するハッシュタグをユーザがカスタマイズできるようになりました. 設定のカスタマイズは拡張機能のオプションから行うことができます.

f:id:mizdra:20180525155527p:plain

テンプレート内に埋め込める変数として, 次の3つをサポートしています.

  • ${title}: 曲のタイトル
  • ${artist}: 曲のアーティスト名
  • ${album}: 曲のアルバム名

おわりに

より使いやすくなった #NowPlaying for Google Play Music を是非お試しください!

Google Play Music 向けの #NowPlaying 拡張機能を公開しました

はじめに

Google Play Music 向けの #NowPlaying 拡張機能, ありそうで無かったので作りました.

chrome.google.com

上記の拡張機能をインストールすると, 右下に曲のシェアボタンが表示されるようになります.

f:id:mizdra:20180523010803p:plainf:id:mizdra:20180523010800p:plain
右下の共有ボタンを押すと共有するSNSメッセージを編集する画面に遷移する.

サポートされるSNS

サポートしているSNSTwitterのみです. 現状他のSNSに対応するつもりは無いので, もし他のSNSにも共有したい人がいれば以下のGitHubリポジトリをForkするなりPullRequest送るなりしてもらえればと思います.

github.com

その他

拡張機能, シュッと作れると思っていたらChromeウェブストアで利用するプロモーション用の画像を用意する必要があったり, ロゴのデザインが指定されていたり, デベロッパー登録手数料として$5払ったりする必要があってちょっと面倒だった… コード書くのは大分速くなった気がするので, そろそろデザインもシュッとできるようになりたいですね.

おわりに

インストール!!! 今すぐ!!! シェアボタン連打!!!

ポケットモンスター・ポケモン・Pokémon・は任天堂・クリーチャーズ・ゲームフリークの登録商標です.

当ブログは @mizdra 個人により運営されており, 株式会社ポケモン及びその関連会社とは一切関係ありません.