Unity ChessMaze 활동하면서 구현해보고 싶었던 것(사슬날리는 것)

먼저, 우리 게임의 주 기능은 컨트롤러를 누르면 사슬이 뻗어 나가 사념에 연결되는 것이다.

 

나는 내 방식대로 이 사슬이 뻗어나가는 것을 구현해보려고 한다.


이론

1. 사슬이 뻗어나가는 위치와 타켓의 위치를 계산한다(Distance)

2. 사슬이 생길 개수를 미리 정한다(이렇게 한 이유는, 사슬의 크기를 고정할 경우 distance의 거리에 따라 사슬의 개수가 부족하거나 초과될 수 있기 때문에 사슬의 개수를 미리 정해서 사슬의 크기를 유동적으로 바꿀 예정이다), 사슬의 크기를 고정시키는 방법도 해볼 예정이다

코드

 

코드는 이러하다 distance를 구하고, 방향을 구한다음, 오브젝트의 길이를 구하면 된다.

이렇게하면 문제점이 하나 발생한다.

 

문제 1

 

이렇게 사슬이 중심정에 서로 겹친다.

사이즈를 조정해주자.

 

수정된 코드

 

위 아래로 겹치기 때문에 절반을 줄여주면 딱 크기가 맞는다.

 

결과

 

이렇게 이제 겹치지 않고 잘나간다.

 

+ 사슬의 길이가 고정일 때

사슬의 길이 고정

 

height는 이제 오브젝트의 길이다.

 

결과

 


 

추가 기능 코드

 

 

end 같은 경우 처음 사슬 날리고 그 사슬들이 타겟에 부착되는 순간을 말한다.

거리에 따라 사슬이 생성되고 삭제됨을 구현해봤다.

 


 

최종본

 


원본 코드 (Ref)

 

Chain 관련

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

public class ChainUtils : MonoBehaviour {
    public const float LENGTH = 0.06f;
    public const float SCALE = 5f;
    public const float DELAY = 0.01f;
    public static ChainUtils main;
    public static GameObject chainPrefab;
    public static WaitForSeconds delayer;
    public GameObject chainPrefabHook;

    public static ChainCore Line(Vector3 start, Quaternion direction, int n) {
        GameObject holder = new GameObject("Chains");
        ChainCore core = holder.AddComponent<ChainCore>();
        core.LENGTH = n;
        core.expanding = true;
        holder.transform.position = start;
        holder.transform.rotation = direction * Quaternion.AngleAxis(-90, Vector3.left);
        holder.transform.localScale = new Vector3(SCALE, SCALE, SCALE);
        main.StartCoroutine(LineI(core, n));
        return core;
    }

    public static GameObject GetChain(Vector3 pos) {
        return Instantiate(chainPrefab, pos, Random.rotation);
    }

    public static ChainCore LineWorld(Vector3 start, Quaternion direction, float length) {
        return Line(start, direction, Mathf.CeilToInt(length / (SCALE * LENGTH)));
    }

    public static ChainCore LineTarget(Vector3 start, GameObject target) {
        GameObject holder = new GameObject("Chains");
        ChainCore core = holder.AddComponent<ChainCore>();

        core.LENGTH = 400; //maximum value as the target keeps moving
        core.expanding = true;
        core.target = target;
        holder.transform.position = start;
        holder.transform.rotation = Quaternion.LookRotation(target.transform.position - start, Vector3.up) * Quaternion.AngleAxis(-90, Vector3.left);
        holder.transform.localScale = new Vector3(SCALE, SCALE, SCALE);

        main.StartCoroutine(LineTargetI(core, target));
        return core;
    }

    private void Awake() {
        main = this;
        chainPrefab = chainPrefabHook;
        delayer = new WaitForSeconds(DELAY);
        Debug.Log("ChainUtils Singleton Set!");
    }

    static IEnumerator LineI(ChainCore holder, int n) {
        for (int i = 0; i < n; i++) {
            ChainLink chain = GetChain(Vector3.up * LENGTH * i + new Vector3(Random.Range(-0.5f, 0.5f), Random.Range(-0.5f, 0.5f), Random.Range(-0.5f, 0.5f))).GetComponent<ChainLink>();//todo pooling
            chain.pos = Vector3.up * LENGTH * i;
            chain.transform.SetParent(holder.transform, false);
            chain.id = i;
            chain.core = holder;
            if (i == n - 1) {
                chain.end = true;
            }
            yield return delayer;
        }
    }

