프로젝트/프로젝트 B

# 001 Development Log

효따 2025. 9. 4. 23:42

https://github.com/Kimhyogyeom/ProjectA

 

GitHub - Kimhyogyeom/ProjectA: Unity-MOBA-Prototype

Unity-MOBA-Prototype. Contribute to Kimhyogyeom/ProjectA development by creating an account on GitHub.

github.com

 


 

https://github.com/Kimhyogyeom/ProjectB

 

GitHub - Kimhyogyeom/ProjectB: Drill-inspired Project

Drill-inspired Project. Contribute to Kimhyogyeom/ProjectB development by creating an account on GitHub.

github.com

 


 

 

ProjectB 작업중 영상

 


 

안녕하세요.😊

 

진짜 날이 많이 더워진 것 같습니다.. 허허..

 

다들 더위 조심하세요..!!

 

 

우선 제가 ProjectB를 작업하면서

 

각 기능별로 블로그에 업로드를 하려고 했었는데요!

 

깃으로 처음부터 작업을 하다 보니

 

Commit도 남아있고, 시간 절약을 위해서

 

하루하루 개발 일지를 남기는 과정으로 작업을 진행하려고 합니다.

 

참고 부탁드립니다.

 


 

먼저 레퍼런스로 참고하는 '드드드드릴' 게임을 플레이해 보며

 

영상을 녹화 후 재생 속도를 최하로 낮추고 계속 탐색을 했었는데요.

 

 

생각이 생각에 꼬리를 물고.. 작업은 지연된 채 시간만 흘러가고 있는 문제가 있었습니다.

 

개발에 정답이란 정해져 있지 않고, 추후 수정 반영 또한 개발 과정에 중요한 요소이기에

 

먼저 다른 생각을 버리고, 작업을 해보기로 했습니다.

 


 

Ground가 내려가는 GroundMoveDown 스크립트입니다. 

using UnityEngine;

public class GroundMoveDown : MonoBehaviour
{
    [SerializeField] private float _downSpeed = 100f;
    [SerializeField] private Transform _groundTr;

    void Update()
    {
        _groundTr.position += Vector3.down * _downSpeed * Time.deltaTime;
    }

}

 

단순히 지정 속도를 [SerializeField]를 통해 유니티 인스펙터창에 노출시켜 주어 테스트하며 값을 맞췄고

 

현재는 1로 낮게 잡아 테스트를 진행하였습니다.

 

또한 Update에서 ground의 position을 아랫 방향 + 스피드 + 델타 타임을 곱해 더해 주었습니다.

 


 

다음은 Ground를 따라다니는 CameraFollowGround 스크립트입니다.

using UnityEngine;

public class CameraFollowGround : MonoBehaviour
{
    [SerializeField] private Transform _targetGround;
    [SerializeField] private float _smoothTime = 0.3f;

    private Vector3 _velocity = Vector3.zero;

    void LateUpdate()
    {
        if (_targetGround == null) return;

        Vector3 targetPos = new Vector3(transform.position.x, _targetGround.position.y - 1.5f, transform.position.z);

        transform.position = Vector3.SmoothDamp(transform.position, targetPos, ref _velocity, _smoothTime);
    }
}

 

SmoothDamp()를 사용해 부드럽게 보간 되어 따라다니게 해 주었으며

 

_targetGround(Ground). position.y 에 -1.5f를 해준 것은

 

Ground가 화면 정 가운데 위치하지 않게 하기 위함입니다.

 


 

다음은 총알을 발사하는 로직을 담은 GunFire 스크립트입니다.

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

public class GunFire : MonoBehaviour
{
    [SerializeField] private GameObject _bulletPrefab;
    [SerializeField] private float _fireRate = 0.5f;
    [SerializeField] private Transform _fireStartPoint;

    [Header("Gun Sprites")]
    [SerializeField] private Sprite _spriteA;
    [SerializeField] private Sprite _spriteB;
    [SerializeField] private SpriteRenderer _gunRenderer;

    private float _fireTimer = 0f;
    [SerializeField] private List<GameObject> _bulletPool = new List<GameObject>();

    void Update()
    {
        _fireTimer += Time.deltaTime;

        if (_fireTimer >= _fireRate)
        {
            GenerateBullet();
            _fireTimer = 0f;
        }
    }

    void GenerateBullet()
    {
        StartCoroutine(SwitchSprite());

        foreach (GameObject bullet in _bulletPool)
        {
            if (!bullet.activeSelf)
            {
                bullet.transform.position = _fireStartPoint.position;
                bullet.transform.rotation = Quaternion.identity;
                bullet.SetActive(true);
                return;
            }
        }

        GameObject newBullet = Instantiate(_bulletPrefab, _fireStartPoint.position, Quaternion.identity);
        _bulletPool.Add(newBullet);
    }

