ぐーるらいふ

底辺。

【Unity】第7回 unity1week「当てる」参加作品「SATELLITE ONE」の開発

お疲れ様です。ぐーるです。

体調最悪のコンディションでしたが、なんとか提出することが出来ました。

思い出と実装をメモっておきたいと思います。

お題について

「当てる」かー、今回は神題だなと。
前提を考えなければアイデアはいくらでも出せそうだ、と。
デザインをほとんど必要とせずサクッと出来る、シンプルなものがいいなぁとか漠然と考えていて
降ってきたアイデアがSATELLITE ONEです。

内部設計について

いつもそうだけど物理的なunity.sceneは一つだけ。

内部的な画面は

  • OpeningScene
  • GameScene
  • ResultScene

の3つ。

各画面間の切り替えはシンプルなステートマシンで管理。

public interface IState{
	void OnStateEnter();
	void OnStateExit();
}

public class StateMachine : MonoBehaviour {

	public List<IState> states = new List<IState>();
	private IState prevState;
	private IState currState;

	public void AddState(IState state)
	{
		foreach (var st in states)
		{
			if (st == state)
			{
				return;
			}
		}
		states.Add(state);
	}

	public void RemoveState(IState state)
	{
		states.Remove(state);
	}

	public void ChangeState<T>() where T : IState
	{
		if (currState is T)
		{
			return;
		}
		if (currState != null) { 

			prevState = currState;
			prevState.OnStateExit(); // 抜ける前にOnStateExit()を呼ぶ
		}
		foreach (var st in states)
		{
			if (st is T)
			{
				currState = st;
				currState.OnStateEnter(); // 切り替わると同時にOnStateEnter()を呼ぶ
			}
		}
	}
}
/**
 * // Zenjectで紐付け
 * [Inject]
 * private MainScene _mainScene;
 * [Inject]
 * private StateMachine _stateMachine;
 * // ~~
 * _stateMachine.AddScene(_mainScene); // 起動時にStateMachineに登録しておく
 * // ~~
 * _stateMachine.ChangeScene<MainScene>(); // _mainScene画面へ切り替え
 */
public class MainScene : MonoBehavior , IState{
	//~~~~~
}

こんな感じでZenjectと組み合わせ。
楽か?って言われるとこの規模だとそこまででも無いけど
Zenject化するのが癖になってしまった。

ターゲットサイトについて

これは円形のプレーンな板をBlenderで用意しておいて、
その上で円を描くShaderを動かしています。

f:id:ghoul_life:20180226153048p:plain
f:id:ghoul_life:20180226231345g:plain

Shader "Custom/Ring" {
	Properties{
		_TargetX("Target X", float) = 0
		_TargetY("Target Y", float) = 0
		_TargetZ("Target Z", float) = 0
		_Alpha("Alpha" , float) = 0
		_Color("Color" , Color) = (0,0,0,0)
		_BG("BG" , Color) = (0,0,0,0)
	}

	SubShader{
		Tags{ "Queue" = "Transparent" "RenderType" = "Transparent" } // alphaに対応するのに必要
		Blend SrcAlpha OneMinusSrcAlpha // alphaに対応するために必要
		LOD 200

		CGPROGRAM
#pragma surface surf Standard alpha:fade
#pragma target 3.0

		float _TargetX;
		float _TargetY;
		float _TargetZ;
		float _Alpha;
		float4 _Color;
		float4 _BG;
		struct Input {
			float3 worldPos;
		};

		void surf(Input IN, inout SurfaceOutputStandard o) {
			float dist = distance(fixed3(_TargetX, _TargetY, _TargetZ), IN.worldPos);
			float val = abs(sin(dist*3.0 - _Time * 100));
			float alpha = _Alpha;
			if (val > 0.98 && alpha > 0){
				o.Albedo = _Color;
				o.Alpha = alpha;
			}
			else {
				// ↓何でも良い
			
				//discard;
				o.Alpha = 0;
				//o.Albedo = _BG;
				//o.Alpha = _BG.a;
			}
		}
		ENDCG
	}
	FallBack "Diffuse"
}
	[SerializeField]
	private Material _ringMat;
	// Update is called once per frame
	void Update () {
		// 適当なinputのラッパーだと思って下さい。なんでもいいです。
		if(_inputManager.isTouchDownOnly(0)){
			RaycastHit hit;
			Ray ray = Camera.main.ScreenPointToRay(_inputManager.GetTouchDownPosition(0));
			if (Physics.Raycast(ray, out hit)){
				var p = hit.point;
				p.y += 0.1f;
				this.transform.position = p;
				// タッチしたポイントをShaderに渡して中心点を移動した位置にする
				_ringMat.SetFloat(Shader.PropertyToID("_TargetX") , p.x);
				_ringMat.SetFloat(Shader.PropertyToID("_TargetY"), p.y);
				_ringMat.SetFloat(Shader.PropertyToID("_TargetZ"), p.z);
			}
		}
	}

