質問をすることでしか得られない、回答やアドバイスがある。

15分調べてもわからないことは、質問しよう!

新規登録して質問してみよう
ただいま回答率
85.46%
Unity

Unityは、Unity Technologiesが開発・販売している、IDEを内蔵するゲームエンジンです。主にC#を用いたプログラミングでコンテンツの開発が可能です。

Q&A

解決済

3回答

14441閲覧

Unity Jointを用いた擬似ワイヤーの仕組みについて

dragonshot

総合スコア4

Unity

Unityは、Unity Technologiesが開発・販売している、IDEを内蔵するゲームエンジンです。主にC#を用いたプログラミングでコンテンツの開発が可能です。

1グッド

0クリップ

投稿2020/05/13 13:31

前提・実現したいこと

 Unity3Dにて所謂ワイヤーアクションと呼ばれるジャンルに興味があり実装したいと考えております。現在のプレイヤーの位置から照準を合わせた箇所をSpring Jointで繋いで移動させているのだろうと考えましたが、遠心力で移動するSpider manのswingのような動きが出せず、方法を模索しております。どのような仕組みで動いているかご教授お願いいたします。

rasukaryu👍を押しています

気になる質問をクリップする

クリップした質問は、後からいつでもMYページで確認できます。

またクリップした質問に回答があった際、通知やメールを受け取ることができます。

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

guest

回答3

0

ベストアンサー

Spring Jointの設定はどのようにしているでしょうかね?
勉強を兼ねてSpring Jointで遊んでみた感想を申し上げますと、糸にぶら下がって飛び回るような動きをさせたい場合はSpring JointのmaxDistanceをキャラクターと接続点を結ぶ距離と等しくして、縮まないけれど伸びない糸...つまりゴムではなくロープのような特性を持たせるのがよさそうに思いました。
たとえば下図ではそのように設定しておりますので、キャラクターが立ち止まっている限りは最初に糸を張った位置から引っ張られることはなく、接続点から遠ざかろうとすると糸の張力により引き戻される動きをしています(糸に強い力がかかるほど色が赤くなるようにしています)。

図1

なお、見た目に整合性を持たせるため、初期位置より接続点に近づくとmaxDistanceを短くしていくようにしました。糸を巻き取りながら近づいていくイメージです。
キャラクターはCapsuleColliderRigidbodyを持ったオブジェクトをStandard Assetsに収録されているThirdPersonCharacterを使って動かしているだけですし、糸はご質問者さんのおっしゃるとおり画面中心の照準が指す位置とキャラクターをSpring Jointで結ぶことで実現しており、とりわけ特殊なことはしておりません。

ジョイントの設定をそのようにしておけば、下図のように空中ブランコみたいに梁を渡っていく動きができるんじゃないでしょうか。

図2

ちなみに、映像の途中で時々画面がくすんでぼやけますが、これは主人公(Spider-chan!と名付けました)の特殊能力で一時的に反応速度を50倍にしているという設定です!...実際のところ、これはただTime.timeScaleを小さくしているだけです。
あいにく私はアクションゲームがたいして得意ではなく、高速で飛び回りながら照準を合わせて糸を張るのが難しくてこのような機能を加えました。近年の市販ゲームでもこういったバレットタイムのような仕掛けをゲームシステムに組み込んでいる例をしばしば見かけるように思います。

とはいえ、縮まないロープでは地上から空中へ飛び上がることができないでしょうから、プレイヤーの操作によってロープをゴムに変える機能を持たせるといいかと思います。たとえば下図ではボタン操作によりジョイントのmaxDistanceを1に変えています。
柱に張った白い糸が赤色に変わり、急激に強い張力が加わって地上から飛び上がります。

図3

他にも、今回の実験では操作が複雑になりそうで実装しませんでしたが、左右の手からそれぞれ糸を発射できるようにするのも面白いかもしれません。1本の糸で飛び上がるのだと高確率で接続点に激突してしまうので高くまで飛び上がるのが難しいですが、両手から2本の糸が出せるのならスリングショットのような感じでV字に糸を張って安全に飛び上がることができそうです。

糸を張るスクリプトについて

キャラクターには下図のようなコンポーネントが付いています。見た目の面はUNITY-CHAN! OFFICIAL WEBSITEの「SDユニティちゃん 3Dモデルデータ」に収録されている「SD_unitychan_humanoid」がベースになっており、操作の面はStandard Assetsに収録されている「ThirdPersonController」がベースになっています。

図4

大部分のコンポーネントはそれらのアセットに収録されているものやUnity組み込みのもので、StringCasterだけが独自に作成した糸張り担当のコンポーネントです。

lang