    private IEnumerator SwitchSprite()
    {
        _gunRenderer.sprite = _spriteB;

        yield return new WaitForSeconds(0.1f);
        _gunRenderer.sprite = _spriteA;
    }
}

 

Update()에 타이머를 정해주어 일정 시간마다 GenerateBullet() 함수가 실행되게 해 주었습니다.

 

오브젝트는, 미리 Unity에 생성해 두어 간단한 Pooling 방식으로 풀어보려고 했습니다.

 

각 총알 오브젝트 중 비활성화된 것이 있으면, 해당 오브젝트를 활성화하며

 

만일, 모든 오브젝트가 없을 때 Instantiate()를 사용해 프리팹을 생성하게 해 줬습니다.

 

또한

 

코루틴에서 실행되는 _spriteA/B 이미지는 총알을 발사하기 전/후의 Gun 이미지입니다.

 

마음에 드는 이미지가 없었기에 이전에 작업했던 Pixilart가 생각이 났고,

 

https://www.pixilart.com/

 

Pixilart - Share & Create Art Online

Pixilart, free online drawing editor and social platform for everyone. Create game sprites, make pixel art, animated GIFs, share artwork and socialize online.

www.pixilart.com

 

바로 '만들어보자~'라는 생각으로 작업을 해주었습니다.😎

 

 

생각보다 미적 감각이 있을 수도..?

 


 

다음은 발사된 총알의 컨트롤을 담당하는 GunBullet 스크립트입니다.

using UnityEngine;

public class GunBullet : MonoBehaviour
{
    [SerializeField] private int _bulletDamage = 50;
    [SerializeField] private float _bulletSpeed = 3f;
    [SerializeField] private DamageTextSpawner _damageTextSpawner;

    void Update()
    {
        transform.Translate(Vector2.down * _bulletSpeed * Time.deltaTime);
    }

    void OnTriggerEnter2D(Collider2D collision)
    {
        if (collision.CompareTag("DeathZone"))
        {
            this.gameObject.SetActive(false);
        }
        else if (collision.CompareTag("BreakGround"))
        {
            collision.GetComponent<BreakGroundController>()?.TakeDamage(_bulletDamage);
            DamageTextSpawner spawner = FindObjectOfType<DamageTextSpawner>();
            spawner.ShowDamage(_bulletDamage, transform.position);
            this.gameObject.SetActive(false);
        }
    }
}

 

Update()에서 Translate를 활용해 아래 방향으로 이동되게 해 주었으며

 

DeathZone은 테스트를 할 때 만든 Collider 오브젝트이며,

 

BreakGround의 태그는, 아래에 생성되어 있는 파괴할 그라운드입니다.

 

TakeDamage()는 파괴할 그라운드가 대미지를 입는 모션.

DamageTextSpawner 스크립트는 총알에 파괴할 그라운드가 닿으면 Text가 표시될 작업이 담겨 있습니다.

 


 

먼저 그라운드 생성 로직을 담고 있는 BreakGroundGenerate 스크립트부터 살펴보겠습니다.

using UnityEngine;

public class BreakGroundGenerate : MonoBehaviour
{
    [SerializeField] private GameObject _breakGroundPrefab;
    [SerializeField] private int _generateCount = 5;
    [SerializeField] private float _ySpacing = 0.5f;

    private Color color1 = new Color(1f, 1f, 1f);
    private Color color2 = new Color(1f, 0.733f, 0.506f);

    void Start()
    {
        for (int i = 0; i < _generateCount; i++)
        {
            float yPos = -(_generateCount - 1 - i) * _ySpacing;
            Vector3 pos = transform.position + new Vector3(0, yPos, 0);

            GameObject breakGround = Instantiate(_breakGroundPrefab, pos, Quaternion.identity, transform);

            SpriteRenderer sr = breakGround.GetComponent<SpriteRenderer>();
            if (sr != null)
            {
                sr.color = (i % 2 == 0) ? color1 : color2;

                sr.flipX = (i % 2 != 0);
            }
        }
    }
}

 

먼저 반복문 for를 통하여 파괴될 그라운드의 배치를 Start() 함수에서 해주었습니다.

 

영상을 유심히 살펴보니 파괴될 그라운드가 겹쳐 있는 게 보였고, 

 

가장 맨 위에 있는 오브젝트의 오더가 높은 것을 확인하였습니다.

 

그리하여 처음엔 오브젝트들의 오더를 조절해주려고 했었는데요

 

 