ステージ制作について

SATELLITE ONEのステージは以下の2つの要素から成る。

  • フロア

そして敵は以下の要素を持っている

  • 移動ルートポイント
  • ルートポイントごとの移動速度
  • 移動イージング

回転とかは賑やかしなので割愛。

ステージ作りがとにかく面倒だなと。
フロアはしょうがないとして、敵の移動ルートを量産出来れば、
ステージがどんどん作れそうだなと考えた。

ステージ制作画面

別でステージ製作専用のエディタを用意した。

f:id:ghoul_life:20180226231457p:plain

  1. 敵を選択して選択状態に
  2. 動かしたい所をクリック
  3. AddPointボタンを押下でルートポイントを追加

という流れでどんどんルートポイントを置いていくことが出来る。
Playで動きの確認、そしてSaveでcsv出力している。
loadももちろん完備。

ここまで必要なのか?と思ったが
結局ゲーム側でも必要な機能がほとんどだったのでついでで。

csvのロードについて

ステージを追加量産するために
Application.dataPathを利用してパスでファイルを読むようにしてたら
当たり前だけどWebGLに出力したら動かない。
(開発中は紐付けなくてもcsvファイルを新規追加読み込み可能なので便利だった)

FileInfo fi = new FileInfo(Application.dataPath + "/Resources/stage/data/" + stageId + ".csv");
var result = new List<string>();
StreamReader reader = new StreamReader(fi.OpenRead());
while (reader.Peek() > -1) {
	string line = reader.ReadLine();
	result.Add(line);
}
return result;

Editor上だけ使って実ゲームではTextAsset化して泥臭く紐付けて使うことに…。
とほほ。

[SerializeField]
private TextAsset[] _stageDatas;

おまけ

人工衛星なんですが、これはUnity標準のプリミティブオブジェクトの組み合わせで作ってます。
最初は無かったんですが、余りに味気なさすぎたので急遽追加しました。
適当に置いているだけなのですが、ちょっとゲームらしくなってくれたかなと。

f:id:ghoul_life:20180226232144p:plain

感想

後はBGMのON/OFF付けたり、Post Processing Stack付けて微妙な色合いを出したり。頭痛と闘いながら調整。

docs.unity3d.com

今回もどうにか参加できました。
何故インフルエンザなんてかかるのか!?
ちゃんと予防接種受けてるのに。

前回は可愛い感じだったので、今回は硬めに。
みなさんお疲れ様でしたー!
次は可愛めで行きたい!

【Unity】Androidビルドを転送すると INSTALL_PARSE_FAILED_MANIFEST_MALFORMED が出る【NCMB】

ゲームが出来た!実機で動かそう!

と思い、ビルドを行い、adbで実機に転送しようとした所

xxxxx.apk: 1 file pushed. 3.6 MB/s (47771508 bytes in 12.536s)
          pkg: /data/local/tmp/xxxxxxx.apk
Failure [INSTALL_PARSE_FAILED_MANIFEST_MALFORMED]

というエラーが出てしまった。

「あー、アプリ名に.(ドット)が入ってるのがマズいのかな?もしくはパッケージ名辺りが悪いとか」

と当てずっぽうで修正に入ったら変にハマってしまったのでメモっておく。

原因と解決策

原因はNCMBのUnitySDKプラグインを入れていまして、その中にある
Plugins/Android/AndroidManifest.xml

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    package="YOUR_PACKAGE_NAME" >
 <!-- Put your package name here. -->

  <uses-sdk android:minSdkVersion="14"/>

    <!-- [START gcm_permission] -->
    <uses-permission android:name="com.google.android.c2dm.permission.RECEIVE" />
    <uses-permission android:name="android.permission.WAKE_LOCK" />
    <!-- Put your package name here. -->
    <permission android:name="YOUR_PACKAGE_NAME.permission.C2D_MESSAGE"
        android:protectionLevel="signature" />
    <!-- Put your package name here. -->
    <uses-permission android:name="YOUR_PACKAGE_NAME.permission.C2D_MESSAGE" />
    <!-- [END gcm_permission] -->
