のらぬこの日常を描く

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

【これならできる】unityの2Dゲームで背景画像を多重スクロールさせる

最近 unity始めてみました。

今は書籍のサンプルゲームにアレンジを加えて遊んでいる段階なので、ちゃんとしたものができるまでにはもうしばらく掛かりそうです。

 

今回の本題とはそれますが、まずは僕が使っている教科書の紹介です。

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

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

 

 kindle版の電子書籍ですが、お値段500円、紙の本の約1/5ほどの値段で購入できます。

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

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

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

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

 

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

qiita.com

 

 

さて本題です。

上で紹介した書籍を見ながら作ったシューティングゲーム、残念ながら背景は固定表示だったので、これをスクロール表示するように直してみました。

 

背景のスクロールに関しては、ググると幾つかの記事が出てくるのですが、おそらく知ってて当然的な部分が端折られていて、そのままだとうまく動かなかったりしたので、僕がやった手順を記載しておきます。

 

今回は、雲の上、というシチュエーションを想定して、背景(空)、雲(遠方)、雲(至近距離)の3つの背景を重ねて表示してみます。

 

まずは、背景用に下記のような画像(3種類)を用意します。

空の画像、雲(手前)の画像、雲(奥側)の画像の3枚をPNGファイルとして作成します。

雲の画像は、雲以外の部分を透過指定したPNGとして保存してください。 

f:id:noranuk0:20161024215520p:plain

 

 

 UnityプロジェクトのAssetsの下に、「Textures」などのフォルダを作り、作成した3枚の画像を追加します。

なお、以下の説明では、画像の名前を、それぞれ「background」「cloud_back」「cloud_front」としています。

 

追加したら、Inspectorビューで画像の設定を下図の赤枠内のように変更してください。

f:id:noranuk0:20161024221303p:plain

f:id:noranuk0:20161024221310p:plain

f:id:noranuk0:20161024221317p:plain

 

プルダウンメニューから「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は下記のような感じになると思います。

f:id:noranuk0:20161024225007p:plain

 

この状態で実行すると、手前の雲は速く、後ろ側の雲はゆっくりとスクロールするのが確認できると思います、

 

 

Unity記事の続編はこちら

noranuk0.hatenablog.com

 

 

今回は以上となります。

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

姫繰三六五の全絵柄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の壁紙にできたりと取扱がとても便利なのですが、やっぱり不正コピーの問題とかで難しいのかな。

【値下万歳】auひかりユーザさん必見!ホーム型の料金体系が結構前に改定されていた!【拡散歓迎】

えっ!?私の通信費高すぎ!?

 

今回は、auひかりホーム(一戸建てタイプ)を契約した方のうち2015年以前に契約をした方は、2015年初頭に新設された新しいプランに料金プランを変更することで、月々の通信費が「確実に」安くなりますよ、というお話です。

 

注意

この記事の情報は、2016年10月時点での情報をもとに書かれています。

今後、プラン改定などがあった場合、ここに書かれた内容とは異なるプランシステムに変更される可能性があります。ご留意の上お読みいただければと思います。

 

auひかり料金プランのおさらい

auひかりの料金プランは、大きく分けて、集合住宅向けのマンションプランと、一戸建て住宅向けのホームプランの2種類があります。

ホームプランは、通常プランと、いわゆる2年縛りで更新月以外の月に解約すると、約1万円の違約金が発生する「ギガ得プラン」というものがあります。

実は、2015年の初頭に新しいプラン「ずっとギガ得プラン」というプランが新設されていました。

このプランは、3年縛りで、更新月以外の月に解約すると約15000円の違約金が発生する代わりに、ギガ得プランよりも更に料金が安くなるプランです。

 

今回の記事は、「auひかり ホームプラン」をすでに契約されている方向けに書いています。マンションにお住まいの方、集合住宅タイプの方は残念ながら対象外となります。

 

さて、すでに「通常プラン」「ギガ得プラン」で契約されている方が、契約プランを変更する場合、以下の点が気になるんじゃないかと思います。

  1. どれくらい安くなるのか?
  2. 手続きはめんどくさくないのか?
  3. ギガ得プランから変更した場合、更新月以外の月にプラン変更しても違約金は発生しないのか?
  4. 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 ネットをお使いの方であれば、以下のページから手続きができるようです。

