양창은 | 게임 개발 포트폴리오

게임 프로젝트

Sand Bingo

Unity C# 1인 개발 모래 시뮬레이션 통계 기반 봇 v0.8 2026.02 – 2026.03
Sand Bingo 로고
▶ MP4
  • 빙고 게임과 모래 시뮬레이션을 결합한 싱글플레이 퍼즐 게임
  • 플레이어의 모래 투하 분포를 계산하여 대응하는 봇 개발
  • 현재 핵심 게임플레이 구현 완료 + 추가 콘텐츠 기획 중
스크린샷
Sand Bingo 스크린샷 1 Sand Bingo 스크린샷 2 Sand Bingo 스크린샷 3 Sand Bingo 스크린샷 4
스크립트 구조
Sand Bingo 스크립트 구조 다이어그램
주요 구현 사항
SandSimulator

· 픽셀 단위 셀룰러 오토마타*로 모래 물리를 구현. 매 행마다 스캔 방향을 무작위로 반전시켜 좌우 편향 없는 자연스러운 낙하를 연출함

* 각 모래 입자가 매 시뮬레이션 스텝마다 아래, 좌하, 우하 순서로 이동 가능 여부를 판단해 자연스러운 낙하와 모래 누적을 표현

· 변경이 발생한 픽셀/셀만 dirty set에 등록해 해당 프레임에 변경된 영역만을 렌더링하여 연산을 최적화함

관련 코드 펼치기
public bool SimulatePhysics()
{
    bool sandMoved = false;
    for (int y = 1; y < height - 1; y++)
    {
        bool scanLeft = Random.value > 0.5f;
        for (int x = 1; x < width - 1; x++)
        {
            int currentX = scanLeft ? x : width - 1 - x;
            if (IsSand(grid[currentX, y]))
                if (UpdateSand(currentX, y)) sandMoved = true;
        }
    }
    return sandMoved;
}
bool UpdateSand(int x, int y)
{
    CellType sandType = grid[x, y];
    if (grid[x, y - 1] == CellType.Empty)
    {
        grid[x, y] = CellType.Empty;
        grid[x, y - 1] = sandType;
        MarkPixelDirty(x, y); MarkPixelDirty(x, y - 1);
        MarkCellDirtyByPixel(x, y); MarkCellDirtyByPixel(x, y - 1);
        return true;
    }
    int direction = Random.value > 0.5f ? 1 : -1;
    if (grid[x + direction, y - 1] == CellType.Empty)
    { /* 대각 낙하 */ ... return true; }
    else if (grid[x - direction, y - 1] == CellType.Empty)
    { /* 반대 대각 */ ... return true; }
    return false;
}
GameManager

· 프레임레이트와 무관하게 모래 물리가 일정한 속도로 동작하도록 경과 시간을 누적해 고정 간격으로 시뮬레이션을 실행, FPS가 급락해도 한 프레임에 최대 3회로 실행 횟수를 제한해 과다한 연산을 방지함

· 모래 정착 판정을 단순 대기가 아닌 isSandMoving 플래그 확인 방식으로 구현함. 정착 후 점수 연산, 셀 제거, 추가 정착 대기를 코루틴으로 연쇄 처리함

관련 코드 펼치기
void UpdatePhysicsWithFixedTimestep()
{
    physicsAccumulator += Time.deltaTime;
    if (physicsAccumulator > PhysicsTimestep * 3)
        physicsAccumulator = PhysicsTimestep * 3;
    while (physicsAccumulator >= PhysicsTimestep)
    {
        UpdateSimulation();
        physicsAccumulator -= PhysicsTimestep;
    }
}
IEnumerator WaitForSandSettlement()
{
    isSandMoving = false;
    float settlementTimer = 0f;
    while (settlementTimer < 0.4f)
    {
        if (isSandMoving) { settlementTimer = 0f; isSandMoving = false; }
        else settlementTimer += Time.deltaTime;
        yield return null;
    }
    // 점수 계산 -> 셀 제거 -> 함수 재귀 호출 or 플레이어 턴 전환
    ScoreResult result = sandSimulator.CalculateScoreAndGetCells();
    if (result.cellsToRemove.Count > 0)
    {
        SetCurrentStageScore(GetCurrentStageScore() + result.oasisScore - result.mudScore);
        yield return new WaitForSeconds(0.6f);
        sandSimulator.RemoveCells(result.cellsToRemove);
        StartCoroutine(WaitForSandSettlement()); yield break;
    }
    SwitchPlayer();
}
BotController