그러다 보니 Gun에서 발사되는 Bullet의 오더도 같이 변경을 해주는 번거로운 작업이 발생했었습니다.

 

그리하여 미리 count를 정해주어, 맨 처음 생성되는 오브젝트를 제일 하단에 오게 해 주었고

 

각 ySpacing 간격으로 배치를 해주었습니다.

 

또한 

 

color와 flipX를 조절해 주는 부분은

 

현재 파괴될 그라운드가 하나밖에 없기에, 단조로움을 순화시켜주기 위합입니다.


 

다음은 파괴될 그라운드가 Damage를 입는 로직이 담긴 BreakGroundController 스크립트입니다.

using TMPro;
using UnityEngine;
using System.Collections;

public class BreakGroundController : MonoBehaviour
{
    [SerializeField] private int _breakGroundHp = 1350;
    [SerializeField] private TextMeshProUGUI _breakGroundHpText;

    private Vector3 _originalScale;

    void Awake()
    {
        _originalScale = transform.localScale;
    }

    public void TakeDamage(int damage)
    {
        _breakGroundHp -= damage;
        _breakGroundHpText.text = _breakGroundHp.ToString();

        StopAllCoroutines();
        StartCoroutine(HitEffect());

        if (_breakGroundHp <= 0)
        {
            gameObject.SetActive(false);
        }
    }

    private IEnumerator HitEffect()
    {
        Vector3 targetScale = _originalScale * 1.1f;
        float t = 0f;
        while (t < 0.1f)
        {
            transform.localScale = Vector3.Lerp(_originalScale, targetScale, t / 0.1f);
            t += Time.deltaTime;
            yield return null;
        }
        transform.localScale = targetScale;

        t = 0f;
        while (t < 0.1f)
        {
            transform.localScale = Vector3.Lerp(targetScale, _originalScale, t / 0.1f);
            t += Time.deltaTime;
            yield return null;
        }
        transform.localScale = _originalScale;
    }
}

 

먼저 TakeDamage()가 실행되면 대미지를 입은 것으로 판단하여

 

파괴될 그라운드의 Hp에 받은 Damage만큼 깎아 주었고

 

코루틴을 사용해 파괴될 그라운드의 Scale을 1.1배 키웠다가 원래대로 되돌려 주는 작업을 해주었습니다.

 

이 부분은 지난번에도 작업한 DOTween으로 작업을 해볼까 했는데,

 

포트폴리오용으로 직접 구현을 해보고 싶은 마음이 컸습니다 😄

 


 

다음은 대미지 Text를 컨트롤하는 DamageText 스크립트입니다.

using UnityEngine;
using TMPro;

public class DamageText : MonoBehaviour
{
    [SerializeField] private float _floatSpeedY = 50f;
    [SerializeField] private float _floatSpeedX = 20f;
    [SerializeField] private float _duration = 1f;

    [SerializeField] private TextMeshProUGUI _damageText;
    private float _timer;
    private float _randomXDir;

    public void Play()
    {
        _timer = 0f;
        _randomXDir = Random.Range(-_floatSpeedX, _floatSpeedX); // 랜덤 방향
        gameObject.SetActive(true);
    }

    private void Update()
    {
        _timer += Time.deltaTime;
        float progress = _timer / _duration;

        transform.position += new Vector3(_randomXDir, _floatSpeedY, 0) * Time.deltaTime;

        float alpha = Mathf.Lerp(1f, 0f, progress);
        _damageText.color = new Color(_damageText.color.r, _damageText.color.g, _damageText.color.b, alpha);

        if (_timer >= _duration)
        {
            Destroy(gameObject);
        }
    }
}

 

Update()에서 보면 transform(DamageText)의 포지션을 랜덤 방향으로 이동하게 해 주었으며

 

텍스트의 color의 Alpha값 또한 서서히 사라지게끔 구현을 해보았습니다.

 

이후 일정 시간이 지나면 Destroy()로 오브젝트가 파괴됩니다.

 

이 DamageText를 활성화시켜주는 함수 Play() 함수는 아래에서 같이 보겠습니다.


 

DamageTextSpawner 스크립트입니다.

using UnityEngine;
using TMPro;

public class DamageTextSpawner : MonoBehaviour
{
    [SerializeField] private GameObject _damageTextPrefab;
    [SerializeField] private Canvas _canvas;

    public void ShowDamage(int damage, Vector3 worldPosition)
    {
        Vector3 screenPos = Camera.main.WorldToScreenPoint(worldPosition);

        GameObject dmgTextObj = Instantiate(_damageTextPrefab, _canvas.transform);

        dmgTextObj.transform.position = screenPos;

        TextMeshProUGUI tmp = dmgTextObj.GetComponent<TextMeshProUGUI>();
        tmp.text = damage.ToString();

        dmgTextObj.GetComponent<DamageText>().Play();
    }
}

 