www.au.kddi.com

 

他のプロバイダーを利用中の場合、例えば @nifty であれば、こちらから手続きできます。

support.nifty.com

 

その他のプロバイダーの場合も、例えば sonetの場合なら「sonet auひかり」などのワードで検索すれば、おそらくすぐに手続きのページが見つかると思います。

 

さらに、ギガ得プランから変更する場合、更新月以外であっても違約金などは発生しないため、いつでもすぐにプラン変更が可能です。

 

また、auスマートフォンをお持ちで、スマートバリューを利用している場合には、スマートバリューの割引の恩恵も継続して受けることができます。

 

なお、今回書いた情報は、

www.au.kddi.com

 

でも確認いただくことができます。

 

 

さて、この「ずっとギガ得プラン」ですが、開始から1年半以上は経過していますが、僕がこのプランの存在を知ったのは実は割と最近です。

プラン新設にすぐに気づいていれば少しだけ節約できたのになー、と思ってこの記事を書きました。

 

願わくば、一人でも多くの「ちょっと高めの料金を払い続けているauひかりユーザさん」に、この記事が目に止まってくれること期待です。

 

 

 

後半戦に突入したペルソナ5、ちょっと飽きてきたのでその原因を考えてみる

発売日に予約までして買ったペルソナ5、もうかなりの方がクリアしているようですね。

 

僕はようやく8月終わり頃まで進めた所です。

ちょうど、メジエドをなんとかしたあたりです。

 

仲間も増えて、特に双葉ちゃんには是非ともこれから頑張ってほしいところなのですが、

 

ここに来て問題発生です!
 

始めた頃はすごくおもしろいと感じていたのですが、

 

Webでペルソナ5について検索したりすると、なんか情報操作されてるんじゃないかって思うほど大絶賛されているんですが

 

正直かなり飽きてきました!

 

てことで、今回は、40時間ほどプレイした結果を踏まえて、ペルソナ5の不満点というお題で、まだプレイ途中ですが、ネガティブ系の感想、レビューを書いていきたいと思います。

 

日常パート

UXがちょっと残念。

今時のギャルゲーのUXをを見習ってほしい。

 

大きな不満点は2つです。

 

一つ目。

開放したコープ(P4で言うコミュ)のコープランク一覧画面がほしかった(ギャルゲで言う好感度一覧画面)。

ギャルゲと違い、1キャラ集中で好感度(コープランク)上げるというよりは、主要キャラ重点的に全キャラしっかり上げて行く感じのゲームなので誰がどの程度のコープランクなのか一覧で見えるとすごくありがたいのですが、残念ながらありません。

 

二つ目。

移動パートでは、メニューから移動先を簡単に選べるようになっているので、どうせなら、どの場所に誰がいるのかもマップやメニュー要素上に表示してほしかった。

 

診療所の先生とかルブランのご主人等常に固定ポジションな方はいいとしても、竜司とか杏とか、ある程度の行動範囲を持ってる人は、探し回らないといけない、かつ、常にいるとも限らないので探すのがめんどくさいです。

ただ、それをやってしまうと、なんでお前(主人公)は全キャラの行動を最初から把握してるんじゃっていうツッコミも出て来るかと思います。

それなら、メッセージングアプリで相手にメッセージ(例えば、「今どこにいる?」「今日は空いているか?」みたいな)を送れるようにする、とかやりようはあったんじゃないかと感じました。

 

イベントが進む場所には一応カードアイコンが出ているのですが・・・

f:id:noranuk0:20161030104500j:plain

 

下の図だと、渋谷のどこ行けばいいのかはわかりません。また、誰のイベントが進むのかもわかりません。<br/>

選んだ先もマップが広がっているので、ぐるっとあるき回って探さなきゃいけないのもマイナスポイント。

f:id:noranuk0:20161030104538j:plain

 

知識、器用さ、優しさ、魅力、度胸、パラメータって必要?

ステ上げにはそれなりの時間を消費する割に、コープランク上げの足止め要素としてしか機能してないのがなんとも残念。

更に、 このパラメータ上げるには、花屋でバイトとか、ファミレスで勉強とか時間進めるだけのイベント消化しなきゃあかん。

 