· 플레이어가 모래를 놓은 X좌표를 매 턴 수집해 평균과 표준편차를 계산, 모래의 중심과 분포를 수치화함

· 표준편차를 오프셋으로 활용해 플레이어의 투하 분포에 대응하여 봇의 투하 위치를 결정함. 단계별로 투하 횟수(2개/3개)와 크기(0.8셀/1.0셀/1.2셀)를 다르게 적용해 난이도를 차별화함

관련 코드 펼치기
void CalculateStatistics()
{
    float sum = 0;
    foreach (int x in oasisSandXPositions) sum += x;
    averageX = sum / oasisSandXPositions.Count;
    float varianceSum = 0;
    foreach (int x in oasisSandXPositions)
    {
        float diff = x - averageX;
        varianceSum += diff * diff;
    }
    standardDeviation = Mathf.Sqrt(varianceSum / oasisSandXPositions.Count);
}
void ExecuteEasyBot()  // 2개 투하, 작은 크기
{
    float offset = Mathf.Max(standardDeviation, cellPixelSize * 0.5f);
    float leftX = averageX - offset;
    float rightX = averageX + offset;
    sandSimulator.DropSandRectangle(leftX, targetY, 1.0f, 0.8f, BrownSand);
    sandSimulator.DropSandRectangle(rightX, targetY, 1.0f, 0.8f, BrownSand);
}
void ExecuteHardBot()  // 3개 투하, 중간 크기
{
    float offset = Mathf.Max(standardDeviation, cellPixelSize * 0.5f);
    sandSimulator.DropSandChunk(averageX - offset, targetY, BrownSand);
    sandSimulator.DropSandChunk(averageX,           targetY, BrownSand);
    sandSimulator.DropSandChunk(averageX + offset,  targetY, BrownSand);
}

Sisyphus

Unity 3D C# 1인 개발 모작 2026.01
Sisyphus 스크린샷 1 Sisyphus 스크린샷 2
  • 크림 게임즈의 〈The Game of Sisyphus〉를 모작한 항아리류 게임
  • 회전 망치, 굴러오는 돌, 낙빙으로 난이도를 높여가며 3단계 스테이지로 구성
주요 구현 사항
PlayerController

· 마우스 누적값을 Quaternion.Euler에 직접 대입해 카메라 기준점, 카메라, 플레이어 오브젝트 세 축의 회전을 단일 Update 흐름 안에서 동기화함

· 낙하 하한선 판정 외에 맵 경사각을 미리 계산한 tanY_Z를 이용해 경사면 이탈 여부를 판별하고 자연스러운 복귀 지점을 결정함

· 점프 쿨타임으로 연속 점프를 방지하고, 매 프레임 중력을 추가 적용해 충돌 등으로 플레이어가 의도치 않게 떠오르는 상황을 줄임

관련 코드 펼치기
void Update()
{
    // 카메라와 캐릭터 회전 동기화
    mouseX += Input.GetAxis("Mouse X");
    mouseY -= Input.GetAxis("Mouse Y");
    bodyPos.transform.rotation = Quaternion.Euler(mouseY * camRotSpeed, mouseX * camRotSpeed, 0);
    cam.transform.rotation   = Quaternion.Euler(mouseY * camRotSpeed, mouseX * camRotSpeed, 0);
    cam.transform.position   = camPos.position;
    transform.rotation       = Quaternion.Euler(0, mouseX * camRotSpeed, 0);
    // 하한선 또는 경사 기준선 밑으로 떨어질 시 리스폰
    float playerY = transform.position.y;
    float playerZ = transform.position.z;
    if (playerY < -20)
    {
        transform.position = GameManager.instance.playerRespawnPoint;
        rigid.velocity = Vector3.zero;
    }
    else if (playerY < playerZ * tanY_Z - 25)
    {
        Vector3 respawnPoint = new Vector3(0, playerY + 30, playerZ - 5);
        transform.position = respawnPoint;
        rigid.velocity = Vector3.zero;
    }
    if (isMoving)
    {
        moveVector = transform.TransformDirection(moveVector);
        moveVector.Normalize();
        rigid.AddForce(moveVector * moveConstant);
    }
    if (Input.GetKeyDown(KeyCode.Space) && jumpElapsedTime > 1)
    {
        rigid.AddForce(Vector3.up * jumpConstant, ForceMode.Impulse);
        jumpElapsedTime = 0;
    }
    rigid.AddForce(Vector3.down * dragDownConstant);
}
SceneChanger

