neoacoさんの ゲーム作ったよ報告書

いつでも ねむい  げーむ ちょっとつくる

unity1week(あける)で作ったゲームに籠めた主観的優しさの解説

 あけましておめでとうございます。
 年末からずっとunity1weekのゲーム製作に取り掛かりっきりだったので、GooglePlayStoreでのリリースを終えて一区切りついたことにして、この記事を書いています。
(UnityAdsでbannerが表示されないという現実から目を背けてのお届けです)
(違う広告サービスにしないといけない予感)
(メンド `o´ クサイ)


1.こんなゲームを作りました

遊んでほしい! とにかく遊んでほしい!!!!

unityroom.com

play.google.com


2.こんなところに気を配りました(優しさの発露)

2-1.スマフォ ファースト バット ドントフォゲット ピーシー

 このゲームを考えたときに、スマートフォンで遊んでもらう方が面白いだろうなと思ったので、初めからスマートフォンを意識した画面サイズではじめました。
 いつものPC向けなら800*600とかの横長の長方形で作りますね。その方がPCで遊びやすいから。
 でも、大きすぎてもPCで遊び辛いので、縦を800pxにしました。
 この大きさなら、unityroomのウィンドウの中でプレイしてもちゃんと見える大きさです。
 これは個人的な感想なんですが、初心者は結構そのあたりが無頓着というか、やたらゲーム画面サイズが大きい人が多い気がします。
 そんなに画面が大きい必要性があるか? ブラウザゲームだぞ? 頼むからウィンドウ内に収まっててくれ(祈) 全画面前提みたいなでかいサイズをやめろ(怒)
 うまい人は手ごろなサイズに要素をうまく配置してて、経験の違いを見せつけられる感がある。

2-2.開封進捗の継続

このゲームを作るにあたって、「開封途中にドラッグが途切れても、続きからドラッグできる」という機能は絶対必要だと決めてました。
 ついでに開けたり閉めたりできたらいいな~~~と。
 なので、

Update(){
    if (Input.GetMouseButtonDown(0)) {
        mouseStart = Input.mousePosition;
    }
    if (Input.GetMouseButtonUp(0)) {
        progress = length;
    }
    // 上に開ける
    length = progress + (Input.mousePosition.y - mouseStart.y);
}

みたいな感じで、一旦話したところまでの長さを記録しておきました。

2-3.開封進捗のわかりやすさ

 イマジン...... イマジン...... `o´
 ゲームプレイの様子を想像してみる……
 開ききったかどうかがプレイヤーとしてはわからないんじゃないか?

 というわけで、開きつつあるのか、どのくらいまで開いたのか、まだ開ききってないのか、をプレイヤーに判ってもらうために、背景色を進捗に合わせて変わってもらうようにしました。
 初めはにしてたんですけど、繰り返しテストプレイしていると「じわじわ変わるグラデーションって、最後で行ったのかどうか判らんな…」ということに気付いて、今の紫、水色、緑、黄色、ピンクの5色になりました。
最後のピンクのところだけはすごく急激に変わるようにしてあるんですが、黄色からピンクに変わるのにグラデーションかけたら一回白を通るんですよね。どうにもならなかったしこれはこれでいいか……と思っちゃったのでそのままなんですけど、どうしたら正解だったのかは未だちょっと悩んでます。

 背景といってもPlaneを立ててるだけで、Material.colorをAnimationで無理やり数値をいじってるんでテクニックも何もない。DOTweenとか使えばきれいにできるのかな~とか一瞬頭をよぎったんですが、なんにせよ「一週間のうち4日はモデリングしてた」というクソスケジュールの所為で全部の動きをAnimationでやってるんですよ。新しいことに挑戦している場合じゃない。
 結果、アニメーション部分が自分でも驚く速さ作れたわ……そういうこともあるわな。

2-4.飛ばせない演出の間はタイマーが止まる

 完全に偶然だったんですが、飛ばせない開封演出中はタイマーが止まります。
 すばらしい。なんてプレイヤーフレンドリーなんだ。
 制限時間のあるヤツで演出が飛ばせないとイライラしませんか? ここタイムロスじゃん!!!!!!!と怒りを開発者に向けがちだと思うんですよね。
 本当に偶然だったんですよ。全部出来上がってタイマー付けたら止まったんですよ。
 でも、ここって意外と気になる部分だな……と思って、明記しました。
 
 どういう理由で偶然止まったかというと「OPEN!の演出中はドラッグを受け付けないようにするためにboolを設定していた」ので、そのboolにあわせてタイマーがストップするんですよ。ラッキー。

2-5.ランキングの演出

 ここは締め切り後にアプデした部分。

 ランキングシステムを作ってくれた夫にめちゃくちゃ頼み込んで、今までついていなかった機能「ランキングを更新したら上書きする」「サーバーから自分の情報を取得する」をつけてもらった!!!!!
 ので、「すでにランキングしていると文字が赤色になっている」「ランキングを更新した時に文字が虹色にゲーミングする」という演出をつけました!
 やった~~~。本物のゲームみたいだぞ。

2-6.ちゃんと複数形になる

 これはアプリ化するまですっかり頭から抜けていたんですが、ゲーム画面のスコア表示が2以上でちゃんと「Box」が「Boxes」になります(ツイート文はBoxes表記だけ)。
 英語圏で売っていくには単複に気を付けていかねばね。あっちはうるさいもんな。


3.今後の課題

 ローカライズやりたい~~~!!!!!
 いや、unityroomのお客さんはほぼ100%日本語わかる人だろうから初めから日本語で作ればいいだけなんですけど、これはアプリ化前提だったから英語で作ったの!
 言語を選択して表記が変えられるようになりたい
 使ってるフォント、せめて小学生が習う漢字くらい網羅していてくれ~~~~~(届かぬ祈り)(フリーフォントの宿命)

 あ、BGMのON-OFF機能はめんどくさくてつけてないだけです(ONの表示だけしてある)


4.作ったゲーム遊んでね!

 以上、如何にneoacoさんの優しさで満たされているのかがお判りいただけたと思いますので、ぜひともゲームを遊んでくださいね! 遊んでくださいね!!!!!!

unityroom.com

play.google.com


ではアプリの広告の入れ替え作業へ帰ります……。 サムゥイ `o´/ ̄ ̄\

あほげー第33回で作ったゲームの作り方

あほげー第33回(お題:TWIN)で作ったゲームはどうやって作ったのか、というお話。
そのまんまなんですけど、作ったゲームの振り返りはやっておいた方がいいと思ったんでやります。
作業中に記録を残しているわけではないので、もう記憶はうすぼんやりしている。

1.作ったゲーム

あほげー第33回で作ったゲームはこちら↓
neoaco.com
おらが村にもツインタワー建てるっぺよ!
どこの方言ですかと聞かれましたがとくに特定の地域をイメージしているわけではありません。

概要:予算を配分してツインタワーを伸ばす。エンドレス。

2.どうやってゲームの案を出したのか

神経衰弱を作る気満々だったんだけど、一晩かけて絵を描く作業をやってみたら全然描けなかったので呻いていたら、
夫が「予算配分ゲームとかどう? そういうの作ったことないでしょ? 人件費とか材料費とかさぁ」というアドバイスをくれたので、鵜呑みにした。

3.Unityの準備

直前にアップデートするのは好くないということは皆さん十分にご存知だと思うんですが、私も知っています。
なので、Unityのバージョンは「2019.4.8」です。
アップデートするの忘れとったわ……。

最近アセットいっぱい買ったし使いたいな~~とか思ってたんだけど、
無料の「DOTween」と有料の「Text Animator for Unity」しか使ってないね。
Text Animator for Unityを使うためにテキストメッシュプロを導入し、フォントはM+を使ってます。
assetstore.unity.com
assetstore.unity.com
mplus-fonts.osdn.jp

最後の方についカッとなってDOTweenの有料版買ったけど、解決しなかったので、買わなくてもよかった。このアセットは今後に活用したい。

4.試作

何事にも試作が必要。
2Dでプロジェクトを作成したら、
「予算に応じて」「ビルが伸びる」
の部分だけを作ってみる。
こんな感じ↓

CubeからBoxColliderを取り除いたもの(Tower)と、スライダー2本、ボタン1個。
f:id:neoaco:20201207162202p:plain
スライダー2本の値を決定ボタンを押したらタワーという名のCubeへ送り、伸びてもらう。

スライダーが整数ごとに動いてほしいので…
f:id:neoaco:20201207162754p:plain
↑このWholeNumbersのチェックを入れると、min~Maxの間を1ずつ増減するようになる。いまなら0~10。

もう完成したスクリプトしかないのでうすぼんやりと書くけど

TowerCtrl.cs

public void 予算決定ボタン(){
    float 各予算 = スライダーの値;
 if(予算の合計 < 1000万円){
        // 人件費か資材費か、低い方の値段に合わせて建築する
        float 建築階数 = Mathf.Min(人件費, 資材費);
        ビル建築();
    }
}

void ビル建築(){
    Vector3 ビルの高さ = タワー.transform.localScale;
    ビルの高さ.y += 建築階数;
}

こんな感じ。スライダーやボタンは[SerializeField]で設定しておく。これで、ボタンを押すたびにどんどん黒い四角が伸びていく。

………そうだね、上下に伸びていくね。
これはちょっとした細工でコントロールできる。
空のオブジェクトにCubeのオブジェクトを入れる
→Cubeのオブジェクトをサイズの半分(この場合は0.5)上げて、Cubeの下端が親の空オブジェクトの位置になるようにする
→親オブジェクトのScaleを変更するようにする
f:id:neoaco:20201207170108p:plain
これで上に向かって伸びているように見えるようになる。

ゲームの仕組みはこれだけ。
もうできたじゃん!!!!
実際、ボタンを押すたびに伸びるビルをみると達成感がある。
あとは、予算配分は「人件費」「資材費」「重機レンタル費」「広告費」の四つにするので、スライダーを複製して四つにして、ビルの高さを伸ばす計算式をちょっと考えたりする。
もうできたじゃん!!!!!!!!!!

5.ゲームっぽくする

5-1:伸びるように見せる

このままではボタンを押すと即建築完了で伸ばしている感が無い。ニョキニョキ伸びて欲しい。
ので、伸びていく様子を作らなければならない。
こういう「徐々に変化させる」のはコルーチンで書くこともできるんだけど、
超超超超便利なDOTweenを使うとすごく早い。

//Tweener = DOScaleY(終点の値,何秒かけてやるか).相対的にやる();
buildTweener = タワー.transform.DOScaleY(建築階数, 5).SetRelative();

これだけ。これで、今の高さから建築階数分縦に伸びてくれる。
f:id:neoaco:20201207173616g:plain
やったー、のびたー。

……なんでTweenerなの?と聞かれると困るんですが、後々別な部品で「Tweenerじゃないと困る」という場面が出てくるので、全部まとめてTweenerにした気がする。
ここは別にTweenでもよかったんじゃないかな。
記憶があやふやで申し訳ない。

5-2:予算配分にゲーム性を持たせる

予算はどんどん増えていってべらぼうに伸びて欲しいな、と思っていたので
・使い切ると次年度は増額
・余ると公共事業の定めとして減額
という仕様にすることに。増える量は定額だと面白くないからランダムにして、余った分は丸ごと減額にすることに。

ただ、そうなると、余らないようにすればいいだけなので、
それぞれの費用のスライダーを手動で設定していると、
「ボタンを押す→伸びるのを待つ」
しか中身のない単調なものになってしまうので、
その配分のところをゲームにするということで

ここで完全にゲームの雰囲気が「予算配分ゲーム」から「スロットゲーム」へと変貌しましたね。
悲しいね。
僕には小難しいゲームを作ることなんてできないんだ……。

ま、そんなことはさておき、スライダーを動かすのもDOTweenでラクチンポイなのである。

// DOValue(最大値,何秒で動かすか).SetLoop(ループ回数(-1は無限),LoopType.Yoyo(行ったり来たりする)).SetEase(直線);
Tweener 人件費用 = 人件費スライダー.DOValue(humanS.maxValue, 1).SetLoops(-1, LoopType.Yoyo).SetEase(Ease.Linear);

f:id:neoaco:20201208152112g:plain
完成したあとから撮影したからいろいろ装飾がしてありますね。
ちょっと解説。
・スライダーの数値を読み取って表示している
・スライダーのハンドルに何の費用かわかるように文字を配置した
・ゲームっぽくするために、それぞれ移動スピードを違うようにした(はずだけど重と広のスピードが同じだな……)

ここで、neoacoさんは思うわけです。
総額がちゃんと予算になってるかどうか見るのめんどくさいから、
残りの予算に合わせてスライダーのMAX値が減ってほしいな……
と。
普通のTweenだと、動き始めたときの変数の値を後生大事に抱えているわけですが、
Tweenerだと途中で変えられるんですよ。
決定ボタンを押して一個ずつ予算を決定していくようにして、使った予算は総額から消していくようにする。

TowerCtrl.cs
public void 予算決定ボタン() {
    // ボタンを一個押すと一つ決まり、予算から引かれ、次のMAXとなる
    if (DecidePhase == 0) {
        人件費スライダーTweener.Pause();
        予算残り = 総予算 - 人件費スライダー.value;
        資材費スライダー.maxValue = 予算残り;
        資材費スライダーTweener.ChangeEndValue(資材費.maxValue);
        資材費スライダーTweener.Restart();
        決定ボタンText.text = "資材費 決定";
        DecidePhase = 1;
    } else if (DecidePhase == 1) {
        ・
        ・
        ・
    }
}

ま、ゲームをプレイしてもらえばどんな感じかわかると思うので……

とりあえず、ゲームっぽくなったのでこれで良し。

6.タワーを育てる

黒い四角ではタワーと言い難いので、タワーの絵を準備します。ツインタワーってタワーじゃなくて実質ツインビルみたいなところあるし、ビルっぽい絵になった。私は悪くない。

それをSpriteEditorでちょいと一工夫
f:id:neoaco:20201208171037p:plain
・緑色のコライダーみたいな四角が表示されるので、それを繰り返してほしい幅へ変更
・Pivotを下部中央に変更

シーンに配置してからも一工夫
f:id:neoaco:20201208172430p:plain
・DrawModeをTiledに変更
(TileModeがContinuousであることを確認)
このモードの恩恵を与るためには「SpriteのSizeを変更」しなくてはならないので、

タワー(親)
 ┣サイズ測る用空オブジェクト
 ┃(CubeのレンダーをOFFにしたものでもよい)
 ┗ビルの絵

という感じの親子構造にして、タワーの絵の高さはUpdateの中で監視することにする。

TowerCtrl.cs
void Update(){
    タワーの絵.size = new Vector2(2.12f, タワーのサイズ測る用空オブジェクト.transform.localScale.y);
}

これで、

f:id:neoaco:20201208180306g:plain

7.めでたしめでたし

いや~、これでできましたね、すばらしい、もうこれで完成ですわ~

8.めでたくない部分も書いておきます

やりたかったけどできなかったこととも言います。
ゲームをプレイしてもらえばわかると思うんですが、伸びている最中になんのお知らせも入らないのってさみしくないですか?
達成感がないというか……
本当は「ある数値を達成すると文字が色がつくかアニメーションするかなんかしておめでとう感を出す」とかやりたかったんですよ
でもね、なんかうまくいかないというか、なんか、思った感じにできないんですよね
DOTweenもTextAnimationForUnityもどっちも、せわしなく変更されている文字列についてはどうもアニメーションできないっぽいんですよ
あら~~~困ったわね~~~~~
まぁ動かす仕組みを考えたら難しいのかな、と、あほげーが終わってから納得しました

8.1:改善策

数値を超えたらお祝いするやつは「達成数値ごとにフラグをつくる」「カットイン演出を入れる」とすればいいんだろうな、と思ってはいます。
それを作るほどの労力はかけたくないし、実際の建物の絵をかくのは難しいし、やってないんですけど。
うう~~~~~ん、文字だけの演出したかったな~~~~。
残念無念。

9.ということでゲームを遊んでください

neoaco.com
いっぱい遊んでツイートしてくださいな!

unity一週間ゲームジャム (お題:ふえる)のソースコードを公開する流行りに乗りたい

この8月にやっていたunity一週間ゲームジャム (お題:ふえる)が終わった後、ツイッターでソースを公開するのが流行っているようなので便乗してみます。

作ったゲーム

ふえるだけですむとおもうな unityroom.com
玉がどんどん増えていくブロック崩しです。
玉が増えるだけだとすぐ終わっちゃうし面白くないので、消したブロックがどんどん復活するようにしました。
復活するだけだとすぐ終わっちゃうし面白くないので、復活した奴がどんどん固くなるようにしました。全5段階。

ソース

秘伝のたれ……というほどのことでもないんだけど、オープニングとかエンディングとかその辺は見ても面白くないと思うので、
ゲームの全体をコントロールしてるところとかだけにします。

・全部で四つ
・プレイヤー(棒)、ブロック総合、ブロック個別、弾が死んだと判断するところ
・頑張ったのはブロック関連

player.cs(棒)
------------
#pragma warning disable 649
// SerializeFieldを使ってると、使ってるのに使ってないとうるさいので、
// ビルドする前にこれをつけることにしている

using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;

public class playerCtrl : MonoBehaviour{

    Vector3 pos = new Vector3(0,-7.15f,0);
    Animator Anim;
    bool isPlay=false;
    [SerializeField]
    GameObject DeadLine;
    [SerializeField]
    GameObject GameEndObject;
    [SerializeField]
    GameObject GameClearObject;
    [SerializeField]
    GameObject ResultBoardObject;
    [SerializeField]
    GameObject PlayerResultObject;
    [SerializeField]
    GameObject GiveUpObject;
    [SerializeField]
    GameObject GiveUpButton;
    bool CanGiveUp = false;
    [SerializeField]
    Text PlayTime;
    System.Diagnostics.Stopwatch sw = new System.Diagnostics.Stopwatch();
    TimeSpan TotalDateTime;
    int playTime;
    GameObject[] BGM;
    [SerializeField]
    GameObject BGMobject;

    private void Awake() {
        // BGMのチェック
        BGM = GameObject.FindGameObjectsWithTag("BGM");
        if (BGM.Length < 1) { Instantiate(BGMobject); BGM = GameObject.FindGameObjectsWithTag("BGM"); }
        DontDestroyOnLoad(BGM[0]);
    }

    void Start(){
        // ボール出すアニメ
        Anim = transform.GetChild(0).GetComponent<Animator>();
        Anim.speed = 0;
        GameEndObject.SetActive(false);
        GameClearObject.SetActive(false);
        ResultBoardObject.SetActive(false);
        GiveUpObject.SetActive(false);
        GiveUpButton.SetActive(false);
    }

    void FixedUpdate(){
            pos = Input.mousePosition;
            pos = Camera.main.ScreenToWorldPoint(pos);
            pos.y = -7.15f;
            pos.z = 0;
            if (pos.x < -9) {
                pos.x = -9;
            }
            if (pos.x > 9) {
                pos.x = 9;
            }
            transform.localPosition = pos;
        if (isPlay) {
            TotalDateTime = sw.Elapsed;
            PlayTime.text = TotalDateTime.ToString(@"mm\:ss\.ff");
            if (!CanGiveUp && sw.ElapsedMilliseconds > 300000) { GiveUpButton.SetActive(true); }
            // ストップウォッチ関数最高
        }
    }

    void MoreIncrease() {
        Anim.speed /= 0.95f;
        if (Anim.speed > 5) { Anim.speed = 5; }
    }

    void GameStart() {
        isPlay = true;
        Anim.speed = 1;
        sw.Start();
    }

    void GameEnd() {
        Anim.speed = 0;
        isPlay = false;
        sw.Stop();
        // 死んだ球数を数えないようにする
        DeadLine.SendMessage("GameEnd");
        // ゲーム終了処理用UI
        GameEndObject.SetActive(true);
    }

    void GameClear() {
        Anim.speed = 0;
        isPlay = false;
        sw.Stop();
        DeadLine.SendMessage("GameEnd");
        GameClearObject.SetActive(true);
    }

    void SendScore() {
        playTime = (int) sw.ElapsedMilliseconds;
        PlayerResultObject.SendMessage("GetScore",playTime);  // 確かこれがランキングに値を渡してるんじゃなかったかな(曖昧な記憶)
    }

    public void CallGiveUp() {
        Anim.speed = 0;
        isPlay = false;
        sw.Stop();
        DeadLine.SendMessage("GameEnd");
        GiveUpObject.SetActive(true);
    }
}
blockCtrl.cs(ブロック総合)
---------
#pragma warning disable 649

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using System;
using UnityEngine.UI;
using System.Data;

public class blockCtrl : MonoBehaviour{

    [SerializeField]
    GameObject block;
    GameObject[,] blockArray;
    int[,] statusArray;
    [SerializeField]
    Text BlockText;

    int statusA;
    int statusB;
    int statusC;

    int yoko = 9;
    int tate = 7;
    int lastBlock;

    GameObject Player;
    bool isPlay = false;

    void Start(){
        // ブロックの配置
        blockArray = new GameObject[yoko, tate];
        statusArray = new int[yoko, tate];
        Player = GameObject.FindGameObjectWithTag("Player");
        for (int xx = 0; xx < yoko; xx++) {
            for (int yy = 0; yy < tate; yy++) {
                GameObject bl = Instantiate(block, new Vector3(-8 + xx*2, 8.8f - yy*1.5f, 0), Quaternion.identity);
                bl.transform.name = xx + "," + yy;
                bl.transform.parent = this.gameObject.transform;
                blockArray[xx,yy] = bl;
                statusArray[xx,yy] = 1;
                if (yy == 0) { bl.SendMessage("UpStatus"); statusArray[xx, yy] = 2; }
            }
        }
        lastBlock = yoko * tate;
        BlockText.text = lastBlock.ToString();
    }

    void FixedUpdate(){
        if (isPlay) {
            lastBlock = 0;
            foreach (int i in statusArray) {
                if (i != 0) { lastBlock += 1; }
            }
            if (lastBlock == 0) {
                //gameclear
                isPlay = false;
                Player.SendMessage("GameClear");
            }
            BlockText.text = lastBlock.ToString();
        }
    }

    void GetDeadBall() {
        // ランダムで選んだブロックを一段階固くする
        int xxx = UnityEngine.Random.Range(0,yoko);
        int yyy = UnityEngine.Random.Range(0,tate);
        statusArray[xxx,yyy] += 1;
        GameObject UpBlock = GameObject.Find(xxx + "," + yyy);
        UpBlock.SendMessage("UpStatus");
    }

    void GetBlockStatus(string blockName) {
        // 各ブロックのステータスの管理
        // ブロックの名前は xx,yy という名前になっているので、分割してどの場所のブロックなのかを特定している
        string[] arr = blockName.Split(',');
        statusA = Int32.Parse(arr[0]);
        statusB = Int32.Parse(arr[1]);
        statusC = Int32.Parse(arr[2]);
        statusArray[statusA,statusB] = statusC;
    }

    void NoBall() {
        isPlay = false;
    }

    void GameStart() {
        isPlay = true;
    }
}
eachBlock.cs(ブロック個別)
---------
#pragma warning disable 649

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class eachBlock : MonoBehaviour{

    int status = 1;
    GameObject BC;
    string report;
    [SerializeField]
    Sprite status1;
    [SerializeField]
    Sprite status2;
    [SerializeField]
    Sprite status3;
    [SerializeField]
    Sprite status4;
    [SerializeField]
    Sprite status5;
    Collider2D col;
    SpriteRenderer spr;
    AudioSource AS;
    [SerializeField]
    AudioClip AC;

    void Start(){
        BC = GameObject.FindGameObjectWithTag("GameController");
        col = GetComponent<Collider2D>();
        spr = GetComponent<SpriteRenderer>();
        AS = GetComponent<AudioSource>();
    }

    void Update() {
        if (status == 5) {
            spr.sprite = status5;
            col.isTrigger = false;
            spr.color = new Color(1, 1, 1, 1);
        } else if (status == 4) {
            spr.sprite = status4;
            col.isTrigger = false;
            spr.color = new Color(1, 1, 1, 1);
        } else if (status == 3) {
            spr.sprite = status3;
            spr.color = new Color(1, 1, 1, 1);
            col.isTrigger = false;
        } else if (status == 2) {
            spr.sprite = status2;
            spr.color = new Color(1, 1, 1, 1);
            col.isTrigger = false;
        } else if (status == 1) {
            spr.sprite = status1;
            spr.color = new Color(1, 1, 1, 1);
            col.isTrigger = false;
        } else {
            spr.color = new Color(1, 1, 1, 0);
            col.isTrigger = true;
        }
    }

	private void OnCollisionEnter2D(Collision2D collision) {
        status -= 1;
        if (status < 1) { status = 0; AS.PlayOneShot(AC);}
        report = transform.name + "," + status;
        BC.SendMessage("GetBlockStatus",report);
        // SendMessageは一つの値しか送れないので、ブロック名(xx,yy)にステータスをくっつけてstringとして送信
    }

    void UpStatus() {
        status += 1;
        if (status > 5) { status = 5; }
        report = transform.name + "," + status;
        if (BC != null) {
            BC.SendMessage("GetBlockStatus", report);
        }
    }
}
DeadLineRepoat.cs(玉を殺すところ)
---------
#pragma warning disable 649

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;

public class deadLineReport : MonoBehaviour{

    GameObject BC;
    GameObject Player;
    bool isPlay = true;
    int ballcounter;
    int deadBallCounter;
    [SerializeField]
    Text LiveBallText;
    [SerializeField]
    Text DeadBallText;

    void Start(){
        ballcounter = 1;
        deadBallCounter = 0;
        BC = GameObject.FindGameObjectWithTag("GameController");
        Player = GameObject.FindGameObjectWithTag("Player");
        LiveBallText.text = ballcounter.ToString();
        DeadBallText.text = deadBallCounter.ToString();
    }

	private void OnCollisionEnter2D(Collision2D collision) {
        if (collision.transform.tag == "ball") {
            if (isPlay) {
                BC.SendMessage("GetDeadBall");
                Player.SendMessage("MoreIncrease");
                ballcounter -= 1;
                deadBallCounter += 1;
                if (ballcounter == 0) {
                    //gameOver
                    BC.SendMessage("NoBall");
                    Player.SendMessage("GameEnd");
                    GameEnd();
                }
                LiveBallText.text = ballcounter.ToString();
                DeadBallText.text = deadBallCounter.ToString();
            }
        }
	}

    void popBall() {
        if (isPlay) {
            ballcounter += 1;
            LiveBallText.text = ballcounter.ToString();
        }
    }

    void GameEnd() {
        isPlay = false;
    }
}


こんなもんですかね。まるっと公開するのはちょっと恥ずかしいですな。
一番工夫を凝らしたと思っているところはブロックのステータスを受け渡しするところです。
ブロックを復活させるにはDestroyしてはいけないので、どうしたものかと考えていたんですが、
GWにゲームつくった時に文字列を分割したな、と思いだして、採用しました。
ランキング周りは作ってもらったんでナンモワカランです。
他にはボールのコントロールするやつ、ゲームの背景を設定するやつ、タイトル画面、カウントダウンのアニメにつけるやつなど、スクリプトファイル数だけで言えば16個作りました。中身がほとんどコピペのやつもある。

いや~~~昔よりはマシな書き方をしていると思いたいですけどね。
昔はひどかったからね。関数名とか何をつけていいのかわからなくてsusieとかsasoribi とかつけてたからね。
いまだに一行で「if (status < 1) { status = 0; AS.PlayOneShot(AC);}」とか書くけど、これはマジで例外で、本当に一文しかないしこれ以上発展しないやつだけです。何行にもわたってる方が見辛いやつもあるやん、こう、同じ数値が縦に並んでてほしいときとかあるやん。wasdでプレイヤーを移動させるときとかさぁ。(言い訳)


以上、独りぼっちゲーム開発のneoacoさんのソースでした。
最近は夫を巻き込みつつあるので、独りぼっちって言いづらくなってきたが、発注しないと何もしてもらえないのでやはり独りぼっちであってると思う。

FlashゲームをPhaser 3で作り直すぞ 第3回

※このシリーズは、一日3時間くらい作業した生録画的な感じでお伝えしております。

2020年6月8日

全てシヴィライゼーションが悪い。あとマイクラも。

気を取り直して作業を再開する。開発メモを残しておいてよかった、次に何をすればいいのかわかりやす~い。
つまりそろそろやりたくない詰めの作業ということです。
本日はリザルト画面を作りますわよ!

・リザルト画面を作る

というわけでリザルト画面の画像を用意。
10問答えたらゲームを中止してリザルト画面へ移行だ!
totalAnserNumとかいう苦肉の策的名前を準備して、回答するたびに+1する。
10になったらリザルト画面を表示するようにする。

// chaking()
}else if(gamePhaseNum === 2){
    // ゲーム中
    totalAnserNum += 1;
    if(totalAnserNum < 10){
       // 略
    }else{
        // 10問答えたのでリザルトへ移行
          // 表示しているものを全部非表示にする
          // 次のゲームフェイズへ移行
          gamePhaseNum = 3;
    }
}

10問答えたらすぐに結果表示してもいいけど、ここでワンクッション置いた方が丁寧な気がする。
「終了!」とか表示して、それからリザルトを表示することにした。

タイトル画面からプレイをしてみる。
おー、いいね、リザルト画面まで出来たぞ。これでほとんどできているというわけだ!!!!!

・ツイートボタンとリトライボタン

リザルトを表示したら、その内容をツイートしてもらいたい。
リトライするのにF5アタックはしてほしくない。
というわけでボタンを追加します。

※※※※
こんな面倒なことをしなくても画像ボタンでいいじゃない、と思う場合は
6/9のボタンの中身の話までスキップせよ
※※※※

こんな注釈をするくらいなので本当に本当にただの寄り道です。
ボタンを、CSSで作ったボタンにしたくないですか。
ぼくは、やりたいです。

まずはここを参考にする。
https://phaser.io/examples/v3/view/game-objects/dom-element/form-input
↑この中で読み込まれている"assets/text/loginform.html"が必要なので、GitHubで探しに行く……。
https://github.com/photonstorm/phaser3-examples/blob/master/public/assets/text/loginform.html
これこれ。
まーでもこれはログインフォーム全部なのでちょっと量が多すぎる。
欲しいところだけ書くと……

<style>
.button {
  color: #ffffff;
  background: #1b95e0;
  width: 100px;
  height: 60px;
  font-size: 18px;
  text-decoration: none;
}
</style>
<div id = "BT">
<button class="button">
  ついーとする
</button>
</div>

こんな感じ。あとは皆さんのCSS力でカスタマイズしていただければ……。僕は角を丸くしたり影をつけたりした。
リトライボタンの方は、あとでテキストとカラーをプログラムで変更することにする。
このあとはPhaserの見本通りに書き込んでいけばいい。

// preload()
this.load.html("nameform", "button.html");

// create()
element1 = this.add.dom(100, 420).createFromCache("nameform");
element2 = this.add.dom(210, 420).createFromCache("nameform");
// 色を変える
element2.getChildByProperty("className", "button").style.backgroundColor = "red";
// テキストを変える
element2.getChildByProperty("className", "button").innerText = "もっかいやる";

これでボタンが設置される。
ほかのボタンと同じようにAlphaを0にしておけば見えないし使えないようになるのでそうしておいて、リザルト画面で見えるようにすればいい。
色変えるの調べるのに手間取ったから今日はここまで。


2020年6月9日

ラジオ体操第一をすると肩が爆発しそうなくらい痛くて熱くなる。不健康の極み。
もうちょっとだけボタンの話が続くんじゃ。

・ボタンを動かせるようにする

ボタンを押して反応するようにするには、イベントリスナーを付ける必要がある。

// create()
// 昨日ボタンを書いたすぐ下に書くとわかりやすい
element1.addListener("click");
element1.on("click", function(pointer){
// やらせたい動作をここに書く
});

リトライボタンも同じように書く。
リトライボタンの方が中身は簡単、今出てる表示を全部非表示にして、タイトル画面を出して、数字をリセットするだけ。
ツイートボタンは……

window.open("https://twitter.com/intent/tweet?text= " + encodeURIComponent("ツイート内容"));

と書いておくと、新しいウィンドウでツイート画面を出してくれる。
ツイート画面っていうか、ツイッターを開いて発言状態にしてくれるっぽい。
昔と挙動が変わったな。この調子で画像添付できるようにならないかなぁ。


・できた! ガハハ!  ……と、その前に

気がかりな点が二つある。
1.得点の計算はどうする?(現状、正答=1ptとしか数えてない)
2.スコア表示のフォント……どうする?

・得点計算

Flash版と同じように、早く答えると点が高いようにしたい。
というわけで、時間の計測をすることにする。

時間の計測
問題の表示 →計測開始
左右どちらかのボタンを押す →計測終了

この時間の差を測って、得点に変換しよう。
早い方が良いってことは……1問5秒ぐらいで答えてほしいから……
5秒から引いた回答までの時間を得点にしよう。
正解しても点が貰えなかったら寂しいから、5秒以上は総じて1点だ。
時間を計測するっていうのはちょっと面倒くさくて、始めの時刻と終わりの時刻を記録して差を求めることになる。

// 時間の計測開始
timerSatr = new Date();
// 時間の計測終了
timerEnd = new Date();
// スコアの計算(ミリ秒なので5s = 5000 ms)
timeScore = 5000 - (timerE - timerS);

これらを問題表示と回答ボタンのところに書いておけばいい。
いいね~ 数字もべらぼうな数字になって楽しいな! ワハハ

・フォント

フォントねぇ……全部がGoogleWebフォントだったり、ゲームでまるっと載せていいライセンスのフォントだったりで
通信の重さとかしったこっちゃねー!状態なら日本語フォントとかそのまま入れちゃうんだけど
(参考:https://phaser.io/examples/v3/search?search=font のWebFontってかいてあるやつ)
ゲーム中の文章も数少なく全てを画像で済ませちゃったので、
スコアの表示をするためだけにフォントが必要になってくる……。
いや、全部フォントをそろえたいだけのアレなので、あれですよ、
ゲーム中のフォントをそもそもgoogleWebFontにあるフォントで作ればいいだけなので、
私がたぬき油性マジックを使いたいだけのエゴなので、
数字だけのビットマップフォントを作りました……。
(参考:http://narudesign.com/devlog/create-bitmap-font/
禁止事項に触れてないから大丈夫。たぬき油性マジックの懐の広さよ。ありがとうありがとう。
これで画像さえあればビットマップフォントが作れるようになったぞワハハ。

そんなことよりゲーム中の表示ね。
https://phaser.io/examples/v3/view/game-objects/bitmaptext/static/change-font
これかな。

フォントを読み込んで、
テキストの表記をただのテキストからビットマップフォントの書き方へ変更

// preload()
this.load.bitmapFont("tanuki", "assets/fonts/bitmapfont-export.png", "assets/fonts/bitmapfont-export.xml");
// create()
// x, y, fontname, text, fontsize
scoreText = this.add.bitmapText(315,350,"tanuki", " ", 102);

これで様子を……いいね! できたできた


完成したジャン! すごいじゃん!
やったー! やったー!


どれどれ……サーバにアップロードして……ツイートして……完成! 完成です!
やったー!

結局全部で一か月くらいかかっとるんかーーーーーい!

・出来上がったゲーム

出来たゲームはこれだ!
遊んでくれ!!!!!
neoaco.com

FlashゲームをPhaser 3で作り直すぞ 第2回

※このシリーズは、一日3時間くらい作業した生録画的な感じでお伝えしております。

2020年5月18日

毎日ゲーム作ってて気がくるくるしてきたので、しばらくゲームで遊ぶばっかりの日々を過ごした。
精神が正常化してきたので続きの作業をする。`ヮ´ワハハ

・ゲーム中のクリックを整理する

調査の結果、image.setInteractiveでボタン化したオブジェクトは、Alphaが0だと非アクティブ化することが判明。
色つきの四角を仮置きして、見えなくするために透明すると動かなくなるっていうワケ……マジ?
兎に角、この理屈がわかったことにより、ボタンの整理ができるというものだ。ワハハ`ヮ´

・スタートボタンを押す
 → 選択肢の表示
 → 全面ボタンの非アクティブ化

・左右どちらかを押す
 → ○×の表示
 → 全面ボタンのアクティブ化

・全面ボタンを押す
 → ○×の非表示
 → 選択肢の表示
 → 全面ボタンの非アクティブ化


アクティブ/非アクティブはsetAlpha()で1にしたり0にしたりすればよい。
よいな。よし。できた。

これで大まかな仕組みはできたので、しばらくは画像制作のターンとなる。
うわーめんどくさい。でも自分で準備したいから頑張るしかない。


2020年5月20日

ぜぇぜぇ、選択肢の絵を描いてきたぞ。あほげーの時、二日で作ったはずなんだけど、こんなに大量に絵を描いたって本当か? 信じられん……。

ということで絵を描いてきたら選択肢が全部で12個になったので、リストを増やしたり画像を差し替えたりウンヌンする。

ウンヌン! ウンヌン! ウンヌン!
(※効果音のみでお送りしております)

これでよし。
表示してみる。それぞれの画像をちょっと修正したい気持ちがムクムクしてくるが、今はその時ではない……。


フォントを導入したりしようかといろいろ試してみたけど、変動する表記は数字だけなので止めることにした。
日本語フォントは重いし……。
どうするのかはまたあとで考えよう。

・ゲームのトップ画面と、フェーズ移行について

ゲームの中身はできたので、そのほかの大まかな流れについて考える時が来た。

タイトル画面
↓
説明文
↓
ゲーム
↓
結果発表
↓
タイトル画面に戻る

フェーズ分けはこんな感じかな。
今はゲームの部分しかない。


シーンを増やすという手もあるが、まだよくわかってないので、フラグ管理でどうにかすることにした。
新しくgamePhaseNumというintを導入する。
これで、今どのフェーズにいるのかを管理し、全面クリックがどのような挙動をするのかを決める。

上から順番に数字を割り振って、全部で4フェーズ。
gamePhaseNumを使って、今どの場面なのかをチェックする関数を作る。

// create()
// 全面クリック用のボタンを作る
AllArea =  this.add.image(250, 250, "AllArea").setInteractive();
AllArea.on("pointerdown", function (pointer) {Checking();});

function Checking(){
    if(gamePhaseNum === 0){
        // タイトル中にクリックされたら起こることを書く
    }else if(gamePhaseNum === 1){
        // 説明文表示中に(以下略
        // ゲーム最初の設置を行う
    }else if(gamePhaseNum === 2){
        // ゲーム中に…(ここは5/18の内容が入るところ)
    }else if(gamePhaseNum === 3){
        // 結果発表……
    }
}

画面をクリックすると次のフェーズに移る感じ。
ゲーム中はAllAreaのAlphaを0にしておけば反応しない。よしよし。

いや~、今日はいっぱい頑張たな。
続きはまた明日。

FlashゲームをPhaser 3で作り直すぞ 第1回

※このシリーズは、一日3時間くらい作業した生録画的な感じでお伝えしております。

~前回までのあらすじ~

Phaserで新しいゲーム作りたいけど、その前にFlashChromeに殺されるから過去のお気に入りのゲームを移植しておこう。

移植するゲームはこれだ!

あほげー第15回 トミート High & Low
neoaco.com


・ゲーム概要
二つの選択肢のうち、より「トミー」な方を選ぶ。
全10問。


Flash版ゲームの仕様
(たしか)15種類くらいの選択肢。
ランダムに二つ提示し、よりトミーな方を選ぶと加点される。
速く判断した方が点が高い。

Flash版を作るときに頑張った点
どっちがよりトミーかを判断するために、選択肢をミートな方から順番に数値を付け、数値が大きい方がトミーとした(入れ知恵)。
選択肢を選んで、○×が出て、次の設問へ移るようにした。

この仕様を継承し、Phaserで移植……というか、作り直し!をやっていこうと思う。

2020年5月11日

思い立って開始。
やっぱHDDの中にデータ無い……。

・とりあえずの画像の作成

どうせコードで使いまわせるところなど無いので、画像は諦める。悲しい。
とりあえず、仮に数字0~9で作ることにした。
f:id:neoaco:20200610154138p:plain
これを分割して使うようにする。

・index.htmlとgame.jsの作成

チュートリアルを見ながらconfigを書けばいいのだけれど、センタリング大好きマンとしてはゲームをウィンドウのセンターにおいてほしいので

// config
autoCenter: Phaser.Scale.CENTER_BOTH,

これを追加している。
あと、index.htmlのどこにゲームを表示してほしいかということを書くために

parent: "center",

これも追加している。
表示したいdivのidをここに書く。
私はheader>center>footerという構造にしているので、"center"となる。
(※はじめはゲームだけしか表示しないから要らないじゃんと思ってたんだけど、DOMでボタンを表示しようとしたら、このparentの設定が無いとうまくいかなかった。罠。)

ついでだから、configを全部載せておこう。
このゲームは物理演算を使わないので、すでに項目から削除している。

var config = {
    type: Phaser.AUTO,
    width: 500,
    height: 500,
    backgroundColor : 0xffffff,
    autoCenter: Phaser.Scale.CENTER_BOTH,
    parent: "center",
    dom: {
        createContainer: true
    },
    scene: {
        preload: preload,
        update: update,
        create: create,
    }
};
・画像の読み込み

分割して使うので、スプライトシートとしてプリロードで読み込む。
https://phaser.io/examples/v3/view/textures/sprite-sheet
creat()でadd.imageすればよい。画面の外に表示しとこ。

tomeet0 = this.add.image(-200,-200,"tomeet",0);	
//(x,y,スプライトシートの名前,何番目の画像か)

これを9まで作って、配列に入れる。

tomeetList = [tomeet0,tomeet1, ... tomeet8,tomeet9];

これで、画像を数字で比較できるようになったぞ。

・画像の表示

取り敢えずランダムに選ばれて出てきてほしい。
概要:数字をランダムで選ぶ→その数字の絵を見えるところに置く

スペースキーで配置できるようにする。

 // create()
KeySpace = this.input.keyboard.addKey(Phaser.Input.Keyboard.KeyCodes.SPACE);

 // update()
if(Phaser.Input.Keyboard.JustDown(KeySpace)){
    LeftNum = Phaser.Math.Between(0,9);
    setTomeet(); 	// 選ばれた画像を設置する動作は外に出した
    console.log("Space is JustDown : " + LeftNum);
	// 念のために押された時に得た数値をコンソールにも書く
}

 // setTomeet()
 tomeetList[LeftNum].setPosition(50,50);

これでindex.hmtlにてスペースキーを押すとランダムに数字が出る。出るんだってば。
早くに大きい数字が出ると小さい数字は隠れてしまって出てないかもしれないけど、まぁリロードすれば確認できるからいい。ここは出てさえすればいいのだ。
(後でゲームの機能を作りこむときに作業するから気にしない)


同じように右にも出るようにする。
同じものが出たら困るので、左と同じものが出たらもう一度抽選することにする。

LeftNum = Phaser.Math.Between(0,9);
RightNum = Phaser.Math.Between(0,9);
while (RightNum === LeftNum){
    RightNum = Phaser.Math.Between(0,9);
}

はじめはwhileじゃなくてifで条件付けしてたんだけど、10個くらいの選択肢だと結構な頻度でかぶっちゃうみたいなのでwhileにした(怒)
まあこれでかぶることは無くなったのでヨシとする。



2020年5月12日

なんだこれ、クッソ暑いやんけ。夏だ。

・どっちをクリックしたんですか問題

このゲーム、
1.どっちをクリックしたのか
2.もう一方と比べて数値はどうなのか
を見極めなければならないわけで。
どうやろうかな……。
このトミートリストは数字が小さいほどトミーとするので、

右をクリックした
    → 右 < 左 なら正解
左をクリックした
    → 左 < 右 なら正解
そうでなければ不正解

という判定をすれば良さそう。
"画像とは別に"クリックする用のボタンを上から表示すればいい。
クリック用の枠(元ゲーム参照)を作って、クリックしたら上記の関数に働いてもらうようにする。
https://phaser.io/examples/v3/view/input/mouse/click-sprite

// ボタンの設置
LeftB =  this.add.image(145, 350, "button", 0).setInteractive();
LeftB.on("pointerdown", function (pointer) {answerL();});

ボタン用の関数も書く。

// answerL()
if(LeftNum < RightNum){
    score += 1;
}else{}

これで取り敢えずコンソールにでも表示しておけば、左が正解の時はスコアが増える!
よしよし。
右ボタン用を作ったり、もしアレだったらスコアの表示をつけてもいい。


今日はここまで。
あれっ……なんか作業した量のわりに全然進んでない気がするぞ。
まぁいいか。

FlashゲームをPhaser 3で作り直すぞ 第0回

※このシリーズは、一日3時間くらい作業した生録画的な感じでお伝えしております。

2020年5月11日。

この日、neoacoは慢心していた。
あほげーGW緊急開催(5/3-5/5開催)のゲームをPhaserJSで楽勝に作り上げたからだ。
この調子で古いFlashゲームを移植しよう。

ゲーム作ったときのデータがどっか行ってしまったことを思い出さなかったことにして、やりやすそうなものから手を付けることにした。


Phaserの導入は各自で何とかしてくれ。そこまで面倒みられんぞ。
公式のチュートリアルを一通り眺めたことのある人向けに書くので、チュートリアルは見ておいてね。

・よく見るページ

Phaserのトップページ
https://phaser.io/
チュートリアル(Making your first Phaser 3 gameのこと)
https://phaser.io/tutorials/making-your-first-phaser-3-game/part1
公式コードサンプル
https://phaser.io/examples
公式APIドキュメントより見やすい一覧表みたいやなつ(非公式だと思う)
https://rexrainbow.github.io/phaser3-rex-notes/docs/site/


※"var なんとかかんとか"は、基本的に全部まとめてpreload()の前に書くこととする。
 なんで一番初めに書くかというと、関数の合間に書くととっちらかるし、
 関数の位置を動かしたら定義されてなくてエラーが出るとかいうしょうもないミスをなくすためである。


では早速! 次の記事からゲームを作り始めたいと思いマス。
続く。