迷宮(パレス)パート

シナリオで行く、固定構造の迷宮がパレスです。

 

やたら長くてワンパターンなギミック。

 

基本は、ジャンプ、よじ登る、飛び降りる、通気孔とかの穴に潜って進んで行きます。

 

スイッチを押すと仕掛けが動いたり罠が解除されたり等のギミックももちろんあります。

ただ、ジャンプなどができる場所、スイッチなどのギミックも、スキャン(周りの状況を調査)すると表示されるので、とにかくそれを見つけて○ボタンだけで、基本は一本道です。

 

各パレス毎に、例えば美術館のパレスだったら絵の中に入って進むとか、銀行のパレスだったら迷宮の構造が金庫のシリンダー錠みたいになってたりとか、見せ場もあり、そこは楽しい部分もあるのですが、いかんせん、それ以外が長いんです。

 

ギミックについては、FF13のラストダンジョンや、女神転生3ノクマニ、DDSアバタールチューナーみたいにもうちょっと凝ったものが色々欲しかった所です。

 

あと、個人的には、一つ一つのパレスを短くして、数を増やして欲しかった。

 

迷宮(メメントス)パート

こちらは、P4のランダムダンジョン的な構造の、サイドシナリオで潜ることになるダンジョンです。

途中のボスシャドー倒しつつ、階段ひたすら即降りなコンテンツ。

 今回、攻略済みのパレスには潜れないので、レベリングと過去のパレスでしか遭遇しないシャドーも捕まえられるようにっていうのもあるんだろうけど、ちょっと単調で、しかもこれも意外と深いのですぐに飽きてきます。

モルガナの秘密がメメントスにあるらしいので、最終的には最後までひたすら降りないと行けないのかな。。

 

階層数はもうちょい減らしても良かったんじゃ。。。とまだ2?3?層ですが思い始めております。

 

バトル

「オニ」みたいに、全体物理持ち、物理耐性、属性弱点なしとかいう厄介ものも稀にいますが、基本的には「不意打ち→先制攻撃→弱点狙い→適宜キャラチェンジしつつ1more狙い→全敵ダウン→フルぼっこ」という流れです。

最初のうちは楽しいのですが、こればっかりだと、結局めんどくさいだけになってしまいます。

 

このシステムのもとになっていると思っているのが、女神転生3の頃に出てきた「プレスターンバトル」です。

ブレスターンバトルとは、ターン毎にキャラ数と同じ数のブレス(行動ポイント)が与えられて、通常ヒットや補助回復魔法で1つ、何もしない(行動スキップ)やクリティカルヒット、弱点ヒットで1/2個、逆に攻撃がブロックされた場合は2つ、攻撃が反射されると残り行動ポイント全消失という感じで、ブレスを消費して行動を起こしていくシステムです*1

弱点を突けばかなり有利にはなる(逆に突かれるとかなり不利になる)けど、味方→敵→味方→…のターン制の仕組みは維持されていたため、メリハリのある戦闘が楽しめました。

 

弱点を突くと戦闘がかなり有利になる、だったのを、とにかく属性攻撃で弱点狙いの一辺倒にしてしまったのが、面倒くさいだけになってしまった原因かな。

 

ここまではザコ戦の話です。

 

反して、ボス戦は毎回とても楽しいです。

相手も特別な演出で固有の攻撃をしてきますし、こちらもそれに対してボス戦専用に選べるようになる行動も使いつつ応戦します。

ここが見せ場でもあるので、演出やアクションにこだわるのも当然といえば当然なのですが、ここはよくできていると思います。

 

最後に

あれこれネガティブ要素を並べ立ててしまいましたが、シナリオ、取り上げているテーマ、随所に挿入されるムービーや音楽などの演出はとても素晴らしいと思っています。

キャラクターごとのシナリオも、どれも面白いです。

 

だからこそ、色んな所でテンポが悪かったり、単調になってしまうのがもったいないと感じます。

 

多分、最後までクリアはすると思います。

 

願わくば、パッチで対応できそうな箇所は今後改善されたりするといいなーと思ったり、

また、Vita版、真ENDが無いってあたりから推察すると、なんとなく完全版が出てきそうな気はしているんですが、完全版ではそこら辺にも手が加えられていると良いなーと思ってます

 

 

 ちなみに、購入日に書いたレビューはこちらです。導入部分はめちゃめちゃ好印象だったのですが・・・

