
development diary
Developed: 떠오른 아이디어가 재밌는지 확인하기 위해 프로토타입을 만들어 보았습니다.
- 프로젝트 : RCPointer
- 소요 시간 : 1시간 30분
- 설명 : 마우스를 90도 돌려서 게임 패드를 잡듯이 엄지 손가락으로 마우스 휠을 돌려
플레이어의 좌우 방향을 제어합니다. 마우스의 휠을 RC카 컨트롤러에서 RC카의 방향을
컨트롤하는 바퀴 모양의 회전하는 부분을으로 생각하여 RC카 컨트롤러의 핸들 부분을 돌릴 때
자동차의 핸들을 돌리는 것처럼 느끼는데, 마우스의 휠을 돌리는 것도 그와 비슷한
느낌이 들 수 있게 플레이어의 방향을 마우스의 휠로 조정하여 그냥 키를 눌러 좌, 우로 움직이는 것이 아닌
자신이 휠, 즉 핸들을 돌려 현실과의 동화율을 높입니다. 현실과의 동화율이 높아지면
뇌가 게임을 진짜라고 인식하여 더 깊고 빠른 몰입을 할 수 있으며 조작의 만족감, 새로움 등으로
게임 플레이에 많은 영향을 미칠 수 있다고 생각합니다. 앞으로 가기 버튼을 누르면 부스터가 나가고 뒤로 가기
버튼을 누르면 방향이 앞을 바라보기 때문에 정밀한 방향 조절이 힘들어 정확히 앞을 바라보기 힘들다는
조작의 불편함을 없앴습니다.
- 설명 : 마우스를 90도 돌려서 게임 패드를 잡듯이 엄지 손가락으로 마우스 휠을 돌려


