のらぬこの日常を描く

ノージャンルのお役立ち情報やアニメとゲームの話、ソフトウェア開発に関する話などを中心としたブログです。

【PS3】画面をスクショしてネット経由でPCに取り込む方法

こんにちは。のらぬこです。

ゲーム中の画面をキャプチャーして、スクリーンショットとして保存したくなったことないですか?

用途は様々だと思いますが、例えばブログやTwitterfacebookに写真をアップするためだったり、ちょっといい感じのシーンなので保存しておきたいとかでしょうか。

今回から数回に分けて、僕が持っているゲーム機限定にはなりますが、ゲーム中画面を静止画キャプチャーしてPCに取り込む方法を書いてみたいと思います。

なお、必要なものは、ゲーム機とPC、あと、ゲーム機によってはデータ受け渡し用のUSBメモリーがあったほうがいいです。

お金をかければ、静止画だけでなく動画もキャプチャー出来たりとやれることが色々増えるのですが、うん万単位のお金がかかる*1ため、そういうのは今回は諦めることにします。

また、PCに取り込む際、ケーブルで繋いだり、USBメモリーを抜き差ししたりというのはちょっとめんどくさいと思うので、なるべくWiFiなどのネットワークを介して転送する方法を紹介しようと思います。

case 1 : PS3

ということで、まずは、PS3の話をしたいと思います。

前置きとか

PS3ゲームには、スクリーンショット保存に対応したゲームと対応していないゲームがあります。

対応していないゲームの場合は、残念ながらお金をかけずにスクリーンショットの保存、PCへの取り込みを行うことは出来ません。

ちなみに、どのゲームが対応しているか、なのですが、残念ながらゲームのパッケージなどにはおそらく記載されていないため、購入して遊んでみるまではスクリーンショットが保存可能かは分からないと思われます。

スクリーンショットの取り方

ゲームで遊んでいるとき、今のシーンを保存したいって思ったタイミングでコントローラのPSボタン(ちょうど中央の丸い小さなボタン)を押します。

XMBが表示されるので、矢印ボタンで「フォト」に移動します。

スクリーンショットが保存できるゲームの場合には、この中に「スクリーンショットの保存」という項目が表示されます。

f:id:noranuk0:20161110233827j:plain

保存できないゲームの場合には、どこを探してもそういう項目は見つかりません。

f:id:noranuk0:20161110233833j:plain

項目を選択後、「〇ボタン」を押すとスクリーンショットが保存されます。

保存したスクリーンショットの取り込み

今年の初めごろまでは、フォトギャラリーアプリから、Picasa というサービスを利用することで、インターネット経由でPCにファイルを送ることができました。

しかし、Picasaのサービスが終了してしまったため、これが使えなくなってしまいました。

今でも一応、USBメモリーなどを使わずに、画像ファイルをPCに転送することもできますが、少しめんどくさいです。

そのやり方ですが、ファイルをアップロードできる外部のサービスを利用します。

PS3のWebブラウザでファイルをアップロードできるサービスを開き、画像ファイルを一旦外部のサーバにアップロードします。

アップロードしたら、今度はPCからそのサービスに接続して、先ほどアップロードしたファイルを持ってくればいいわけです。

この方法であれば、ネットにさえ繋がっていれば、USBメモリーや、PSPなどを用意する必要はありません。

ただ、PS3のWebブラウザ、かなりしょぼいので、今どきのリッチなWebUIはきちんと表示してくれません。 gmail, google drive, onedrive と試しましたがすべて正常にページがロードできませんでした。

いくつかのサービスを試してみたのですが、とりあえず僕が試した限りでは、「普通のあぷろだ」でうまくいきました。

ということで、「普通のあぷろだ」を使用して、PS3内の画像をPCに転送する方法を説明します。

まずは、PS3でWebブラウザを立ち上げます。

f:id:noranuk0:20161111004514j:plain

「普通のあぷろだ」のページに行きます。△ボタンを押すと、検索というメニューがあるので、「あぷろだ」で検索すると一番上に出てきます。 f:id:noranuk0:20161111004536j:plain

なお、毎回検索するのもめんどくさいので、何度も使いたいという方は、Webブラウザのお気に入りに登録しておくとよいと思います。

「普通のあぷろだ」のサイトが表示されたら、画面真ん中あたりの「参照」ボタンを押します。

参照ボタンを押すと、PS3に保存された画像を選ぶことができるので、PCに送りたいスクショ画像を選んでください。

f:id:noranuk0:20161111004722j:plain

ダウンロードパスワード、削除キーを入力して、「利用規約に同意して投稿する」を選べば、ファイルがアップロードされます。

アップロードが完了したのを確認したら、PCで「普通のあぷろだ」のページを開いて、目的のファイルをダウンロードします。

f:id:noranuk0:20161111005009p:plain

なお、このやり方は、Webブラウザが搭載されたゲーム機であれば、どのゲーム機でも(おそらく)使うことが出来る方法です。

覚えておけば、なにかの役に立つかもしれません。

また、外部のあぷろだを使わなくても、PCの中にじぶん専用あぷろだを立てることも少し頑張れば可能です。

機会があれば、紹介したいと思います。

記事書きました ⇛ nodejs+expressで自PCにオレオレあぷろだをつくる - のらぬこの日常を描く

今回の話は以上となります。

次回は、PS4でゲーム中のスクリーンショットを撮ってPCに持っていく方法を書いていきたいと思います。

*1:ビデオキャプチャーボードとかカメレオンとか

全部無料⇒プライベートgitリポジトリ+CI環境+Issue管理。 VisualStudio Team servicesのススメ

こんにちは。のらぬこです。

gitリポジトリやタスク管理ツール、CI環境等、開発に必要な周辺ツールも最近はクラウドで使えるものが色々出てきて、自分であれこれサービスを立てなくてもソース管理やタスク(Issue)管理、自動テスト/ビルド等の環境が整うようになってきました。

Gitリポジトリだとgithubbitbucketなどが有名ですし、タスク管理ならTrelloなどがあります。

また、CI環境についてはJenkinsが有名ですが、クラウドサービスだとCirleCITravisCIなどが存在します。

さて、この手のサービスの紹介記事では全く話題に上がらないけど、 これらの機能が、プライベートな環境で、無料、無制限に使えるすごいサービスがあります。

今回紹介するのは、Microsoft が運営している「Visual Studio Team services」というサービスです。

Visual Studio Team servicesとは

Visual Studio Team servicesとは、プライベートgitリポジトリredmineとjenkinsあたりのサービスが1つになったようなクラウドベースの開発支援サービスです。

Visual Studio と銘打ってますが、VisualStudio持ってなくても、MSDNサブスクリプション持っていなくても誰でも無料で使うことができます。

そして、無料で使えるにもかかわらず、プロジェクト数やリポジトリ容量などの制限がとても緩いのが特徴です。

ただし、5人以上のチームでプロジェクトを共有する場合には有償となるようです。

詳細は、公式ページの説明を読んでいただくのが早いかもしれません。