    static IEnumerator LineTargetI(ChainCore holder, GameObject target) {
        int i = 0;
        ChainLink chain = null;
        while (i * LENGTH * SCALE < Vector3.Distance(holder.transform.position, target.transform.position)) {
            if(i > 0 && i % 4 == 3) yield return delayer;
            chain = GetChain(Vector3.up * LENGTH * i + new Vector3(Random.Range(-0.5f, 0.5f), Random.Range(-0.5f, 0.5f), Random.Range(-0.5f, 0.5f))).GetComponent<ChainLink>();//todo pooling
            chain.pos = Vector3.up * LENGTH * i;
            chain.transform.SetParent(holder.transform, false);
            chain.id = i;
            chain.core = holder;
            i++;
        }
        if (chain != null) chain.end = true;
        holder.LENGTH = i;
        //holder.offset = i - Vector3.Distance(holder.transform.position, target.transform.position) / (SCALE * LENGTH);
        //holder.Link();
    }
}

 

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using static ChainUtils;

public class ChainLink : MonoBehaviour {
    private const float TIME = 0.3f;
    private float t = 0;
    private bool anim = true;
    public MeshRenderer mesh;
    public GameObject chainFx;

    public Vector3 pos;
    public int id;
    public ChainCore core;
    public bool end;

    private void Awake() {
        anim = true;
        t = 0;
    }

    void Update() {
        if (anim) {
            t += Time.deltaTime;
            if (t >= TIME) {
                transform.localPosition = pos;
                transform.localRotation = Quaternion.identity;
                transform.localScale = Vector3.one;
                anim = false;
                if (end) {
                    core.expanding = false;
                    if (core.target != null) core.Link();
                    Instantiate(chainFx, transform.position, Quaternion.identity);
                }
            }
            else {
                transform.localPosition = pos * (t / TIME) + transform.localPosition * (1 - t / TIME);
                transform.localRotation = Quaternion.Lerp(transform.localRotation, Quaternion.identity, t / TIME);
                transform.localScale = Vector3.one * t / TIME;
            }
        }
        else {
            //transform.localPosition = pos + Vector3.down / SCALE * LENGTH * core.offset;
            transform.localScale = Vector3.one * Mathf.Clamp01((core.LENGTH - id) + 1 - core.offset);
            mesh.enabled = core.LENGTH - id > core.offset - 1;
        }
    }
}

 

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

public class ChainCore : MonoBehaviour {
    public int LENGTH;
    [Range(0, 100)] public float offset;
    public GameObject target;
    public bool linked;
    public bool expanding = true;

    private Vector3 lastp;

    void Update() {
        if (target != null) {
            transform.rotation = Quaternion.LookRotation(target.transform.position - transform.position, Vector3.up) * Quaternion.AngleAxis(-90, Vector3.left);
            if (linked) {
                //transform.position += (target.transform.position - lastp);
                //lastp = target.transform.position;
            }
        }
        if(offset > LENGTH) Destroy(gameObject);
    }

    public float RealLength() {
        return (LENGTH - offset) * ChainUtils.SCALE * ChainUtils.LENGTH;
    }

    public float MaxLength() {
        return LENGTH * ChainUtils.SCALE * ChainUtils.LENGTH;
    }

    public void SetLength(float l) {
        offset = Mathf.Clamp(LENGTH - l / (ChainUtils.SCALE * ChainUtils.LENGTH), 0f, LENGTH + 1);
    }

    public void Link() {
        linked = true;
        lastp = target.transform.position;
    }
}

 

 

Controller

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.EventSystems;
using UnityEngine.XR.Interaction.Toolkit;

public class VRRayController : MonoBehaviour {
    public const int MAGNET_LAYER = 6;
    public const float PULL_STR = 200f, STALL_TIME = 3f, YEET_STR = 600f, HOLD_LINGER = 0.5f;

    public PlayerControl pcon;
    public Magnet magnet;//not null if hovering is true, and vice versa
    public bool hovering, connected, lastShoot;
    public ChainCore chain;//not null if lastChain is true, and vice versa
    public GameObject triggerEffectHolder;

    //tests
    public bool testGrip, testTrigger, overrideTarget = false;
    public GameObject testTarget;

    public enum ControllerType { Left = 0, Right = 1 };
    public ControllerType CT;
    RaycastHit hit;

    private float holdf = 0;
    private bool holdb = false;

    void Start() {
        holdb = false;
        holdf = 0;
        triggerEffectHolder.SetActive(false);

        Debug.Log(CT.ToString() + " Controller Active!");
    }