~~~~~

この

YOUR_PACKAGE_NAME

これだった…。
これをそのままにしてビルドして入れようとしてたので、エラーになっていたのでした。
これを修正したらあっさりインストール出来ました。

まずググろう

ほんとに…分からなかったらまずググろう。反省。
無駄な三十分を過ごしたー!あー!

【Unity】[TIPS] DoTweenで画像のalpha値をアニメーションさせたい

canvasgroupならFadeIn , Outがすぐ出来るみたいだけど、
uGUIのImageならどうやるのかをメモ。
ちょっと調べれば分かることですが…。

DOTween.ToAlphaを使う

var  fadeImage = GetComponent<Image>();
fadeImage.enabled = true;
var c = fadeImage.color;
c.a = 1.0f; // 初期値
fadeImage.color = c;

DOTween.ToAlpha(
	()=> fadeImage.color,
	color => fadeImage.color = color,
	0f, // 目標値
	1f // 所要時間
);

DoTween.ToAlphaを使うと、colorのalpha値に対して処理を行うことが出来る。
そのイベントを値を入れたいImageに渡してあげればOK。

沢山あったらどうなるんや

こんなテストコードで軽く動かしてみる。

public Image[] testImages;

public void fadeTest(){
        for (var i = 0; i < testImages.Length; i++)
        {
            // 初期化
            var img = testImages[i];
            var c = img.color;
            c.a = 1.0f;
            img.color = c;

            DOTween.ToAlpha(
                () => img.color,
                color => img.color = color,
                Random.Range(0.0f, 0.2f), // 目標値
                Random.Range(1f, 3f) // 所要時間
            ).SetLoops(-1 , LoopType.Yoyo);
        }
}

f:id:ghoul_life:20180201173209g:plain

問題なし。便利ですね!

【Unity】uGUIで円形のプログレスバーを簡単に作る

お疲れ様です。ぐーるです。
uGUIでの円形プログレスバーの作り方をメモっておきます。

手順

1.一番下になる背景を作る

Imageを作り、円形の画像を当てはめ、名前を「ProgressBar」としておく
f:id:ghoul_life:20180131181258p:plain

ここに実際に使用した200x200の白い円形のpng画像を貼ってあるのですが
見えませんね。見えません。見えないのでイメージ画像も一緒に貼ってあります。
f:id:ghoul_life:20180131171339p:plain
f:id:ghoul_life:20180131171345p:plain

2.中央のマスク部分を作る

1で作ったProgressBarをコピーして子要素にする。その後大きさをちょっと縮小(100x100)して、
名前を「Center」にしておく

f:id:ghoul_life:20180131181308p:plain

3.ゲージ部分を作る

1で作ったProgressBarをコピーして子要素にする。

f:id:ghoul_life:20180131181315p:plain

大きさはそのままにして、色を好きな色に変更、
Image TypeをFilledにします。
f:id:ghoul_life:20180131181340p:plain
f:id:ghoul_life:20180131181348p:plain

4.完成

Image Typeの下にあるImage Amountの値を変えれば
円形プログレスバーの出来上がりです。
f:id:ghoul_life:20180131181359g:plain

uGUIスゲー

あっという間にできてしまった。
画像ではShadow入れて見やすくしていたり、名前を付けてたりしますが、
その辺はもちろん自由です。空中にゲージだけ浮かせたいって人は1の背景も要らないですね。

【Unity】ソロだけど、Zenjectを使ってみたい。超初心者向け?

万年ソロプレイなぐーるです。よろしくお願いします。
また間が空いてしまいました。
なんていうか年末年始は問題がよく出たり、人が抜けたり人が増えたり
と運用をメインにしているとバタバタな期間だったりします。
2-3月は少し落ち着くといいなあ。(4月からはまたバタつく)

zenjectを使ってみたい

github.com


C#用のDI container。
オジサン的にはSpringとかseasorとかむかーし使ってたなぁなんて印象です。
個人的にこういうの結構好きでして、多人数開発になると「こうかはばつぐんだ!」っていう感じです。
とはいえ、個人で使う分には手に余るような部分が多く、手を出しづらいのですよね。