Visual Studio Team Services - クラウド開発ツール | Microsoft Azure

  • 無制限で無料のプライベート コード リポジトリ
    • 作成できるのはプライベートリポジトリのみですが、容量、数の制限はありません。
    • なお、パブリック公開リポジトリは作れません。
  • すべてを 1 か所から追跡
    • redmine のようなチケットベースのタスク、障害管理システムが使えます
  • クラウドの機能を活用した継続的な統合とデプロイ
    • Visual Studio のプロジェクトだけでなく、xamarin、mavenandroidxcodeなどにも対応した高性能なCI環境が使えます。
    • Azreであれば、自動デプロイの機能がCIツールに備わっています。
    • AWSなどMS以外の環境へのデプロイについては、頑張ればできるのかさすがに無理なのかは調べてません。

欠点

ユーザインタフェースが英語のみです。

トップページは日本語なのですが、中のコンテンツは英語のみです。

普段から、EclipseAndroid studio、Unityやxcodeなどをお使いで、英語環境での開発に慣れていれば困ることはきっとないと思いますが、普段からVisual Studio等の日本語開発環境を使われている(とてもうらやましい)方は最初ちょっと大変かもしれません。

また、ネイティブなandroidアプリ開発で使用する場合、CI環境(自動Build)がうまく動かない可能性があります。

androidアプリな方は、以下の記事の通りBitBucket + bitrise + fabric(Crashlytics)の組み合わせがおすすめです。 noranuk0.hatenablog.com

とにかく使ってみる

以下、2016年11月の情報です。登録方法や、使用開始までの画面などは今後変更されるかもしれません。

まずは、gitリポジトリが使えるようになるまでの話をします。

[STEP1] 公式サイトを開いて、画面左下の「はじめる」ボタンを押します

[STEP2] Microsoftアカウントの情報を入力します。

  • Windowsのサインインで使用しているアカウントやOnedrive/outlookのアカウントがあればそれでもかまいません
  • お持ちでない場合には、http://www.outlook.com からメールアカウントを取得してしまいましょう
  • f:id:noranuk0:20161108234055p:plain

[STEP3] アカウントの作成画面で必要な情報を入力します

  • プロジェクトダッシュボードのURL、コード管理に使用するツールを選択します。
    • コード管理ツールはgitでよいかと思います。
  • 続行ボタンを押せば完了です。確認画面などもなく、すぐにアカウントが作成されます。
  • f:id:noranuk0:20161108234105p:plain

[STEP4] アカウント作成完了!

  • Visual Studio Team services の各種サービスを使うための準備はこれで完了です
  • アカウント作成と同時に、MyFIrstProjectというプロジェクトが作成され、gitリポジトリも使えるようになっています。
  • この画面の説明はたぶん不要と思うので省略です。
  • 鍵の登録方法は、Lern more about SSH のリンク先に書かれています。
  • SSH 以外にhttps も使えます。この場合は https + basic認証となります。
  • 作成されたgitリポジトリはプライベートリポジトリとなっており、自分(とプロジェクトに参加させたチームメンバ)以外は参照することができません。
  • f:id:noranuk0:20161108234113p:plain

[STEP5] 新しいプロジェクトを作る

  • さすがに MyFirstProject をずっと使い続けるわけにもあかんので、本番用プロジェクトを作ります。
  • 画面左上の「MyFirstProject」と書かれている部分をクリックするとプルダウンメニューが出てきます。
  • 下のほうにある「New Team Project」を選択します。
  • モーダル内に必要な情報を入力し、Create project ボタンを押せば、数秒ほどで新しいプロジェクトが作成されます。
  • f:id:noranuk0:20161108234134p:plain

まとめ&最後に

というわけで、10分ほどで、ほぼ無制限の容量を持つプライベートなgitリポジトリを無料で使えるようになりました。

githubやbitbucketでもプライベートリポジトリを使うことはできます。

しかし、githubの場合は若干お金がかかります。bitbucketには無料プランもありますが、容量の制限が存在します。

その点、Visual Studio Team servicesであれば、容量などを気にすることなくプライベートなgitリポジトリを使うことができます。

さらに、タスク管理や、CI環境なども無料で使用できるのですから、個人開発者さん等、その手のお金はなるべくかけたくないといった方、一度試してみてはいかがでしょうか。

Visual Studio Team serivcesのCI環境の話については、需要がありそうであれば、いずれ機会を見て書きたいと思います。

今回の話は以上となります。

この記事がどなたかの参考になれば幸いです。

Visual Studio Team ServicesにプッシュしたリポジトリをAzureに自動でデプロイする方法を記事にしました。 AzureのWebAppから無料プランを選択すれば、Webサービスホスティングまで全部無料(ただしそれなりの制限はあります)で運用することもできるようになります。 noranuk0.hatenablog.com

超久しぶりに再開した「ペルソナQ]、2年の間を開けてクリアしました!

こんにちは。のらぬこです。

今回は、最近クリアしたゲーム、ATLUSから2014年に3DSで発売された「ペルソナQ シャドウ オブ ザ ラビリンス」のお話です。

f:id:noranuk0:20161106111634j:plain

2014年の中ごろに3DSで発売されたペルソナシリーズの外伝?的なゲーム「ペルソナQ」。

発売日直後に購入したけど途中で投げて、その後長らく放置されていたのですが、先日ようやくクリアしました。

f:id:noranuk0:20161106113159j:plain

プレイ後感を一言で言うならこんな感じ↓

プレイしてよかった!

クリアしてよかった!!

シナリオもよかったし、ダンジョンゲームとしても最後まで面白かった!!

クリアまでのプレイ時間は約90時間。
サブクエストは大体クリア。
クリア時のレベルは70くらいです。
マップは大体埋めていますが、100%ではないです。
ただ、マップ埋め報酬宝箱はゲームコインのお世話になりつつ一応全部開けてます。

レベルに関しては、65くらいでクリアできる想定っぽいのでちょっと高めかも。

f:id:noranuk0:20161106113209j:plain

ペルソナQって?

登場キャラクターは「ペルソナ3」「ペルソナ4」(+今作オリジナルキャラ二人)、システムは「世界樹の迷宮」と同じ感じの3Dダンジョンゲームです。

細かい話は、序盤のネタばれありますがWikipedia参照いただけるとイメージつきやすいかと。

ゲームシステムについても、ベースとなっているのは世界樹の迷宮ですが、バトルなんかにはペルソナらしさがうまいこと取り入れられています。

f:id:noranuk0:20161106115854j:plain

レビューっぽいこと

シナリオについて

公式さんにより、シナリオのネタバレは固く禁止されているので控えますけど、裏にシリアスで重たいテーマが隠れているのですが、ペルソナキャラがいつものあんな感じ(ペルソナ4アニメ版想定)でなんかいろいろやりつつ話を進めていく感じです。 舞台(拠点)となるのは文化祭真っ最中の某学校なのですが、校内での会話パートもほわ~んとしてまったり楽しめます。 コミュランクの要素とか「知識」「魅力」などのステ上げ等、本編ペルソナ的な要素はないので、校内パートはおまけ要素的な意味合いも大きいとは思いますが結構なボリュームもあったし、いかにも学校の文化祭っぽい関連クエストなんかもそこそこあってすべての要素がしっかり楽しめました。

迷宮攻略

迷宮の謎解き(ギミック)は、そこそこ頭を使います。

また、本編とは一応無関係の一部サブクエストで依頼される謎解きは結構難しいです。

基本はFOE(超強敵)を避けつつ少しずつ道を開いていくっていうスタイルですが、5つの迷宮それぞれに異なるアレンジ、仕掛けが組み込まれていて最後まで楽しめました。

