ICON公式ブログ

ICON公式ブログ

静岡大学コンテンツ制作サークルICONの公式ブログです

Unityで美少女JKを召喚し、赤スパを投げて優勝した話【ICONサマーブログリレー2020 6日目】

f:id:iconcreator:20200926111727p:plain



この記事はICONサマーブログリレー2020 6日目の記事です。

はじめに

おはようございます。今年度プロ班班長のCrepeです。サマーブログは初参加でございます。見苦しい点もあるかと思いますが、ご容赦ください。

さて、今回は供養も兼ねて、私の趣味に捧げた数か月間(まぁ大学の授業もあったので実際はそんなに長くないですが)の有様を垂れ流そうという趣旨でお送りいたします。ふわっと概要を紹介するだけなので、内容はないです。(書きたいことしか書いてない)気楽に見ていって下さい。

なお、今回の記事では細部をすっ飛ばしているので、もし真似して再現したいとなると、ある程度はUnityの基本的知識が必要になります。予めご了承ください。

では、参りましょう。

美少女JKを召喚する。

本記事は健全なコンテンツです

ということで、美少女JKを召喚して優勝していくことにするわね。字面だけ見ると隠語感がすごくて、犯罪の臭いがしてきますが、ICONは健全な組織です。万が一、私が人道を外れたときは、会長に私の息の根を止めて貰うよう約束を(一方的に)しています。安心ですね。

まあ、こんな感じの物を目指します。※画像はイメージです。*1
(ステージはユニティちゃんライブステージ! -Candy Rock Star-を使用しています。)

f:id:iconcreator:20200924162058p:plain

 

今回、使用する3Dモデルは、カバー株式会社が運営するVtuber事務所『ホロライブ』所属、夏色まつりさんMMDモデル(© 2019 cover corp.)となります。著作権表記はここでよいのか、戦々恐々としております。
このモデルは公式が提供しているモデルです。公式が二次創作に寛大で助かります…。

そしてまぁ、本人が美少女JKといっているのできっと美少女です。というか普通にかわいいと思います。ホロライブのヤベー奴。ひとまず、本記事の趣旨の一つ、赤スパで殴るならこの方でしょう。

ちなみに、この記事はホロライブ二次創作ガイドライン、および、ファイルに添付されているReadmeにしたがって執筆されています。(万が一ヤバそうな点があればこっそり教えてください。美少女JKが初音ミクさんにすり替わることでしょう。) 

御託はここまで、いざ実装です。

UnityにMMDモデルをインポートする

xr-hub.com

同じ手順でやればできます、読んで。(丸投げ)

ひとつ付け加えますと、上述の方法では踊るモデルが大きく動く場合、つまりフォーメーションの変化があるモーションなどは後述するCollider(当たり判定)がうまくモデルに追従しませんでした。

Blenderを用いてインポートする方法なら問題ないと思われますが、上述の手順に従うなら、位置の変化の少ないモーションデータ(上の記事で用いているものなど)を使うことをお勧めします。

赤スパを投げる

赤スパとは


YouTubeで活動なさっているVtuberさんの方々が主に提供するコンテンツにYouTube Liveによる生配信があります。生配信では、我々視聴者もチャット機能を用いてライバーとほぼリアルタイムで対話をすることができます。

そして、そのチャットの文章と一緒にお金を投げられるシステムがあり、これが「スーパーチャット」と呼ばれるものです。チャット欄で自分のメッセージを目立たせる権利を購入できる機能なのですが、要は投げ銭というやつです。

このスーパーチャット、通称「スパチャ」は投げた金額に応じて色が変わり、1万円以上で赤色となります。これが赤スパと呼ばれます。

最近、この赤スパを大量に投げつけることで、ライバーの反応を楽しむという石油王たちの遊びがたまに見られます。今回モデルに使用する夏色まつりさんも、この被害者(?)であり、この記事で作るゲームの元ネタになります。

赤スパを投げる(物理(演算))


ということで、まずは下準備から始めていきましょう。とりあえず、ない袖は振れぬと申しますし、お金のモデルを用意するところから始めましょうか。Asset Storeを漁るなり,自分で作るなりご自由に。RigidbodyとColliderは必ずくっつけておいてください。
f:id:iconcreator:20200924162336p:plain
今回は札束をなげます。紙をなげたときの軌道計算とか無理です。「スパチャの上限5万円だぞw」「ドル札じゃんw」「剛体に逃げるな」など様々な声があると思いますが聞こえません。
次に、仕様を考えていきます。今回理想とする挙動は


1.画面をタッチしたら「赤スパ」が出現する。
2.そのまま、ゆっくり指を動かすとそれに「赤スパ」が追従する。
3.素早くフリックすると、「赤スパ」がその方向に飛んでいく。

とします。言い忘れていましたが、今回はAndroidバイス向けに作るので実機としてスマホを想定しています。