· 씬 전환을 코루틴으로 처리해 페이드 애니메이션이 끝난 뒤에 씬이 로드되도록 했으며, 알파값을 경과 시간 비율로 계산해 프레임레이트와 무관하게 전환 시간을 일정하게 유지함

· SetFadeAlpha를 단일 메서드로 분리해 페이드인·아웃 양쪽에서 재사용하고 유지 보수를 쉽게 함

관련 코드 펼치기
// 씬 진입 시 페이드인, 씬 전환 시 페이드아웃 후 로드
public void SceneOpen(string sceneName)
{
    StartCoroutine(FadeIn(sceneName));
}
public void SceneChange(string sceneName)
{
    StartCoroutine(FadeOutAndLoad(sceneName));
}
private IEnumerator FadeIn(string sceneName)
{
    ShowCursor(sceneName);
    float fadeElapsedTime = 0f;
    while (fadeElapsedTime < fadeDuration)
    {
        fadeElapsedTime += Time.deltaTime;
        SetFadeAlpha(1f - (fadeElapsedTime / fadeDuration));
        yield return null;
    }
    SetFadeAlpha(0f);
}
private IEnumerator FadeOutAndLoad(string sceneName)
{
    float fadeElapsedTime = 0f;
    while (fadeElapsedTime < fadeDuration)
    {
        fadeElapsedTime += Time.deltaTime;
        SetFadeAlpha(fadeElapsedTime / fadeDuration);
        yield return null;
    }
    SetFadeAlpha(1f);
    SceneManager.LoadScene(sceneName);
}
private void SetFadeAlpha(float alpha)
{
    Color color = fadeCoverImage.color;
    color.a = alpha;
    fadeCoverImage.color = color;
}

Flight Shooter

Unity C# 1인 개발 습작 2025.12
Flight Shooter 스크린샷
  • 적 비행기의 공격을 회피하며 격추하는 간단한 탑다운 슈팅 게임
  • 직선으로 나가는 총알과 적을 탐색해 유도되는 미사일을 별도로 구현
주요 구현 사항
PlayerController

· 미사일 발사 시 동일 위치에서 두 인스턴스를 생성하고 하나의 leftSide 값만 변경해 최소한의 코드로 좌우 분기 발사를 구현함

· 입력 벡터를 정규화 후 AddForce로 적용해 대각선 이동 시에도 속도가 일정하게 유지됨

관련 코드 펼치기
void Update()
{
    pushForce = new Vector2(0, 0);
    if (bulletCooltime < bulletMaxCooltime) bulletCooltime++;
    if (missileCooltime < missileMaxCooltime) missileCooltime++;
    if (Input.GetKey(KeyCode.A)) pushForce.x = -1;
    else if (Input.GetKey(KeyCode.D)) pushForce.x = 1;
    if (Input.GetKey(KeyCode.W)) pushForce.y = 1;
    else if (Input.GetKey(KeyCode.S)) pushForce.y = -1;
    // 일반 총알 발사
    if (Input.GetKey(KeyCode.J) && bulletCooltime == bulletMaxCooltime)
    {
        Vector2 tempPosition = transform.position;
        tempPosition.y += 1f;
        Instantiate(bulletPrefab, tempPosition, Quaternion.identity);
        audioSource.Play();
        bulletCooltime = 0;
    }
    // 유도 미사일 발사
    if (Input.GetKey(KeyCode.K) && missileCooltime == missileMaxCooltime)
    {
        Vector2 tempPosition = transform.position;
        GameObject m1 = Instantiate(missilePrefab, tempPosition, Quaternion.identity);
        GameObject m2 = Instantiate(missilePrefab, tempPosition, Quaternion.identity);
        m2.GetComponent<PlayerMissileController>().leftSide = false;
        audioSource.Play();
        missileCooltime = 0;
    }
    pushForce.Normalize();
    playerRb.AddForce(pushForce * 15);
}
PlayerMissileController