例えば、動く床をうまいこと利用して最短経路でたどり着くとか、敵をうまく誘導して道を作るなどのギミックが、迷宮のテーマに沿ってうまくアレンジされています。

構造も、特に4つ目の迷宮とかは悪意を感じるほどによく練られています。

とりあえずマップ埋めて鍵拾ってドア開けて進んでいくみたいなタイプではないので、攻略サイトとか見ないで進めるのであれば、かなりやりごたえがあると思います。

ちなみに僕は、たぶん3回ほど攻略サイトのお世話になりました。

戦闘パート

戦闘もかなり歯ごたえがあり、終盤も「ムド」「ハマ」系は1回の戦闘につき1回までっていう個人的縛り入れたせいで、難易度EASYにしたにもかかわらず相当きつかったです(主にMP的に)。

今作には、スキル(MP消費、HP消費)で敵の弱点を突くと、次ターンのスキル消費が0になるっていうシステムがあります。 半面、コストはかなり高めに設定されており、またMP回復アイテムや、保健室(HP/MP回復施設)の利用料もそれなりの値段がします。 なお、弱点攻撃しても敵が必ずダウンするわけではなく、弱点突いても普通に反撃もしてきます。

このバランスは賛否あるようですが、個人的には、最近のアトラスRPGの「弱点突いてダウンさせてフルボッコ」的な味付けよりは、今作のシステムのほうがいいかなーと思っています。

まとめ

ひと月半ほど前ペルソナ5買ったんですが途中で飽きまして、はるか昔に途中で投げてたペルソナQを再開したら意外と面白くて、最後まで一気にプレイしてしまいました。

ちなみに、ペルソナQを途中で投げた理由なんですが、あんま覚えてないんですが、某所のギミックが解けなかったから。だと思います。

再開してみたら、あれ、どこで詰まってたんだろうっていう感じですんなり先に進めたので、実際どうだったかちょっと不明。

ちなみに、ペルソナQですが、定価は7000円くらいするのですが、現在は新品、中古共に、かなり安く購入することができます。

ペルソナシリーズが好きで難易度高めのダンジョン物がお好きな方には楽しめると思います。

よろしければ、遊んでみてはいかがでしょうか、ってことで。

ペルソナQ シャドウ オブ ザ ラビリンス - 3DS

ペルソナQ シャドウ オブ ザ ラビリンス - 3DS

  • 発売日: 2014/06/05
  • メディア: Video Game

ペルソナよく知らないけど世界樹ファンってかたは、うーん、どうだろう。。。

追記

(祝)ペルソナQ2発売決定! 先日、ペルソナQ2発売の発表がありました。 ペルソナ3、4に加え、ペルソナ5のキャラクターが参戦し、ボリュームもその分大幅アップと期待しております。 ペルソナ5は本編は途中で投げてしまいましたが、執筆時点で放映中のアニメ版は楽しく見させていただいております。

ペルソナQ2 ニュー シネマ ラビリンス - 3DS

ペルソナQ2 ニュー シネマ ラビリンス - 3DS

  • 発売日: 2018/11/29
  • メディア: Video Game

追加DLCで配信されるペルソナ3,4,5のバトルBGMが、先着購入特典として付与されるようです。 ちなみに、ペルソナQみたいな追加ナビヴォイスも配信されるんでしょうか。 個人的には、ダンジョントラベラーズ2-2のように、追加ダンジョンなども配信されるとうれしいのですね。 追加シャドウ数体+経験値&所持金増加アイテムみたいな感じだとちょっとがっかりかもw

おまけ

プレイ途中で投げ出してしまったペルソナ5の記事はこちら

noranuk0.hatenablog.com

【android】firebaseをdebug版で有効化すると、アプリ権限周りに不具合が出る可能性アリ?

どうもこんにちは。のらぬこです。

今日はandroid開発のお話です。

今回話すこと

firebase-analytics, firebase-crashを試してみたところ、デバッグビルド版で外部SDカードのアプリパッケージストレージ(/storage/sdcard1/Android/data/{packageName}/file/...)への書き込みができなくなったという、何を言ってるか解らないと思うが俺も何が起きたのかわからない系のお話です。

半日ほどはまりました。

前座其の零 - firebaseって?

さて、タイトルにもある「firebase」ですが、これは、今年の中ごろにgoogleにより提供が開始されたBaaSとかmBaaSとか呼ばれている系のサービス群です。

BaaSとかmBaaSとか言われても、いまいちよくわからないのですが、サーバと通信することで成り立つようなアプリのサーバーサイドの機能を仮想化したもの?みたいな感じのようです。

例えば、ログイン機能、ユーザデータ保存機能、プッシュ通知機能などです。

僕が開発中のアプリでも、現在「firebase-ads(アプリ内広告)」「firebase-analytics(google analyticsのアプリ版)」「firebase-crash(クラッシュレポート収集)」の3つのサービスを利用しています。

前座其の壱 - 導入 ~ 使い始めまで

Firebaseとは何ぞや的な話とか、どんなことができるのかとか、アプリに組み込むにはどうすればいいんじゃとかは、公式日本語ドキュメントにかなり詳しく書かれています。

Firebase

ざっくり言うとこんな感じです。

  • Firebase console にログインして、アプリのapplicationId(package name)を登録する
  • アプリ固有の情報が書かれたgoogle-services.json というファイルのリンクが表示され、これををダウンロードしてプロジェクトの app/ に配置しろと説明があるのでその通りにする。
  • build.gradle に 以下のようなdependency を追加
    compile 'com.google.firebase:firebase-ads:9.6.1'
    compile 'com.google.firebase:firebase-crash:9.6.1'
    compile 'com.google.firebase:firebase-analytics:9.6.1'

analyticsとcrash はこれでおしまいです。
adsは広告表示用のコードを layoutで指定するなり javaでコーディングするなりが必要ですが数行書くだけで完了です。

前座其の弐 - google-services.json って何さ

前項で出てきた、google-services.json ですが、この中には何が書かれているのか、ちょっと覗いてみました。
中には、何かのハッシュコード的なものとかアプリのapplicationIdなどが書かれているようです。

おそらく、アプリとFirebaseのアカウントを紐づけるための情報が書かれているんだと思います。

debugとreleaseでパッケージ名変えてるんだが

build.gradleに下記のようなこと書いている方結構いらっしゃると思います

    buildTypes {
        debug {
            ....
            applicationIdSuffix '.debug'
            ....
        }
    }

こんな風にを書いておけば、debugビルドとreleaseビルドでアプリのapplicationIdを変えることができます。

例えば、アプリの applicationIdが下記のような感じだったとします。

applicatoinId : net.noranuk0.hoge.hoge

build.gradle内に上に書いたような指定を記載すると、debugビルド版のapplicationIdが以下のようになります。

applicatoinId : net.noranuk0.hoge.hoge.debug

なるほど、一台のスマホデバッグ版とリリース版両方のアプリを同居させられるわけね。

でも、少し話を戻します。

google-services.json」には release版のapplicationIdが書かれています。

debug版だけapplicationId変えちゃうとおかしなことにならない?

その辺はちゃんと考えられていて、ビルドタイプごとに「google-services.json」を使い分ける仕組みがちゃんと用意されています。