- 사용한 코드 : 재미만을 검증하는 프로토타입이기 때문에 코드는 AI를 사용해 빠르게 짜서 1시간 30분만에 0부터 1까지 전부 개발할 수 있었습니다.
더보기
using UnityEngine;
using Unity.Cinemachine; // Cinemachine v3 (Unity 6 권장)
public class PlayerMovement : MonoBehaviour
{
[Header("Move / Tilt")]
public Rigidbody2D rb;
public float speedChange = 0.5f;
public float maxSpeed = 3f;
public float maxRotation = 30f;
public float rotationSmooth = 5f;
[Header("Cinemachine")]
public CinemachineImpulseSource impulseSource; // 같은 오브젝트에 있으면 자동 할당 권장
public CinemachineCamera cineCam; // 메인 시네마신 카메라 할당(필수)
[Header("Booster Settings")]
public KeyCode boosterMouseButton = KeyCode.Mouse3; // GetMouseButtonDown(3)
public float boostHeight = 3f; // 원래 y에서 +얼마나 올릴지
public float boostRiseTime = 0.2f; // 상승 시간
public float boostHoldTime = 0.6f; // 유지 시간
public float boostFallTime = 0.25f; // 복귀 시간
public float zoomOutAmount = 1.5f; // OrthographicSize 증가량
// Forward 버튼(마우스 4)로 즉시 정지
public KeyCode resetMouseButton = KeyCode.Mouse4;
private bool isBoosting;
private float baseY;
private float baseOrthoSize;
public bool IsBoosting => isBoosting; // 읽기 전용 프로퍼티 추가
private void Awake()
{
if (impulseSource == null) impulseSource = GetComponent<CinemachineImpulseSource>();
}
private void Update()
{
float scroll = Input.mouseScrollDelta.y;
// --- 즉시 정지(Forward 버튼) ---
if (Input.GetKeyDown(resetMouseButton))
{
rb.linearVelocity = Vector2.zero;
transform.rotation = Quaternion.identity;
return;
}
// --- Booster 트리거 ---
if (Input.GetKeyDown(boosterMouseButton) && !isBoosting)
{
StartCoroutine(BoostRoutine());
}
// --- 휠 입력 → 속도 변경 (부스터 중에도 허용) ---
if (scroll != 0f)
{
Vector2 v = rb.linearVelocity;
v.x += scroll * speedChange;
v.x = Mathf.Clamp(v.x, -maxSpeed, maxSpeed);
rb.linearVelocity = v;
}
// --- 속도 → 회전 보간 (부스터 중에도 정상 동작) ---
float t = rb.linearVelocity.x / maxSpeed; // -1~+1
float targetAngle = -t * maxRotation;
float newZ = Mathf.LerpAngle(transform.eulerAngles.z, targetAngle, Time.deltaTime * rotationSmooth);
transform.rotation = Quaternion.Euler(0f, 0f, newZ);
}
private System.Collections.IEnumerator BoostRoutine()
{
if (cineCam == null) yield break;
isBoosting = true;
// 기준값 저장
baseY = transform.position.y;
var lens = cineCam.Lens;
baseOrthoSize = lens.OrthographicSize;
float targetY = baseY + boostHeight;
float targetOrtho = baseOrthoSize + zoomOutAmount;
// 1) 상승(플레이어 y, 카메라 줌 아웃)
yield return LerpOverTime(
boostRiseTime,
(alpha) =>
{
float y = Mathf.Lerp(baseY, targetY, Smooth01(alpha));
var p = transform.position;
p.y = y;
transform.position = p;
var l = cineCam.Lens;
l.OrthographicSize = Mathf.Lerp(baseOrthoSize, targetOrtho, Smooth01(alpha));
cineCam.Lens = l;
}
);
// 2) 유지
if (boostHoldTime > 0f) yield return new WaitForSeconds(boostHoldTime);
// 3) 하강 및 카메라 복귀
yield return LerpOverTime(
boostFallTime,
(alpha) =>
{
float y = Mathf.Lerp(targetY, baseY, Smooth01(alpha));
var p = transform.position;
p.y = y;
transform.position = p;
var l = cineCam.Lens;
l.OrthographicSize = Mathf.Lerp(targetOrtho, baseOrthoSize, Smooth01(alpha));
cineCam.Lens = l;
}
);
// 스냅 보정
{
var p = transform.position; p.y = baseY; transform.position = p;
var l = cineCam.Lens; l.OrthographicSize = baseOrthoSize; cineCam.Lens = l;
}
isBoosting = false;
}
// 부드러운 이징(Lerp 콜백 실행)
private System.Collections.IEnumerator LerpOverTime(float duration, System.Action<float> onStep)
{
if (duration <= 0f) duration = 0.0001f;
float t = 0f;
while (t < 1f)
{
t += Time.deltaTime / duration;
onStep?.Invoke(Mathf.Clamp01(t));
yield return null;
}
onStep?.Invoke(1f);
}
// 3t^2-2t^3 (부드러운 가감속)
private float Smooth01(float x) => 3f * x * x - 2f * x * x * x;
private void OnTriggerEnter2D(Collider2D collision)
{
if (collision.CompareTag("Enemy"))
{
// 부스터 중에는 흔들리지 않음
if (isBoosting)
{
collision.transform.GetChild(0).GetComponent<ParticleSystem>().Play();
collision.GetComponent<Renderer>().enabled = false;
return;
}
if (impulseSource != null)
{
impulseSource.GenerateImpulse();
}
collision.GetComponent<Renderer>().enabled = false;
}
}
}
using UnityEngine;
using System.Collections;
public class RandomTopDropper : MonoBehaviour
{
[Header("Move")]
[SerializeField] private float normalFallSpeed = 2.0f; // 평상시 속도
[SerializeField] private float boostFallSpeed = 3.5f; // 부스터 중 속도
[SerializeField] private bool useRigidbody = false; // true면 Rigidbody2D.linearVelocity 사용
[Header("Spawn Timing (sec)")]
[SerializeField] private Vector2 waitRange = new Vector2(0.5f, 2.0f); // 다음 등장까지 랜덤 대기시간
[Header("Screen Offsets (world)")]
[SerializeField] private float spawnTopOffset = 0.5f; // 화면 위로 살짝 위에서 스폰
[SerializeField] private float despawnBottomOffset = 0.5f; // 화면 아래로 완전히 내려가게 여유
[Header("Optional")]
[SerializeField] private float xPadding = 0.2f; // 좌우 가장자리 여유(월드)
[SerializeField] private SpriteRenderer spriteRenderer; // 비워두면 자동 GetComponent
[SerializeField] private PlayerMovement player; // PlayerMovement 참조(인스펙터에 할당)
private Camera cam;
private Rigidbody2D rb;
private void Awake()
{
player = GameObject.Find("Player").GetComponent<PlayerMovement>();
cam = Camera.main;
if (spriteRenderer == null) spriteRenderer = GetComponent<SpriteRenderer>();
rb = GetComponent<Rigidbody2D>();
}
private void Start()
{
// 시작할 때 보이지 않게
if (spriteRenderer != null) spriteRenderer.enabled = false;
// 물리 사용 안 하면 Rigidbody2D 영향 제거
if (!useRigidbody && rb != null) rb.linearVelocity = Vector2.zero;
StartCoroutine(Loop());
}
private IEnumerator Loop()
{
while (true)
{
// 1) 랜덤 대기
float wait = Random.Range(waitRange.x, waitRange.y);
yield return new WaitForSeconds(wait);
// 2) 스폰 위치 계산 (화면 위쪽, 랜덤 X)
Vector3 topWorld = GetTopWorld();
float leftX = GetLeftWorldX() + xPadding;
float rightX = GetRightWorldX() - xPadding;
float spawnX = Random.Range(leftX, rightX);
Vector3 spawnPos = new Vector3(spawnX, topWorld.y + spawnTopOffset, transform.position.z);
// 3) 위치 초기화 + 보이기
transform.position = spawnPos;
if (spriteRenderer != null) spriteRenderer.enabled = true;
// 4) 이동(부스터 연동 속도)
if (useRigidbody && rb != null)
{
// 속도를 매 프레임 갱신하면서 바닥 도달까지 이동
while (transform.position.y > GetBottomWorld().y - despawnBottomOffset)
{
float fallSpeed = GetCurrentFallSpeed();
rb.linearVelocity = new Vector2(0f, -fallSpeed);
yield return null;
}
rb.linearVelocity = Vector2.zero;
}
else
{
// Transform 수동 이동 (매 프레임 부스터 속도 반영)
while (transform.position.y > GetBottomWorld().y - despawnBottomOffset)
{
float fallSpeed = GetCurrentFallSpeed();
transform.position += Vector3.down * (fallSpeed * Time.deltaTime);
yield return null;
}
}
// 5) 화면 아래로 내려갔으면 숨김
if (spriteRenderer != null) spriteRenderer.enabled = false;
// 이후 while(true)로 다시 랜덤 대기 → 스폰 반복
}
}
private float GetCurrentFallSpeed()
{
// player가 지정되지 않았거나 부스터가 아니면 normal, 부스터 중이면 boost
bool boosting = (player != null) && player.IsBoosting;
return boosting ? boostFallSpeed : normalFallSpeed;
}
// ───────────────────────── 화면 범위 계산 유틸 ─────────────────────────
private Vector3 GetTopWorld()
{
// Viewport (0.5, 1) : 화면 상단 중앙
return cam != null
? cam.ViewportToWorldPoint(new Vector3(0.5f, 1f, GetZDistance()))
: new Vector3(0, 5, 0);
}
private Vector3 GetBottomWorld()
{
// Viewport (0.5, 0) : 화면 하단 중앙
return cam != null
? cam.ViewportToWorldPoint(new Vector3(0.5f, 0f, GetZDistance()))
: new Vector3(0, -5, 0);
}
private float GetLeftWorldX()
{
return cam != null
? cam.ViewportToWorldPoint(new Vector3(0f, 0.5f, GetZDistance())).x
: -8f;
}
private float GetRightWorldX()
{
return cam != null
? cam.ViewportToWorldPoint(new Vector3(1f, 0.5f, GetZDistance())).x
: 8f;
}
// 카메라-오브젝트 간 z거리 (2D에서는 대개 |cam.z - obj.z|)
private float GetZDistance()
{
if (cam == null) return 10f;
return Mathf.Abs(transform.position.z - cam.transform.position.z);
}
}
using UnityEngine;
public class SimpleVerticalLooper : MonoBehaviour
{
[SerializeField] private float normalSpeed = 2f; // 평상시 속도
[SerializeField] private float boostSpeed = 4f; // 부스터 중 속도
[SerializeField] private float minY = -5f; // 이 위치보다 내려가면
[SerializeField] private float resetY = 5f; // 이 위치로 순간이동
[SerializeField] private PlayerMovement player; // PlayerMovement 스크립트 참조
private void Update()
{
if (player == null)
return;
// 부스터 중이면 빠른 속도, 아니면 일반 속도
float currentSpeed = player.IsBoosting ? boostSpeed : normalSpeed;
// 계속 아래로 이동
transform.position += Vector3.down * currentSpeed * Time.deltaTime;
// 특정 y보다 아래로 내려가면 resetY로 순간이동
if (transform.position.y <= minY)
{
Vector3 pos = transform.position;
pos.y = resetY;
transform.position = pos;
}
}
}
Result: 프로토타입을 처음 만들어봤는데 AI를 사용해 바이브 코딩으로 기능을 빠르게 빠르게 구현하니까 게임이 순식간에 만들어져 성취감이나 개발의 재미가 쏠쏠해 바이브 코딩의 중독성을 느꼈습니다. 그런데 기능을 조금만 구현해도 구조가 꼬여서 바이브 코딩의 단점도 알게 되어 정말 급할 때가 아니면 잘 쓰지 않을 것 같습니다.
'포트폴리오 > 개발 일기' 카테고리의 다른 글
| [개발 일기] 공격 범위 표시하기 (0) | 2025.11.04 |
|---|---|
| [개발 일기] 앵그리버드 포물선 (0) | 2025.10.27 |
| [개발 일기] 카드 야바위 뽑기 구현 (0) | 2025.09.10 |
| [개발 일기] 스테이지 선택 화면 만들기 (2) | 2025.08.15 |