· 발사 직후 좌우 초기 충격력을 가해 미사일이 바깥쪽으로 퍼진 뒤 적을 향하는 궤적을 물리 기반으로 연출함

· 초기 쿨타임으로 분기 구간을 확보한 뒤 bestFound 플래그로 가장 가까운 적을 처음 1번만 탐색해 이후 방향을 고정하고, sqrMagnitude로 sqrt 연산을 생략해 탐색 효율을 높임

관련 코드 펼치기
void Start()
{
    missileRb = GetComponent<Rigidbody2D>();
    enemies = GameObject.FindGameObjectsWithTag("Enemy");
    // 초기 충격력으로 좌우 분기 궤적 시작
    if (leftSide) missileRb.AddForce(new Vector2(-initialForce, 0));
    else missileRb.AddForce(new Vector2(initialForce, 0));
}
void Update()
{
    // 초기 쿨타임 동안 유도를 보류해 좌우 분기 구간 확보
    if (initialCooltime < maxCooltime) { initialCooltime++; return; }
    // 가장 가까운 적 탐색을 최초 1번만 수행
    if (!bestFound && enemies.Length > 0)
    {
        GameObject nearest = null;
        float bestDistance = 1000f;
        foreach (var enemy in enemies)
        {
            if (enemy == null) continue;
            float distance = (enemy.transform.position - transform.position).sqrMagnitude;
            if (distance < bestDistance)
            {
                bestFound = true;
                nearest = enemy;
                bestDistance = distance;
            }
        }
        if (nearest != null)
        {
            moveDirection = (nearest.transform.position - transform.position).normalized;
            float angle = Mathf.Atan2(moveDirection.y, moveDirection.x) * Mathf.Rad2Deg;
            transform.rotation = Quaternion.Euler(0f, 0f, angle - 90f);
        }
    }
    missileRb.AddForce(moveDirection * moveForce);
}

기타 프로젝트

HoYoLab-box

Python GitHub Actions 웹 API 토이 프로젝트 2023.12
HoYoLab-box
  • GitHub Actions로 일정 주기마다 게임 활동 스탯을 Gist에 업데이트하는 기능 구현
  • HoYoverse의 커뮤니티 플랫폼 HoYoLab의 프로필 데이터 API와 연동
  • 26년 3월 기준 레포지토리 Star 12개, Fork 5개 달성

아이유 TTS 파인튜닝

Python TTS 데이터셋 제작 모델 전이학습 2022.08 – 2022.10
아이유 TTS 파인튜닝
  • 기존 한국어 TTS 모델에 직접 제작한 아이유 음성 데이터셋으로 파인튜닝하여 개인화된 TTS를 제작
  • 노이즈 제거·문장 분리·발음 표기까지 데이터셋을 직접 제작 (181개 파일, 총 11분 25초 분량)
  • 표준 발음법을 참조하여 모든 음성 파일에 스크립트 라벨링

For Life Photos

Python Streamlit AI 모델 통합 팀 5인 2022.08
  • 사진의 구도·수평·포즈를 AI로 분석해 최적의 인생샷을 자동으로 선별하는 웹 서비스
  • 팀원들이 학습한 3개의 모델 연결·통합 및 Streamlit 기반 웹 서비스 구현 담당
  • 팀장 및 PM 역할 병행, 5명의 팀 프로젝트 중 기여도 35%

교육 및 활동

2025. 02

삼성전자 DX 부문 동계 대학생 S/W 알고리즘 특강

삼성전자

자료구조 및 알고리즘 학습, 삼성전자 S/W 개발 임직원 멘토링 교육 총 4주간 이수 및 수료

2023. 05 – 2023. 11

Project Astro-Warp CBT

HoYoverse

서브컬처 턴제 RPG 〈붕괴: 스타레일〉 비공개 베타테스트 심층 그룹 참여 (v1.2 – v1.5)

2022. 04 – 2022. 10

데이터기반 인공지능 시스템 엔지니어 양성 과정 7기

영우글로벌러닝 (현 에티버스러닝)

데이터분석 · 머신러닝 · 자연어처리 · 컴퓨터비전 교육 총 880시간 이수 및 수료

학력 및 자격증

2023. 09

정보처리기사

한국산업인력공단

2022. 02

연세대학교 졸업

이학사 (물리학) · 문학사 (철학) — 복수전공

2017. 03

연세대학교 이과대학 물리학과 입학