noranuk0.hatenablog.com

 

 

*1:厳密にはもうちょっと細かいルールがありました

のらぬこ旅行記2016 〜 長野編

先日(10月9日〜)、三連休を利用して、1泊2日で長野に行ってきました。

直前で台風18号が迫ってきたりと、天気がちょっと心配でしたが、初日は曇り2日目はいい感じの秋晴れと、天気にも割りと恵まれ、各所で色々堪能することが出来ました。

長野には過去何度か行ったことあるんですが、今回は(たぶん)わりと定番っぽい場所をめぐる感じのルートでした。

初日ノ巻

朝6時半、家を出たときは、まだ小雨が降っておりました。
長野の予報は雨のち曇となっていたので、到着までにはやんでくれるといいなーと思いつつ出発。
が、8時回った頃には割りとどしゃ降りに。
あー、これはやばいんじゃないかって内心それなりに心配していたんですが、最初の目的地につく頃にはとりあえず雨も上がり、微妙に青空も見えてきて、とりあえず一安心でした。

信州駒ヶ根千畳敷カール

中央アルプス駒ヶ岳ロープウェイにも乗ってきました!
雲海はきれいだったけど、さすが連休二日目という感じで結構混んでました。 f:id:noranuk0:20161012231810j:plain

荒野って感じです。ゲームとかだとそこそこ強い敵がうろついてる感じかなー f:id:noranuk0:20161012232046j:plain

ロープウェイの待ち時間に食べたわさびコロッケ。
見かけほどわさび~~~!って感じはなかったかも。 f:id:noranuk0:20161012233457j:plain

二日目ノ巻

大王わさび農場

まず行ったのが大王わさび農場です。 今回で多分5回くらいです。