    void Update() {
        //face towards override target (for testing)
        if(overrideTarget && testTarget != null) {
            transform.forward = testTarget.transform.position - transform.position;
        }

        //grip
        if (lastShoot || (Grip(CT) && Physics.Raycast(transform.position, transform.forward, out hit, 100000, 1 << MAGNET_LAYER) && IsValid(hit))) {
            if (!hovering) {
                hovering = true;
                magnet.Hover(CT);
            }
        }
        else {
            if (hovering) {
                hovering = false;
                magnet.Unhover(CT);
                magnet = null;
            }
        }

        //trigger
        if (lastShoot) {
            if (!Trigger(CT)) {
                //detach
                RemoveChain();
            }
            else if(!connected) {
                //chain is extending
                if (!chain.expanding) {
                    //chain done expanding
                    connected = true;
                }
            }
        }
        else {
            if(hovering && Trigger(CT)) {
                //shoot
                lastShoot = true;
                connected = false;
                chain = ChainUtils.LineTarget(transform.position, magnet.gameObject);
            }
        }

        if(lastShoot && chain != null){
            chain.transform.position = transform.position;
            if (connected) {
                UpdateChainLength(magnet);
                PullTowards(magnet);
            }
        }

        //holding effects
        holdf = Mathf.Clamp01(holdf + Time.deltaTime / HOLD_LINGER * (hovering ? 1 : -1));
        if(holdf > 0) {
            if (!holdb) {
                holdb = true;
                triggerEffectHolder.SetActive(true);
            }
            triggerEffectHolder.transform.localScale = Vector3.one * holdf;
        }
        else {
            if (holdb) {
                holdb = false;
                triggerEffectHolder.SetActive(false);
            }
        }
    }

    public void RemoveChain() {
        if (!lastShoot) return;
        lastShoot = false;
        connected = false;
        if (chain.expanding) StartCoroutine(RemoveChainPreI(chain));
        else StartCoroutine(RemoveChainI(chain));
        chain = null;
    }

    private void UpdateChainLength(Magnet target) {
        //rotate core: try to go to pos
        Vector3 pos = transform.position;
        float len = Vector3.Distance(pos, target.transform.position);
        if (len > chain.MaxLength()) {
            //try to move rigidbody closer
            pcon.rigid.MovePosition(pcon.rigid.position + (target.transform.position - pos).normalized * (len - chain.MaxLength()));
            if (Vector3.Distance(pos, target.transform.position) > chain.MaxLength() + 0.3f) {
                //break the chain, the pull is obstructed and there is no hope whatsoever
                RemoveChain();
                return;
            }
            //wrong color
            if (!target.hasColor) pcon.lastColorExists = false;
            if (pcon.lastColorExists && target.white == pcon.lastWhite) {
                WrongColor(target);
                StartCoroutine(RemoveChainI(chain));
                chain = null;
                return;
            }
            if (target.hasColor) pcon.lastWhite = target.white;

            len = chain.MaxLength();
        }
        //chainge offset based on len
        chain.SetLength(len);
    }

    private void PullTowards(Magnet target) {
        float len = Vector3.Distance(target.transform.position, transform.position);
        //pull with force
        if (len > target.radius) {
            pcon.rigid.AddExplosionForce(PULL_STR * /*Mathf.Clamp01((len - target.radius) / (target.radius * 5f)) * */ -1, target.transform.position, len * 2f);
        }
        else {
            pcon.rigid.AddExplosionForce(PULL_STR * Mathf.Clamp01(1f - len / (target.radius)), target.transform.position, len * 2f);
        }
        //open cage
        if (len < target.radius * 2f) target.CheckOpen();
    }

    private void WrongColor(Magnet target) {
        pcon.WrongColor();
        pcon.rigid.AddExplosionForce(YEET_STR, target.transform.position, Vector3.Distance(target.transform.position, transform.position) * 2f);
    }

    public static ControllerType Other(ControllerType type) {
        if (type == ControllerType.Left) return ControllerType.Right;
        return ControllerType.Left;
    }

    private bool IsValid(RaycastHit hit) {
        if (hit.transform.gameObject.TryGetComponent(out magnet) && !magnet.Hovered(Other(CT))) return true;
        magnet = null;
        return false;
    }

    public bool Grip(ControllerType lr) { //Grip: activate chain yeet mode
        if (testGrip) return true;
        if (lr == ControllerType.Left) return OVRInput.GetDown(OVRInput.Button.PrimaryHandTrigger);
        else return OVRInput.GetDown(OVRInput.Button.SecondaryHandTrigger);
    }

    public bool Trigger(ControllerType lr) { //Trigger: yeet chain
        if (testTrigger) return true;
        if (lr == ControllerType.Left) return OVRInput.GetDown(OVRInput.Button.PrimaryIndexTrigger);
        else return OVRInput.GetDown(OVRInput.Button.SecondaryIndexTrigger);
    }

    IEnumerator RemoveChainI(ChainCore chain) {
        while (chain != null && chain.gameObject != null) {
            chain.offset += Time.deltaTime * 20f;
            chain.transform.position += (chain.target.transform.position - transform.position).normalized * Time.deltaTime * 20f * ChainUtils.SCALE * ChainUtils.LENGTH;
            yield return null;
        }
    }

    IEnumerator RemoveChainPreI(ChainCore chain) {
        chain.enabled = false;
        float t = 0;
        while (t < 0.5f) {
            t += Time.deltaTime * 20f;
            chain.transform.localScale = Vector3.one * ChainUtils.SCALE * (t / 0.5f);
            yield return null;
        }
        Destroy(chain.gameObject);
    }
}