【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 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'
- 他、build.gradleにプラグインの設定とかを追記する
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カードのアプリパッケージストレージへの書き込みできました。
今回のお話は以上となります。
どなたかのご参考になれば幸いです。
買うならコレ!一番お勧めの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ケーブルでできます。また、充電中でも使い続けることができるようになっているのもありがたいです。
遅延についても、「音楽」「ゲーム」「動画」などで使用していますが、音ゲー以外で違和感を感じたことはありません。
なお、現在は、この製品の後継品として、同一メーカー産から新商品が出ています。
写真を見る限り、少しサイズが大きくなったような印象は受けますが、レビュー評価も高いですし、aptx-LL対応や光端子でのデジタル出力等が追加されており、特に対応製品をお持ちの方であれば、恩恵も受けやすいのかと思います。
しかも、amazonでの販売価格は旧製品とほとんど変わらりません(19/9/22現在)。
総括
もちろん、この商品以外にも他社さんからも同等の機能を持った商品が色々と出ています。
性能を比較したブログ記事なんかも幾つか出ております。
一押し製品はブログによっても割れており、いくつかのブログを読んでみると意外と面白いです。
ちなみに、性能に関して言えば「aptXに対応しているか」「充電中も使用できるか」「省電力機能(接続中でも無音状態が続くと自動的に電源が切れる等)」という点を除いて、どの製品も大差はありません。
ただし、Amazonなどのショップレビューを見ると、以下のようなネガティブなレビューも掲載されています。
- 接続が不安定、接続に毎回時間がかかる
- 無音部が続くと自動ミュートするような省電力機能が仇となり、小音量部分が続くと勝手にミュートしてしまう
- 複数の機器とのペアリングがうまくいかない
- 音飛びが酷い
等です。
個人的には、これらのレビューが多く書き込まれている商品は避けたほうが良いかと思っています。
ケーブルのわずらわしさから解放されるbluetoothオーディオトランスミッタ、購入を検討されている方は、実際に購入された方のレビューもちら見しつつ、良さげなものを選んでいただければと思います。
僕が使っているノイズキャンセリング機能のついたBluetoothイヤホン、ヘッドホンの紹介記事も書きました。購入検討されている方は合わせて読んでいただけるとうれしいです。 noranuk0.hatenablog.com
amazon echo と google 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を、一括で有効/無効化したい
特にいらなかった機能
- 複数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に色々手を加えて遊んでみた話を書きました。
今回は、その続として、東方などの弾幕シューティングゲームで登場するような弾幕をあれこれ作ってみたお話です。
Unity使えば、当たり判定は自動で行ってくれる(オブジェクト同士が衝突したら、コールバックメソッドが自動で呼び出される)ため、衝突判定のロジックは不要です。
なので、弾幕作る場合も、弾道の数式をコード化すればお仕事完了っていうのが便利ですね。
ちなみに、Unityの場合、重力以外の力でオブジェクトをあれこれ動かす場合や、衝突時に慣性の法則以外の挙動を与えたい場合には、オブジェクトにC#コードをアタッチして動きや衝突時の動作をC#で書くことになっています*1。
ということで、弾の動きはC#コードで制御していくことになります。
なお、以下に掲載している画像内のキャラクターは以下の書籍のサンプルデータに含まれる画像を使用しております。
Unity4.6/5.0でつくる 2Dゲーム制作入門 [改訂第二版]
- 作者: 尾関俊介
- 出版社/メーカー: Wiz Publishing
- 発売日: 2015/03/20
- メディア: Kindle版
- この商品を含むブログ (1件) を見る
全方位弾
記事書いておいてあれですが、弾幕げーとか東方とか全然詳しくないので、この言い方でいいのかわからないですが、敵の周り全方位に同時にばらまかれるタイプの弾幕です。
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弾とかとセットで使う感じでしょうか。
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++; } } }
うずまき弾
ロールケーキみたいな渦巻きを描くような弾幕です。
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秒ほどディレイを入れて弾間隔をあけています。
下の図のように、これ単体だと画面端からそのまま大きく敵の後ろに回れば、切り返し不要で避けられるので、例えば画面右半分レーザーで覆うとかして移動範囲狭めたほうが良いかと。
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面?ボスが使ってたようなやつ
これにどういう名前がついていて、どう説明すればいいかわからないので、とりあえず下の図を見てください。
青い弾は時計回り、赤い玉は反時計回りに回転しつつ、円が徐々に大きくなっていく弾幕です。
この弾幕はこれまでと少し実装方法が異なります。
これまでの弾幕は、弾は直進していましたが、この弾幕の弾の軌跡は曲線を描きます。
これまで紹介した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ゲーム制作入門 [改訂第二版]
- 作者: 尾関俊介
- 出版社/メーカー: Wiz Publishing
- 発売日: 2015/03/20
- メディア: Kindle版
- この商品を含むブログ (1件) を見る
kindle版の電子書籍ですが、お値段500円、紙の本の約1/5ほどの値段で購入できます。個人の方が書かれた書籍で、値段も安いですが、サンプルを作りながら覚えていくタイプの入門書としてはとても良い本だと思います。
内容は、ミニゲーム(マウスクリックで敵を消すゲーム)、ボス戦オンリー(?)のシューティングゲーム、動く床や当たると死ぬ罠が配置されたステージをクリアしていくアクションゲーム、の3つのゲームを書籍の手順に従って実際にUnityに触れながら作り上げていくような構成になっています。
スマホ対応についての記述は無いので、すべての操作をタップでどうにかするようなゲームを作るとなると、別途Webで調べるなり別の書籍を漁るなど必要になってくると思いますが、とりあえず何かゲームを作りながらあれこれ覚えていくというスタンスでしたらとてもわかり易い本だと思います。
さらに、ツールの操作も図付きで操作を1つ1つ説明しているため、途中で躓くことも無いと思います。
書籍のレビューに、「作者が作った独自の基底クラスを継承させて、その独自クラスのメソッドやプロパティを多用している」との指摘もありますが、そのクラス自体はUnityAPIの薄いラッパークラスとなっており、オブジェクトの生ハンドルの隠蔽や、使用しない引数を省略するためのラッパーメソッドが定義されているくらいです。ソースコードも提供されているため、Unity本来のAPIではどのように記述するかという点についても、C#でのメソッド呼び出し、基本的な演算子、条件分岐くらいのコードが読めれば容易に理解できると思います。
なお、画像リソースや完成済のUnityプロジェクトもWebからダウンロードできるようになっているため、絵が描けないのでゲーム用の画像用意するところでまず躓いてしまうという心配もなく、また、とりあえず出来上がっているコードをあれこれ触ってみたいという方にもおすすめです。
ちなみに、書籍の作者さまが書かれたチュートリアル(書籍の第一章に相当する内容です)が、Qiitaにも公開されています。
また、いろいろな本を読み漁ってみたいという方は、kindle unlimited に加入するのも割とお勧めです。
kindle unlimited は、月額980円(2017年7月現在)のみで、kindle unlimitedの対象書籍を無制限に読むことができるサービスです。
対象書籍のラインナップも、上で紹介した書籍も含め、個人で出版された書籍や市販の技術書等、結構豊富に取り揃えられています。
月額980円ですが初月は無料なので、お試しでひと月だけ加入して、ずっと持っておきたい本だけ購入するみたいな使い方もありかと思います。
kindle white paper 等の kindle端末を持っていなくても、android や iOS端末をお持ちであれば、アプリ版のkindleから利用することもできます。
もし興味ございましたら、kindle unlimited の対象書籍を幾つかリストアップしておきますので、ご参考までにどうぞってことで。
- Amazon.co.jp: unity - 読み放題対象タイトル: Kindleストア
- Amazon.co.jp: javascript - 読み放題対象タイトル: Kindleストア
- Amazon.co.jp: ruby - 読み放題対象タイトル: Kindleストア
kindle unlimited の申込みは下記リンクから行えます
もしご興味持っていただけたら、検討してみてはいかがでしょうか?
最後までお読みいただいてありがとうございました。
【これならできる】unityの2Dゲームで背景画像を多重スクロールさせる
最近 unity始めてみました。
今は書籍のサンプルゲームにアレンジを加えて遊んでいる段階なので、ちゃんとしたものができるまでにはもうしばらく掛かりそうです。
今回の本題とはそれますが、まずは僕が使っている教科書の紹介です。
Unity4.6/5.0でつくる 2Dゲーム制作入門 [改訂第二版]
- 作者: 尾関俊介
- 出版社/メーカー: Wiz Publishing
- 発売日: 2015/03/20
- メディア: Kindle版
- この商品を含むブログ (1件) を見る
kindle版の電子書籍ですが、お値段500円、紙の本の約1/5ほどの値段で購入できます。
中身のほうは、ミニゲーム(マウスクリックで敵を消すゲーム)、ボス戦オンリー(?)のシューティングゲーム、動く床や当たると死ぬ罠が配置されたステージをクリアしていくアクションゲーム、の3つのゲームを書籍の手順に従って実際にUnityに触れながら作り上げていくような構成になっています。
スマホ対応についての記述は無いので、全操作タップでどうにかするようなゲームを作るとなると、別途Webで調べるなり別の書籍を漁るなど必要になってくると思いますが、とりあえず何かゲームを作りながらあれこれ覚えていくというスタンスでしたらとてもわかり易い本だと思います。
さらに、ツールの操作も図付きで操作を1つ1つ説明しているため、途中で躓くことも無いと思います。
なお、画像リソースや完成済のUnityプロジェクトもWebからダウンロードできるようになっているため、絵が描けないのでゲーム用の画像用意するところでまず躓いてしまうという心配もなく、また、とりあえず出来上がっているコードをあれこれ触ってみたいという方にもおすすめです。
ちなみに、書籍の作者さまが書かれたチュートリアル(書籍の第一章に相当する内容です)が、Qiitaにも公開されています。
さて本題です。
上で紹介した書籍を見ながら作ったシューティングゲーム、残念ながら背景は固定表示だったので、これをスクロール表示するように直してみました。
背景のスクロールに関しては、ググると幾つかの記事が出てくるのですが、おそらく知ってて当然的な部分が端折られていて、そのままだとうまく動かなかったりしたので、僕がやった手順を記載しておきます。
今回は、雲の上、というシチュエーションを想定して、背景(空)、雲(遠方)、雲(至近距離)の3つの背景を重ねて表示してみます。
まずは、背景用に下記のような画像(3種類)を用意します。
空の画像、雲(手前)の画像、雲(奥側)の画像の3枚をPNGファイルとして作成します。
雲の画像は、雲以外の部分を透過指定したPNGとして保存してください。
UnityプロジェクトのAssetsの下に、「Textures」などのフォルダを作り、作成した3枚の画像を追加します。
なお、以下の説明では、画像の名前を、それぞれ「background」「cloud_back」「cloud_front」としています。
追加したら、Inspectorビューで画像の設定を下図の赤枠内のように変更してください。
プルダウンメニューから「Game Object > 3D Object > Quad」を選び、シーンに Quadオブジェクトを追加します。
追加したQuadオブジェクトの名前を、Backとし、「background」の画像を割り当てます。
同様に、「Middle」「Front」という名前でQuadオブジェクトを作成し、それぞれ「cloud_back」「cloud_front」の画像を割り当てます。
次に、「Background」という名前でC#スクリプトを作成し、以下のようなコードを記載します。
using UnityEngine; public class Background : MonoBehaviour { public float speed = 0.1f; void Update() { float x = Mathf.Repeat(Time.time * speed, 1); Vector2 offset = new Vector2(x, 0); GetComponent().sharedMaterial.SetTextureOffset("_MainTex", offset); } }
「Front」「Middle」「Back」Quadオブジェクトすべてに、上で作成した「Background」C#スクリプトをアタッチします。
「Front」「Middle」「Back」のInspectorを開き、以下のように設定します。
position:z を それぞれ、0.1, 0.2, 0.3 に設定 scale:x, scale:y を画面内にいい感じに収まるように調整 シェーダの設定を、Unlit/Transparent に設定 Backgroundスクリプト内のspeedの値を 0.1~0.5程度の値に設定 ※ Front側を大きな数字に、Back側を小さな数字にします
設定後のInspectorは下記のような感じになると思います。
この状態で実行すると、手前の雲は速く、後ろ側の雲はゆっくりとスクロールするのが確認できると思います、
Unity記事の続編はこちら
今回は以上となります。
お読みいただきありがとうございました。
姫繰三六五の全絵柄368種類を、公式さんがタペストリーとして販売するらしい
以前、368名の萌系絵師様の素敵イラストが毎日日替わりで拝める萌系日めくりカレンダー「姫繰三六五 2017 Edition」をこちらの記事でご紹介いたしました。 noranuk0.hatenablog.com
僕も日めくりカレンダー+イラスト集(上下巻)は即買いして、来年までは大事に大事に保管している状態です。
さて、今回、その全ての柄が、タペストリ(全368種類)として公式さんから受注販売されるようです。
現在は、1本3240円で以下のサイトから予約注文ができるようになっています。
ちょっと欲しかったけどまさか流石にやらないだろうなーって思っていたので、 ツイッターのTLで見かけたときは、「うぁwまじかよw」って思っちゃいました。
1本1本の値段は、B2タペにしては良心的の1本3240円です。
なお、368枚のタペストリーすべて買った場合どうなるかというと、
価格は、3240円×368枚=119万2320円。
必要スペースは、B2サイズが幅が約52センチ、高さが約73センチなので
縦に2つ並べて飾ると、必要幅9568cm(約96メートル)です。
てことで、つまりは25メートル四方の部屋をほぼ一周埋めつくせる感じになります。
うーん、ちょっとやってみたい。
けどそんなお金も空間も持ち合わせていないので僕には無理です。
表紙のイラストとか結構好みだし、他にも2,3本欲しい柄はあるのですが、いかんせん普段いる部屋は既に飾るスペースがまったく無く、残念ながらこちらは見送ります。
こういうのは、デジタルデータでいただけると、スマホやPCの壁紙にできたりと取扱がとても便利なのですが、やっぱり不正コピーの問題とかで難しいのかな。
ランナウト姫繰三六五 2017 Edition 【カレンダー】 ※入荷次第順次ご発送となります。
- ジャンル: おもちゃ・ホビー・ゲーム > 趣味・コレクション > その他
- ショップ: ソフマップ楽天市場店
- 価格: 3,240円
【値下万歳】auひかりユーザさん必見!ホーム型の料金体系が結構前に改定されていた!【拡散歓迎】
えっ!?私の通信費高すぎ!?
今回は、auひかりホーム(一戸建てタイプ)を契約した方のうち2015年以前に契約をした方は、2015年初頭に新設された新しいプランに料金プランを変更することで、月々の通信費が「確実に」安くなりますよ、というお話です。
注意
この記事の情報は、2016年10月時点での情報をもとに書かれています。
今後、プラン改定などがあった場合、ここに書かれた内容とは異なるプランシステムに変更される可能性があります。ご留意の上お読みいただければと思います。
auひかり料金プランのおさらい
auひかりの料金プランは、大きく分けて、集合住宅向けのマンションプランと、一戸建て住宅向けのホームプランの2種類があります。
ホームプランは、通常プランと、いわゆる2年縛りで更新月以外の月に解約すると、約1万円の違約金が発生する「ギガ得プラン」というものがあります。
実は、2015年の初頭に新しいプラン「ずっとギガ得プラン」というプランが新設されていました。
このプランは、3年縛りで、更新月以外の月に解約すると約15000円の違約金が発生する代わりに、ギガ得プランよりも更に料金が安くなるプランです。
今回の記事は、「auひかり ホームプラン」をすでに契約されている方向けに書いています。マンションにお住まいの方、集合住宅タイプの方は残念ながら対象外となります。
さて、すでに「通常プラン」「ギガ得プラン」で契約されている方が、契約プランを変更する場合、以下の点が気になるんじゃないかと思います。
- どれくらい安くなるのか?
- 手続きはめんどくさくないのか?
- ギガ得プランから変更した場合、更新月以外の月にプラン変更しても違約金は発生しないのか?
- auスマートバリュー(スマホもauで契約されている場合)は適用されたまま移行できるのか?
まずは、どれくらい安くなるのか、という内容です。
まずは、以下の表を御覧ください。
こちらが、それぞれの料金プランの月額使用料になります。
プラン | 1年目 | 2年目 | 3年目以降 |
---|---|---|---|
通常プラン | 6,300 | 6,300 | 6,300 |
ギガ得プラン | 5,200 | 5,200 | 5,200 |
ずっとギガ得プラン | 5,100 | 5,000 | 4,900 |
契約内容にもよりますが、IP電話オプション、無線LAN機能使用料などのオプション料金は別途加算されます。また、申込時にキャンペーンなどを利用した場合、この価格とは異なっている場合もあります。
2015年以前にauひかりに加入された方は、おそらく殆どの方がギガ得プランというプランを選択されていると思います。
これを、ずっとギガ得プランに変更した場合、上の表を参照すると、最大(3年目以降)月額300円が値引きされることになります。
値引き額は正直たかが知れていますが、手続きもWebからプランの設定を変更するだけなので、10分程度で完了します。
ですので、やっておいても損はないんじゃないかと個人的には思います。
ただし、プラン変更すると、これまでは2年毎に解約無料月が設定されていたものが、3年毎になったり、違約金の額も1.5倍になるので、その辺をどう割り切るかという考慮の余地は出てくるのかと思います。
手続方法は、契約しているプロバイダによって異なりますが、基本的にはWebからプラン変更の申し込みをするだけでいいようです。
au one ネットをお使いの方であれば、以下のページから手続きができるようです。
他のプロバイダーを利用中の場合、例えば @nifty であれば、こちらから手続きできます。
その他のプロバイダーの場合も、例えば sonetの場合なら「sonet auひかり」などのワードで検索すれば、おそらくすぐに手続きのページが見つかると思います。
さらに、ギガ得プランから変更する場合、更新月以外であっても違約金などは発生しないため、いつでもすぐにプラン変更が可能です。
また、auのスマートフォンをお持ちで、スマートバリューを利用している場合には、スマートバリューの割引の恩恵も継続して受けることができます。
なお、今回書いた情報は、
でも確認いただくことができます。
さて、この「ずっとギガ得プラン」ですが、開始から1年半以上は経過していますが、僕がこのプランの存在を知ったのは実は割と最近です。
プラン新設にすぐに気づいていれば少しだけ節約できたのになー、と思ってこの記事を書きました。
願わくば、一人でも多くの「ちょっと高めの料金を払い続けているauひかりユーザさん」に、この記事が目に止まってくれること期待です。