わさびソフトクリーム、変わらず美味。写真取るの忘れたくらいには美味でした(汗

農園内で見かけた水車。そういえばどんな用途なのかしら f:id:noranuk0:20161012232332j:plain

上高地

今回のメインです。
大正橋〜河童橋まで歩いてきました。
紅葉はまだまだな感じでしたが、緑と水がとにかく綺麗でした。

このグラでネトゲとかやってみたい!

f:id:noranuk0:20161012232908j:plain

f:id:noranuk0:20161012232943j:plain

f:id:noranuk0:20161012233029j:plain

f:id:noranuk0:20161012233049j:plain

f:id:noranuk0:20161012233109j:plain

f:id:noranuk0:20161012233127j:plain

時間の都合で明神池までは行けなかったのですが、機会があれば行ってみたいです。

乗鞍山頂畳平

最後に行ったのは、乗鞍山頂畳平です。外気温5℃前後。 ただ、風はそんなになかったので、あんまり寒いとは感じませんでした。

畳平のお花畑をぐるーーっと一周歩いてみたかった!
ぐるーっと一周歩いてきました、、、が、10月初旬、外気温10度未満、ということで、残念ながら花は咲いていませんでしt。

f:id:noranuk0:20161012235035j:plain

f:id:noranuk0:20161012235047j:plain

そして帰路に

中央道渋滞30km(ぇ

21時過ぎには帰宅予定だったのですが、結局家ついたのは深夜1時過ぎでした・・・

最後はちょっとかなり疲れたけど二日間まるまる楽しめました。

今度は別の季節に行ってみようと思いました(冬以外)。

Re:ゼロから始める手作り弁当 ~ 挫折せずに続ける秘訣

所謂「弁当男子」的なことを、ここ5年ほど続けています。

f:id:noranuk0:20161008145337j:plain

出社時間が早くなったとか、最近寒くて起きられないからとか、いろいろな理由で長期挫折することも多々あります。

ここ暫くは会社近くにある400円のお弁当屋さんで買っていたのですが、今週からまたお弁当持参生活始めることにしました*1

ちなみに、これが400円のお弁当です。
f:id:noranuk0:20161011130106j:plain

大きな一口チキンカツが3枚。野菜はインゲンが1本。
お弁当の種類はいくつかあるんですが、だいたいこんな感じです。
安くてお腹も膨れて美味しんだけど、毎日これはちょっと厳しい。
最近太ってきたし・・・・

というわけで今回は、お弁当持参となると毎朝毎朝眠い中ちょっと頑張らないといけないのに、どうやってモチベーションを保っていくか?「自作弁当持参生活を長続きさせるコツ」について書きたいと思います。

そもそもなんでお弁当持参を始めたの?

一番最初にお弁当持ってきた理由は忘れました、が。

とりあえず、今のところの理由は幾つかあるんですけど

  • 外食って、ランチメニューでも意外と高い

    • つまりは節約のため
  • 市販弁当って揚げ物ばっか、炭水化物ばっかりだし

    • ようするに健康のため
  • 昼休み、外に出歩くとか、みんなでお昼ランチとかめんどくさい

    • 結局はコミュ障のため

ざっくりいうとこんな感じ。てか、これが全てです。

めんどくさくない?

もちろんめんどくさいです。

めんどくさいけど、それなりに毎日食べても嫌にならない程度のものを、なるべく時間を掛けずに作るようにしています。

まずは1週間でも続けてみる

僕の場合、朝はあんまり頑張らない、代わりに週末ちょっと頑張る感じです。
朝に色々やること増やすと、10分寝坊するとその日はDEAD ENDなフラグたったりするので。。

ご飯の準備

昔は、ご飯は週末に炊いて冷凍保存してました。
炊き上がったご飯、冷凍できます。解凍も電子レンジでサクッとできます。
ご飯冷凍用のタッパーとか100円ショップに売ってます。
炊けたご飯を1食分づつ詰めて適当に冷ましてから冷凍庫にポイ。
朝起きたら本日のお弁当分をレンジでチンして、その間におかずを詰めて、ご飯が解凍できたらご飯も詰めて完了です。

ただ最近は、電子レンジ専用の炊飯鍋を購入し、毎朝レンジで炊いています。
レンジ対応の炊飯鍋も、プラスチックのものから土鍋っぽいものまで色々出てるので、アマゾンなり楽天なり近所のイトーヨーカドーなりで探してみてください。

なお、電子レンジでご飯炊く場合、気をつけなければならないことが一つだけあります。

あきたこまち」は非推奨です。
名指しで銘柄指定しましたが、特に嫌いとか恨みとかは無いです。

炊飯ジャーなどで普通に炊く分には美味しいと思います。
でも、電子レンジで炊く場合、マニュアル通りに炊いても、芯が残る率が異常に高いです。
今までいろいろ試した中では、ちょっとお高い部類のお米になりますが、銘柄的にはミルキークイーンがお薦めです。

お米研ぐのめんどい!とか、洗剤でお米洗うとか健康的にほんとに大丈夫?とか感じた方は、無洗米おすすめです。
ずっと昔、当時の友人に、無洗米使ってる言うたら、何故かものっそいディスられたんですが。
水の冷たい季節、朝起きて、部屋もやっぱり寒いのに、更に冷たい水でお米頑張って研ぐの結構しんどいです。
普通のお米と比べて風味が落ちるとか言われてますし、無洗米ゴリ押しする気は無いですが、特に冬場はちょっとだけ楽ですよってことで。

おかずの準備

一応気にはしていること

少なくともコンビニオリジンよりは栄養バランスに配慮してみる
せっかく自分で作るんだから、好きなもので固めた結果毎日肉ONLY。でも、まあいいとは思いますが。
せっかくなので、素人目線でも栄養バランス的なことも多少は気にしてみると良いかと。

おかずも週末にまとめて調理

1週間同じおかずでもまあいいじゃない
独り身で料理はじめると、割と悩ましいのが食材をきれいに使い切るのがなかなか難しい、てことでないでしょうか?

野菜なども下ごしらえだけして冷凍とかすればそれなりに持つ物もあります。
が、僕はそういうのもめんどくさいので、日曜日に5日分の量のおかずをまとめて作っています。

結果、5日間、お昼のおかず、少なくとも主菜は毎日一緒になるわけですが、まあそこはあんまりこだわってないんで気にしてません。

余裕があれば、付け合せくらいはちょっと変えてみてもいいかもと思います。

もう暫く頑張って続けてみる

お弁当箱は食べたらすぐ洗う

夜7時、残り少ない1日をゲームして動画見てダラダラして過ごしたいのに、微妙に匂いの篭ってきたお弁当箱を洗わなければならない、て考えると、朝、お弁当詰め込む時点で既に微鬱です。
これ、割りとお弁当持参のモチベーション下げる原因になります。

食べ終わったお弁当箱は、会社の給湯室ですぐに洗ってしまいましょう。
給湯室がないとか、給湯室までやたら遠いい会社とか超最悪です。
はっきりいって即転職を考えるレベルです

うまくできたらツイッターに写真上げて自己顕示欲を満たす

うまく作れた、きれいに詰められたときなんかはたまにツイッターに投稿します。
僕のクラスタが、アニゲム系、ソフトウェアエンジニア、若干の身内。な感じなのでほぼ反応ないですが。
所詮はつぶやき、自己満足なので、RT0、いいね0キニシナイ、ですw

たまに変わり種を作ってみる。当然これもツイッターに写真あげて自己顕示y(ry

ちょっと変わり種作ってみます。こういうときも、とりあえずツイッターに写真投稿します。

余談ですが、上のツイートに上がってる写真(マグロ漬け丼)を作ったときに初めて、

酢飯って、ご飯とお酢の他に砂糖も入れる」って知りました!

原価厨になる

あえて高級食材を選ぶとかしない限り、原価200円〜300円の間に収まると思います。
しかもこれをお弁当として買うといくらくらいだろ、外で食べるといくらくらいだろって考えてみます。
浮いたお金でゲーム買うとか、コミケ予算枠増やすとか。夢が広がります。

今回は以上となります。

完全に自己満足記事ですが、お読みいただいてありがとうございました

*1:行きつけのお弁当屋さんも、コスパは割といいし、値段の割に肉多めで味も結構美味しいのですが、毎日食べるには味付けとかメニュー的にちょっと不健康過ぎなして気がしてきたので

【androidアプリ開発】 MediaPlayer コンポーネントの使い勝手を改善する

android の MediaPlayer コンポーネントって割りと糞だと思う的な

androidで音楽やサウンドを再生する際、google謹製のMediaPlayerというコンポーネントを利用することができます。 このコンポーネントには、setDataSource(), play(), pause(), stop() などのメソッドが定義されています。

このコンポーネントはおそらく、androidアプリに音楽再生機能を比較的容易に実装できるようにするという目的で提供されていると思われますが、後述しますが少々罠いところがあります。

なお、MediaPlayerの使い方については、下記の記事などに比較的詳しく書かれています。

Androidで動く携帯Javaアプリ作成入門(30):Androidアプリでマルチメディアを扱うための基礎知識 (1/3) - @IT

この他にも、google で 「android MediaPlayer java」などのキーワードで検索すると、いくつか解説記事が出てきます。

以下、MediaPlayerの使い方はなんとなく知ってる、くらいの知識をお持ちという前提で話を進めます。

MediaPlayer ですが、以下の理由で、僕は正直相当問題のあるコンポーネントだと思っています。

  • MediaPlayer の内部状態がほぼ完全にブラックボックスです。状態取得メソッドなどは用意されていません。

    • その為、MediaPlayerの状態管理を、アプリ側でも二重管理しなければならないという事態が発生します。
  • prepareメソッド(曲の再生準備をするメソッドです。おそらく音楽データをメモリー上にバッファリングする等をしていると推測)の非同期版、prepareAsync というメソッドがありますが、この中で例外が発生すると、異常があったことがアプリ側に全く通知されません。

    • ローカル端末内の曲を扱う分にはおそらくあまり問題にならないのですが、インターネット上の曲をダイレクトに再生しようとした場合に問題になる可能性があります。
    • WebServerが404などのエラーレスポンスを返したとき、prepare() は IOException を投げるのですが、prepareAsync() は例外が発生したことを通知してくれません(onPrepared()やonError() などのリスナーも呼ばれません)。
    • この状態になると、アプリ側からはMediaPlayerの状態の追跡が不可能になります(処理完了待ちなのか、例外発生で処理が終了したのかがわからなくなるため)
  • インターネット上のファイルを再生しようとしたとき、サーバエラーに対してのエラーハンドリングがカスタマイズできません。

    • prepare()、もしくはprepareAsync() 呼び出し時に、ファイルを読みに行くのですが、エラー発生時、サーバが401、404、410、500等、何を返しても、一定時間(それなりに長い)開けて何度かリトライするという動作になっています。
  • android6.0でちょいましなインターフェースができたのですが、バックポートされていないので実質使えません。

ローカル端末の決まったファイルを再生するなどの用途であればエラーハンドリングもそんなに神経使うことも無いですし、そこそこ使いやすいと思います。

しかし、

ネットワークメディアプレーヤーを作ろう!

とか考え出すと

googleに対して、憎悪の念しか持てなくなってしまいます(少なくとも僕は)

文句垂れ流してどうにかなるもんでもないので、とりあえずなんとかしてみよう的な

少し気が収まってきたので(やっと)本題です。

これらの問題を(一部)解決する、MediaPlayerEx という、なんかちょっとWindowsっぽい名前のclassを書いてみたのでここで公開します。

やったこと

  • prepareAsync を再実装して、例外発生時には、OnPrepareAsyncFailedListener が呼び出されるようにしました。
  • 再生準備ができているかを表す状態をclassにもたせました。
  • エラーが発生していたらリセットする resetIfError というメソッドを追加しました
  • setDataSource で設定したファイル名(URL)を後から参照できるようにしました。

やれなかったこと

  • インターネット上のファイルを再生しようとしたとき、サーバエラーに対してのエラーハンドリングがカスタマイズできないのは相変わらず。

というわけで、以下、ソースになります。
プロジェクトの一部を切り取ってきただけなので、そのままビルドするとエラーになるかもしれません。
その際は、過不足部分を適当に補完して上げてください。

public class AudioPlayService extends Service {
    public enum PrepareStatus {
        NotPrepared, Preparing, Prepared, Error
    }
}

public class MediaPlayerEx extends MediaPlayer
        implements MediaPlayer.OnPreparedListener,
        MediaPlayer.OnErrorListener {


    public interface OnPrepareAsyncFailedListener {
        void onPrepareAsyncFailed(MediaPlayerEx mp);
    }

    private String lastDataSource;
    private String lastDataSourceOriginalPath;
    private AudioPlayService.PrepareStatus prepareStatus = AudioPlayService.PrepareStatus.NotPrepared;
    private OnPreparedListener preparedListener = null;
    private OnErrorListener errorListener = null;
    private OnPrepareAsyncFailedListener prepareAsyncFailedListener = null;

    public AudioPlayService.PrepareStatus getPrepareStatus() {
        return prepareStatus;
    }

    public MediaPlayerEx(int id) {
        super();
        super.setOnPreparedListener(this);
        super.setOnErrorListener(this);
        this.id = id;
    }

    public String getLastDataSource() {
        return lastDataSource;
    }

    public String getLastDataSourceOriginalPath() {
        return lastDataSourceOriginalPath;
    }

    public void setDataSource(long musicId) throws IOException {
        String path = musicIdToFilePath(musicId);
        if (StringUtils.isNotEmpty(path)) {
            String url = musicFilePathToUrl(path);
            try {
                lastDataSource = url;
                setDataSource(url);
                Log.d(MediaPlayerEx.class.getSimpleName(), "load start :" + url);
                this.lastDataSourceOriginalPath = path;
                this.lastMusicId = musicId;
                try {
                    this.lastDataSource = URLDecoder.decode(url, "UTF-8");
                } catch (IllegalArgumentException e) {
                    this.lastDataSource = url;
                }
            } catch (IOException e) {
                Log.e(MediaPlayerEx.class.getSimpleName(), "cannot set datasource:" + url);
                throw e;
            }
        }
    }

    public long getLastMusicId() {
        return lastMusicId;
    }

    @Override
    public void setDataSource(String source) throws IOException {
        try {
            Log.d(MediaPlayerEx.class.getSimpleName(), String.format("[setDataSource start] %d - %s", this.id, source));
            super.setDataSource(source);
            Log.d(MediaPlayerEx.class.getSimpleName(), String.format("[setDataSource end] %d",
                    this.id));
        } catch (IOException e) {
            Log.e(MediaPlayerEx.class.getSimpleName(),
                    String.format("[setDataSource failed] %d", this.id), e);
            throw e;
        }

    }

    public boolean resetIfError() {
        if (prepareStatus == AudioPlayService.PrepareStatus.Error ||
                (prepareStatus == AudioPlayService.PrepareStatus.Preparing &&
                        System.currentTimeMillis() - prepareStartTime > 15000)) {
            Log.e(MediaPlayerEx.class.getSimpleName(), "prepareError MediaPlayer reset");
            reset();
            prepareStatus = AudioPlayService.PrepareStatus.NotPrepared;
            return true;
        } else {
            return false;
        }
    }

    public void setOnPrepareAsyncFailedListener(OnPrepareAsyncFailedListener listener) {
        this.prepareAsyncFailedListener = listener;
    }

    @Override
    public void setOnPreparedListener(OnPreparedListener listener) {
        this.preparedListener = listener;
    }

    @Override
    public void setOnErrorListener(OnErrorListener listener) {
        this.errorListener = listener;
    }

    @Override
    public void onPrepared(MediaPlayer mp) {
        prepareStatus = AudioPlayService.PrepareStatus.Prepared;
        if (this.preparedListener != null) {
            preparedListener.onPrepared(mp);
        }
    }

    @Override
    public boolean onError(MediaPlayer mp, int what, int extra) {
        if (getPrepareStatus() == AudioPlayService.PrepareStatus.Prepared) {
            Log.e(MediaPlayerEx.class.getSimpleName(), String.format("onError(%d) what:%d extra",
                    this.id, what, extra));
            if (prepareStatus == AudioPlayService.PrepareStatus.Preparing) {
                prepareStatus = AudioPlayService.PrepareStatus.Error;
            }
            return (this.errorListener != null) &&
                    this.errorListener.onError(mp, what, extra);
        } else {
            Log.e(MediaPlayerEx.class.getSimpleName(), String.format("onError(%d) what:%d extra",
                    this.id, what, extra));
            return true;
        }
    }

    @Override
    public void reset() {
        try {
            Log.d(MediaPlayerEx.class.getSimpleName(), String.format("[reset start] %d", this.id));
            super.reset();
        } finally {
            Log.d(MediaPlayerEx.class.getSimpleName(), String.format("[reset end  ] %d", this.id));
        }
        prepareStatus = AudioPlayService.PrepareStatus.NotPrepared;
    }

    @Override
    public void stop() {
        super.stop();
        prepareStatus = AudioPlayService.PrepareStatus.NotPrepared;
    }

    @Override
    public void prepare() throws IOException {
        if (prepareStatus == AudioPlayService.PrepareStatus.NotPrepared) {
            prepareStatus = AudioPlayService.PrepareStatus.Preparing;
            prepareStartTime = System.currentTimeMillis();
            try {
                Log.d(MediaPlayerEx.class.getSimpleName(), String.format("[prepare start] %d", this.id));
                super.prepare();
            } catch (IOException e) {
                prepareStatus = AudioPlayService.PrepareStatus.Error;
                throw e;
            } finally {
                Log.d(MediaPlayerEx.class.getSimpleName(), String.format("[prepare end  ] %d", this.id));
                prepareStartTime = 0;
            }
            prepareStatus = AudioPlayService.PrepareStatus.Prepared;
        }
    }

    @Override
    public void prepareAsync() {
        new Thread(MediaPlayerEx.class.getSimpleName() + "#prepareAsync") {
            @Override
            public void run() {
                long startTimeMillis = System.currentTimeMillis();
                try {
                    prepare();
                } catch (IOException | IllegalStateException e) {
                    Log.e(MediaPlayerEx.class.getSimpleName(),
                            String.format("%d - prepareAsync failed(%s)", MediaPlayerEx.this.id, getLastDataSource()), e);
                    if (prepareAsyncFailedListener != null) {
                        prepareAsyncFailedListener.onPrepareAsyncFailed(MediaPlayerEx.this);
                    }
                }
                if (System.currentTimeMillis() - startTimeMillis > 10000) {
                    Log.w(MediaPlayerEx.class.getSimpleName(),
                            "MediaPlayerEx#prepareAsync() too slow." + String.valueOf(System.currentTimeMillis() - startTimeMillis));
                }
            }
        }.start();
    }
}

こんかいは以上となります。

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