実装のポイントとなるのは、画面上の指の動きという二次元の動きをどう三次元の動作(今回は前方に赤スパを投射すること)に結び付けるか、スワイプとフリック入力をどう区別するか、の2点です。何はともあれ、コードを書きます。

 

 

コードを見る
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.EventSystems;

public class ThrowSC : MonoBehaviour
{
    private Vector2 startPos, endPos, direction;
    //フリック開始位置、終了位置、フリックした方向

    private GameObject holdingMoney;
    //生成された赤スパを格納する変数

    float startTime, endTime, timeInterval, tempTime;
    //フリック開始時間、終了時間、フリックにかかった時間
    //tempTimeはstartTimeからの経過時間。

    bool isHolding;
    //赤スパが生成されているか否か。


    //[SerializeField]と書くと、private変数でもInspector上で値を変更できるようになる。
    //当時これをやたら使いたくなる呪いにかかっていた。あるあるだと思ってる。

    [SerializeField] float throwForceInXandY = 1f;
    //左右にどの程度力を加えるか

    [SerializeField] float throwForceInZ = 50f;
    //前方にどの程度力を加えるか

    [SerializeField] float TorqueStrength = 1f;
    //赤スパを回転させるためのトルクの強さ。

    [SerializeField] float howClose = 1f;
    //赤スパとカメラの距離

    [SerializeField] float flickTime = 0.4f;
    //このタッチから指を離すまでの時間がこの値(秒)以下ならフリック入力とみなす。

    [SerializeField] float moneyPeriod = 5f;
    //投げた赤スパが何秒残るか。このクラスで管理するのはよくない気がする。

    [SerializeField] float sensitivity = 0.005f;
    //...なんだこれ?多分微調整したときの名残?

    [SerializeField] GameObject moneyModel;
    //赤スパのPrefabを格納。
    //Inspector上で赤スパのモデルをドラッグ&ドロップしてattachしましょう。

    Rigidbody rb;
    //生成した赤スパのRigidbodyを格納しておく変数

    void Update()
    {
        if (Input.touchCount > 0)
            //触れている指の本数が0より大きいか判定(いらないかも)
        {
            if (Input.GetTouch(0).phase == TouchPhase.Began)
                //指が画面に触れた瞬間
            {
                if (!IsTouchJoycon(Input.GetTouch(0).position))
                    //joystickに触れてないか判定
                {
                    startPos = Input.GetTouch(0).position;
                    startTime = Time.time;
                    //startPosとstartTimeを記録

                    Vector3 firstHoldingPos = Input.GetTouch(0).position;
                    firstHoldingPos.z = howClose;
                    Vector3 firstPos = Camera.main.ScreenToWorldPoint(firstHoldingPos);
                    holdingMoney = Instantiate(moneyModel, firstPos, Quaternion.identity);
                    //タッチした場所に赤スパ生成

                    rb = holdingMoney.GetComponent<Rigidbody>();
                    rb.useGravity = false;
                    rb.isKinematic = false;
                    //赤スパのRigidbodyを取得して、物理特性を切っておく。

                    holdingMoney.transform.rotation
                    = Quaternion.Euler(new Vector3(25, 0, 120)
                    + Camera.main.transform.rotation.eulerAngles);
                    //いい感じの角度に調整

                    isHolding = true;
                    //赤スパが生成されたのを記録
                }
            }

            if (isHolding)
                //指が画面に触れている間中
            {
                Vector3 holdingPos = Input.GetTouch(0).position;
                holdingPos.z = howClose;
                Vector3 newPos = Camera.main.ScreenToWorldPoint(holdingPos);
                holdingMoney.transform.position = newPos;
                //指のある位置に赤スパを移動

                holdingMoney.transform.rotation
                    = Quaternion.Euler(new Vector3(25, 0, 120)
                    + Camera.main.transform.rotation.eulerAngles);
                //角度調整。いらないかもしれない

                if (startTime > 0) tempTime = Time.time - startTime;
                if (tempTime > flickTime)
                {
                    startTime = Time.time;
                    startPos = Input.GetTouch(0).position;
                }
                //flickTime(秒)ごとにstartPosとstartTimeを更新
            }

            if (Input.GetTouch(0).phase == TouchPhase.Ended && isHolding)
                //指が離れて、かつ赤スパが生成されているか判定
            {
                endPos = Input.GetTouch(0).position;
                endTime = Time.time;
                timeInterval = endTime - startTime;
                direction = startPos - endPos;
                //endPosとendTime,timeInterval,directionを記録

                if (timeInterval < flickTime)//フリック入力か判定
                {
                    rb.useGravity = true;
                    //重力を適用

                    rb.AddTorque(new Vector3(0, TorqueStrength, 0));
                    //赤スパにトルクを加える。偶力ってやつです。

                    Vector3 localForceDirection = new Vector3(-direction.x * throwForceInXandY,
                        -direction.y * throwForceInXandY,
                        throwForceInZ * Vector3.Distance(startPos, endPos) * sensitivity / timeInterval);
                    //カメラからみた赤スパに加わる力のベクトル

                    rb.AddForce(Camera.main.transform.rotation * localForceDirection);
                    //カメラの向きを考慮して、力を加える。

                    isHolding = false;
                    //赤スパを手放したので記録

                    Destroy(holdingMoney, moneyPeriod);
                    //5秒後に自害せよ、赤スパ。
                }
                else
                //フリックじゃないなら…
                {
                    rb.useGravity = true;
                    rb.AddTorque(new Vector3(0, TorqueStrength, 0));
                    isHolding = false;
                    Destroy(holdingMoney, moneyPeriod);
                    //回転させて落とすだけ
                }
            }
        }

    }