重い腰を上げる

まずは「zenject」で検索。

Unity3DのDIフレームワーク、Zenjectの紹介 | Aiming 開発者ブログ

ふむふむ、installerとcontextとbindかぁ。
え、bindするには必ずinstallerをアタッチしないとダメなのかな?
と思ったのだけど、公式のreadmeを見てみると、すごくシンプルでも動きそう?

やってみた手順

1. unityで適当なプロジェクトを作る
2. Asset Storeでzenjectを検索して、プロジェクトにインポートする
3. 上部メニューGameObject > Zenject > SceneContext を押下して、SceneContextを作成
4. uGUIで適当にテストするbuttonを作る

f:id:ghoul_life:20180122204841p:plain

4. 以下の適当スクリプトを作る(計3つ)
DIContainerに使いまわしたいオブジェクトをBindする。
そして使いたい所でResolveするだけ。

f:id:ghoul_life:20180122205822j:plain

Node.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

// DIContainerにbindして色々な所で使いたいクラス
public class Node {
	Node(){
		Debug.Log("Node class New : " + this.GetHashCode());
	}

	public void print(string message)
	{
		Debug.Log("node print == " + message + " | hashCode == " + this.GetHashCode());
	}
}
Exec1.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using Zenject;

public class Exec1 : MonoBehaviour {
	[Inject]
	DiContainer container;

	public void Bind(){
		// 以下の3つのbind typeがある。
		Debug.Log("bind");
		//container.Bind<Node>().AsSingle();
		//container.Bind<Node>().AsTransient();
		container.Bind<Node>().AsCached();
	}

	public void Resolve(){
		var node = container.Resolve<Node>();
		node.print("exec1");
	}

	public void UnBind(){
		Debug.Log("unbind");
		container.Unbind<Node>();
	}
}
Exec2.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using Zenject;

// 使う所その2
public class Exec2 : MonoBehaviour {
	[Inject]
	DiContainer container;

	public void Resolve(){
		var node = container.Resolve<Node>();
		node.print("exec2");
	}
}

5. 上で作ったbuttonにイベントを割り当てる
6. 動かしてみる

結果

最小構成で簡単にbindして使う事が出来た。

containerにbindする時以下の3つを指定できる。
これを理解しておこう。

AsSingle()

f:id:ghoul_life:20180122210320p:plain

bind , unbindを繰り返してもnewは一度しか発生せず、resolveの結果は変わらなかった。
つまりunbindしてもzenject内にオブジェクトを保持していて、
再bind時にはそれが利用されているようだ。
ということはGCの対象にならず、アプリを終了するまで開放することが出来ないだろう。

AsTransient()

f:id:ghoul_life:20180122210500p:plain

resolveする度にnewをしている。
そのため、毎回別のオブジェクトとして扱われる。

AsCached()

f:id:ghoul_life:20180122210711p:plain

AsSingle()と挙動が似ている。unbind、再bindを行うとnewされるようだ。(newされる瞬間は最初のResolve時)
つまり、unbind時にはzenjectからも開放されているため、GCの対象になってくれる。
最も使いやすいのでは無いだろうか。迷ったらとりあえずコレで良さそう。

こんな挙動の違いを見ることが出来た。

終わりに

シンプルに使おうと思うとこんなに簡単なんですね。
個人開発でしかunityを使ったことが無いので、こういった開発補助ライブラリ
は余り使わなくてもいいのかもしれないけど、メモとして残しておく!

都内は大雪です。

【Unity】InkPainterをちょっと理解してみたかった

お疲れ様です。
年末商戦とか関係なく休日が潰れてしまうぐーるです。
ゼノブレイド2を買ったのですが、全然出来なくて困ってます。

InkPainter

突然ですが、InkPainterってご存知でしょうか
f:id:ghoul_life:20171212221512p:plain
https://www.assetstore.unity3d.com/jp/#!/content/86210
Unityでスプラトゥーンごっこが出来ちゃう!しかも無料!

製作者さんはこちら
esprog.hatenablog.com
twitter.com

「おおー、すげー」で終わらせたいのだけど、

内部的にどうなってるのかな?
最小限で必要な要素ってなんだろう?

とふと気になったのでちょっと調べてみました。