프리팹인 DamageText를 받아와 Instantiate()를 사용해 생성을 시켜주며

 

DamageText.text에 대미지를 표시되게 해 줍니다.

 

또한

 

마지막으로 Play() 함수를 호출합니다.

 

 

이 ShowDamage() 함수는 

 

위에서 본 GunBullet 스크립트에서 "BreakGround"가 닿았을 때 호출 됩니다.

using UnityEngine;

public class GunBullet : MonoBehaviour
{
    [SerializeField] private int _bulletDamage = 50;
    [SerializeField] private float _bulletSpeed = 3f;
    [SerializeField] private DamageTextSpawner _damageTextSpawner;

    void Update()
    {
        transform.Translate(Vector2.down * _bulletSpeed * Time.deltaTime);
    }

    void OnTriggerEnter2D(Collider2D collision)
    {
        if (collision.CompareTag("DeathZone"))
        {
            this.gameObject.SetActive(false);
        }
        else if (collision.CompareTag("BreakGround"))
        {
            collision.GetComponent<BreakGroundController>()?.TakeDamage(_bulletDamage);
            DamageTextSpawner spawner = FindObjectOfType<DamageTextSpawner>();
            spawner.ShowDamage(_bulletDamage, transform.position);
            this.gameObject.SetActive(false);
        }
    }
}

 


 

마지막으로 MinerController 스크립트입니다.

using System.Collections;
using UnityEngine;

public class MinerController : MonoBehaviour
{
    [SerializeField] private byte _minerDirection;
    [SerializeField] private Transform _startPosLeft;
    [SerializeField] private Transform _startPosRight;
    [SerializeField] private Transform _endPos;
    [SerializeField] private float _minerMoveSpeed = 2f;

    private void Start()
    {
        StartCoroutine(MiningLoop());
    }

    private IEnumerator MiningLoop()
    {
        while (true)
        {
            if (_minerDirection == 0)
            {
                yield return StartCoroutine(MoveToPosition(_startPosLeft.position));
            }
            else if (_minerDirection == 1)
            {
                yield return StartCoroutine(MoveToPosition(_startPosRight.position));
            }


            for (int i = 0; i < 3; i++)
            {
                yield return new WaitForSeconds(1f);
            }

            yield return StartCoroutine(MoveToPosition(_endPos.position));
        }
    }

    private IEnumerator MoveToPosition(Vector3 target)
    {
        while (Vector3.Distance(transform.position, target) > 0.01f)
        {
            Vector3 dir = (target - transform.position).normalized;

            transform.Translate(dir * _minerMoveSpeed * Time.deltaTime, Space.World);

            yield return null;
        }

        transform.position = target;
    }
}

 

_minerDirection = 0(왼쪽) or 1(오른쪽)입니다.

 

해당 _minerDirection에

 

yield return StartCoroution()을 사용하여, 코루틴이 완전히 끝날 때까지 기다린 후 넘어가도록 해주었습니다.

 

 

MoveToPosition은 현재 transform(miner)이 target까지의 거리가 0.01f보다 크면 이동하게 해 주었고,

 

A - B의 방향으로 Translate()를 활용해 이동하게 해 주었습니다.

 

이후 MoveToPosition() 코루틴이 끝나면

 

For문을 통해 1초씩 기다립니다. (총 3초)

 

이 해당 부분에 채굴 애니메이션 혹은 스프라이트 변경으로 : 채굴하는 모션을 해줄 것입니다.

 


 

 

영상을 다시 보니 파괴될 그라운드가 Hp가 0 이하가 되어 사라질 때 파괴되는 이펙트도 있어야 할 것 같고

 

전체적으로 Ground가 내려가는 느낌이 너무 부족하기에, Sprite 및 연출 효과도 많이 신경을 써야 할 것 같습니다.😅

 

하지만 원하는 Sprite를 구하는 게 쉽진 않을 것 같은 느낌도 들지만..!

 

음..

 

FixilArt를 적극 활용해 봐야겠습니다.😎

 

 

그래도 역시 뭔가 하나씩 하나씩 개발되어 가는 과정을 보면 참 재미있는 것 같습니다.

 


 

감사합니다.

'프로젝트 > 프로젝트 B' 카테고리의 다른 글

# 005 Development Log  (0) 2025.09.09
# 004 Development Log  (1) 2025.09.07
# 003 Development Log  (1) 2025.09.06
# 002 Development Log  (2) 2025.09.05
인사말  (0) 2025.09.04