74回生 harady
こんにちは。74回生のharady(原井)です。今回初めて部誌を書くので多分初めましてだと思います。偉大なる先輩方の精進っぷりを指をくわえて眺めていたら、気づいたらもう中3になってました。嘘やろ...。時がたつのは早いので皆さん気を付けましょう。
今回の文化祭に向けて僕は、VRゲーム「寿司(が)打」を作りました。この部誌では、このゲームをUnity+Oculusで3(00*4以上)分かけて作った様子をまとめました。一応ツールなどは小冊子にまとめたのですがここにも書いておくと、材料は
です。VRゲームはおろか、普通のゲームですらUnityで完成させたことがなかったのでUnityの使い方として良くない点があるかもしれませんが、大目に見て下さい。また作り始めたのが4月に入ってからなので、随所に手抜き工事の痕跡が残っています。許ちて。それと表記揺れがあるかもしれませんが、見逃して下さい。
それでは「寿司(が)打」制作記の始まり始まり~
まず、何をするにしても、行動計画を立てることは重要です。このゲームの制作過程を数段階に分けて書くと、
といった感じになるかと思います。以降ではこれに従って書いていきます。
このゲームのコンセプトは初めから決まっていました。「寿司の気持ちになるゲーム」です。回転寿司の寿司の気持ちになるVRゲームを作ったら面白いんじゃね?w、というネタ会話からすべては始まりました。
作る前から決めていた仕様は
ぐらいでしょうか。書いてから気付きましたが、ただの回転寿司ですね。これらの実装はプログラミングの項にまとめます。
制作を開始する前に少し準備が必要です。まずVR対応するUnityのProject側の設定をしなくてはなりません。これは簡単で、上のメニューバーからEdit → ProjectSettings → Player → Other Settings → Virtual Realty supportedにチェックを入れるだけです。Oculusを接続していたらおそらく自動で認識されます。
あとOculusの公式が配布しているOculusPlatformとOVRとOvrAvatarをUnityにインポートして下さい。これらの詳しい使い方はググって...。一応OvrAvatarの使い方だけ説明すると、OvrAvatar → Content → Prefabs → LocalAvatarをヒエラルキーにコピーすればよいです。カメラにはOVR Camera RigとOVR Managerをアタッチします。
いざ制作スタート! ...モデルが、無い...。3Dモデルが無いゲームを3Dゲームと呼んでいいのか?断じて否だ!
はい、というわけでまずモデルを用意するところから始めましょう。自分の欲しい3Dモデルは、WebのフリーモデルやUnityのAsset Storeを探せば大抵見つかりますが、探すのに時間が掛かるし、目的のモデルが見つかるとも限りません。そんな時は自分でモデルを用意してしまいましょう!(嘘つけ絶対こっちのほうが時間掛かるやろ)
モデル制作はBlenderというフリーソフトを使って行いました。Blenderの使い方はkota氏が部誌にまとめていますが皿とレーンの作り方を大雑把にまとめると、
断面の半分を作ってスクリューモディファイアで1周し、それに再分割曲面モディファイアを適用した...んだと思います。覚えてません...テクスチャはUV展開して然るべき場所にネットで見つけた六角形の画像を加工して貼り付けました。
直線部分と円周部分を分けて作りました。円周部分の作り方は皿と同じです。テクスチャはめんどくさかったのでUV展開せず、Blenderのマテリアル割り当て機能を使いました。
完成したらオブジェクトの原点?(Pivot)をCtrl+Alt+Shift+Cで重心に動かしましょう。これを怠るとUnityでの作業がとてもやりにくくなります。
Blenderを使う上で覚えておいたほうが良いショートカットがいくつかあります。AキーやUnDoのCtrl+ZやReDoのCtrl+Shift+Zなどは必須だと思います。他にもたくさんあるのでググるなりなんなりして下さい。
できた3Dモデルを.fbxでメッシュのみエクスポートしておきます。(.blendのままでも良いみたいなのですが、ライトなどがくっついてきて邪魔だったので.fbxにしました)。ここで問題になるのが座標系です。Blenderは右手座標系のZ-up、Unityは左手座標系のY-upなのでBlenderで作ったモデルをそのままエクスポートするとUnityにインポートする際に向きが変わってしまいます。エクスポートするときに左下のメニューで前方をZ、上をYに設定し、トランスフォームを適用にチェックを入れましょう。これをしないと後々少しめんどくさいことになります。
さぁUnityを使うぞ! ...どうやって画面を作るんだ...
Unityを使ったゲーム制作は、まずモデルを配置するところから始まります。まずはUnityのメニューバーのGameObject → 3D Object → Cubeで立方体を作りましょう。こいつを拡大して床にします。このままでは白色1色で味気ないのでテクスチャを設定します。Asset Storeで見つけたフリーの木の床のテクスチャを使いました。
床ができたら次はレーンの配置です。先程作ったレーンをUnityにインポートして(0,~,0)に置きます。Y座標は上下を表すので適宜調整しましょう。インポートする際はモデルとテクスチャを一緒にインポートしましょう。そうしないとモデルにテクスチャがつきません(多分)。
レーンが置けたら次は皿を設置します。皿を動かすプログラムの関係で円周部と直線部の境目に置きました。ここで僕は気付きました。なんか皿がのっぺりしてる...
この問題の解決には少し時間がかかりました。Blender側でマテリアルをいじっても全然反映されないのです。考えてみるとこれは当たり前で、Blenderで設定しているのはあくまでBlenderでレンダリングする際の設定なので、この設定がUnityに反映される訳がないのです。という訳でUnity側でマテリアルを編集しましょう。
めでたく皿が光るようになりました。やれやれ、やっとできた...ダメでした。ちょっと前に書いたBlenderのエクスポート設定を知らなかった自分は普通に皿を回転させて使っていたのです。ここに大きな罠が潜んでいて、Unityにおいて回転されたオブジェクトのx,y,z軸は回転する前と角度が変わってしまうのです。モデルをもう一度エクスポートし直せばいいのですが、そうと知らなかった当時の僕は皿のモデルの親にEmptyを作り、Emptyを回転させることで解決しました。この問題はこのあと腕を実装するときにも再び発生します。何はともあれひとまず問題は解決しました。
あとは適当に壁や天井を作りましょう。ここは本質ではないので手抜きです。
天井を設置すると太陽光(最初からあるDirectional Light,平行光線)が通らなくなり画面が暗くなります。そこで天井の中心付近にPoint Lightを1個設置しました。
これで一通り配置できました。お次はプログラミングです。一番面倒、というかこれが本編かもしれません。
おい、このゲーム動かねぇぞ! 当たり前だろ、プログラム1個も書いてないんだぞ! はい。プログラムのないゲームが動くはずがないのでプログラミングしていきましょう。ちなみに言語はC#です。
まずはプレイヤーの移動スクリプトです。回転寿司ですのでレーンの上をレーンに沿って動く必要があります。ここからはスクリプトを貼り付けて解説するスタイルで行きます。ちなみにファイル名は適当です。というわけで移動スクリプト...の前に一つ。都合上スクリプトの名前をゲーム内での名前と変えている場合があります。本来スクリプトの名前とclassの後に書くものは同じでなければなりません。
Move.cs
using System.Collections; using System.Collections.Generic; using UnityEngine; public class moveOther : MonoBehaviour { Vector3 center; Vector3 center2; float starttime; // Use this for initialization void Start() { center = GameObject.Find("center").transform.position; center2 = center; center2.x = -center.x; starttime = Time.time; } // Update is called once per frame void Update () { if (transform.position.x > 4f) { transform.RotateAround(center2, Vector3.up, -180f / 14f * Time.deltaTime); } else if (transform.position.x < -4f) { transform.RotateAround(center, Vector3.up, -180f / 14f * Time.deltaTime); } else if (transform.position.z < 0) { transform.position += new Vector3(0.5f * Time.deltaTime, 0, 0); } else { transform.position += new Vector3(-0.5f * Time.deltaTime, 0, 0); } } }
もっと短くなるかもしれませんが、これをもとに説明していきます。
最初の3行はおまじないだと思ってて下さい。"名前空間"でググると大体わかると思います。
次のpublic~
もUnityでスクリプトを作ると最初から書いてある部分で、変更してはなりません。
実際の処理は次の行からです。最初の数行では処理に使う変数を定義しています。変数とは色々な型の値を取り扱うもので、座標を表す変数や整数を表す変数など様々なものがあります。ここでは移動する為に必要な座標を表す変数と時間を記録する変数を定義しています。
'//'はコメントアウトです。//をかいた行は無視されるのでコメントを書くことができます。
次のVoid Start()
でStart
関数を定義しています。Start
のあとの'{'から、数行下の'}'までがStart
関数です。関数というのは、足し算引き算等の様々な処理をまとめて記述したもので、関数を呼び出すだけで、関数内で書かれた処理がすべてまとめて行われます。Start
関数というのはUnityがスクリプトを実行するときに、最初に1回だけ実行される関数で、ここで値の代入やオブジェクトの初期配置等の処理を行います。今回のスクリプトでは皿を動かすときの目印となる地点の座標と、タイマーの最初の時間を決めています。
次のVoid Update()
でUpdate
関数を定義しています。Update
関数はUnityが毎フレーム実行する関数です。フレームというのはすべての処理を始めてから終わるまでの時間で、1秒間に数十から数百回フレームが加算されます。フレーム数はプログラムの重さによって変わります。重いゲームでは、処理に時間がかかるので、必然的にフレーム数は落ちます。秒間フレーム数は常に変化するので、移動用の関数などをUpdate
関数内で使う場合には後述する処理が必要です。
ではUpdate()
関数内の移動処理を解説していきます。はじめのifは条件分岐です。()内の条件を満たしていると{}内の処理が行われます。その次のelse内に書かれた処理は直前のif文の条件を満たしていない場合に実行されます。else ifは直前のif文の条件を満たしていない、かつ()内の条件を満たしているときに実行されます。詳しく見ていきましょう。
3つの()内で皿が今どこにいるかを判断しています。直線部なのか円周部なのか、みたいな感じです。直線部ならtransform.position(皿の場所)のX座標を1秒に0.5ずつ増やし、円周部ではtransform.RotateAround()
関数でcenterという座標を中心に毎秒180/14°回しています。Vector3というのは3次元ベクトルを表す型で、Time.deltaTime
は先程書いたUpdate
関数の問題点を解決する為に書いています。Time.deltaTime
でそのフレームを処理するのにかかった時間(秒)を取得することができます。毎フレームで秒速 * 秒だけ処理を行うことで一定の変化を表現できるというわけです。長くなるので次からは一部抜粋して載せていこうと思います。
このゲームでは腕が飛んでくる仕様なのでお次は腕を生成するスクリプトと生成した腕を動かすスクリプト。その前に腕の説明をしておくと、現状Unityの標準の3DモデルであるCylinderを利用しています。文化祭本番では腕のモデルになってる...かもしれません。まずは生成から
eatSushi.cs
public class eatsushi : MonoBehaviour { Transform arm_point; Transform dish; public GameObject arm_pf; float time; // Use this for initialization void Start() { arm_point = transform.Find("arm"); dish = GameObject.Find("Main Camera").transform; Vector3 add = arm_point.transform.position; add.x = add.x + Random.Range(-0.5f, 0.5f) - 0.5f; add.y += Random.Range(-0.5f, 0.5f); arm_point.transform.position = add; arm_point.transform.LookAt(dish); time = 0; Instantiate(arm_pf,arm_point.transform.position,arm_point.transform.rotation); } // Update is called once per frame void Update() { time += Time.deltaTime; if (time > 5f) { Instantiate(arm_pf, arm_point.transform.position, arm_point.transform.rotation); time = 0; } } }
ちょっと長いですね...では解説していきます。
まずこのスクリプトは捕食者に対してアタッチします。
はじめのTransform
はオブジェクトの位置や角度などをまとめて記憶する型です。座標と角度を指定して使う関数の引数に指定できます。GameObject
は、1つのオブジェクトの持つ情報をすべて格納できる型です。
Start
関数内では生成すべき腕を読み込み、所定の座標に乱数を足した座標を生成し、Instantiate()
で生成しています。また、一定時間ごとに生成する為にタイマーを設定しています。
Update
関数内で5秒ごとに生成する処理を記述しています。Start
関数内で処理を開始した時間を記録しておき、Time.time
で取得した今の時間との差が5を超えたときにタイマーを0にセットし、腕を生成すればいいわけです。
お次は腕を動かすスクリプト。命名が適当だけど許ちて。
Shot.cs
public class momove : MonoBehaviour { Transform dish; // Use this for initialization float starttime; int attack; void Start () { dish = GameObject.Find("TrackingSpace/CenterEyeAnchor").transform; //transform.Rotate(new Vector3(1, 0, 0) * 180); transform.LookAt(dish); starttime = Time.time; attack = Random.Range(0, 4); } // Update is called once per frame void Update () { if (attack == 0) normalAttack(); else if (attack == 1) fastAttack(); else ballisticAttack(); } void normalAttack() { transform.LookAt(dish); transform.Rotate(new Vector3(1, 0, 0) * 90); transform.Translate(Vector3.up * Time.deltaTime * 0.75f); } void fastAttack() { transform.LookAt(dish); transform.Rotate(new Vector3(1, 0, 0) * 90); transform.Translate(Vector3.up * Time.deltaTime * 1f); } void ballisticAttackfh() { transform.Translate(transform.up * Time.deltaTime * 1.5f); } void ballisticAttacklh() { transform.LookAt(dish); transform.Rotate(new Vector3(1, 0, 0) * 90); transform.Translate(Vector3.up * Time.deltaTime * 2f); } void ballisticAttack() { if (Time.time - starttime < 3f) { ballisticAttackfh(); } else { ballisticAttacklh(); } } }
長い...。この関数は腕自体にアタッチします。生成するスクリプトから制御しようと思ったのですが、複数生成されたオブジェクトのそれぞれに個別の処理をすることができなかったので、腕自体にアタッチしました。結果的にこのほうがコードが書きやすくなってると思います。
腕には3種類の飛ばし方を設定しました。Start
関数内でランダムに切り替えています。またTransform
型のdish
にVRの目を指定し、そこをめがけて腕が飛ぶようにしています。この仕様によって目の前に腕が迫ってくる演出ができます。
3種類それぞれの説明をする前に1つだけ。各関数のどれも、移動する前にRotateしていたり、移動の方向もオブジェクトに対する前方ではなく上方になっています。ここには大きな罠(?)が潜んでいて、Unityの円柱はもともと前方が側面になっているのです(多分)。腕の親にEmptyを設定して、このEmptyを回せば解決する気がするのですが、実験している時間がなかったのでこのような形になっています。
では3種類を見ていきましょう。
normalAttack
はnormalなAttackです。最もゆっくり飛んできます。最初にdish(目の中心)の方向を向き、少し上に書いた処理を行い、上方向(要するに皿の方向)めがけて秒速0.75で飛ばしています。
fastAttack
はfastなAttackです。速いです。処理方法はnormalAttack
と同じですが、速度が秒速1になっています。
ballisticAttack
はballisticAttackfh
とballisticAttacklh
から出来ています。まず上に飛んで行って、そこから皿めがけて飛んでくる仕様です。飛び始めてからの時間3秒以下の時ballisticAttackfh
が実行され、腕は秒速1.5で上へ飛んでいき、その後ballisticAttacklh
が実行され下向きに秒速2で飛んできます。
どれも飛ばし方自体は大したことありませんね。
では次。パンチ&当たり判定です。多分ここが一番大変でした。飛んできた腕をパンチする部分と、消しきれなかった腕が顔に当たる処理です。2つとも貼ります。
hit.cs
using System.Collections; using System.Collections.Generic; using UnityEngine; using UnityEngine.VR; public class hit : MonoBehaviour { float vl; Vector3 dif; Vector3 dif1; Rigidbody rb; AudioClip sound; die Die; int score = 0; // Use this for initialization void Start () { rb = this.GetComponent<Rigidbody> (); dif = InputTracking.GetLocalPosition(VRNode.RightHand); sound = GetComponent<AudioSource>().clip; Die = GameObject.Find("CenterEyeAnchor").GetComponent<die>(); } void OnTriggerEnter(Collider other) { if (other.gameObject.tag == "enemy") { Destroy(other.gameObject); if (vl > 2f) { Ddie(); GetComponent<AudioSource>().PlayOneShot(sound); } } } // Update is called once per frame void Update () { dif1 = InputTracking.GetLocalPosition(VRNode.RightHand) - dif; vl = Vector3.Magnitude(dif1); vl = vl / Time.deltaTime; dif = InputTracking.GetLocalPosition(VRNode.RightHand); } void Ddie() { Die.combo++; Die.score += Die.scorep; Die.hit = 0; } }
die.cs
public class die : MonoBehaviour { public int hit = 0; public int combo = 0; public int scorep = 2000; public int score = 0; // Use this for initialization void Start () { } void OnTriggerEnter(Collider other) { if (other.gameObject.tag == "enemy") { hit++; Destroy(other.gameObject); } } // Update is called once per frame void Update () { if (hit >= 6) { hit = 0; combo = 0; } if (combo >= 5) { scorep += 10000; } } }
上が(右)腕と用スクリプトで下がプレイヤーへの当たり判定用のスクリプトです。右腕と書いたのは、スクリプト中でRightHandと書いてある部分を左腕ではLeftHandにする必要があるからです。VR機器のデータを取り扱う場合には上のスクリプトのようにusing UnityEngine.VR
を記述する必要があります。上のスクリプトで音を鳴らす処理をしている箇所があります。音はAsset Storeなどで探しましょう。さすがに自作する気力はありませんでした。音が用意出来たら右腕(OvrAvatarを使っている場合)Tracking Space直下のRightHandAnchorにアタッチします。
先に上のスクリプトの処理の中身を書きます。上のスクリプトではパンチに関する判定を行っています。具体的には自分の腕が飛んできた腕に一定の速度以上でぶつかったときに飛んできた腕を消すという処理です。では説明していきます。
初めの変数の宣言でRigidbody
とdie
という新しい型が登場しています。Rigidbody
はUnityが物理演算の為に使う項目です。die
型の変数を宣言すると、その変数がdie.cs(下のスクリプト)に書いてある内容を取得する為に使えるようになります。Start
関数内でdie
型のDieに、CenterEyeAnchor(目の中心)にアタッチされたdie.csを代入しています。あるオブジェクトに付いている要素を取得するには、対象のオブジェクト.GetComponent<取得したい要素>()
を実行すれば良いです。対象のオブジェクトは、対象がアタッチ先のGameObject
の場合は書かなくてもよいですが(わかりやすくする為にthisと書いてもよい)、そうでない場合はGameObject.Find("対象の名前")
で対象のGameObject
を発見できます。またdifに右腕の座標(現実世界における)を代入しています。
OnTriggerEnter
関数は物体が他の物体に当たった時に呼び出される関数です。関数の中身は自分で記述します。この関数を利用する為には衝突する物体と衝突される物体の双方にColliderとRigidbodyコンポーネントがアタッチされている必要があります。()内のotherは関数の引数です。ここに衝突した相手の情報が入って関数が呼び出されます。衝突した相手のtag(詳細はググって)がenemyの時に衝突相手を消します。この場合だと飛んできた腕が消えます。また次のif文は衝突時のプレイヤーのパンチの速さによって分岐します。パンチが一定速度以上の時に音が鳴り、Ddie関数が呼ばれます。Ddie
関数は下のdie.csで定義したcombo
を増やす処理とスコアを加算する処理とhitを帳消しにする処理を記述しました。
パンチの速度の検知が少し大変でした。試行錯誤の結果、現在の拳の座標(dif1)と1フレーム前の拳の座標(dif)の3次元ベクトルの差を取得してベクトルの長さを計算し、Time.deltaTime
で割っています。なぜTime.deltaTime
で割るかですが、理由は簡単で、速さ = 距離 / 時間だからです。ベクトルの長さはVector3.Magnitude()
で求められます。この関数は与えられた座標のsprt(x*x + y*y + z*z)
を返します。
図: 3次元ベクトルの長さ
はじめはRightHandAnchorのUnity内での座標を取得して処理しようと思ったのですが、皿ごと動くので常に値が変わってしまうという問題があり、次に皿に対する相対座標でやろうとしたのですがそれも上手くいかず困っていたのですが、普通にコントローラーの現実の座標を取得することで処理できました。ちなみにここではUnityの標準の機能を使ってコントローラーの位置を取得していますが、別にOculusの機能を使っても構いません。
パンチ判定はUpdate関数内で行っています。毎フレーム更新されるのでいつOnTriggerEnterが呼び出されても大丈夫です。では下のスクリプトの解説に移りましょう。下のスクリプトはCenterEyeAnchorにアタッチします。
combo
は見ての通りコンボです。scorepはスコアをどれだけ加算するかを示しています。combo
の数に応じてこの値を増やします。score
も見ての通りスコアです。3つともpublic
と書いていますが、これを書くことによってこの変数が他のスクリプトから参照できるようになります。今回はhit.csで3つの値を加算するのでそれぞれpublic
をつけています。このスクリプトは目の中心にアタッチされています。目の中心を中心とする球状に当たり判定が存在し、ここに飛んできた腕が当たると、(パンチに失敗すると)飛んできた腕が消え、hitが加算されます。hitが6以上になるとコンボが途切れます。hit.csのDdie
内の処理によって、プレイヤーが飛んできた腕をパンチした際にhitが0になるよう設定されているので、6連続で飛んでくる腕を触ることすらできなかった場合のみコンボが途切れる仕様です。
お次はスコア表示。これはスコアの計算の実装は済んでいるので表示するだけです。スクリプトを書く前にスコアを表示する場所を用意して、そこにCanvasをWorldSpaceで設置し、直下に白色のImageとTextを2つ配置しました。Textが意味不明にぼやけましたが、Canvas ScalerのDynamic Pixel Per Unitの値を大きくすることで解決します。やりすぎ注意。片方は単純にScoreという文字列を書くだけです。もう片方はその少し下に配置し、先ほど計算したscoreの値を表示します。Canvasの下のどの順番で並べるかで表示順が変わるので気を付けて下さい。
では表示スクリプト。
NowScore.cs
using System.Collections; using System.Collections.Generic; using UnityEngine.UI; using UnityEngine; public class NowScore : MonoBehaviour { Text targetText; die Die; // Use this for initialization void Start () { targetText = GetComponent<Text>(); Die = GameObject.Find("CenterEyeAnchor").GetComponent<die>(); } // Update is called once per frame void Update () { targetText.text = Die.score.ToString(); } }
このスクリプトはスコアを表示する為のTextにアタッチします。
Textを扱う際にはusing UnityEngine.UI
を書きます。このスクリプトは少し理解しにくいです。
初めにText
型のtargetText
を宣言しています。ここにStart
関数内でスコアを表示する為のTextというGameObject
のText要素を代入します(わかりにくいですね)。またDieにはCenterEyeAnchorのdieを代入します。あとは簡単です。フレーム毎にtargetText.text(targetTextの文字列)にDie.score
を代入すれば良いだけです。ただしDie.score
はint
(整数)でtargetText.text
はstring
(文字列)なのでDie.score
をToString()
で文字列に変換する必要があります。
ちなみに画像に使われているフォントはGreen EnergyフォントとMKSD-UltraLightフォントです。左のフォントはとある飲料で見たことがある人がいるかもしれません。
PlayerPrefs機能を使っています。詳細はググって下さい。
どうでしたか?VR3(00*4)分クッキング。初めて部誌を書くので読みにくい文章になってしまったかもしれません。あるいはスクリプトの印象しか残らないかも...。まぁこの部誌でVRゲームの作り方やゲーム制作の手軽さが少しでも分かってもらえたら嬉しいです(絶対手軽さは伝わりませんねw)。今回のゲーム制作で僕は初めてUnityでゲームを完成させました。またBlenderも初めて使いました。すべての工程の感想としては、「なにか問題が起こった時に原因を考えて直す作業や、自分の作りたい物をモデリングするのは楽しい」ということでしょうか。また一つアドバイスをすると、わからない部分が出てきたり謎のエラーが発生することはよくあります。そんな時はすぐにググりましょう。Google先生は常に私たちに道を示してくれます。あと、ベクトルの長さの図は吉岡先輩に作って頂きました。ありがとうございました。
...なんかめちゃくちゃ堅苦しくなりましたね(ほんまか?)。偉大なる先輩方に倣ってネタ性に溢れるクッソ面白い部誌を書くつもりだったのですが...。どうやらまだ精進が足りないようです。
今回のゲームはNeutralさんのタイピングゲーム「寿司打」から名前を貰っています。部員もよくやる(?)とても素晴らしいタイピングゲームです。皆さんも挑戦してみて下さい。筆者は雑魚なので高級コース15000円ぐらいが限界です。プロタイプerになりたい。
部誌を書き始めたのが本番数日前なので誤字等あるかもしれませんが大目に見て下さい...。まだ文化祭が終わってないなら是非寿司(が)打をやってみて下さいね。ではまた来年の部誌で会いましょう!