最小構成の全体の流れ

まずは流れを掴むためにnormalmapもheightmapも無視した状態の最小構成を調査しました。
最小限必要な処理の流れを抜き出してみると

  1. materialからmainTextureを取り出して、RenderTextureにコピー
  2. materialのrenderTextureとmainTextureを差し替える(renderTextureがセットされた状態になる)
  3. bufferを利用してrenderTextureにインクを描画する

というすごくシンプルな実装という事がわかった。

最小構成に整理したスクリプト

オリジナルのInkPainterを読むと色々と機能があるが、
最低限必要なものだけを抜き出して整理したスクリプトはこんな感じ。


最小構成のシェーダー

shaderもcgincで共通化されていた箇所を取り出してきて、
1ソース内に収めて、整理したらこんな感じ。


Unityプロジェクトに落とし込む

1.Unityでprojectを作る
f:id:ghoul_life:20171212190149p:plain

2.planeを生成してカメラを調整でもしておく
f:id:ghoul_life:20171212190153p:plain

3.Scriptを作る(ソースは上に)
f:id:ghoul_life:20171212190211p:plain

4.Shaderを作る(同じくソースは上に)
f:id:ghoul_life:20171212190218p:plain

5.リソースを用意する(brushとmainTexture)
f:id:ghoul_life:20171212190208p:plain
f:id:ghoul_life:20171212190248p:plain
f:id:ghoul_life:20171212190257p:plain
↑使用したリソース二つなのだけど、見えないかも…?

6.materialを作り、planeにアタッチ
f:id:ghoul_life:20171212190156p:plain

7.Scriptもplaneにアタッチ
f:id:ghoul_life:20171212190221p:plain

8.実行する
f:id:ghoul_life:20171212190446p:plain
マウスでドラッグした箇所にブラシで設定したテクスチャの
形に描画をすることが出来るようになっており、
ゲームを止めて起動し直すと全てが初期化されているのが
確認出来ました。(リソースを破壊してない)

感想

この実装ではマウスでやってるけど、ボールが転がった跡にしたり、
一定時間で消したりとか使用用途はありそう。

平たく言えば

「テクスチャの上にブラシテクスチャで描画出来るようにしただけ」

になってしまったが、
mainTextureをコピーしてRenderTextureを作り、
それを利用してカスタマイズした表現をするという方法を理解して貰えればと。

normalmapも合わせて色の変化やにじみとかもやれるようにすると
もっと表現の幅が広がりそうなのでもしかすると続きを書くかも。

とりあえず一旦ここまで。
ゼノブレイド2やるぞーやるぞー…。

追記

Unity 5.6.0以前で開くと、エラーが出ていた。
Shaderに

#include "UnityCG.cginc"

が必要みたいだったので、追記しておきました。

【Unity】第六回 unity1week「space」参加作品「スペースキューブ」の開発

ぐーるです。
ついに始まった、unity1week。
皆勤賞で今回も参加しました。

f:id:ghoul_life:20171121143440p:plain

今回の作品はこちら。
https://unityroom.com/games/spaceproj

unityちゃんが宇宙貨物をスペースシャトルに運ぶゲームです。
三次元的に登ったり降りたり動かしたり重ねたりといった工夫を凝らして
ステージをクリアするパズルゲームです。全10ステージ。

お題について

「space」宇宙モノか隙間モノか…と漠然と考える。

大きな分類で考えると

で二分されそう。

うーん、どっちかって言うとシューティングゲームかなぁ。
なんて思っていたのだけど。どうも食指が動かない。

まぁ、グラディウスとかのような名作をイメージしてしまうのは
必然というかそういうのしか頭に浮かばない。
じゃあ、カジュアルゲーム
これも前にやったしなぁ。と考え直す。
「!」
と来たのは隙間は隙間でも箱をどこに置くか?といったスキマを埋める系のゲーム。
じゃあこれにしてみようかな、と作り始めた。

まず初めに

ざっくりイメージを固める。

ステージがあって、荷物は1つまたは複数で、荷物を指定の位置に置くことが出来たらクリア

というシンプルなものをイメージ。
spaceは「スキマに置け!」みたいな語呂合わせで何とか乗り切ってしまえばいいや。
と軽く考えていたが、全体的な雰囲気を宇宙っぽくしようと考える。

blender

