teratail header banner
teratail header banner
質問するログイン新規登録

回答編集履歴

1

糸張りスクリプトを追記

2021/06/25 21:10

投稿

Bongo
Bongo

スコア10816

answer CHANGED
@@ -19,4 +19,217 @@
19
19
 
20
20
  ![図3](0f1a8f2aaca330f98252880b968de531.gif)
21
21
 
22
- 他にも、今回の実験では操作が複雑になりそうで実装しませんでしたが、左右の手からそれぞれ糸を発射できるようにするのも面白いかもしれません。1本の糸で飛び上がるのだと高確率で接続点に激突してしまうので高くまで飛び上がるのが難しいですが、両手から2本の糸が出せるのなら[スリングショット](https://ja.wikipedia.org/wiki/%E3%82%B9%E3%83%AA%E3%83%B3%E3%82%B0%E3%82%B7%E3%83%A7%E3%83%83%E3%83%88)のような感じでV字に糸を張って安全に飛び上がることができそうです。
22
+ 他にも、今回の実験では操作が複雑になりそうで実装しませんでしたが、左右の手からそれぞれ糸を発射できるようにするのも面白いかもしれません。1本の糸で飛び上がるのだと高確率で接続点に激突してしまうので高くまで飛び上がるのが難しいですが、両手から2本の糸が出せるのなら[スリングショット](https://ja.wikipedia.org/wiki/%E3%82%B9%E3%83%AA%E3%83%B3%E3%82%B0%E3%82%B7%E3%83%A7%E3%83%83%E3%83%88)のような感じでV字に糸を張って安全に飛び上がることができそうです。
23
+
24
+ # 糸を張るスクリプトについて
25
+
26
+ キャラクターには下図のようなコンポーネントが付いています。見た目の面は[UNITY-CHAN! OFFICIAL WEBSITE](https://unity-chan.com/)の「SDユニティちゃん 3Dモデルデータ」に収録されている「SD_unitychan_humanoid」がベースになっており、操作の面は[Standard Assets](https://assetstore.unity.com/packages/essentials/asset-packs/standard-assets-for-unity-2018-4-32351)に収録されている「ThirdPersonController」がベースになっています。
27
+
28
+ ![図4](1fa3ff56b39a8023dfee283dc1df84de.png)
29
+
30
+ 大部分のコンポーネントはそれらのアセットに収録されているものやUnity組み込みのもので、`StringCaster`だけが独自に作成した糸張り担当のコンポーネントです。
31
+
32
+ ```lang-csharp
33
+ using UnityEngine;
34
+ using UnityEngine.UI;
35
+
36
+ namespace SpiderChan
37
+ {
38
+ // Animator、Rigidbody、LineRendererを必須としている
39
+ // Animator...糸を射出しているかどうかに応じて右手を前に出したり戻したりするのに使用
40
+ // Rigidbody...スクリプト中で直接操作してはいないが、SpringJointの動作に必要
41
+ // LineRenderer...糸を画面上に描画するために使用
42
+ [RequireComponent(typeof(Animator), typeof(Rigidbody), typeof(LineRenderer))]
43
+ public class StringCaster : MonoBehaviour
44
+ {
45
+ [SerializeField] private float maximumDistance = 100.0f; // 糸を伸ばせる最大距離
46
+ [SerializeField] private LayerMask interactiveLayers; // 糸をくっつけられるレイヤー
47
+ [SerializeField] private Vector3 casterCenter = new Vector3(0.0f, 0.5f, 0.0f); // オブジェクトのローカル座標で表した糸の射出位置
48
+ [SerializeField] private float spring = 50.0f; // 糸の物理的挙動を担当するSpringJointのspring
49
+ [SerializeField] private float damper = 20.0f; // 糸の物理的挙動を担当するSpringJointのdamper
50
+ [SerializeField] private float equilibriumLength = 1.0f; // 糸を縮めた時の自然長
51
+ [SerializeField] private float ikTransitionTime = 0.5f; // 糸の射出中に右手を前に伸ばしたり、糸を外した時に右手を戻したりする時の腕位置の遷移時間
52
+ [SerializeField] private RawImage reticle; // 糸を張れるかどうかの状況に合わせて、このRawImageの表示を照準マーク・禁止マークに切り替える
53
+ [SerializeField] private Texture reticleImageValid; // 照準マーク
54
+ [SerializeField] private Texture reticleImageInvalid; // 禁止マーク
55
+
56
+ // 各種コンポーネントへの参照
57
+ private Animator animator;
58
+ private Transform cameraTransform;
59
+ private LineRenderer lineRenderer;
60
+ private SpringJoint springJoint;
61
+
62
+ // 右手を伸ばす・戻す動作のスムージングのための...
63
+ private float currentIkWeight; // 現在のウェイト
64
+ private float targetIkWeight; // 目標ウェイト
65
+ private float ikWeightVelocity; // ウェイト変化率
66
+
67
+ private bool casting; // 糸が射出中かどうかを表すフラグ
68
+ private bool needsUpdateSpring; // FixedUpdate中でSpringJointの状態更新が必要かどうかを表すフラグ
69
+ private float stringLength; // 現在の糸の長さ...この値をFixedUpdate中でSpringJointのmaxDistanceにセットする
70
+ private readonly Vector3[] stringAnchor = new Vector3[2]; // SpringJointのキャラクター側と接着点側の末端
71
+ private Vector3 worldCasterCenter; // casterCenterをワールド座標に変換したもの
72
+
73
+ private void Awake()
74
+ {
75
+ // スクリプト上で使用するコンポーネントへの参照を取得する
76
+ this.animator = this.GetComponent<Animator>();
77
+ this.cameraTransform = Camera.main.transform;
78
+ this.lineRenderer = this.GetComponent<LineRenderer>();
79
+
80
+ // worldCasterCenterはUpdate中でも毎回更新しているが、Awake時にも初回更新を行った
81
+ // ちなみに今回のキャラクターの場合は、キャラクターのCapsuleCollider中心と一致するようにしている
82
+ this.worldCasterCenter = this.transform.TransformPoint(this.casterCenter);
83
+ }
84
+
85
+ private void Update()
86
+ {
87
+ // まず画面中心から真っ正面に伸びるRayを求め、さらにworldCasterCenterから
88
+ // そのRayの衝突点に向かうRayを求める...これを糸の射出方向とする
89
+ this.worldCasterCenter = this.transform.TransformPoint(this.casterCenter);
90
+ var cameraForward = this.cameraTransform.forward;
91
+ var cameraRay = new Ray(this.cameraTransform.position, cameraForward);
92
+ var aimingRay = new Ray(
93
+ this.worldCasterCenter,
94
+ Physics.Raycast(cameraRay, out var focus, float.PositiveInfinity, this.interactiveLayers)
95
+ ? focus.point - this.worldCasterCenter
96
+ : cameraForward);
97
+
98
+ // 射出方向のmaximumDistance以内の距離に糸接着可能な物体があれば、糸を射出できると判断する
99
+ if (Physics.Raycast(aimingRay, out var aimingTarget, this.maximumDistance, this.interactiveLayers))
100
+ {
101
+ // reticleの表示を照準マークに変え...
102
+ this.reticle.texture = this.reticleImageValid;
103
+
104
+ // その状態で糸発射ボタンが押されたら...
105
+ if (Input.GetButtonDown("Shot"))
106
+ {
107
+ this.stringAnchor[1] = aimingTarget.point; // 糸の接着点末端を設定
108
+ this.casting = true; // 「糸を射出中」フラグを立てる
109
+ this.targetIkWeight = 1.0f; // IK目標ウェイトを1にする...つまり右手を射出方向に伸ばそうとする
110
+ this.stringLength = Vector3.Distance(this.worldCasterCenter, aimingTarget.point); // 糸の長さを設定
111
+ this.needsUpdateSpring = true; // 「SpringJoint要更新」フラグを立てる
112
+ }
113
+ }
114
+ else
115
+ {
116
+ // 糸接着不可能なら、reticleの表示を禁止マークに変える
117
+ this.reticle.texture = this.reticleImageInvalid;
118
+ }
119
+
120
+ // 糸を射出中の状態で糸収縮ボタンが押されたら、糸の長さをequilibriumLengthまで縮めさせる
121
+ if (this.casting && Input.GetButtonDown("Contract"))
122
+ {
123
+ this.stringLength = this.equilibriumLength;
124
+ this.needsUpdateSpring = true;
125
+ }
126
+
127
+ // 糸発射ボタンが離されたら...
128
+ if (Input.GetButtonUp("Shot"))
129
+ {
130
+ this.casting = false; // 「糸を射出中」フラグを折る
131
+ this.targetIkWeight = 0.0f; // IK目標ウェイトを0にする...つまり右手を自然姿勢に戻そうとする
132
+ this.needsUpdateSpring = true; // 「SpringJoint要更新」フラグを立てる
133
+ }
134
+
135
+ // 右腕のIKウェイトをなめらかに変化させる
136
+ this.currentIkWeight = Mathf.SmoothDamp(
137
+ this.currentIkWeight,
138
+ this.targetIkWeight,
139
+ ref this.ikWeightVelocity,
140
+ this.ikTransitionTime);
141
+
142
+ // 糸の状態を更新する
143
+ this.UpdateString();
144
+ }
145
+
146
+ private void UpdateString()
147
+ {
148
+ // 糸を射出中ならlineRendererをアクティブにして糸を描画させ、さもなければ非表示にする
149
+ if (this.lineRenderer.enabled = this.casting)
150
+ {
151
+ // 糸を射出中の場合のみ処理を行う
152
+ // 糸のキャラクター側末端を設定し...
153
+ this.stringAnchor[0] = this.worldCasterCenter;
154
+
155
+ // キャラクターと接着点の間に障害物があるかをチェックし...
156
+ if (Physics.Linecast(
157
+ this.stringAnchor[0],
158
+ this.stringAnchor[1],
159
+ out var obstacle,
160
+ this.interactiveLayers))
161
+ {
162
+ // 障害物があれば、接着点を障害物に変更する
163
+ // これにより、糸が何かに触れればそこにくっつくようになるので
164
+ // 糸全体が粘着性があるかのように振る舞う
165
+ this.stringAnchor[1] = obstacle.point;
166
+ this.stringLength = Mathf.Min(
167
+ Vector3.Distance(this.stringAnchor[0], this.stringAnchor[1]),
168
+ this.stringLength);
169
+ this.needsUpdateSpring = true;
170
+ }
171
+
172
+ // 糸の描画設定を行う
173
+ // 糸の端点同士の距離とstringLengthとの乖離具合によって糸を赤く塗る
174
+ // つまり糸が赤くなっていれば、SpringJointが縮もうとしていることを示す
175
+ this.lineRenderer.SetPositions(this.stringAnchor);
176
+ var gbValue = Mathf.Exp(
177
+ this.springJoint != null
178
+ ? -Mathf.Max(Vector3.Distance(this.stringAnchor[0], this.stringAnchor[1]) - this.stringLength, 0.0f)
179
+ : 0.0f);
180
+ var stringColor = new Color(1.0f, gbValue, gbValue);
181
+ this.lineRenderer.startColor = stringColor;
182
+ this.lineRenderer.endColor = stringColor;
183
+ }
184
+ }
185
+
186
+ // 右腕の姿勢を設定し、右腕から糸を出しているように見せる
187
+ private void OnAnimatorIK(int layerIndex)
188
+ {
189
+ this.animator.SetIKPosition(AvatarIKGoal.RightHand, this.stringAnchor[1]);
190
+ this.animator.SetIKPositionWeight(AvatarIKGoal.RightHand, this.currentIkWeight);
191
+ }
192
+
193
+ // SpringJointの状態を更新する
194
+ private void FixedUpdate()
195
+ {
196
+ // 更新不要なら何もしない
197
+ if (!this.needsUpdateSpring)
198
+ {
199
+ return;
200
+ }
201
+
202
+ // 糸射出中かどうかを判定し...
203
+ if (this.casting)
204
+ {
205
+ // 射出中で、かつまだSpringJointが張られていなければ張り...
206
+ if (this.springJoint == null)
207
+ {
208
+ this.springJoint = this.gameObject.AddComponent<SpringJoint>();
209
+ this.springJoint.autoConfigureConnectedAnchor = false;
210
+ this.springJoint.anchor = this.casterCenter;
211
+ this.springJoint.spring = this.spring;
212
+ this.springJoint.damper = this.damper;
213
+ }
214
+
215
+ // SpringJointの自然長と接続先を設定する
216
+ this.springJoint.maxDistance = this.stringLength;
217
+ this.springJoint.connectedAnchor = this.stringAnchor[1];
218
+ }
219
+ else
220
+ {
221
+ // 射出中でなければSpringJointを削除し、糸による引っぱりを起こらなくする
222
+ Destroy(this.springJoint);
223
+ this.springJoint = null;
224
+ }
225
+
226
+ // 更新が終わったので、「SpringJoint要更新」フラグを折る
227
+ this.needsUpdateSpring = false;
228
+ }
229
+ }
230
+ }
231
+ ```
232
+
233
+ 当初の回答文では言及していなかったのですが、糸の特性として「糸が障害物に触れた場合、接着点をそこに変更する」という挙動をする特徴があります。動かした様子の図をご覧いただきますと、そのような動きをしているのを見て取れるかと思います。
234
+ これによりスクリプトを単純化することができ、キャラクターと接着点の両末端を`SpringJoint`でつなぐだけの簡単な管理でまかなえるようになっています。
235
+ それに対して、たとえば私はプレイしたことはないのですが「[ユニティちゃんWA!](https://www.youtube.com/watch?v=sdh0u4dCZn0)」のワイヤーには粘着性はなく、最初にアンカーを撃ち込んだ位置が維持されているように見えます。ワイヤーが障害物に触れてもそこにひっかかるだけの挙動をしていますが、こういったことを実現するにはもっと複雑なスクリプトを設計する必要があるでしょうね。