1using UnityEngine; 2using UnityEngine.UI; 3 4namespace SpiderChan 5{ 6 // Animator、Rigidbody、LineRendererを必須としている 7 // Animator...糸を射出しているかどうかに応じて右手を前に出したり戻したりするのに使用 8 // Rigidbody...スクリプト中で直接操作してはいないが、SpringJointの動作に必要 9 // LineRenderer...糸を画面上に描画するために使用 10 [RequireComponent(typeof(Animator), typeof(Rigidbody), typeof(LineRenderer))] 11 public class StringCaster : MonoBehaviour 12 { 13 [SerializeField] private float maximumDistance = 100.0f; // 糸を伸ばせる最大距離 14 [SerializeField] private LayerMask interactiveLayers; // 糸をくっつけられるレイヤー 15 [SerializeField] private Vector3 casterCenter = new Vector3(0.0f, 0.5f, 0.0f); // オブジェクトのローカル座標で表した糸の射出位置 16 [SerializeField] private float spring = 50.0f; // 糸の物理的挙動を担当するSpringJointのspring 17 [SerializeField] private float damper = 20.0f; // 糸の物理的挙動を担当するSpringJointのdamper 18 [SerializeField] private float equilibriumLength = 1.0f; // 糸を縮めた時の自然長 19 [SerializeField] private float ikTransitionTime = 0.5f; // 糸の射出中に右手を前に伸ばしたり、糸を外した時に右手を戻したりする時の腕位置の遷移時間 20 [SerializeField] private RawImage reticle; // 糸を張れるかどうかの状況に合わせて、このRawImageの表示を照準マーク・禁止マークに切り替える 21 [SerializeField] private Texture reticleImageValid; // 照準マーク 22 [SerializeField] private Texture reticleImageInvalid; // 禁止マーク 23 24 // 各種コンポーネントへの参照 25 private Animator animator; 26 private Transform cameraTransform; 27 private LineRenderer lineRenderer; 28 private SpringJoint springJoint; 29 30 // 右手を伸ばす・戻す動作のスムージングのための... 31 private float currentIkWeight; // 現在のウェイト 32 private float targetIkWeight; // 目標ウェイト 33 private float ikWeightVelocity; // ウェイト変化率 34 35 private bool casting; // 糸が射出中かどうかを表すフラグ 36 private bool needsUpdateSpring; // FixedUpdate中でSpringJointの状態更新が必要かどうかを表すフラグ 37 private float stringLength; // 現在の糸の長さ...この値をFixedUpdate中でSpringJointのmaxDistanceにセットする 38 private readonly Vector3[] stringAnchor = new Vector3[2]; // SpringJointのキャラクター側と接着点側の末端 39 private Vector3 worldCasterCenter; // casterCenterをワールド座標に変換したもの 40 41 private void Awake() 42 { 43 // スクリプト上で使用するコンポーネントへの参照を取得する 44 this.animator = this.GetComponent<Animator>(); 45 this.cameraTransform = Camera.main.transform; 46 this.lineRenderer = this.GetComponent<LineRenderer>(); 47 48 // worldCasterCenterはUpdate中でも毎回更新しているが、Awake時にも初回更新を行った 49 // ちなみに今回のキャラクターの場合は、キャラクターのCapsuleCollider中心と一致するようにしている 50 this.worldCasterCenter = this.transform.TransformPoint(this.casterCenter); 51 } 52 53 private void Update() 54 { 55 // まず画面中心から真っ正面に伸びるRayを求め、さらにworldCasterCenterから 56 // そのRayの衝突点に向かうRayを求める...これを糸の射出方向とする 57 this.worldCasterCenter = this.transform.TransformPoint(this.casterCenter); 58 var cameraForward = this.cameraTransform.forward; 59 var cameraRay = new Ray(this.cameraTransform.position, cameraForward); 60 var aimingRay = new Ray( 61 this.worldCasterCenter, 62 Physics.Raycast(cameraRay, out var focus, float.PositiveInfinity, this.interactiveLayers) 63 ? focus.point - this.worldCasterCenter 64 : cameraForward); 65 66 // 射出方向のmaximumDistance以内の距離に糸接着可能な物体があれば、糸を射出できると判断する 67 if (Physics.Raycast(aimingRay, out var aimingTarget, this.maximumDistance, this.interactiveLayers)) 68 { 69 // reticleの表示を照準マークに変え... 70 this.reticle.texture = this.reticleImageValid; 71 72 // その状態で糸発射ボタンが押されたら... 73 if (Input.GetButtonDown("Shot")) 74 { 75 this.stringAnchor[1] = aimingTarget.point; // 糸の接着点末端を設定 76 this.casting = true; // 「糸を射出中」フラグを立てる 77 this.targetIkWeight = 1.0f; // IK目標ウェイトを1にする...つまり右手を射出方向に伸ばそうとする 78 this.stringLength = Vector3.Distance(this.worldCasterCenter, aimingTarget.point); // 糸の長さを設定 79 this.needsUpdateSpring = true; // 「SpringJoint要更新」フラグを立てる 80 } 81 } 82 else 83 { 84 // 糸接着不可能なら、reticleの表示を禁止マークに変える 85 this.reticle.texture = this.reticleImageInvalid; 86 } 87 88 // 糸を射出中の状態で糸収縮ボタンが押されたら、糸の長さをequilibriumLengthまで縮めさせる 89 if (this.casting && Input.GetButtonDown("Contract")) 90 { 91 this.stringLength = this.equilibriumLength; 92 this.needsUpdateSpring = true; 93 } 94 95 // 糸発射ボタンが離されたら... 96 if (Input.GetButtonUp("Shot")) 97 { 98 this.casting = false; // 「糸を射出中」フラグを折る 99 this.targetIkWeight = 0.0f; // IK目標ウェイトを0にする...つまり右手を自然姿勢に戻そうとする 100 this.needsUpdateSpring = true; // 「SpringJoint要更新」フラグを立てる 101 } 102 103 // 右腕のIKウェイトをなめらかに変化させる 104 this.currentIkWeight = Mathf.SmoothDamp( 105 this.currentIkWeight, 106 this.targetIkWeight, 107 ref this.ikWeightVelocity, 108 this.ikTransitionTime); 109 110 // 糸の状態を更新する 111 this.UpdateString(); 112 } 113 114 private void UpdateString() 115 { 116 // 糸を射出中ならlineRendererをアクティブにして糸を描画させ、さもなければ非表示にする 117 if (this.lineRenderer.enabled = this.casting) 118 { 119 // 糸を射出中の場合のみ処理を行う 120 // 糸のキャラクター側末端を設定し... 121 this.stringAnchor[0] = this.worldCasterCenter; 122 123 // キャラクターと接着点の間に障害物があるかをチェックし... 124 if (Physics.Linecast( 125 this.stringAnchor[0], 126 this.stringAnchor[1], 127 out var obstacle, 128 this.interactiveLayers)) 129 { 130 // 障害物があれば、接着点を障害物に変更する 131 // これにより、糸が何かに触れればそこにくっつくようになるので 132 // 糸全体が粘着性があるかのように振る舞う 133 this.stringAnchor[1] = obstacle.point; 134 this.stringLength = Mathf.Min( 135 Vector3.Distance(this.stringAnchor[0], this.stringAnchor[1]), 136 this.stringLength); 137 this.needsUpdateSpring = true; 138 } 139 140 // 糸の描画設定を行う 141 // 糸の端点同士の距離とstringLengthとの乖離具合によって糸を赤く塗る 142 // つまり糸が赤くなっていれば、SpringJointが縮もうとしていることを示す 143 this.lineRenderer.SetPositions(this.stringAnchor); 144 var gbValue = Mathf.Exp( 145 this.springJoint != null 146 ? -Mathf.Max(Vector3.Distance(this.stringAnchor[0], this.stringAnchor[1]) - this.stringLength, 0.0f) 147 : 0.0f); 148 var stringColor = new Color(1.0f, gbValue, gbValue); 149 this.lineRenderer.startColor = stringColor; 150 this.lineRenderer.endColor = stringColor; 151 } 152 } 153 154 // 右腕の姿勢を設定し、右腕から糸を出しているように見せる 155 private void OnAnimatorIK(int layerIndex) 156 { 157 this.animator.SetIKPosition(AvatarIKGoal.RightHand, this.stringAnchor[1]); 158 this.animator.SetIKPositionWeight(AvatarIKGoal.RightHand, this.currentIkWeight); 159 } 160 161 // SpringJointの状態を更新する 162 private void FixedUpdate() 163 { 164 // 更新不要なら何もしない 165 if (!this.needsUpdateSpring) 166 { 167 return; 168 } 169 170 // 糸射出中かどうかを判定し... 171 if (this.casting) 172 { 173 // 射出中で、かつまだSpringJointが張られていなければ張り... 174 if (this.springJoint == null) 175 { 176 this.springJoint = this.gameObject.AddComponent<SpringJoint>(); 177 this.springJoint.autoConfigureConnectedAnchor = false; 178 this.springJoint.anchor = this.casterCenter; 179 this.springJoint.spring = this.spring; 180 this.springJoint.damper = this.damper; 181 } 182 183 // SpringJointの自然長と接続先を設定する 184 this.springJoint.maxDistance = this.stringLength; 185 this.springJoint.connectedAnchor = this.stringAnchor[1]; 186 } 187 else 188 { 189 // 射出中でなければSpringJointを削除し、糸による引っぱりを起こらなくする 190 Destroy(this.springJoint); 191 this.springJoint = null; 192 } 193 194 // 更新が終わったので、「SpringJoint要更新」フラグを折る 195 this.needsUpdateSpring = false; 196 } 197 } 198}

当初の回答文では言及していなかったのですが、糸の特性として「糸が障害物に触れた場合、接着点をそこに変更する」という挙動をする特徴があります。動かした様子の図をご覧いただきますと、そのような動きをしているのを見て取れるかと思います。
これによりスクリプトを単純化することができ、キャラクターと接着点の両末端をSpringJointでつなぐだけの簡単な管理でまかなえるようになっています。
それに対して、たとえば私はプレイしたことはないのですが「ユニティちゃんWA!」のワイヤーには粘着性はなく、最初にアンカーを撃ち込んだ位置が維持されているように見えます。ワイヤーが障害物に触れてもそこにひっかかるだけの挙動をしていますが、こういったことを実現するにはもっと複雑なスクリプトを設計する必要があるでしょうね。

投稿2020/05/15 14:44

編集2021/06/25 21:10
Bongo

総合スコア10807

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

dragonshot

2020/05/16 08:51

詳細な回答及び解説ありがとうございます。 Spring Jointをバネやゴムとして扱うのではなくロープのように使用するという発想とプレイヤーの操作によってロープをゴムに変える機能を持たせるという機能で実装し一度遊んでみたいと思います。 少し気になったのですが、回答者様が作成された主人公の特殊能力(バレットタイム)について、Time.timeScaleを小さくするという点は理解できたのですが、ボタンを押してから指定した秒数間だけtimeScaleを変更して元に戻すということができません。 おそらく以下のような形になると思うのですが、 void Update() { if (Input.GetMouseButton(0)) { // ここで指定した秒数間timeScaleを変更後、元のtimeScaleである1に戻したい Test(); } } public void Test() { if(Time.timeScale != 0.5f) { Time.timeScale = 0.5f; } else { Time.timeScale = 1; } } 本筋から逸れるコメントでお手数をおかけして申し訳ありませんが、アドバイスをいただけたらと思います。
Bongo

2020/05/16 10:34

時間をゲーム内時間で計るか実時間で計るかで方法を変える必要がありそうですね。 ご質問者さんがコメントに例示されましたTestメソッド(実行するたびにtimeScaleを1から0.5に、0.5から1に切り替える)を使うとしますと、仮にゲーム内時間で5秒後にTestを再実行する場合(1回目のTest実行でtimeScaleが0.5になるので、実時間では10秒後)には、一例としてはMonoBehaviour.Invoke(https://docs.unity3d.com/ja/current/ScriptReference/MonoBehaviour.Invoke.html )を使う手が考えられるでしょう。 void Update() { if (Input.GetMouseButtonDown(0)) { Test(); Invoke(nameof(Test), 5.0f); } } 実時間で5秒後にTestを再実行する場合(同じくtimeScaleが0.5になるので、ゲーム内時間では2.5秒後)には、WaitForSecondsRealtime(https://docs.unity3d.com/ja/current/ScriptReference/WaitForSecondsRealtime.html )を使って待機するコルーチンを動かす手が使えそうです。 void Update() { if (Input.GetMouseButtonDown(0)) { Test(); StartCoroutine(InvokeTest(5.0f)); } } IEnumerator InvokeTest(float time) { yield return new WaitForSecondsRealtime(time); Test(); }
dragonshot

2020/05/17 12:12

返信が遅くなってしまい申し訳ありません。試行錯誤の末、Spring Joint部分に関して上手く実現することができ、満足しています。追加のtimeScaleはゲーム性を大事にしながら実装するか考えたいと思います。この度は丁寧な回答と解説ありがとうございました。
rasukaryu

2021/06/24 12:38

実際にどんなスクリプトを書いたのですか? ぜひ教えてください!!
Bongo

2021/06/24 21:25

私あてのコメントでしょうかね(それともdragonshotさん)? 私の方はまだコードを残しておいてありますので提示可能ではあるのですが、実験用の比較的小規模なシーンとはいえ、スクリプト類を全部だと字数制限の都合上回答2つ分になってしまいそうです。 rasukaryuさんとしては特に知りたい部分についてのご希望はあるでしょうか? なるべく範囲を絞っていただけるとありがたいところではあります。たとえばdragonshotさんのご質問の主題から外れた部分(カメラワークだとかバレットタイムだとか)を除いた、糸を張る部分だけであれば1回答でまかなえそうです。 ただ、回答としての提示用にコメントだとかを入れた状態にはなっていませんので、ちょっと整形に時間をいただきたいですが...
rasukaryu

2021/06/25 07:36

Bongoさんへのコメントです!! 糸を張るところです!! そこでずっとつまずいていて… ぜひ、教えてください!!
Bongo

2021/06/25 21:11

糸張りスクリプトを追記しました。当初の回答で申し上げたように、さほど複雑なことはしていないつもりですが...ご参考になれば幸いです。
rasukaryu

2021/06/25 23:06

お忙しい中、本当にありがとうございます! このスクリプトを参考にしながらゲームづくりを進めていきたいと思います!!
rasukaryu

2021/06/26 00:55

できました~~‼ 最高です!!本当にありがとうございました!!
rasukaryu

2021/06/26 04:37

面倒くさいかもしれませんが、2本の糸を出すにはどのようにしたらよいでしょうか?
Bongo

2021/06/27 00:14

糸を2本に増やす案を検討してみました。ついに投稿字数が尽きてしまったので、別回答になりますがご容赦ください。 安直ですが、糸張り機構を2セット用意すればいいんじゃないか...と考えました。比較的軽微な変更で済んだように思います。
rasukaryu

2021/06/27 06:05

おおおおおお! 本当にありがとうございます! 僕も頑張ってBongoさんみたいなアンサーになれるよう努力します!
dosidosi

2021/06/29 00:54

外野から質問失礼いたします。 Bongoさんにぜひご教授いただきたいことがあります。糸をはるスクリプト以外のSpring Managerやその他のスクリプトはどのように書かれているのでしょうか?Standard Assetsを入れてみましたが、該当するコードがなく・・・ThirdPersonControllerは確認できましたが、ぜひご回答いただけると大変助かります。ご検討よろしくお願いいたします。
Bongo

2021/06/29 01:20

それらのコンポーネントは「SDユニティちゃん 3Dモデルデータ」の方に付属していたものですね。「SD_unitychan_humanoid」のプレハブには、すでにそれらがアタッチ済みになっているかと思います。
guest

0

マウスポインタを狙った糸発射、手にあるオブジェクトからの糸発射、動くオブジェクトに対する糸接着について

StringCasterを下記のように変更しました。以前のコードと重複するコメントは削除し、変更を加えた部分に新たにコメントを入れています。

lang

1using UnityEngine; 2using UnityEngine.UI; 3 4namespace SpiderChan 5{ 6 [RequireComponent(typeof(Animator), typeof(Rigidbody))] 7 public class StringCaster : MonoBehaviour 8 { 9 [SerializeField] private string castButtonName = "Shot"; 10 [SerializeField] private string contractButtonName = "Contract"; 11 [SerializeField] private float maximumDistance = 100.0f; 12 [SerializeField] private LayerMask interactiveLayers; 13 [SerializeField] private Vector3 casterCenter = new Vector3(0.0f, 0.5f, 0.0f); 14 [SerializeField] private Transform casterCenterTransform; // 糸の射出位置を表すオブジェクト...これがnullの場合は従来通りcasterCenterを使う 15 [SerializeField][Min(0.0f)] private float obstacleDetectionMarginStart = 1.0f; // 自分に十分近い物体は糸遮蔽判定から除外させる 16 [SerializeField][Min(0.0f)] private float obstacleDetectionMarginEnd = 1.0f; // 接着点に十分近い物体は糸遮蔽判定から除外させる 17 [SerializeField] private float spring = 50.0f; 18 [SerializeField] private float damper = 20.0f; 19 [SerializeField] private float equilibriumLength = 1.0f; 20 [SerializeField] private AvatarIKGoal ikGoal = AvatarIKGoal.RightHand; 21 [SerializeField] private float ikTransitionTime = 0.5f; 22 [SerializeField] private RawImage reticle; 23 [SerializeField] private Texture reticleImageValid; 24 [SerializeField] private Texture reticleImageInvalid; 25 [SerializeField] private LineRenderer lineRenderer; 26 27 private Animator animator; 28 private Camera mainCamera; // マウスポインタを狙う方式にしたので、保持しておくコンポーネントを効率化のためTransformからCameraに変える 29 private SpringJoint springJoint; 30 31 private float currentIkWeight; 32 private float targetIkWeight; 33 private float ikWeightVelocity; 34 35 private bool casting; 36 private bool needsUpdateSpring; 37 private float stringLength; 38 private readonly Anchor[] stringAnchor = new Anchor[2]; // 末端情報にRigidbodyも持たせるよう変更 39 private Vector3 worldCasterCenter; 40 41 // アンカーを表す型を定義する 42 private readonly struct Anchor 43 { 44 public readonly Vector3 LocalPosition; 45 public readonly Rigidbody ConnectedBody; 46 47 public Vector3 WorldPosition => this.ConnectedBody == null 48 ? this.LocalPosition 49 : this.ConnectedBody.transform.TransformPoint(this.LocalPosition); 50 51 public Anchor(Vector3 worldPosition, Rigidbody connectedBody = null) 52 { 53 this.LocalPosition = connectedBody == null 54 ? worldPosition 55 : connectedBody.transform.InverseTransformPoint(worldPosition); 56 this.ConnectedBody = connectedBody; 57 } 58 } 59 60 private void Awake() 61 { 62 this.animator = this.GetComponent<Animator>(); 63 this.mainCamera = Camera.main; // マウスポインタを狙う方式にしたので、効率化のため保持しておくコンポーネントをTransformからCameraに変える 64 this.worldCasterCenter = this.transform.TransformPoint(this.casterCenter); 65 } 66 67 private void Update() 68 { 69 // マウスポインタを狙う方式に変更 70 // なお、ここでもしcasterCenterTransformがセットされているならば 71 // そちらのワールド座標をworldCasterCenterとして採用する 72 this.worldCasterCenter = this.casterCenterTransform != null ? this.casterCenterTransform.position : this.transform.TransformPoint(this.casterCenter); 73 var mousePosition = Input.mousePosition; 74 var pointerRay = this.mainCamera.ScreenPointToRay(mousePosition); 75 var aimingRay = new Ray( 76 this.worldCasterCenter, 77 Physics.Raycast(pointerRay, out var focus, float.PositiveInfinity, this.interactiveLayers) 78 ? focus.point - this.worldCasterCenter 79 : pointerRay.direction); 80 81 // マウスポインタを狙っていることを視覚的に示すため、reticleの位置もmousePositionに合わせる 82 var reticleTransform = this.reticle.transform as RectTransform; 83 var reticleRootCanvas = reticle.canvas.rootCanvas; 84 if (RectTransformUtility.ScreenPointToLocalPointInRectangle( 85 reticleTransform.parent as RectTransform, 86 mousePosition, 87 reticleRootCanvas.renderMode == RenderMode.ScreenSpaceOverlay ? null : reticleRootCanvas.worldCamera, 88 out var reticlePosition)) 89 { 90 reticleTransform.localPosition = reticlePosition; 91 } 92 93 if (Physics.Raycast(aimingRay, out var aimingTarget, this.maximumDistance, this.interactiveLayers)) 94 { 95 this.reticle.texture = this.reticleImageValid; 96 97 if (Input.GetButtonDown(this.castButtonName)) 98 { 99 this.stringAnchor[1] = new Anchor(aimingTarget.point, aimingTarget.rigidbody); // 接着対象がRigidbodyを持っている場合、それも覚えておく 100 this.casting = true; 101 this.targetIkWeight = 1.0f; 102 this.stringLength = Vector3.Distance(this.worldCasterCenter, aimingTarget.point); 103 this.needsUpdateSpring = true; 104 } 105 } 106 else 107 { 108 this.reticle.texture = this.reticleImageInvalid; 109 } 110 111 if (this.casting && Input.GetButtonDown(this.contractButtonName)) 112 { 113 this.stringLength = this.equilibriumLength; 114 this.needsUpdateSpring = true; 115 } 116 117 if (Input.GetButtonUp(this.castButtonName)) 118 { 119 this.casting = false; 120 this.targetIkWeight = 0.0f; 121 this.needsUpdateSpring = true; 122 } 123 124 this.currentIkWeight = Mathf.SmoothDamp( 125 this.currentIkWeight, 126 this.targetIkWeight, 127 ref this.ikWeightVelocity, 128 this.ikTransitionTime); 129 130 this.UpdateString(); 131 } 132 133 private void UpdateString() 134 { 135 // stringAnchorの型を変えたので、それに適合するよう変更 136 if (this.lineRenderer.enabled = this.casting) 137 { 138 this.stringAnchor[0] = new Anchor(this.worldCasterCenter); 139 140 // 糸の始点~終点ぴったりで遮蔽物判定を行うと、自分自身や接着点自身が判定にひっかかって 141 // 不安定な挙動の原因になりそうに思い、いくらか余裕を設けるように変更 142 var anchorPositionStart = this.stringAnchor[0].WorldPosition; 143 var anchorPositionEnd = this.stringAnchor[1].WorldPosition; 144 if (this.stringLength > (this.obstacleDetectionMarginStart + this.obstacleDetectionMarginEnd)) 145 { 146 var start = Vector3.MoveTowards( 147 anchorPositionEnd, 148 anchorPositionStart, 149 this.stringLength - this.obstacleDetectionMarginStart); 150 var end = Vector3.MoveTowards( 151 anchorPositionStart, 152 anchorPositionEnd, 153 this.stringLength - this.obstacleDetectionMarginEnd); 154 if (Physics.Linecast(start, end, out var obstacle, this.interactiveLayers)) 155 { 156 this.stringAnchor[1] = new Anchor(obstacle.point, obstacle.rigidbody); 157 anchorPositionEnd = this.stringAnchor[1].WorldPosition; 158 this.stringLength = Mathf.Min( 159 Vector3.Distance(anchorPositionStart, anchorPositionEnd), 160 this.stringLength); 161 this.needsUpdateSpring = true; 162 } 163 } 164 165 this.lineRenderer.SetPosition(0, anchorPositionStart); 166 this.lineRenderer.SetPosition(1, anchorPositionEnd); 167 var gbValue = Mathf.Exp( 168 this.springJoint != null 169 ? -Mathf.Max(Vector3.Distance(anchorPositionStart, anchorPositionEnd) - this.stringLength, 0.0f) 170 : 0.0f); 171 var stringColor = new Color(1.0f, gbValue, gbValue); 172 this.lineRenderer.startColor = stringColor; 173 this.lineRenderer.endColor = stringColor; 174 175 // もし視覚的な糸(lineRendererによる線の描画)だけでなく、物理的な糸(springJoint)の起点も 176 // casterCenterTransformの動きにフルに追従させたい場合、糸を張っている時は常時needsUpdateSpringを 177 // trueにすることになる 178 // しかし、物理挙動に目に見えるレベルの異常を起こしやすいようで、ちょっとおすすめしがたい... 179 // this.needsUpdateSpring = true; 180 } 181 } 182 183 private void OnAnimatorIK(int layerIndex) 184 { 185 // stringAnchorの型を変えたので、それに適合するよう変更 186 this.animator.SetIKPosition(this.ikGoal, this.stringAnchor[1].WorldPosition); 187 this.animator.SetIKPositionWeight(this.ikGoal, this.currentIkWeight); 188 } 189 190 private void FixedUpdate() 191 { 192 if (!this.needsUpdateSpring) 193 { 194 return; 195 } 196 197 if (this.casting) 198 { 199 if (this.springJoint == null) 200 { 201 this.springJoint = this.gameObject.AddComponent<SpringJoint>(); 202 this.springJoint.autoConfigureConnectedAnchor = false; 203 this.springJoint.spring = this.spring; 204 this.springJoint.damper = this.damper; 205 } 206 207 // casterCenterの代わりにcasterCenterTransformを使った場合、casterCenterTransformが 208 // 移動することにより射出位置が動的に変わる可能性が出てくる 209 // そこで、anchor設定をこの位置に引っ越した上で、現在のworldCasterCenterを逆変換して 210 // アンカー座標を算出、anchorにセットすることにした 211 this.springJoint.anchor = this.transform.InverseTransformPoint(this.worldCasterCenter); 212 this.springJoint.maxDistance = this.stringLength; 213 214 // connectedAnchorだけでなく、connectedBodyも設定するように変更した 215 // 従来のコードは接着先をワールド空間上の絶対座標に決め打ちしていたが、接着相手が 216 // Rigidbodyを持っていた場合は、それに接続されることになる 217 this.springJoint.connectedBody = this.stringAnchor[1].ConnectedBody; 218 this.springJoint.connectedAnchor = this.stringAnchor[1].LocalPosition; 219 } 220 else 221 { 222 Destroy(this.springJoint); 223 this.springJoint = null; 224 } 225 226 this.needsUpdateSpring = false; 227 } 228 } 229}

これにより、下図のように動く物体に対して糸が接着するようになりました。
なお、映像中の赤い足場はキネマティックなRigidbodyを持っており、それをMovePositionMoveRotationで動かしているだけで、とりわけ特別なことはしていません。

図1

手のオブジェクトからの糸発射については、彼女は手足が短いので手そのものでは分かりづらいかと思い、手に持たせたメイスのような謎の棒から発射させています。棒の先端の球体をcasterCenterTransformにセットしていますので、下図のように糸が描画されます。

図2

ただしこれは見た目をそう見せているだけで、SpringJointの起点は糸のつなぎ替えのタイミングだけで更新されます。
UpdateString末尾のコメントアウトされているthis.needsUpdateSpring = true;を有効にすることで、見た目だけでなく実際に起点を追従させられるはずですが、それをやると下図のように接続相手の運動を阻害して異常な挙動を起こしてしまいました...

図3

投稿2021/09/11 19:17

Bongo

総合スコア10807

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

rasukaryu

2021/09/12 02:53

丁寧な回答ありがとうございます。本当に感謝しております。早速試してみようと思います。
rasukaryu

2021/09/12 04:14

きれいにできました!!ありがとうございました。
rasukaryu

2021/09/12 04:27

unity3D総合一位すごいですね!!!
rasukaryu

2021/09/20 02:15

色々と試してみたところ、やはり糸がキャラクターの体にくっついてしまいます。「StringCaster」の「[SerializeField][Min(0.0f)] private float obstacleDetectionMarginStart = 1.0f; // 自分に十分近い物体は糸遮蔽判定から除外させる」 を色々といじっていますが、どうしてもくっつきます。どうしたらいいですか?
Bongo

2021/09/20 06:32

キャラクターのレイヤーはどうなっているでしょうかね? StringCasterのインスペクター上にある「Interactive Layers」で接着可能なレイヤーを選択できるはずですが、ここにもしキャラクターオブジェクトのレイヤーも含まれているようでしたら、それを除外してみてください。 もしレイヤーをいじることができない場合(たとえば他のスクリプトの動作の都合上、キャラクターだけ別のレイヤーに設定することは許されない...とか)、ご指摘のように自分自身に十分近い地点には接着させないよう修正案を検討してみますのでコメントください(お試しいただいた「obstacleDetectionMarginStart」は、糸が他の物体に遮られて接着する場合の判定に影響するものでして、糸の射出時の判定開始地点には特に制限を設けてはいませんでした...確かにそれでは片手落ちだったかもしれませんね)。
rasukaryu

2021/09/20 10:10

ほんとうにていねいな回答ありがとうございます。レイヤーを少し変更してみます。
rasukaryu

2021/09/21 10:12

レイヤーを変更すると無事にプレイヤーに糸がつかず、変な動きもしませんでした。 ありがとうございました。
guest

0

2本の糸を発射する実験

まず、StringCasterに下記のような変更を加えました。

diff

1using UnityEngine; 2using UnityEngine.UI; 3 4namespace SpiderChan 5{ 6 // Animator、Rigidbody、LineRendererを必須としている 7 // Animator...糸を射出しているかどうかに応じて右手を前に出したり戻したりするのに使用 8 // Rigidbody...スクリプト中で直接操作してはいないが、SpringJointの動作に必要 9- // LineRenderer...糸を画面上に描画するために使用 10- [RequireComponent(typeof(Animator), typeof(Rigidbody), typeof(LineRenderer))] 11+ [RequireComponent(typeof(Animator), typeof(Rigidbody))] 12 public class StringCaster : MonoBehaviour 13 { 14+ [SerializeField] private string castButtonName = "Shot"; // 糸射出ボタンのInput Manager割り当て名称 15+ [SerializeField] private string contractButtonName = "Contract"; // 糸収縮ボタンのInput Manager割り当て名称 16 [SerializeField] private float maximumDistance = 100.0f; // 糸を伸ばせる最大距離 17 [SerializeField] private LayerMask interactiveLayers; // 糸をくっつけられるレイヤー 18 [SerializeField] private Vector3 casterCenter = new Vector3(0.0f, 0.5f, 0.0f); // オブジェクトのローカル座標で表した糸の射出位置 19 [SerializeField] private float spring = 50.0f; // 糸の物理的挙動を担当するSpringJointのspring 20 [SerializeField] private float damper = 20.0f; // 糸の物理的挙動を担当するSpringJointのdamper 21 [SerializeField] private float equilibriumLength = 1.0f; // 糸を縮めた時の自然長 22+ [SerializeField] private AvatarIKGoal ikGoal = AvatarIKGoal.RightHand; // 糸射出時に接着点へ向ける体の部位 23 [SerializeField] private float ikTransitionTime = 0.5f; // 糸の射出中に右手を前に伸ばしたり、糸を外した時に右手を戻したりする時の腕位置の遷移時間 24 [SerializeField] private RawImage reticle; // 糸を張れるかどうかの状況に合わせて、このRawImageの表示を照準マーク・禁止マークに切り替える 25 [SerializeField] private Texture reticleImageValid; // 照準マーク 26 [SerializeField] private Texture reticleImageInvalid; // 禁止マーク 27+ [SerializeField] private LineRenderer lineRenderer; // 糸描画用のLineRenderer 28 29 // 各種コンポーネントへの参照 30 private Animator animator; 31 private Transform cameraTransform; 32- private LineRenderer lineRenderer; 33 private SpringJoint springJoint; 34 35 // 右手を伸ばす・戻す動作のスムージングのための... 36 private float currentIkWeight; // 現在のウェイト 37 private float targetIkWeight; // 目標ウェイト 38 private float ikWeightVelocity; // ウェイト変化率 39 40 private bool casting; // 糸が射出中かどうかを表すフラグ 41 private bool needsUpdateSpring; // FixedUpdate中でSpringJointの状態更新が必要かどうかを表すフラグ 42 private float stringLength; // 現在の糸の長さ...この値をFixedUpdate中でSpringJointのmaxDistanceにセットする 43 private readonly Vector3[] stringAnchor = new Vector3[2]; // SpringJointのキャラクター側と接着点側の末端 44 private Vector3 worldCasterCenter; // casterCenterをワールド座標に変換したもの 45 46 private void Awake() 47 { 48 // スクリプト上で使用するコンポーネントへの参照を取得する 49 this.animator = this.GetComponent<Animator>(); 50 this.cameraTransform = Camera.main.transform; 51- this.lineRenderer = this.GetComponent<LineRenderer>(); 52 53 // worldCasterCenterはUpdate中でも毎回更新しているが、Awake時にも初回更新を行った 54 // ちなみに今回のキャラクターの場合は、キャラクターのCapsuleCollider中心と一致するようにしている 55 this.worldCasterCenter = this.transform.TransformPoint(this.casterCenter); 56 } 57 58 private void Update() 59 { 60 // まず画面中心から真っ正面に伸びるRayを求め、さらにworldCasterCenterから 61 // そのRayの衝突点に向かうRayを求める...これを糸の射出方向とする 62 this.worldCasterCenter = this.transform.TransformPoint(this.casterCenter); 63 var cameraForward = this.cameraTransform.forward; 64 var cameraRay = new Ray(this.cameraTransform.position, cameraForward); 65 var aimingRay = new Ray( 66 this.worldCasterCenter, 67 Physics.Raycast(cameraRay, out var focus, float.PositiveInfinity, this.interactiveLayers) 68 ? focus.point - this.worldCasterCenter 69 : cameraForward); 70 71 // 射出方向のmaximumDistance以内の距離に糸接着可能な物体があれば、糸を射出できると判断する 72 if (Physics.Raycast(aimingRay, out var aimingTarget, this.maximumDistance, this.interactiveLayers)) 73 { 74 // reticleの表示を照準マークに変え... 75 this.reticle.texture = this.reticleImageValid; 76 77 // その状態で糸発射ボタンが押されたら... 78- if (Input.GetButtonDown("Shot")) 79+ if (Input.GetButtonDown(this.castButtonName)) 80 { 81 this.stringAnchor[1] = aimingTarget.point; // 糸の接着点末端を設定 82 this.casting = true; // 「糸を射出中」フラグを立てる 83 this.targetIkWeight = 1.0f; // IK目標ウェイトを1にする...つまり右手を射出方向に伸ばそうとする 84 this.stringLength = Vector3.Distance(this.worldCasterCenter, aimingTarget.point); // 糸の長さを設定 85 this.needsUpdateSpring = true; // 「SpringJoint要更新」フラグを立てる 86 } 87 } 88 else 89 { 90 // 糸接着不可能なら、reticleの表示を禁止マークに変える 91 this.reticle.texture = this.reticleImageInvalid; 92 } 93 94 // 糸を射出中の状態で糸収縮ボタンが押されたら、糸の長さをequilibriumLengthまで縮めさせる 95- if (this.casting && Input.GetButtonDown("Contract")) 96+ if (this.casting && Input.GetButtonDown(this.contractButtonName)) 97 { 98 this.stringLength = this.equilibriumLength; 99 this.needsUpdateSpring = true; 100 } 101 102 // 糸発射ボタンが離されたら... 103- if (Input.GetButtonUp("Shot")) 104+ if (Input.GetButtonUp(this.castButtonName)) 105 { 106 this.casting = false; // 「糸を射出中」フラグを折る 107 this.targetIkWeight = 0.0f; // IK目標ウェイトを0にする...つまり右手を自然姿勢に戻そうとする 108 this.needsUpdateSpring = true; // 「SpringJoint要更新」フラグを立てる 109 } 110 111 // 右腕のIKウェイトをなめらかに変化させる 112 this.currentIkWeight = Mathf.SmoothDamp( 113 this.currentIkWeight, 114 this.targetIkWeight, 115 ref this.ikWeightVelocity, 116 this.ikTransitionTime); 117 118 // 糸の状態を更新する 119 this.UpdateString(); 120 } 121 122 private void UpdateString() 123 { 124 // 糸を射出中ならlineRendererをアクティブにして糸を描画させ、さもなければ非表示にする 125 if (this.lineRenderer.enabled = this.casting) 126 { 127 // 糸を射出中の場合のみ処理を行う 128 // 糸のキャラクター側末端を設定し... 129 this.stringAnchor[0] = this.worldCasterCenter; 130 131 // キャラクターと接着点の間に障害物があるかをチェックし... 132 if (Physics.Linecast( 133 this.stringAnchor[0], 134 this.stringAnchor[1], 135 out var obstacle, 136 this.interactiveLayers)) 137 { 138 // 障害物があれば、接着点を障害物に変更する 139 // これにより、糸が何かに触れればそこにくっつくようになるので 140 // 糸全体が粘着性があるかのように振る舞う 141 this.stringAnchor[1] = obstacle.point; 142 this.stringLength = Mathf.Min( 143 Vector3.Distance(this.stringAnchor[0], this.stringAnchor[1]), 144 this.stringLength); 145 this.needsUpdateSpring = true; 146 } 147 148 // 糸の描画設定を行う 149 // 糸の端点同士の距離とstringLengthとの乖離具合によって糸を赤く塗る 150 // つまり糸が赤くなっていれば、SpringJointが縮もうとしていることを示す 151 this.lineRenderer.SetPositions(this.stringAnchor); 152 var gbValue = Mathf.Exp( 153 this.springJoint != null 154 ? -Mathf.Max(Vector3.Distance(this.stringAnchor[0], this.stringAnchor[1]) - this.stringLength, 0.0f) 155 : 0.0f); 156 var stringColor = new Color(1.0f, gbValue, gbValue); 157 this.lineRenderer.startColor = stringColor; 158 this.lineRenderer.endColor = stringColor; 159 } 160 } 161 162 // 右腕の姿勢を設定し、右腕から糸を出しているように見せる 163 private void OnAnimatorIK(int layerIndex) 164 { 165- this.animator.SetIKPosition(AvatarIKGoal.RightHand, this.stringAnchor[1]); 166- this.animator.SetIKPositionWeight(AvatarIKGoal.RightHand, this.currentIkWeight); 167+ this.animator.SetIKPosition(this.ikGoal, this.stringAnchor[1]); 168+ this.animator.SetIKPositionWeight(this.ikGoal, this.currentIkWeight); 169 } 170 171 // SpringJointの状態を更新する 172 private void FixedUpdate() 173 { 174 // 更新不要なら何もしない 175 if (!this.needsUpdateSpring) 176 { 177 return; 178 } 179 180 // 糸射出中かどうかを判定し... 181 if (this.casting) 182 { 183 // 射出中で、かつまだSpringJointが張られていなければ張り... 184 if (this.springJoint == null) 185 { 186 this.springJoint = this.gameObject.AddComponent<SpringJoint>(); 187 this.springJoint.autoConfigureConnectedAnchor = false; 188 this.springJoint.anchor = this.casterCenter; 189 this.springJoint.spring = this.spring; 190 this.springJoint.damper = this.damper; 191 } 192 193 // SpringJointの自然長と接続先を設定する 194 this.springJoint.maxDistance = this.stringLength; 195 this.springJoint.connectedAnchor = this.stringAnchor[1]; 196 } 197 else 198 { 199 // 射出中でなければSpringJointを削除し、糸による引っぱりを起こらなくする 200 Destroy(this.springJoint); 201 this.springJoint = null; 202 } 203 204 // 更新が終わったので、「SpringJoint要更新」フラグを折る 205 this.needsUpdateSpring = false; 206 } 207 } 208}

そして、キャラクターにはStringCasterを2つアタッチします。インスペクター上には新しく糸射出ボタンの名前欄、糸収縮ボタンの名前欄、糸射出時に接着点へ向ける部位を選択するポップアップ、LineRendererの参照欄が出現しますので、それらに個別に設定を行いました。

図1

LineRendererが2つ必要ですが、こちらは1つのオブジェクトに複数アタッチすることはできないようです。そこでキャラクターからはLineRendererを削除して、別途LineRenderer専用のゲームオブジェクトを2つ作り(LeftStringとRightString)、それを参照させています。

この状態で動かしたところ、下図のように両腕からそれぞれ糸を発射できるようになりました。薄々予想していたけれど案の定...という感想ですが、私のアクションゲーム能力では2本の糸をコントロールするのはきついものがありました(ゲームパッドを使ってLトリガー・Rトリガーで糸をコントロールすることにしたため、キーボードで操作するよりはマシな印象でしたが...)。
空中に飛び上がってから、さらに立体機動的モーションに繋げるとなるとかなり慣れが必要そうです。

図2

投稿2021/06/27 00:14

Bongo

総合スコア10807

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

rasukaryu

2021/06/27 06:07

すごい! 本当に尊敬します!
rasukaryu

2021/06/27 06:28

できました!! すごい!!本当に丁寧に教えてくださりありがとうございました。
rasukaryu

2021/09/05 09:25

失礼します。 これは、カメラの中心からレイキャストを飛ばしていますが、マウスカーソルからレイキャストを飛ばして糸をくっつける、みたいなことはできますか?また、できるなら、できればその方法も教えていただければ光栄です。
Bongo

2021/09/05 20:20

StringCasterのUpdate冒頭に... // まず画面中心から真っ正面に伸びるRayを求め、さらにworldCasterCenterから // そのRayの衝突点に向かうRayを求める...これを糸の射出方向とする this.worldCasterCenter = this.transform.TransformPoint(this.casterCenter); var cameraForward = this.cameraTransform.forward; var cameraRay = new Ray(this.cameraTransform.position, cameraForward); var aimingRay = new Ray( this.worldCasterCenter, Physics.Raycast(cameraRay, out var focus, float.PositiveInfinity, this.interactiveLayers) ? focus.point - this.worldCasterCenter : cameraForward); という部分がありますが、ここでカメラ真っ正面のレイの代わりにマウスポインタへ向かうレイを使えばいいかと思います。 あの部分を下記のようにしてみてはいかがでしょうか。 this.worldCasterCenter = this.transform.TransformPoint(this.casterCenter); var pointerRay = Camera.main.ScreenPointToRay(Input.mousePosition); var aimingRay = new Ray( this.worldCasterCenter, Physics.Raycast(pointerRay, out var focus, float.PositiveInfinity, this.interactiveLayers) ? focus.point - this.worldCasterCenter : pointerRay.direction);
rasukaryu

2021/09/06 06:44

なるほど。わかりやすい回答ありがとうございます! 早速試してみます。
rasukaryu

2021/09/06 09:24

できました。ゲームの完成に近づいています!!ありがとうございます。
rasukaryu

2021/09/10 10:48

すみません。どうしても、できないことがります。糸の発射位置(casterCenter)を手にあるオブジェクトに変更するのは可能でしょうか?
rasukaryu

2021/09/10 11:16

たくさんすみません。つながったオブジェクトが動いても、接着点の位置は変わりません。オブジェクトと接着点を一緒に動かすにはどうしたら良いですか?
Bongo

2021/09/11 19:18

コメントいただいた件について対処案を検討してみました。変更箇所がStringCasterスクリプトの全域に分散しており、変更部分だけ提示するのでは分かりづらいかと思いまして、全文を追記することにしました。そのため文章量の都合上別回答になってしまいましたがご容赦ください。 「糸の発射位置を手にあるオブジェクトに変更する」という件についてですが、これはどの程度の精度が必要でしょうかね? 別回答で追記しましたスクリプトでは、さしあたりジョイントの更新が必要なタイミング...つまり糸を張った時や糸の間に障害物がひっかかったタイミングのみで発射位置の更新を行う形になります。 「手にあるオブジェクト」となりますと、たとえばキャラクターのアニメーションによってキャラクター本体の原点と手のオブジェクトの相対的位置がコロコロ変化するだろうと想像されますが、ジョイントによる物理的な糸をそれに完全に追従させるようにしようとすると、実質的に毎フレームジョイントの起点をつなぎ替えることになりそうです。 UpdateStringメソッド末尾のコメントアウトした部分を有効にすることでそのような動作になるはずですが、試した限りでは回答中で申し上げたように異常な動作を誘発してしまいそうです。手のオブジェクトとキャラクター中心の位置のズレがさほど大きくないのであれば、回答で提示しましたようにLineRendererだけ追従させてSpringJointの追従は妥協しても違和感は小さいんじゃないかと思いますが、どうですかね...? 物理的な発射点も追従しないとまずいようでしたら、構想のみで試してはいないのですが、手のオブジェクトにもRigidbodyを用意してキャラクター本体とジョイントで接続することで実現可能かもしれません。 これなら多分糸のジョイントを毎フレームつなぎ替えることなく発射点を移動できそうな気がしますが、おそらくスクリプト全域に波及する変更が発生しそうですので、また別回答となってしまうかと思います...
rasukaryu

2021/09/12 02:53

なるほど、納得です。ありがとうございます。
guest

あなたの回答

tips

太字

斜体

打ち消し線

見出し

引用テキストの挿入

コードの挿入

リンクの挿入

リストの挿入

番号リストの挿入

表の挿入

水平線の挿入

プレビュー

15分調べてもわからないことは
teratailで質問しよう!

ただいまの回答率
85.46%

質問をまとめることで
思考を整理して素早く解決

テンプレート機能で
簡単に質問をまとめる

質問する

関連した質問