ちょっと怪しげなキューブにすれば、それっぽく見えるだろう。
でもモデルのUV画像を取得して編集するにはどうすればいいんだ?
という事で調査して手順化して下記にまとめておきました。

ghoul-life.hatenablog.com

モデルづくり

ゲーム内で使ったモデルはunich-chanを覗いて全て自作です。
今思えばunich-chanも止めて宇宙服の人とかにすれば良かったかなー。

キューブはこんな感じでBlenderで。

f:id:ghoul_life:20171121131441p:plain

やっとプログラムに入る

テンプレートやTweenアニメーションエンジンは一切使わないで全部自作。
unityの基本APIのみで実装していきます。

ステージ構成

大きく分けて3つに分類しました。

  • Floor
  • Gimmick
  • Player
Floor

足場になるオブジェクト。プレイヤーは動かすことが出来ないシンプルなもの。コードもシンプル。
起点と終点をセット出来て、配置して命令(InitMove)を出すと終点に移動してから固定される。

また、Stateを持っていて、基点となるGameManagerが状態を監視している。
StateがCreateFinishになると「あ、ステージ作成完了したんだな」と分かるよくあるヤーツです。

今回のゲームでは1つしか無かったが、親クラス(FloorObj)で管理していて、
色々なオブジェクトを使えるようにしている。

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

public class FloorManager : SingletonMonoBehavior<FloorManager> {

    protected STATE_STAGE _state;
    protected List<GameObject> gameObjList;
    public GameObject floorObjPrefab;
    private const float ADD_INIT_Y_VAL = 20.0f; // 初期位置の高さ。ここから降ってくる

    // Update is called once per frame
    void Update () {
        // ステージ作成中
        if (State == STATE_STAGE.CREATE)
        {
            bool isCreateFinish = true;
            foreach (var go in gameObjList)
            {
                var fo = go.GetComponent<FloorObj>();
                if (fo.IsInitMove) // まだ初期化動作が終わっていない
                {
                    isCreateFinish = false;
                    break;
                }
            }
            if (isCreateFinish)
            {
                State = STATE_STAGE.CREATE_FINISH;
            }
        }
    }

    public void Init()
    {
        Clean();
        if (gameObjList == null)
        {
            gameObjList = new List<GameObject>();
        }
    }
    /**
     * Create Stage Floor
     */
    public void Create()
    {
        State = STATE_STAGE.CREATE;

        var stageDatas = Define.GetFloorList();

        var xLen = stageDatas.Length;
        for(var xIdx = 0; xIdx < xLen; xIdx++)
        {
            var zLen = stageDatas[xIdx].Length;
            for(var zIdx = 0; zIdx < zLen; zIdx++)
            {
                var yLen = stageDatas[xIdx][zIdx];
                for (var yIdx = 0; yIdx < yLen; yIdx++)
                {
                    var targetPos = new Vector3(xIdx, yIdx, zIdx);
                    var p = new Vector3(xIdx, yIdx + ADD_INIT_Y_VAL, zIdx);
                    CreateFloorObj(targetPos, p);
                }
            }
        }
    }

    public void Clean()
    {
        State = STATE_STAGE.CLEAN;
        CleanGameObjList();
        State = STATE_STAGE.CLEAN_FINISH;
    }

    protected void CleanGameObjList()
    {
        if (gameObjList != null)
        {
            for (var i = 0; i < gameObjList.Count; i++)
            {
                var go = gameObjList[i];
                Destroy(go.gameObject);
            }
            gameObjList.Clear();
        }
    }

    //-------------------------------------------------------------------
    /**
     * floor prefab create
     */
    private void CreateFloorObj(Vector3 targetPos, Vector3 initPos)
    {
        var go = Instantiate(floorObjPrefab);
        var fo = go.GetComponent<FloorObj>();
        if (fo)
        {

            go.transform.SetParent(this.transform);
            go.transform.position = initPos;
            fo.targetPos = targetPos;
            fo.waitTime = Random.Range(0, 2.0f);
            fo.InitMove();

            gameObjList.Add(go);
        }
        else
        {
            Debug.LogError("ERR CreateFloorObj. please add FloorObj script!!");
            Destroy(go);
        }
    }

    public STATE_STAGE State
    {
        get { return _state; }
        set { _state = value; }
    }