(プロジェクトルート)/app/src/debug/google-services.json
(プロジェクトルート)/app/src/release/google-services.json

のような感じで、debug版,relese版のgoogle-services.json を配置してあげると、ビルドタイプごとに使い分けてくれるようになっています*1

問題発生

冒頭で書いた事案が発生しました。

File[] dirs = context.getExternalFilesDirs(Environment.DIRECTORY_DOWNLOADS);
for (File dir : dirs) {
    // EXTERNAL_WRITE_STORAGE権限が無いと何故か例外が飛ぶ
    // さらに、外部ストレージ(SDカード)の場合にはStorage Access Framework で頑張らないとダメ。
    try {
        FileWriter filewriter = new FileWriter(new File(dir, "example.txt);
        ...
    } catch (IOException e) {
        ....
    }
}

firebase-analytics, firebase-crashを組み込んだプロジェクトをdebugビルド版で起動したところ、外部SDカードのアプリパッケージストレージ(/storage/sdcard1/Android/data/{packageName}/file/...)への書き込みができなくなってしまいました。

Firebaseを組み込んでからしばらくたって現象に気が付いたので特定にえらい時間食いました。

内部SDカードへのアプリパッケージストレージへの読み書きは問題なくできてお、ますます混乱したし*2

試しにfirebaseの依存関係を外したところ現象が出なくなりました。

まじかorz.. と思いました。

解決方法

これがfirebaseのバグなのかはわかりませんが

    compile 'com.google.firebase:firebase-ads:9.6.1'
    releaseCompile 'com.google.firebase:firebase-crash:9.6.1'
    releaseCompile 'com.google.firebase:firebase-analytics:9.6.1'

とりあえずdebug時は crashとanalytics無効にすることで対応です。

無事、外部SDカードのアプリパッケージストレージへの書き込みできました。

今回のお話は以上となります。

どなたかのご参考になれば幸いです。

*1:これができるようになったの割と最近みたいです

*2:開発中アプリが(READ|WRITE)_EXTERNAL_STORAGEをもともと要求していたせい

買うならコレ!一番お勧めのBluetoothオーディオトランスミッターをざっくり紹介

どうものらぬこです。

2014年ごろに初めてBluetoothヘッドホンを買ってから、これまでいくつものBluetoothヘッドホンを使ってきました。

Bluetoothのヘッドホン、「有線に比べて音が悪い」とか「遅延が発生する」などの意見もありますが、ケーブル断線の心配をしなくてもいいだとか、曲送りと一時停止くらいならヘッドホンの操作ボタンでどうにか出来る等、やっぱり便利です。

AndroidスマホiPhone、PS vita等のbluetooth対応機器であれば、Bluetooth接続を使ってワイヤレスで音楽などを楽しめます。

また、ニンテンドースイッチニンテンドー3DSなど、Bluetooth非対応の機器で使用したい場合にも、bluetoothイヤホン・ヘッドホンに付属している3.5インチミニジャックケーブルを使用して、ゲーム機やオーディオ機器のヘッドホン出力端子と、bluetoothイヤホン・ヘッドホンのオーディオ入力端子を接続すれば、通常の有線ヘッドホンと同様に音楽などを楽しむことができます。

ですが、世の中には便利なものがあるもので、bluetooth非対応のオーディオ機器やゲーム機でも、bluetooth対応のヘッドホンやスピーカーなどをワイヤレス接続可能にしてくれる機器が存在します。

それが、今回ご紹介する「bluetoothオーディオトランスミッター」という装置になります。

使い方も簡単です。

スマホやゲーム機などのオーディオ機器」と「bluetoothオーディオトランスミッター」を3.5インチミニジャックケーブルで接続し、「bluetoothオーディオトランスミッター」と「bluetoothヘッドホン」をペアリングするだけで、セットアップは完了です。

Bluetoothオーディオトランスミッタには、液晶パネルなどは存在せず、「接続可能なヘッドホン一覧」などを確認することはできないですが、ボタンを押し続けるなどの操作で簡単にペアリングができるようになっています。

前置きが長くなってしまいましたが、今回は、bluetoothオーディオトランスミッターを買ってみようかなと思っている方向けに、選ぶ際の基準みたいなこと、そして、迷ったらとりあえずこれ買っとけ的なおすすめ機種の紹介をしてみたいと思います。

さて、とりあえずamazonで「Bluetooth オーディオトランスミッター」などの言葉で検索すると、結構たくさんの製品が出てきます。

値段も2000円程度のものから6000円オーバーくらいのものまで様々です。

値段が高い=いい音で聴ける、とは限らない

すでにBluetoothヘッドホンをお持ちの方の中にはご存じの方もいらっしゃるかと思いますが、Bluetoothでの音楽転送方式には、どの機器でも使える代わりに若干遅延が発生する「SBC」という方式と、使える機器が限られている代わりに遅延が少なく音質も良いと言われている「aptX(LowLaytency)」という方式があります。

これ以外にも、主にapple製品で使われている「AAC」や、ハイレゾ音源向けの「LDAC(主にソニー製品に対応)」とか「aptX HD」なる規格もあるんですが、今回はハイレゾな話やBluetoothの音質を追求する記事でもないのでこの辺は割愛します。

aptXは本当に高音質なのか?

ちなみに音質についてなんですが、「SBC」より「aptX」のほうが高音質とは言われています(仕様上でも確かにそうなっているみたいです)、正直僕にはよくわかりません。

静かな部屋でそれなりのお値段のヘッドホンを使えば違いが分かるのかもしれないですが、少なくとも、お出かけ中、電車の中で、そこそこの騒音の中で聴く分には聴き比べるのは難しいと思います。

それよりも「ヘッドホンが自分好みの音を鳴らしてくれるか」のほうがよっぽど大事なんじゃないかと思っています。

さて、値段の高いBluetoothオーディオトランスミッターは何がすごいかというと、「aptX」対応だったり、送信側としてだけではなく受信側としても使えると言った付加価値がついているものが多いです。

「aptX」に対応していればひょっとしたら体感でもなんとなくわかるくらいに良い音で聞けるかもしれません。
ただしヘッドホン側が「SBC」にしか対応していない場合には、SBCモードでの転送になるため、トランスミッターだけ「aptX」対応のものを購入しても、aptXの恩恵は受けられません。

お使いのヘッドホンが「aptX」に対応しているかは、googleで「ヘッドホンの型番 aptX」で検索してみてください。メーカーサイトだったり価格コムの掲示板などで、多分答えが見つかります。

ちなみに、もしお持ちのヘッドホンがaptX非対応だったとしても、「このヘッドホン、もしかして音質悪い?」とがっかりする必要はありません。

「SBC」にしか対応していないヘッドホンやBluetoothオーディオトランスミッターのレビューに、「さすがaptx対応は音が違う」みたいな書き込みを見かけたことがありますし、まあ、人の耳なんてそんなもんです。

でもやっぱりaptX対応品をおすすめする理由がある

少し話が逸れてしまいますが、これからヘッドホンを買う、もしくは、既にaptX対応のヘッドホンをお持ちであれば、たとえ音質に違いがなくてもaptX対応の機器でそろえることをお薦めします。

それは、遅延の問題です。

「SBC」は、オーディオ機器が音を鳴らしてからヘッドホンが音を鳴らすまで、公称値で0.2秒ほどの遅延があります。
オーディオプレーヤーとして使ってる分にはなんの問題もないですが、映画やアニメなどの動画鑑賞目的で使用すると、少し違和感が出てくるかもしれません。
ゲーム用途となると、特にアクション系ゲームなどでは結構きついです。

俳優さんやアニメキャラが口を動かし始めた後、少し遅れて声が聞こえてくる。爆発が起こってちょっと遅れて「どか~ん」ていう効果音が鳴る。ゲームのメニューを開いてからちょっと遅れて「ぴろ~ん」とメニューを開いた効果音。

個人差は出てくると思いますが、けっこうな違和感覚える方も出て来るんじゃないかと。

一方、「aptX」の遅延は約70ms(0.07秒)と言われています。音ゲー以外では困ることはおそらくないと思います(aptX対応のヘッドホンとトランスミッターを所有していますが、音ゲー*1はやっぱり厳しいです)。

更に遅延を少なくした「aptX low laytency」では、対応機器はものすごく限られてきますが遅延は35ms(0.035秒)程度のようです。対応製品何も持っていないので確認できないですが、もしかしたら音ゲーも違和感なくプレイできるのかもしれません。

僕が使っているのは・・・

aptX対応のこちらの商品です。

Amazon|[asin:B01EHMPUHO:detail]

aptX対応のヘッドホンを常用しているので、せっかくなのでということでaptX対応のものを買いました。
稀に接続に少し時間がかかる(10秒ほど)こともありますが不具合で困ったことは今のところありません。

サイズの割に電池持ちも良いですし、充電はmicroUSBケーブルでできます。また、充電中でも使い続けることができるようになっているのもありがたいです。

遅延についても、「音楽」「ゲーム」「動画」などで使用していますが、音ゲー以外で違和感を感じたことはありません。

なお、現在は、この製品の後継品として、同一メーカー産から新商品が出ています。

[asin:B01KT2KMWE:detail]

写真を見る限り、少しサイズが大きくなったような印象は受けますが、レビュー評価も高いですし、aptx-LL対応や光端子でのデジタル出力等が追加されており、特に対応製品をお持ちの方であれば、恩恵も受けやすいのかと思います。

しかも、amazonでの販売価格は旧製品とほとんど変わらりません(19/9/22現在)。

総括

もちろん、この商品以外にも他社さんからも同等の機能を持った商品が色々と出ています。

性能を比較したブログ記事なんかも幾つか出ております。

一押し製品はブログによっても割れており、いくつかのブログを読んでみると意外と面白いです。

ちなみに、性能に関して言えば「aptXに対応しているか」「充電中も使用できるか」「省電力機能(接続中でも無音状態が続くと自動的に電源が切れる等)」という点を除いて、どの製品も大差はありません。

ただし、Amazonなどのショップレビューを見ると、以下のようなネガティブなレビューも掲載されています。

  • 接続が不安定、接続に毎回時間がかかる
  • 無音部が続くと自動ミュートするような省電力機能が仇となり、小音量部分が続くと勝手にミュートしてしまう
  • 複数の機器とのペアリングがうまくいかない
  • 音飛びが酷い

等です。

個人的には、これらのレビューが多く書き込まれている商品は避けたほうが良いかと思っています。

ケーブルのわずらわしさから解放されるbluetoothオーディオトランスミッタ、購入を検討されている方は、実際に購入された方のレビューもちら見しつつ、良さげなものを選んでいただければと思います。

僕が使っているノイズキャンセリング機能のついたBluetoothイヤホン、ヘッドホンの紹介記事も書きました。購入検討されている方は合わせて読んでいただけるとうれしいです。 noranuk0.hatenablog.com

amazon echogoogle home mini を買ってみました。期待外れだった部分、未成熟だと感じた部分も含め率直な印象を書いています。 noranuk0.hatenablog.com

最後までお読みいただいてありがとうございました。

*1:初音ミク project DIVA f2 - ps vita版

android向けの軽量Job Systemを作った話

こんにちは。のらぬこです。

今回は、いま開発中のandroidアプリで必要になった、指定されたメソッドを順にバックグラウンドで実行してくれるJobQueueの仕組みを実装した話をします。

既存のものもいくつか調べたのですが、僕が欲しかった機能を満たしているものがぱっと見でなさそうだったので自作することにしました。

欲しかった機能

  • Jobの優先順位がつけられる
  • 失敗時や例外発生時にリトライができる
  • 定期的に繰り返し実行されるJobとしてQueueに登録できる
  • とにかくスケジューラが軽量
    • 短時間で終わるJobを数千単位で登録するような用途のため
  • Jobの登録はマルチスレッド対応

できれば欲しかった機能

  • Jobのグループ化
    • 同一グループのJobを、一括で有効/無効化したい
      • ネットワークが切れたら特定の種類のJobの起動を抑制したい等の用途

特にいらなかった機能

  • 複数Jobの並列実行(あってもよかったかなー)
  • Queueのシリアライズ

実装してみた

てことで、欲しかった機能+できれば欲しかった機能を詰め込んで実装してみました。 android固有のclassやmethodはログ出力の部分くらいしか使っていないので、ほぼ何も変更することなく、他の用途でも使えると思います。

そのうちちゃんとプロジェクト作ってgithubとかに載せるかもしれませんが、とりあえずソースをベタっと貼っておきます。

とりあえず、ソースはgithubにあげました。readmeやサンプルなどは近々更新します(あくまで予定です)。

GitHub - noranuk0/panther-job: Simple and lightweight job system with priorities and group ideas

JobManager.instnce() で JobManagerのインスタンスを取得したら、Job classの派生クラスを Jobとしてガンガン登録すればあとはなんか良きに計らってくれます。

Jobグループ有効化、無効化のメソッドなどもこのclassに用意されています。

リトライ(再実行)の設定は、Jobが完了したとき、例外で落ちたときに呼ばれるCallbackクラスで設定します。
RepeatedlyRunCallback クラスの実装を見てください。

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

/**
 * JobManager.java
 * <p/>
 * Copyright (c) 2016 Hiroyuki Mizuhara
 * <p/>
 * This software is released under the MIT License.
 * http://opensource.org/licenses/mit-license.php
 */
public class JobManager {
    public static final int PRIORITY_MIN = 0;
    public static final int PRIORITY_BELOW_NORMAL = 1;
    public static final int PRIORITY_NORMAL = 2;
    public static final int PRIORITY_ABOVE_NORMAL = 3;
    public static final int PRIORITY_MAX = 4;

    public static final int MAX_JOB_GROUP_ID = 30;

    @SuppressWarnings("unchecked")
    private final List<Job>[] priorityJobList = new ArrayList[PRIORITY_MAX + 1];

    private final long[] jobGroupSuspendLimitTimes = new long[MAX_JOB_GROUP_ID];
    private int currentEnabledJobGroupMask = 0x7fffffff;

    private static JobManager instance;
    private Worker worker;

    private static void initialize() {
        instance = new JobManager();
    }

    public static JobManager instance() {
        if (instance == null) {
            initialize();
        }
        return instance;
    }

    private class Worker extends Thread {
        public Worker() {
            super("Job#WorkerThread");
        }

        private Job get() {

            final int[] workJobSizeArray = new int[PRIORITY_MAX + 1];
            final long[] workJobGroupSuspendLimitTimes = new long[MAX_JOB_GROUP_ID];
            Job result = null;
            while (result == null) {
                long nextScheduledJobTime = Long.MAX_VALUE;
                long currentTimeMillis = System.currentTimeMillis();
                synchronized (priorityJobList) {
                    for (int priority = PRIORITY_MAX; priority >= PRIORITY_MIN; priority--) {
                        workJobSizeArray[priority] = priorityJobList[priority].size();
                    }
                }
                int workActiveJobGroupMask;
                synchronized (JobManager.this) {
                    workActiveJobGroupMask = JobManager.this.currentEnabledJobGroupMask;
                    System.arraycopy(
                            JobManager.this.jobGroupSuspendLimitTimes, 0,
                            workJobGroupSuspendLimitTimes,
                            0, workJobGroupSuspendLimitTimes.length);
                }
                for (int index = 0; index < MAX_JOB_GROUP_ID; index++) {
                    if (workJobGroupSuspendLimitTimes[index] <= currentTimeMillis) {
                        workJobGroupSuspendLimitTimes[index] = 0;
                    } else {
                        if (workJobGroupSuspendLimitTimes[index] < nextScheduledJobTime) {
                            nextScheduledJobTime = workJobGroupSuspendLimitTimes[index];
                        }
                        workActiveJobGroupMask &= ~(1 << index);
                    }
                }
                for (int priority = PRIORITY_MAX; priority >= PRIORITY_MIN; priority--) {
                    List<Job> targetList = priorityJobList[priority];
                    for (int index = 0; index < workJobSizeArray[priority]; index++) {
                        Job target = targetList.get(index);
                        if ((target.getJobGroupMask() & workActiveJobGroupMask)
                                != target.getJobGroupMask()) {
                            continue;
                        }
                        if ((target.getJobGroupMask() & workActiveJobGroupMask)
                                != target.getJobGroupMask()) {
                            continue;
                        }
                        if (nextScheduledJobTime > target.getNextExecuteSystemTime()) {
                            nextScheduledJobTime = target.getNextExecuteSystemTime();
                        }
                        if (target.getNextExecuteSystemTime() > currentTimeMillis) {
                            continue;
                        }
                        targetList.remove(index);
                        target.setNextScheduleTime(0);
                        result = target;
                        break;
                    }
                    if (result != null) {
                        break;
                    }
                }
                if (result == null) {
                    try {
                        synchronized (worker) {
                            if (nextScheduledJobTime < Long.MAX_VALUE) {
                                currentTimeMillis = System.currentTimeMillis();
                                wait(nextScheduledJobTime - currentTimeMillis);
                            } else {
                                wait();
                            }
                        }
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
            return result;
        }

        @Override
        public void run() {
            while (true) {
                final long systemTime = System.currentTimeMillis();
                Job target = get();
                Callback<Job> callback = target.getCallback();
                try {
                    boolean result =
                            target.execute();
                    if (result) {
                        if (callback != null) {
                            callback.success(target);
                        }
                    } else {
                        if (callback != null) {
                            callback.fail(target);
                        }
                    }
                } catch (Exception e) {
                    try {
                        if (callback != null) {
                            callback.exception(target, e);
                        }
                    } catch (Exception ex) {
                        ex.printStackTrace();
                    }
                }

                if (target.getNextExecuteSystemTime() >= systemTime) {
                    JobManager.this.register(target);
                }
            }
        }
    }

    private JobManager() {
        for (int index = PRIORITY_MIN; index <= PRIORITY_MAX; index++) {
            priorityJobList[index] = new ArrayList<>();
        }
        this.worker = new Worker();
        worker.start();
    }

    public void register(Job job) {
        synchronized (priorityJobList) {
            final List<Job> targetList = priorityJobList[job.getPriority()];
            targetList.add(job);
        }
        synchronized (worker) {
            worker.notify();
        }
    }

    public void suspendGroup(int jobGroupId, long wakeUpTime) {
        long current = System.currentTimeMillis();
        synchronized (this) {
            jobGroupSuspendLimitTimes[jobGroupId] = wakeUpTime;
        }
        if (wakeUpTime <= current) {
            synchronized (worker) {
                worker.notify();
            }
        }
    }

    public Map<Class<?>, Integer> count() {
        Map<Class<?>, Integer> result = new HashMap<>();
        synchronized(this) {
            for (List<Job> listJob : priorityJobList) {
                for (Job job : listJob) {
                    if (result.get(job.getClass()) == null) {
                        result.put(job.getClass(), 0);
                    }
                    result.put(job.getClass(), result.get(job.getClass()) + 1);
                }
            }
        }
        return result;
    }
    
    public void enableGroup(int groupId) {
        synchronized (this) {
            currentEnabledJobGroupMask |= (1 << groupId);
        }
        synchronized (worker) {
            worker.notify();
        }
    }

    public void disableGroup(int groupId) {
        synchronized (this) {
            currentEnabledJobGroupMask &= ~(1 << groupId);
        }
    }

    public void cleanUp() {
        synchronized (this) {
            JobManager.instance = null;
            JobManager.initialize();
        }
    }
}
/**
 * Job.java
 * <p/>
 * Copyright (c) 2016 Hiroyuki Mizuhara
 * <p/>
 * This software is released under the MIT License.
 * http://opensource.org/licenses/mit-license.php
 */
public abstract class Job {
    public int getPriority() {
        return priority;
    }

    public int getJobGroupMask() {
        return jobGroupMask;
    }

    public long getNextExecuteSystemTime() {
        return nextExecuteSystemTime;
    }

    public void setNextScheduleTime(long nextExecuteSystemTime) {
        this.nextExecuteSystemTime = nextExecuteSystemTime;
    }

    public Callback getCallback() {
        return callback;
    }

    public static class Builder {
        private int priority = JobManager.PRIORITY_NORMAL;
        private int jobGroupMask = 0;   /* note set */
        private long nextExecuteSystemTime = 0;
        private Callback callback;

        private Builder(int priority) {
            if (priority >= JobManager.PRIORITY_MIN && priority <= JobManager.PRIORITY_MAX) {
                this.priority = priority;
            } else {
                throw new IllegalArgumentException();
            }
        }

        public static Builder create(int priority) {
            return new Builder(priority);
        }

        public Builder nextExecuteSystemTime(long systemTime) {
            this.nextExecuteSystemTime = systemTime;
            return this;
        }

        public Builder addGroup(int addGroupIndex) {
            if (addGroupIndex >= 0 && addGroupIndex <= JobManager.MAX_JOB_GROUP_ID) {
                this.jobGroupMask |= (1 << addGroupIndex);
                return this;
            } else {
                throw new IllegalArgumentException();
            }
        }

        public Builder callback(Callback callback) {
            this.callback = callback;
            return this;
        }

        public int getPriority() {
            return priority;
        }

        public int getJobGroupMask() {
            return jobGroupMask;
        }

        public long nextExecuteSystemTime() {
            return nextExecuteSystemTime;
        }

        public Callback getCallback() {
            return callback;
        }
    }

    public Job(Builder builder) {
        this.priority = builder.getPriority();
        this.jobGroupMask = builder.getJobGroupMask();
        this.nextExecuteSystemTime = builder.nextExecuteSystemTime();
        this.callback = builder.getCallback();
    }

    protected final int priority;
    protected final int jobGroupMask;
    protected long nextExecuteSystemTime;
    protected final Callback callback;

    public String description() {
        return this.getClass().getSimpleName();
    }

    public boolean containJobGroup(int groupId) {
        return ((jobGroupMask >> groupId) & 0x1) != 0;
    }

    public abstract boolean execute() throws Exception;
}
package com.mknk6556655.noranuko.job;

/**
 * Callback.java
 * <p/>
 * Copyright (c) 2016 Hiroyuki Mizuhara
 * <p/>
 * This software is released under the MIT License.
 * http://opensource.org/licenses/mit-license.php
 */
public class Callback<T extends Job> {
    public void success(T sender) {
    }

    public void fail(T sender) {
    }

    public void exception(T sender, Exception e) {
    }

}
package com.mknk6556655.noranuko.job;

/**
 * RepeatedlyRunCallback.java
 * <p/>
 * Copyright (c) 2016 Hiroyuki Mizuhara
 * <p/>
 * This software is released under the MIT License.
 * http://opensource.org/licenses/mit-license.php
 */
public class RepeatedlyRunCallback<T extends Job> extends Callback<T> {
    private int delayTimeIfSuccessful;
    private int delayTimeIfFailed;
    private int delayTimeIfException;

    public RepeatedlyRunCallback(int delayTimeIfSuccessful,
                                 int delayTimeIfFailed,
                                 int delayTimeIfException) {
        this.delayTimeIfSuccessful = delayTimeIfSuccessful;
        this.delayTimeIfFailed = delayTimeIfFailed;
        this.delayTimeIfException = delayTimeIfException;
    }

    public void success(T sender) {
        if (delayTimeIfSuccessful > 0) {
            long current = System.currentTimeMillis();
            sender.setNextScheduleTime(current + delayTimeIfSuccessful);
        }
    }

    @Override
    public void fail(T sender) {
        if (delayTimeIfFailed > 0) {
            long current = System.currentTimeMillis();
            sender.setNextScheduleTime(current + delayTimeIfFailed);
        }
    }

    @Override
    public void exception(T sender, Exception e) {
        if (delayTimeIfException > 0) {
            long current = System.currentTimeMillis();
            sender.setNextScheduleTime(current + delayTimeIfException);
        }
    }
}

そのうちもうちょっとましな説明書くかもしれませんがとりあえず今回はこのへんで、ということで。

お読みいただいてありがとうございました。

Re:ゼロから始める弾幕アルゴリズム ~ Unityで作る弾幕STG

こんにちは、のらぬこです。

先日、Unity入門書に掲載されているSTGに色々手を加えて遊んでみた話を書きました。

noranuk0.hatenablog.com

今回は、その続として、東方などの弾幕シューティングゲームで登場するような弾幕をあれこれ作ってみたお話です。

Unity使えば、当たり判定は自動で行ってくれる(オブジェクト同士が衝突したら、コールバックメソッドが自動で呼び出される)ため、衝突判定のロジックは不要です。

なので、弾幕作る場合も、弾道の数式をコード化すればお仕事完了っていうのが便利ですね。

ちなみに、Unityの場合、重力以外の力でオブジェクトをあれこれ動かす場合や、衝突時に慣性の法則以外の挙動を与えたい場合には、オブジェクトにC#コードをアタッチして動きや衝突時の動作をC#で書くことになっています*1

ということで、弾の動きはC#コードで制御していくことになります。

なお、以下に掲載している画像内のキャラクターは以下の書籍のサンプルデータに含まれる画像を使用しております。

Unity4.6/5.0でつくる 2Dゲーム制作入門 [改訂第二版]

Unity4.6/5.0でつくる 2Dゲーム制作入門 [改訂第二版]

全方位弾

記事書いておいてあれですが、弾幕げーとか東方とか全然詳しくないので、この言い方でいいのかわからないですが、敵の周り全方位に同時にばらまかれるタイプの弾幕です。 f:id:noranuk0:20161029222814p:plain

public class Bullet : MonoBehaviour
{
    public static Bullet Add(float x, float y, float direction, float speed)
    {
        Bullet bullet = ...
        return bullet;
    }
}

public class Enemy : MonoBehaviour
{
    IEnumerator _Update1()
    {
        float baseDir = GetAim();
        int count = 0;
        while (true)
        {
            // 8秒毎に、間隔6度、速度1で敵キャラを中心として全方位弾発射。
            for (int rad = 0; rad < 360; rad += 6)
            {
                Bullet.Add(transform.position.x, transform.position.y, rad, 1);
            }
            yield return new WaitForSeconds(8.0f);
            count++;
        }
    }
}

ワインダー

弾幕の檻に閉じ込められて移動範囲がめちゃ制限されるやつです。
自機狙いの3Way弾とかとセットで使う感じでしょうか。

f:id:noranuk0:20161029225404p:plain

public class Bullet : MonoBehaviour
{
    public static Bullet Add(float x, float y, float direction, float speed)
    {
        Bullet bullet = ...
        return bullet;
    }
}

public class Enemy : MonoBehaviour
{
    // 敵キャラと自キャラの位置関係(角度)を取得
    public float GetAim()
    {
        float dx = target.X - transform.position.x;
        float dy = target.Y - transform.position.y;
        return Mathf.Atan2(dy, dx) * Mathf.Rad2Deg;
    }

    IEnumerator _Update2()
    {
        float baseDirection = GetAim();
        int count = 0;
        while (true)
        {
            // 檻の中心角度を設定。最初の自キャラ角度を中心として+30度~-30度の範囲をゆらゆら動かす
            float dir = baseDirection + Mathf.Sin(count * Mathf.Deg2Rad) * 30.0f;

            // 偶数弾を撃つ感じで、自キャラ角度を外して上下3本づつ0.05秒毎に弾発射
            for (int index = -3; index < 3; index++)
            {
                Bullet.Add(transform.position.x, transform.position.y, dir + index * 30 + 15, 3);
            }
            yield return new WaitForSeconds(0.05f);
            count++;
        }
    }
}

うずまき弾

ロールケーキみたいな渦巻きを描くような弾幕です。

f:id:noranuk0:20161029230424p:plain

public class Bullet : MonoBehaviour
{
    public static Bullet Add(float x, float y, float direction, float speed)
    {
        Bullet bullet = ...
        return bullet;
    }
}

public class Enemy : MonoBehaviour
{
    IEnumerator _Update3()
    {
        int rad = 0;
        while (true)
        {
            Bullet.Add(0, null, 0, transform.position.x, transform.position.y, rad, 1);
            rad += 8;
            yield return new WaitForSeconds(0.05f);
        }
    }
}

切り返し使って避けるやつ

自キャラ狙いの奇数弾が割りと休みなく襲って来るやつです。
ソースは、画面端で切り返し*2できるように、10発撃ったら0.2秒ほどディレイを入れて弾間隔をあけています。

下の図のように、これ単体だと画面端からそのまま大きく敵の後ろに回れば、切り返し不要で避けられるので、例えば画面右半分レーザーで覆うとかして移動範囲狭めたほうが良いかと。

f:id:noranuk0:20161029230649p:plain

public class Bullet : MonoBehaviour
{
    public static Bullet Add(float x, float y, float direction, float speed)
    {
        Bullet bullet = ...
        return bullet;
    }
}

public class Enemy : MonoBehaviour
{
    // 敵キャラと自キャラの位置関係(角度)を取得
    public float GetAim()
    {
        float dx = target.X - transform.position.x;
        float dy = target.Y - transform.position.y;
        return Mathf.Atan2(dy, dx) * Mathf.Rad2Deg;
    }

    IEnumerator _Update4()
    {
        while (true)
        {
            float rad = GetAim();
            for (int index = 0; index < 10; index++)
            {
                Bullet.Add(0, null, 0, transform.position.x, transform.position.y, rad, 2);
                yield return new WaitForSeconds(0.05f);
            }
            yield return new WaitForSeconds(0.2f);
        }
    }
}

永夜叉の2面?ボスが使ってたようなやつ

これにどういう名前がついていて、どう説明すればいいかわからないので、とりあえず下の図を見てください。 f:id:noranuk0:20161029232303p:plain

青い弾は時計回り、赤い玉は反時計回りに回転しつつ、円が徐々に大きくなっていく弾幕です。

この弾幕はこれまでと少し実装方法が異なります。
これまでの弾幕は、弾は直進していましたが、この弾幕の弾の軌跡は曲線を描きます。 f:id:noranuk0:20161029232546p:plain

これまで紹介した4つの弾幕は、敵が弾を発射するときに位置、初速度、発射角を指定すれば、あとはUnityが面倒見てくれたのですが、今回は、弾クラスで弾位置を自前で更新しないと駄目です、多分。
ある式で与えられた曲線状を等速直線運動せよ、みたいなこともUnity標準で書けるのかもしれませんが、まだ修行中の身ゆえそういうところまでは調べきれていないです。
てゆか、この曲線が数式的にはどういう式になるのかもよくわからん。

ということで、この実装方針でいいのか微妙に疑問に思いつつコードです。

/// 敵弾
public class Bullet : MonoBehaviour
{
    // 0: 直進
    // 1: 右回転
    // 2: 左回転
    public int type = 0;
    public MonoBehaviour center;
    protected float rad = 0;
    protected float dist = 0;

    public Sprite spriteBlue = null;
    public Sprite spriteRed = null;

    public static Bullet Add(int type, MonoBehaviour center, int rad, float x, float y, float direction, float speed)
    {
        Bullet bullet = ...
        if (bullet != null) {
            bullet.type = type;
            bullet.center = center;
            bullet.rad = rad;
            bullet.dist = 0;
            if (type == 1)
            {
                bullet.gameObject.GetComponent<SpriteRenderer>().sprite = bullet.spriteBlue;
            } else if (type == 2)
            {
                bullet.gameObject.GetComponent<SpriteRenderer>().sprite = bullet.spriteRed;
            }
            }
        return bullet;
    }

    void FixedUpdate()
    {
        if (type != 0)
        {
            float x = center.transform.position.x + Mathf.Sin(rad * Mathf.Deg2Rad) * dist;
            float y = center.transform.position.y + Mathf.Cos(rad * Mathf.Deg2Rad) * dist;
            Vector3 pos = transform.position;
            pos.Set(x, y, 0);
            transform.position = pos;
            if (type == 1)
            {
                rad += 0.2f;
            } else if (type == 2)
            {
                rad -= 0.2f;
            }
        }
        dist += 0.01f;
    }
}

public class Enemy : MonoBehaviour
{
    IEnumerator _Update5()
    {
        while (true)
        {
            for (int rad = 0; rad < 360; rad += 15)
            {
                Bullet.Add(1, this, rad, transform.position.x, transform.position.y, 0, 1);
                Bullet.Add(2, this, rad, transform.position.x, transform.position.y, 0, 1);
            }
            yield return new WaitForSeconds(2.0f);
        }
    }
}

参考書籍の紹介

最後に、この記事を書く少し前からUnityの学習を始めたのですが、僕が参考にした書籍の紹介をしたいと思います。

Unity4.6/5.0でつくる 2Dゲーム制作入門 [改訂第二版]

Unity4.6/5.0でつくる 2Dゲーム制作入門 [改訂第二版]

kindle版の電子書籍ですが、お値段500円、紙の本の約1/5ほどの値段で購入できます。個人の方が書かれた書籍で、値段も安いですが、サンプルを作りながら覚えていくタイプの入門書としてはとても良い本だと思います。

内容は、ミニゲーム(マウスクリックで敵を消すゲーム)、ボス戦オンリー(?)のシューティングゲーム、動く床や当たると死ぬ罠が配置されたステージをクリアしていくアクションゲーム、の3つのゲームを書籍の手順に従って実際にUnityに触れながら作り上げていくような構成になっています。

スマホ対応についての記述は無いので、すべての操作をタップでどうにかするようなゲームを作るとなると、別途Webで調べるなり別の書籍を漁るなど必要になってくると思いますが、とりあえず何かゲームを作りながらあれこれ覚えていくというスタンスでしたらとてもわかり易い本だと思います。

さらに、ツールの操作も図付きで操作を1つ1つ説明しているため、途中で躓くことも無いと思います。

書籍のレビューに、「作者が作った独自の基底クラスを継承させて、その独自クラスのメソッドやプロパティを多用している」との指摘もありますが、そのクラス自体はUnityAPIの薄いラッパークラスとなっており、オブジェクトの生ハンドルの隠蔽や、使用しない引数を省略するためのラッパーメソッドが定義されているくらいです。ソースコードも提供されているため、Unity本来のAPIではどのように記述するかという点についても、C#でのメソッド呼び出し、基本的な演算子、条件分岐くらいのコードが読めれば容易に理解できると思います。

なお、画像リソースや完成済のUnityプロジェクトもWebからダウンロードできるようになっているため、絵が描けないのでゲーム用の画像用意するところでまず躓いてしまうという心配もなく、また、とりあえず出来上がっているコードをあれこれ触ってみたいという方にもおすすめです。

ちなみに、書籍の作者さまが書かれたチュートリアル(書籍の第一章に相当する内容です)が、Qiitaにも公開されています。

qiita.com

また、いろいろな本を読み漁ってみたいという方は、kindle unlimited に加入するのも割とお勧めです。

kindle unlimited は、月額980円(2017年7月現在)のみで、kindle unlimitedの対象書籍を無制限に読むことができるサービスです。

対象書籍のラインナップも、上で紹介した書籍も含め、個人で出版された書籍や市販の技術書等、結構豊富に取り揃えられています。
月額980円ですが初月は無料なので、お試しでひと月だけ加入して、ずっと持っておきたい本だけ購入するみたいな使い方もありかと思います。

kindle white paper 等の kindle端末を持っていなくても、androidiOS端末をお持ちであれば、アプリ版のkindleから利用することもできます。

もし興味ございましたら、kindle unlimited の対象書籍を幾つかリストアップしておきますので、ご参考までにどうぞってことで。

kindle unlimited の申込みは下記リンクから行えます

Kindle Unlimited会員登録

もしご興味持っていただけたら、検討してみてはいかがでしょうか?

最後までお読みいただいてありがとうございました。

*1:他にもやり方あるのかもしれませんが、未だ勉強中のため、他のやり方は知らんです

*2:画面端に到達したら大きく避けて弾幕の隙間から反対側に避難