    private bool IsTouchJoycon(Vector2 screenPosition)
        //joystickに触れているかを返す関数。
        //参考記事を後述しときます。
    {
        PointerEventData eventDataCurrentPosition
            = new PointerEventData(EventSystem.current);
        eventDataCurrentPosition.position = screenPosition;

        List<RaycastResult> results = new List<RaycastResult>();
        EventSystem.current.RaycastAll(eventDataCurrentPosition, results);

        return results.Count > 0;
    }
}

 

 

(ど素人なのでいろいろと見苦しい点があります。予めご了承ください。)

はい。細かい部分はコードのコメントを読んでもらうとして、要点だけ。

まず、画面をタップすると、「赤スパ」が出現し、その画面上の位置と時間をそれぞれstartPosstartTimeに記録します。そして、画面から指を話すとその位置と時間がendPosendTimeに記録され、startPosとendPosはともに2次元ベクトルなので、差をとることでフリックした方向が得られます。今回はフリックした長さも飛距離に考慮したいので、単位ベクトルへの変換はしないでおきます。また、startTimeendTimeからフリックにかかった時間も得られます。長さ(正確には変位)と時間が得られればフリックの速度がわかりますね。

あとはこれをどの程度挙動に反映させるかをthrowForceInXandYとかsensitivityとかの変数で決めて、モデルに力を加えて前方に飛ばします。

一応放物線を描いているように見えますが、厳密には下図のような軌道を描いています。まぁ、違和感はほとんどないのでよしとします。

f:id:iconcreator:20200924164946p:plain


次に、スワイプとフリックの区別ですが、意外とこれが曲者です。例えば、指をスワイプし続けそのままフリックした場合、どこまでがスワイプで、どこからがフリックなのでしょうか。フリックの速さや距離を「赤スパ」の飛距離に影響させたいので、できるだけ正確な、あるいは違和感の少ない判別方法をとりたいところです。

今回は、flickTimeという変数を設け、タップから指を離すまでの時間がこの値より小さい場合のみフリックとみなすことにし、flickTime(秒)経過するごとにstartPosstartTimeを更新します。この方法だと、必ずしも正確なフリック入力の情報が得られるとは限りません*2が、まぁ、違和感はほt(ry

このコードを適当に空のGameObjectに貼り付けて、InspectorのmoneyModelに赤スパのPrefabをドラッグ&ドロップしましょう。あとは適宜Inspector上で値を変えて微調整してください。「赤スパ」のRigidbodyの値を変更するのも有効です。

優勝する

というわけで、目の前に美少女JKがいて踊っています。そして、我々は札束を投げられるようになりました。まぁ、カメラの位置とかは各々で何とかしてください。あとは、MMDモデルにColliderをつけて、衝突時の処理をスクリプトに書いて、ParticleSystemとかつければそれっぽくなります。

下のgifでは、joystickを付けたり、スマホジャイロセンサーで視点が動かせるようになっています。流石にこっちは参考記事を後述します。ステージなんかなかった。いいね?*3

f:id:iconcreator:20200925225454g:plain


とがき

いかがだったでしょうか。これ作ってる方は楽しいんですけど、果たして読者に伝わっているのか…?

Unityは慣れると色んな機能が実装できて楽しいですね。情報量も多いので、やりたいことがすぐ実現できます。気力と時間があれば…ですが。

みんな!Unity!やろうね!

次に何か書くときまでにもっと文章力を磨きたい…。また、ご縁があればお会いしましょう~。

参考記事

 

*1:スマホではカックカクになるので、ステージは最後に消えます。この辺りを執筆してるときは動くと思っていた。(おばか)

*2:例えば、flickTime=0.4のとき、ゲーム開始後0.5秒後に0.2秒かけてフリックした場合、フリック開始時点(startPos)はゲーム開始後0.4秒後の位置になり、実質そこから0.3秒かけてフリックしたことになってしまいます。

*3:Score表示ミスってますね…。