    public bool isCreate()
    {
        return _state == STATE_STAGE.CREATE;
    }
}
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class FloorObj : MonoBehaviour {

    public Vector3 targetPos; // ここに向かって動く
    public float waitTime; // 動き出すまでに待機時間を設定してランダム性を出す
    private bool isInitMove = true;
    private float time = 20.0f;
	// Use this for initialization
	void Start () {
	}
	
	// Update is called once per frame
	void Update () {
	}

    public void InitMove()
    {
        if (isInitMove)
        {
            StartCoroutine(_InitMove());
        }
        
    }

    private IEnumerator _InitMove()
    {
        yield return new WaitForSeconds(waitTime);

        while(Vector3.Distance(this.transform.position, targetPos) > 0.01f)
        {
            this.transform.position = Vector3.MoveTowards(this.transform.position, targetPos, time * Time.deltaTime);
            yield return new WaitForEndOfFrame();
        }
        this.transform.position = targetPos;
        isInitMove = false;

        yield return null;
    }

    public bool IsInitMove
    {
        get { return isInitMove; }
    }
}
Gimmick

ゴールも荷物も全てギミックとしました。
Carry , Beam といった子クラスがあり、クラス別に独自の動作が出来る。
Rigidbodyは使わず、Colliderのみ。
その中のCarryを抜粋。当たり判定はRaycastで行っています。
詳しくは後述。

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

public class Carry : GimmickObj {

    private bool isMove;
    private float moveTime = 1.0f;
    private float downMoveTime = 5.0f;
    private HIT_DIR hitDir;

    Ray leftRay;
    Ray rightRay;
    Ray forwardRay;
    Ray backRay;
    Ray downRay;

    // Use this for initialization
    void Start () {
        isMove = false;
        hitDir = HIT_DIR.NONE;
    }

    void FixedUpdate()
    {
        var p = this.transform.position;

        RaycastHit hit;
        leftRay = new Ray(p, Define.vectorLeft);
        rightRay = new Ray(p, Define.vectorRight);
        forwardRay = new Ray(p, Define.vectorForward);
        backRay = new Ray(p, Define.vectorBack);
        downRay = new Ray(p, Define.vectorDown);
        var distance = 1.0f;

        if (!isMove)
        {
            // 落下判定
            if (!Physics.Raycast(downRay, out hit, distance, LayerMask.GetMask("Floor", "Gimmick")))
            {
                if (!AudioManager.Instance.IsPlaySE()) AudioManager.Instance.PlaySE(AudioManager.SE.FALL_BOX);
                MoveDown();
            }

            // player接触判定
            if (Physics.Raycast(leftRay, out hit, distance, LayerMask.GetMask("Player")))
            {
                hitDir = HIT_DIR.LEFT;
            }
            else
            if (Physics.Raycast(rightRay, out hit, distance, LayerMask.GetMask("Player")))
            {
                hitDir = HIT_DIR.RIGHT;
            }
            else
            if (Physics.Raycast(forwardRay, out hit, distance, LayerMask.GetMask("Player")))
            {
                hitDir = HIT_DIR.FORWARD;
            }
            else
            if (Physics.Raycast(backRay, out hit, distance, LayerMask.GetMask("Player")))
            {
                hitDir = HIT_DIR.BACK;
            }
            else
            {
                hitDir = HIT_DIR.NONE;
            }
        }
    }

    public bool MoveLeft() { return Move(Define.vectorLeft , moveTime); }
    public bool MoveRight() { return Move(Define.vectorRight, moveTime); }
    public bool MoveForward() { return Move(Define.vectorForward, moveTime); }
    public bool MoveBack() { return Move(Define.vectorBack, moveTime); }
    public bool MoveUp() { return Move(Define.vectorUp, moveTime); }
    public bool MoveDown() { return Move(Define.vectorDown, downMoveTime); }

    // 移動
    private bool Move(Vector3 movePos , float tmpMoveTime)
    {
        if (isMove) { return false; }
        RaycastHit hit;
        var ray = new Ray(this.transform.position , movePos);
        var distance = 1.0f;
        // 衝突判定
        if (Physics.Raycast(ray, out hit, distance, LayerMask.GetMask("Floor" , "Gimmick")))
        {
            var fo = hit.collider.gameObject.GetComponent<FloorObj>();
            var gio = hit.collider.gameObject.GetComponent<GimmickObj>();
            if (fo != null || gio != null)
            {
                // 障害物に衝突している
                return false;
            }

        }


        isMove = true; // 移動中状態フラグを立てる
        var p = this.transform.position;
        p += movePos;

        MoveControl.Instance.Move(this.gameObject, p, tmpMoveTime, FinishMove);

        return true;
    }

    private void FinishMove()
    {
        isMove = false; // 移動完了でフラグを下ろす
    }

    public HIT_DIR HitDir
    {
        get { return hitDir; }
    }
}
Player

プレイヤーです。これも1つしかなかったが、実はFloor同様複数作ることが出来るようになってます。
プレイヤーを切り替えながら橋を架けるようなステージがあっても面白いかも。
と思いましたが、一旦保留に。

オブジェクトの移動

上でお見せしているCarryのソースコードを見れば一目瞭然ですが、
オブジェクトは前後上下左右でRayを飛ばしていて、

「今何とどのように接触しているか?」

を常に取得するようにし、それによって動作を変えるようにしました。
行動を起こす前にも同様にチェックして障害物が無いかを確認。

ここは最後まで悩みました。2Dだと

{0,1,1,0,0,1,0,1}
{0,1,1,0,1,1,0,1}
{0,1,1,0,1,1,0,1}
{0,0,0,0,0,0,0,1}

といったマップを持っておいて、
マップ情報を元に通れるか通れないかを判定することがありますが、今回は不採用。

また、移動処理だけ行うクラスを用意してそこで移動させています。
callbackを設定しておいて、完了通知が飛ばせるようにしてあります。

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

/**
 * objectの移動を司る
 */
public class MoveControl : SingletonMonoBehavior<MoveControl> {

    public void Move(GameObject obj, Vector3 targetPos, float moveTime , System.Action callback)
    {
        StartCoroutine(_Move(obj , targetPos , moveTime , callback));
    }

    private IEnumerator _Move(GameObject obj, Vector3 targetPos, float moveTime , System.Action callback)
    {
        // ある程度まで近づいたら
        while (Vector3.Distance(obj.transform.position, targetPos) > 0.01f)
        {
            obj.transform.position = Vector3.MoveTowards(obj.transform.position, targetPos, moveTime * Time.deltaTime);
            yield return new WaitForEndOfFrame();
        }
        obj.transform.position = targetPos; // 強制的に指定位置に
        
        callback(); // 完了通知

        yield return null;
    }
}


(今思えばSphereでRaycast飛ばして範囲に入ったら、お互いのforwardで内積でも取ればどこで接触しているか取れそう
だなーとは思った…今度やってみよう。)

ジャンプ

最初はありませんでした。ほんとに倉庫番というゲームまんまだったので。

オリジナル要素をもう少し入れたいなーと思った時に3Dらしく上下にも動かしたいなと。
入れてみたら思いの外面白かったのでそのまま採用へ。

MoveControlでMove -> Moveです。
なので二段以上の壁でジャンプすると上には行くが、前には進めないという現象が出るようになっています。
これはこれでカワイイのでそのままにしました。

f:id:ghoul_life:20171121135710g:plain

オブジェクトの再配置

オブジェクト、プレイヤーを画面外に落とすと、初期位置に戻ります。

これ実は「デバッグ機能」でした。

ステージから落ちれない

という仕様にするのが一般的ですが
実装を後回しにしてて、「落ちたら初期位置に戻しゃいいや」と適当スクリプトでガシガシ先に進めてた所
これを利用したステージを思いつき、コレ良いんじゃないかなとそのまま採用へ。

アセットについて

unitychanとSkyboxで使用しました。

周りを見るとみんな一杯使ってるんだなーと。
一週間だからどんどん使った方が良さそうではありますが。
勉強第一。DIY精神で自作頑張ります。(←unityちゃん使ってるヤツが言うセリフではない)

感想

仕事がずっと過渡期でして。
拘束十二時間超えがまれによくある会社とかおかしくないですか。

久しぶりにunity触って、過去の作品の実装とか思い出しながらやってましたが、
思ったよりも忘れてなかった。

自分はこういうゲームが好きだ。
ここ一週間、毎日通勤中にステージ考えて、
evernoteにガシガシアイデアメモってるのも楽しかった。
「家帰ったらUIガガッとやるぞ!」なんて思いながら仕事してました。

もし良かったらちょっとだけやってみて下さいー。
そんなに時間は取らせない…はず。