//구글콘솔 광고 추가가

728x90
반응형

복권 긁기 게임을 만들어 보고자 유니티를 켰다.

요즘엔 유니티 에셋중에 scratch card라는 게 있다던데 찾아보니 유료에셋인 것 같아 gpt와 같이 만들어 봤다.

 

1. 우선 원하는 복권 이미지를 찾고, 스크래치 부분도 복권 이미지에 맞게 하나 만들어 준다.

2. 쉐이더가 두개 필요하다. mask shader와 mask Construction shader를 만들어 주자.

mask shader의 코드
// 메인 텍스쳐와 마스크 텍스쳐를 사용하여 투명도를 조절하는 마스킹 효과를 구현.
// 마스크 텍스쳐의 알파값에 따라 메인 텍스쳐의 투명도가 결정.

Shader "Masked" {
    Properties {
        _MainTex ("Main", 2D) = "white" {} //메인 텍스쳐
        _MaskTex ("Mask", 2D) = "white" {} // 마스크 텍스쳐
    }

    SubShader {
        //렌더링 설정
        Tags {"Queue"="Transparent" "IgnoreProjector"="True" "RenderType"="Transparent"}
        ZWrite Off  // z버퍼 쓰기 비활성화
        ZTest Off   // z테스트 비활성화
        Blend SrcAlpha OneMinusSrcAlpha // 알파 블렌딩 설정
        Pass {
            CGPROGRAM
            #pragma vertex vert     // 버텍스 쉐이더 함수 지정
            #pragma fragment frag   // 프래그먼트 쉐이더 함수 지정
            #pragma fragmentoption ARB_precision_hint_fastest   //프래그먼트 쉐이더 최적화 힌트
            #include "UnityCG.cginc"    // 유니티 내장 함수 포함

            //텍스쳐 샘플러와 변환 매개변수 선언
            uniform sampler2D _MainTex;
            uniform sampler2D _MaskTex;
            uniform float4 _MainTex_ST;
            uniform float4 _MaskTex_ST;

            // 버텍스 쉐이더 입력 구조체
            struct app2vert
            {
                float4 position: POSITION;  // 오브젝트 공간 위치
                float2 texcoord: TEXCOORD0; // 텍스쳐 좌표
            };

            // 버텍스 쉐이더 입려 구조체
            struct vert2frag
            {
                float4 position: POSITION;  // 클립 공간 위치
                float2 texcoord: TEXCOORD0; // 텍스쳐 좌표
            };

            // 버텍스 쉐이더 함수
            vert2frag vert(app2vert input)
            {
                vert2frag output;
                output.position = UnityObjectToClipPos(input.position);    //오브젝트 공간에서 클립 공간으로 변환
                output.texcoord = TRANSFORM_TEX(input.texcoord, _MainTex); //텍스쳐 좌표 변환
                return output;
            }

            // 프래그먼트 쉐이더 함수
            fixed4 frag(vert2frag input) : COLOR
            {
                fixed4 main_color = tex2D(_MainTex, input.texcoord); // 메인 텍스쳐 샘플링
                fixed4 mask_color = tex2D(_MaskTex, input.texcoord); // 마스크 텍스쳐 샘플링

                // 최종 색상 계산 : RGB는 메인 텍스쳐에서, 알파는 메인 텍스쳐와 마스크의 조합
                return fixed4(main_color.r, main_color.g, main_color.b, main_color.a * (1.0f - mask_color.a));
            }
            ENDCG
        }
    }
}

 

maskConstruction shader 코드
// 기본 텍스처와 마스크 텍스처를 사용하여 마스킹 효과를 구현.
// 마스크 텍스처의 빨간 채널 값에 따라 기본 텍스처의 알파 값(투명도) 조절. 
// 마스크 텍스처의 빨간 채널 값이 높을 수록 해당 부분 투명 up. 


Shader "MaskConstruction"
{
    //인스펙터에서 조정 가능한 속성 정의
    Properties
    {
        _MainTex ("Base Texture", 2D) = "white" {}  // 기본 텍스쳐
        _MaskTex ("Mask Texture", 2D) = "black" {}  // 마스크 텍스쳐
    }
    SubShader
    {
        Tags { "Queue"="Transparent" }  //투명 객체로 렌더링 큐 설정
        Pass
        {
            ZWrite Off                          // z버퍼 쓰기 비활성화
            Blend SrcAlpha OneMinusSrcAlpha     // 알파 블렌딩 설정

            CGPROGRAM
            #pragma vertex vert                 // 버텍스 쉐이더 함수 지정
            #pragma fragment frag               // 프래그먼트 쉐이더 함수 지정

            #include "UnityCG.cginc"            //유니티 내장 함수 포함

            sampler2D _MainTex;     // 기본 텍스처 샘플러
            sampler2D _MaskTex;     // 마스크 텍스처 샘플러

            // 버텍스 쉐이더 입력 구조체
            struct appdata_t
            {
                float4 vertex : POSITION;   // 오브젝트 공간 위치
                float2 uv : TEXCOORD0;      // 텍스처 좌표
            };

            // 프래그먼트 쉐이더 입력 구조체
            struct v2f
            {
                float2 uv : TEXCOORD0;       // 텍스처 좌표
                float4 vertex : SV_POSITION; // 클립 공간 위치
            };

            // 버텍스 쉐이더 함수
            v2f vert (appdata_t v)
            {
                v2f o;
                o.vertex = UnityObjectToClipPos(v.vertex); // 오브젝트 공간에서 클립 공간으로 변환
                o.uv = v.uv;                               // 텍스처 좌표 전달
                return o;
            }

            // 프래그먼트 쉐이더 함수
            fixed4 frag (v2f i) : SV_Target
            {
                fixed4 baseColor = tex2D(_MainTex, i.uv); // 기본 텍스처 샘플링
                fixed4 maskColor = tex2D(_MaskTex, i.uv); // 마스크 텍스처 샘플링

                // 마스크를 기본 텍스처에 적용
                baseColor.a *= (1 - maskColor.r); // 마스크 텍스처의 빨간 채널을 기반으로 지우기 효과 적용

                return baseColor; //최종 색상 반환
            }
            ENDCG
        }
    }
}

 

주석으로 설명은 달아뒀으니 shader가 궁금한 사람은 주석을 보며 더 공부해 보는 걸 추천한다.

 

3. 유니티에서 Scratch_Result 오브젝트를 만들어 주고 이전에 준비했던 복권 이미지를 넣어준다.

4.  Scratch_Surface 오브젝트를 만들어 주고 자식으로 camera 하나와 surface 오브젝트 하나를 준비해 준다.

카메라 옵션을 위와 같이 바꿔주고 Surface 오브젝트에는 스프라이트 렌더러 컴포넌트를 추가해 준다. 여기서 복권의 스크래치 회색 부분을 sprite에 넣어 주면 되고 material을 새롭게 만들어 넣어주자.

카메라의 Clear Flags를 Don't Clear로 설정해주면서 프레임 누적효과로 이전 프레임위에 프레임이 그려져 누적되는 효과를 만들 수 있는데 이걸로 지워지는 효과를 줄 수 있다. 이 설정으로 번짐이나 잔상 효과도 만들어 낼 수 있을 듯?

 

우선, Assets 폴더에 Textures 폴더를 만들어 주고 우클릭을 통해 Render Texture를 만들어 준다.

다시 Assets파일로 가서 Materials 폴더를 만들어 주고 우클릭을 통해 Material을 만들어 준다.

여기서 중요한 점은 shader를 아까 만들어 준 Masked로 연결해 주는 것. 그리고 저 별표 쳐둔 곳에 아까 만든 Render Texture를 연결해 주자.

다 만들어 줬다면 Material을 복사해서 하나 더 만들어 주고 이름을 Eraser로 바꿔준다. 그리고 shader를 maskConstruction으로 설정해 두자.

 

Surface 오브젝트로 돌아와서 mask shader로 만들어 준 Material을 연결해 준다.

여기서 Order in Layer값은 아까 만들어 준 Scratch_result보다 높게 수치를 정해주자.

 

5. 이제 거의 다 왔다. camera에 달아줄 스크립트를 만들어주자.(main camera 아님 주의!)

 

>> 픽셀로 검사해서 전체 진행률을 확인해보려 하다가 내 surface의 크기를 전체 픽셀로 체크를 해야 되는데 제대로 체크가 안돼서 박스 콜라이더로 진행률을 확인하는 걸로 바꿨다. 

 

>> 박스 콜라이더로 작업을 하면 원하는 방향대로 진행되지 않음을 확인, 렌더 텍스처와 박스콜라이더를 연동해서 작업해 보고 있는 중이다. 이건 추후에 업로드하겠다.

 

진행률을 안 할 거라면 그냥 이 코드를 적용해 주면 된다. 진행률 부분이 잘 안되는 것 빼고 잘 된다. 작업할 때 잘 안돼서 디버그가 많은데 필요 없다면 알아서 지워주자.

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using System;
public class MaskCamera : MonoBehaviour
{
    public GameObject Dust; // 긁을 수 있는 표면 오브젝트
    public Material EraserMaterial; // 지우개 효과를 위한 머티리얼
    private RenderTexture renderTexture; // 마스크용 렌더 텍스처
    private Texture2D tex; // 픽셀 데이터를 읽기 위한 텍스처

    private Rect screenRect; // 긁기 가능한 영역
    private Action<float> progressCallback; // 진행률 콜백

    private bool requestReadPixels = false;

    private void Start()
    {
        InitializeRenderTexture();
        CalculateScreenRect();
         
        Debug.Log(Dust.GetComponent<Renderer>().material.GetTexture("_MaskTex"));
    }

    private void InitializeRenderTexture()
    {
        // 렌더 텍스처 초기화
        renderTexture = new RenderTexture(Screen.width, Screen.height, 0, RenderTextureFormat.Default);
        renderTexture.Create();

        tex = new Texture2D(renderTexture.width, renderTexture.height, TextureFormat.RGB24, false);

        // 카메라와 Dust 오브젝트에 렌더 텍스처 연결
        GetComponent<Camera>().targetTexture = renderTexture;
        Dust.GetComponent<Renderer>().material.SetTexture("_MaskTex", renderTexture);
        
        Debug.Log($"RenderTexture width: {renderTexture.width}, height: {renderTexture.height}");
    }

    private void CalculateScreenRect()
    {
        // Dust 오브젝트의 화면 영역 계산
        Renderer dustRenderer = Dust.GetComponent<Renderer>();
        screenRect = new Rect(
            dustRenderer.bounds.min.x,
            dustRenderer.bounds.min.y,
            dustRenderer.bounds.size.x,
            dustRenderer.bounds.size.y
        );
        Debug.Log($"ScreenRect initialized: {screenRect}"); // screenRect 값 확인
    }

    public void SetProgressCallback(Action<float> callback)
    {
        progressCallback = callback;
    }

    private void Update()
    {
        if (Input.GetMouseButton(0))
        {
            // 카메라의 viewport 좌표로 변환
            Vector2 viewportPoint = GetComponent<Camera>().ScreenToViewportPoint(Input.mousePosition);
        
            // viewport 좌표를 직접 사용 (0-1 범위) 
            Vector2 localPosition = new Vector2(viewportPoint.x, viewportPoint.y);
        
            // screenRect 체크는 유지
            Vector2 worldPosition = GetComponent<Camera>().ScreenToWorldPoint(Input.mousePosition);
            if (screenRect.Contains(worldPosition))
            {
                Debug.Log($"Viewport Position: {viewportPoint}");
                CutHole(localPosition);
            }
        }

        if (requestReadPixels)
        {
            ReadPixelsAndCalculateProgress();
            requestReadPixels = false;
        }
    }

    private void CutHole(Vector2 normalizedPosition)
    {
        Debug.Log($"CutHole called at position: {normalizedPosition}");
        
        RenderTexture.active = renderTexture; // RenderTexture 활성화

        GL.PushMatrix();
        GL.LoadPixelMatrix(0, renderTexture.width, renderTexture.height, 0);
        if (!EraserMaterial.SetPass(0))
        {
            Debug.LogError("Failed to set material pass!");
            return;
        }
    
        float size = 50f; // 지우개 크기를 픽셀 단위로 설정
        Vector2 pixelPos = new Vector2(
            normalizedPosition.x * renderTexture.width,
            (1- normalizedPosition.y) * renderTexture.height
        );
    
    	// 사각형 그리기 시작과 종료. color는 사각형 색상, Vertex3()는 사각형 꼭지점 정의.
        // pixelPos는 사각형 중심 위치, size는 사각형의 절반 크기, z값은 0으로 2D 평면에 그림.
        GL.Begin(GL.QUADS);
        GL.Color(Color.white);
        GL.Vertex3(pixelPos.x - size, pixelPos.y - size, 0);
        GL.Vertex3(pixelPos.x + size, pixelPos.y - size, 0);
        GL.Vertex3(pixelPos.x + size, pixelPos.y + size, 0);
        GL.Vertex3(pixelPos.x - size, pixelPos.y + size, 0);
        GL.End();
    
        GL.PopMatrix();
        RenderTexture.active = null;
    }

    private void ReadPixelsAndCalculateProgress()
    {
        tex.ReadPixels(new Rect(0, 0, renderTexture.width, renderTexture.height), 0, 0);
        
        float percentScratched = CalculateScratchedPercent(tex);

        progressCallback?.Invoke(percentScratched);
    }

    private float CalculateScratchedPercent(Texture2D texture)
    {
        int scratchedPixels = 0;

        for (int x = 0; x < texture.width; x++)
        {
            for (int y = 0; y < texture.height; y++)
            {
                if (texture.GetPixel(x, y).r == 1) scratchedPixels++;
            }
        }

        return scratchedPixels * 100f / (texture.width * texture.height);
    }

    public void RequestProgressUpdate()
    {
        requestReadPixels = true;
    }
}

 

여기서 CutHole() 함수가 제일 중요하다. 렌더 텍스처에 흰 사각형을 그려주고 이 사각형이 마스크로 사용되면서, 아까 설정해주었던 Surface에 자식으로 만들어 준 카메라의 Don't Clear 설정으로 인해 이전에 그려진 사각형들이 지워지지 않고 누적되어서 긁히고 있는 것처럼 보여준다. 플레이어가 마우스를 누를 때마다 반복적으로 함수를 호출해서 실제로 복권을 긁는 것처럼 시각적 효과를 낼 수 있다.

 

 

 

728x90
반응형
728x90
반응형

너무 빠르게 프로젝트를 만들고 싶어 만들고 난 순간 이름을 대충 지었다는 것을 알았을 때,

원하는 이름으로 다시 바꾸어 보자.

 

1. 마음에 안 드는 유니티 프로젝트의 파일이 있는 곳으로 간다.

 

2. 유니티 프로젝트를 선택하고 이름을 바꿔주자.

 

3. 이름을 바꿔준 파일을 더블 클릭해서 들어간 다음 이전에 이름으로 만들어져 있는 sln 파일을 지워준다.

 

4. 유니티 허브에서 기존에 있던 마음에 안 드는 프로젝트를 리스트에서 제거시켜 준다.

 

5. 원하는 이름으로 바꾼 프로젝트를 다시 추가시켜준다.

 

끝이다. 

728x90
반응형
728x90
반응형

https://github.com/h8man/TurboTrack2D

 

GitHub - h8man/TurboTrack2D: Prototype of 2D arcade style racing game

Prototype of 2D arcade style racing game. Contribute to h8man/TurboTrack2D development by creating an account on GitHub.

github.com

열심히 뜯어봐서 이제 대충 알겠는 pseudo 3d. 다들 같이 봐보자.

 

참고로 여기 프로젝트에서 도로의 스프라이트는 존재하지 않는다. 오로지 메터리얼로 그려주고 있기 때문. 따라서 이 스크립트를 이용한다면 도로의 스프라이트를 따로 줄 수 없다. 지금으로서는 메테리얼의 색깔만 바꿔 줄 수 있다. 이 부분은 수정해 볼 수 있으면 수정해 볼 예정이다.


이 프로젝트에선 크게 HqRenderer, TrackObject, Line 스크립트를 보면 된다.

 

1. Line.cs에서는 도로의 각 세그먼트에 대한 데이터를 저장하는 구조체이다. 이미지에서 볼 수 있는 속성들을 포함하는 곳이기에 이 구조체를 통해 TrackObject에서 배열로 관리가 된다. 

using UnityEngine;

public struct Line
{
    public float x, y, z, w;//3d center of line
    public float X, Y, W; //screen coord
    public float curve, spriteX, clip, scale;
    public Sprite sprite;
    public bool flipX;

    //장애물 충돌처리 때문에 추가
    public bool isObstacle; // 장애물 여부 체크
    
    
    public void project(int camX, int camY, int camZ, int screenWidth2, int screenHeight2, float cameraDepth)
    {
        scale = cameraDepth / (z - camZ);
        X = scale * (x - camX) * screenWidth2;
        Y = scale * (y - camY) * screenHeight2;
        W = scale * w * screenWidth2;
    }
}

 

아. 참고로 나의 경우 장애물을 그려주기 위해 위에서 처럼 코드에 bool변수인 isObstacle을 추가해 주었다.

 

주요 속성으로 spriteX, frequency, segments, sprite 정도는 알아둬야 된다. 
- spriteX는 스프라이트의 x축 위치를 말한다. 이 값으로 화면에 보여주고 싶은 오브젝트의 위치를 정한다.
- frequency는 스프라이트가 반복되는 간격을 말한다. 같은 간격으로 왼쪽, 오른쪽에 물체를 추가해주고 싶다면 같은 값을 입력하면 되고, 만약 다르게 나타내고 싶다면 다른 값을 입력해 주면 어긋나게 오브젝트들이 나온다.
- segments의 경우 시작과 끝지점이다.(x, y값) 이걸로 물체를 어디서부터 어디까지 나오게 하고 싶은지 정하면 된다.
- sprite는 해당 라인에 표시될 스프라이트로 그려주고 싶은 오브젝트의 스프라이트를 넣어주면 된다.

 

 

이제 유니티에 가서 Tracks폴더에 있는 New Track을 켜주고 추가로 그려주고 싶은 애들을 Modifier에 추가해 주자.

아까도 말했듯이 나는 장애물을 추가해주고 싶었기 때문에 왼쪽 장애물과 오른쪽 장애물을 추가해 주었다.

 

2. TrackObject.cs를 봐보자.

여기는 도로 트랙의 전체적인 구성과 속성을 관리하는 ScriptableObject다.

여기서는 트랙을 생성해 주는 Construct() 함수를 봐야 된다. 방금 유니티의 Inspector에서 설정된 TrackModifier들(leftObstacle, RightObstacle)의 속성들이 Construct() 함수를 통해 실제 트랙에 적용되어 화면에 표시되는 것이다.

참고로, 만약 트랙의 길이를 더 길게 하고 싶다면 TrackObject.cs에서 Length 값을 증가시켜 주면 된다.

using HQ;
using UnityEngine;


[CreateAssetMenu( fileName = "New Track",  menuName = "HQ/Track")]
public class TrackObject: ScriptableObject
{
    public int Length;
    public Line[] lines { get; protected set; }
    [HideInInspector]
    public TrackModifier[] Modifier;
    public float roadWidth;
    public int segmentLength;
    public float trackHeight;

    private void OnEnable()
    {
        Construct();
    }

    protected virtual void Construct()
    {
        Length = 1600;
        lines = new Line[Length];

        for (int i = 0; i < Length; i++)
        {
            ref Line line = ref lines[i];
            line.z = i * segmentLength;
            line.w = roadWidth;

            foreach (var m in Modifier)
            {
                if (!m.disabled && m.Segments.InRange(i) && i % m.frequency == 0)
                {
                    line.curve += m.curve;
                    line.spriteX = m.spriteX;
                    line.y += Mathf.Sin(i * m.h) * trackHeight;
                    line.sprite = m.sprite ?? line.sprite;
                    line.flipX = m.flipX;
                    
                    //장애물이면 체크
                    line.isObstacle = m.label.Contains("Obstacle");
                }
            }
        }
    }
    

}

나는 장애물과 플레이어의 충돌처리를 위해 여기서 장애물일 때 1. 에서 추가로 넣어줬던 bool값을 변경해 줬다.

 

3. 자, 이제 마지막 HqRenderer.cs를 봐보자.

여기서 추가해줘야 할 변수는 1개이다. spriteRenderer로 이전에 만들어 뒀던 스프라이트들을 그려줄 변수를 만들어 주자. 나의 경우 원래 오브젝트들을 그려주고 있던 scale과 장애물 scale의 비율이 달랐기에 장애물 스케일을 관리해 줄 ObstacleScale 변수도 추가해 줬다. scale 문제가 없다면 굳이 추가해 줄 필요가 없다.

변수를 만들어 줬다면 Awake() 함수로 가서 기존 코드와 같은 방식으로 새 스프라이트를 생성해 주자. 

 

아, 이렇게 해주기 전에 유니티에서 ProjectedScene에 빈 오브젝트를 하나 추가해 주고 Sprite Renderer를 컴포넌트로 추가해 주자. sprite는 비워두고 다시 코드로 와서 이렇게 코드를 써주면 유니티로 넘어갔을 때 runtimeObstacle이라는 스프라이트가 연결된 걸 확인할 수 있다. 물론 이 스프라이트에는 아직 아무것도 그려져있지 않는 빈 스프라이트다.

 

이제 다시 코드로 와서 수정해 주자. drawSprite() 함수에서 매개변수로 Line을 받아오는 걸 보면 여기서 아까 추가해 준 scale을 변경해 주자. 장애물일 경우엔 내가 넣어준 ObstacleScale을 사용해 주고 장애물이 아닐 경우 원래 있던 SpriteScale을 사용해 주자. 

 

그리고 새롭게 DrawObstacle() 함수를 하나 만들어주자. 여기서 장애물을 그려줄 것이다. DrawObjects() 함수와 거의 똑같이 만들어 주면 된다. 

 

처음엔 DrawObjects() 함수에 같이 그려줬다가 충돌처리 오브젝트들을 따로 관리해 주기 위해 변경해 줬다.

만약 또 다른 오브젝트들을 그려줄 때, 충돌처리가 필요 없다면 DrawObject() 함수에서 처리할 것이고, 충돌처리가 필요한 오브젝트라면 DrawObstacle() 함수에서 처리해 줄 것이다.

하나만 기억하면 된다. Track에 추가 해준 Modifier에 label 이름을 충돌 처리 하고 싶은 오브젝트들에만 + "Obstacle" 해주자. 절대 충돌처리가 필요 없는 애들의 이름에는 Obstacle을 써주면 안 된다. 그 이유는 내가 작업한 코드를 보면 이름에 "Obstacle" 이 있는지 없는지만을 확인해서 나눠주기 때문이다.

 

이제 거의 다 왔다. CheckObstacleCollision() 함수를 만들어 주자. 여기서 충돌 처리를 확인할 것이다. 처음엔 위치값으로 만도 체크를 해봤는데 섬세하게 안 돼서 AABB충돌체크로 충돌 감지를 해봤다.

 

만약에 충돌이 된다면 OnCollision() 함수를 실행시켜 주고 이 OnCollision() 함수에서 충돌했을 때 해주고 싶은 작업을 해주면 된다.

 

마지막으로 Update() 함수에서 DrawObstacle() 함수와 CheckObstacleCollision() 함수를 추가해 주면 모든 게 끝이 난다.

 

화면에 보이는 것처럼 잘 나온다. 굿굿! 

아까도 이야기했듯이 다른 오브젝트들도 넣어주고 싶다면 화면처럼 추가해 주면 끝난다.

728x90
반응형
728x90
반응형

평일 낮 10시 반에 엄마와 함께 영화를 보고 왔다.

엄마는 유독 로마가 배경인 영화들을 좋아한다.

가족여행으로 다녀왔던 로마 여행에서도 제일 설레 보였던 것은 아마도 엄마 아빠 젊었을 적 보았던 영화들의 이유가 크지 않을 까를 생각한다. 특히나 콜로세움 앞에서의 부모님의 얼굴은 그때의 여행에서 통틀어 제일 행복해 보였던 순간으로 기억된다. 사담이지만 부모님과 로마여행을 간다면 벤츠투어를 추천한다. 다리도 안아프고 곳곳을 둘러볼 수 있어서 참 좋았다. 혹시나 부모님이 음식이 입에 안 맞아하신다면 길거리에서 파는 군밤과 생과일 컵을 보일 때마다 사두는 것도 추천한다. 안 맛있을 수가 없다.

 

요즘 나와있는 영화들이 엄청나게 다양한데, 그 많은 영화들 사이에서 콕 찝어 이 영화를 보고 싶다 하신 것도 참 귀여웠다.

잔인한 영화를 좋아하지 않는 엄마가 이 영화를 선택한 부분에서 난 조금 놀라웠다. 

하지만 더 놀라운 일들이 기다리고 있었으니 ㅎㅎㅎ

이상하게 누군가가 칼에 베일 때마다 엄마가 과자를 먹는 게 아닌가. 나중에 물어보니 피가 나오는 게 너무 싫어서 아래를 보다가, 그저 아래에 있던 과자를 보고 손이 간 것이었다. 그게 계속되니 그 타이밍 때마다 과자를 드신 것인데 전후 사정을 모르는 내 입장에선 약간 무서웠다.ㅋㅋ

영화가 3시간이라 하길래 혹시나 입이 구준할까 싶어 가져갔던 과자에서 나홀로 오싹함을 느껴버린 것이다.

 

그렇게 오싹 오싹거리고 있는데 옆에서 자꾸 밝은 핸드폰 화면이 빛을 내고 있었다.

아무래도 이 영화를 선택한 연령층들은 대체적으로 엄마 아빠 나이 때였는데 우리 바로 옆에 앉아 계신 분들은 무려 3시간을 내내 핸드폰을 켜두었다. 나중엔 무얼 하는지 궁금해서 봤다가 엄마와 나는 그냥 영화에 집중하기로 했다.

이유는 바로 게임을 하시고 계셨기 때문인데. ㅎㅎ 그 옆에 분은 화면이 너무 밝아서 도저히 뭘 하고 계신 건지도 못 봤다. 우리 동네 영화관은 전부다 리클라이너관으로 바뀌어서 이제는 앞사람이 핸드폰해도 상관없겠다 싶었는데 오늘로써 옆사람은 해결이 안 되었다는 것을 깨닫게 되었다. 

 

그런 모든 일들이 있었지만 영화가 생각보다 너무 재밌어서 다행히 그 모든  상황을 뒤로 하고 영화에 몰입돼서 보고 나왔다. 정말 별생각 없이 봤는데 내가 제일 재밌게 본 것 같았다. 엄마는 1편이 더 재밌다면서 나중에 한번 봐보라 하셨다.

아마도 난 1편도 찾아 볼 것 같다. 어쩌면 로마에 대해서도 흥미가 좀 생긴 것 같다. 영화를 다 보고 나오니 아빠가 생각났다. 아빠도 아마 엄청 좋아할 텐데 이제 거의 막을 내리고 있는지 예매할 시간대가 좀처럼 맞지 않았다.

퓨리라도 재개봉한다 해서 아쉽지만 그거라도 예매해둘까 싶다. 

 

내가 엄마, 아빠 나이가 되었을 때, 나의 젊었을 때를 생각하며 기억할 영화들이 무엇이 있을지 생각해 보았다.

지금의 내가 좋아하는 영화들은 나 홀로 집에 시리즈, 해리포터 시리즈, 폴라익스프레스 같은 산타가 나오는 영화들인데 먼 훗날 아직까지는 존재의 유무도 확인할 수 없는 나의 아이들에게 이 영화들을 추천해 준다면 과연 오늘날 내가 엄마, 아빠한테 추천을 받았을 때 느꼈던 "멋"을 보일 수 있을까.

결부터가 다르니 난 아무래도 초등학생 때쯤 같이 보는 걸로 노선을 선택해야겠다. 그때라면 과연 최고의 선택일 듯하다.

 

고전 영화들은 대체로 나에겐 흑백 영화들로 기억된다. 어렸을 적에 학교 음악시간에 틀어주던 고전 영화들을 기억하는가. 그때 당시 수업이 끝난 후 자투리 시간에 보여주던 거라 짧으면 10분, 길면 20분쯤 볼 수 있었다. 그래서 그랬는지 모르겠지만 별로 재미도 없었을 내용들이 더욱 재밌게 다가왔던 기억이 있다. 감칠맛 같은 흑백 영화들이었다. 그때 이후로는 다시 찾아보지 않았는데 이번 기회에 고전 영화들을 찾아서 봐볼까 하는 생각이 들었다. 아무래도 재밌는 고전영화 하나 찾아서 나중에 내 아이에게 소개해주는 것도 꽤 나쁘지 않을 것 같았달까. 왜인지 "멋"에 집착하는 나일까 싶기도 했다.

이렇게 집착하는 모습에서 보았을 때 나는 나의 아이들에게 더도 말고 우리 부모님 같은 부모가 되고 싶다. 그렇게 되면 내 자식들도 꽤나 행복하게 살 것 같은 기분이 든다. 더 열심히 살아야지. 그래서 우리 가족이 더, 더 행복해지면 좋겠다.

 

728x90
반응형
728x90
반응형

바로 다음날 작업한 걸 정리하려던 나는 내일 할까 미뤘다가 파일정리를 하면서 Shift + Delete로 프로젝트를 삭제시키는 끔찍한 실수를 저질렀다. 잠시 잠깐 멘붕에 빠져있다가 PuranUtilitiesSetup이라는 복원프로그램을 사용해 봤다.(이 프로그램은 공짜로 풀린 복원 프로그램인 것 같은데 안타깝게도 cs파일은 찾지 못하는 것 같았다. 텍스트 파일이나 사진파일 같은 건 바로 찾을 수 있는 것 같았음.)

https://nomatter-me.tistory.com/209

 

이것 저것 ) 무료 복원 프로그램 Puran Utilities를 아는가.

Puran Utilities 프로그램을 알게 되었다. 복원 프로그램인데 무료로 이용이 가능하다. 현재의 내 경우에는 살리지 못했지만 언젠가 혹시 모를 참담하고도 참담할 상황인, 중요 문서를 쉬프트 딜리

nomatter-me.tistory.com

 

나의 경우는 파일을 찾지도 못했을 뿐더러 중간에 갑자기 노트북이 업데이트되는 해프닝이 있었다. 업데이트가 되길래 약간은 불안했다만 "그저 기우겠지."란 생각으로 더 이상 생각하지 않기로 했다. 그렇게 노트북이 힘겨워하기에 그만두고 다시 구현하는 쪽을 선택해서 지금 다시 올린다. 그래도 한번 만들어 봤다고 시간이 엄청 투자되지는 않아서 다행이었다.

using UnityEngine;
using System.Collections.Generic;

public class ObstacleManager : MonoBehaviour
{
    // 도로의 전체 너비와 카메라 높이 설정
    [Header("Road Settings")]
    public int roadWidth = 1;
    public float cameraHeight = 1000;
    public float speed = 0.8f;
    
    [Header("Obstacle Settings")]
    public GameObject[] obstaclePrefabs;
    [SerializeField] private float obstacleSpeed = 0.8f;
    [SerializeField] private float spawnY = 3.16f;
    [SerializeField] private float endYPosition = -6f;
    [SerializeField] private int poolSize = 5;
    [SerializeField] private int maxObstacles = 3; // 동시에 존재할수 있는 최대 장애물 수
    [SerializeField] private float minObstacleSpacing = 0.5f; // 장애물 X간 최소 간격
    [SerializeField] private float heightCheck = 1f; // 장애물 Y간 최소 간격
    [SerializeField] private int consecutiveNum = 3; //연속 장애물 허용 범위
    [SerializeField] private float spawnRate = 0.02f;
    
    private Queue<GameObject>[] obstaclePools;
    private List<GameObject> activeObstacles = new List<GameObject>();
    private int consecutiveSpawnCount = 0;    // 연속 생성 횟수
    private bool? lastSpawnSide = null;       // 마지막 생성 방향 (null: 초기상태)

    private float spawnTimer;
    private readonly Vector3 scaleVector =Vector3.one;
    private void Awake()
    {
        activeObstacles = new List<GameObject>(maxObstacles);
        InitializeObstaclePools();
    }

    private void InitializeObstaclePools()
    {
        obstaclePools = new Queue<GameObject>[obstaclePrefabs.Length];
    
        for (int i = 0; i < obstaclePrefabs.Length; i++)
        {
            obstaclePools[i] = new Queue<GameObject>();
            for (int j = 0; j < poolSize; j++)
            {
                CreateObstacles(i);
            }
        }
    }

    private void CreateObstacles(int typeIndex)
    {
        GameObject obj = Instantiate(obstaclePrefabs[typeIndex], transform);
        obj.SetActive(false);
        obj.AddComponent<ObstacleData>();
        obstaclePools[typeIndex].Enqueue(obj);
    }

    private void Update()
    {
        spawnTimer += Time.deltaTime;
        if (spawnTimer >= spawnRate)
        {
            spawnTimer = 0f;
            if (Random.value < spawnRate)
            {
                SpawnObstacle();
            }
        }
        UpdateObstacles();
    }

    private void UpdateObstacles()
    {
        Vector3 position;
        float progress, scale, xOffset, newX, newY;
        
        for (int i = activeObstacles.Count - 1; i >= 0; i--)
        {
            GameObject obstacle = activeObstacles[i];
            ObstacleData data = obstacle.GetComponent<ObstacleData>();
            position = obstacle.transform.position;
            
            // 장애물의 진행도 계산 (0~1 사이 값)
            progress = (spawnY - position.y) / (spawnY - (transform.position.y + endYPosition));
            // 진행도에 따른 크기 보간
            scale = Mathf.Lerp(0.1f, 3f, progress);
            // 바깥쪽으로 이동하는 힘을 더 강하게 수정
            xOffset = data.isLeftSide ? -progress * 4f : progress * 4f;
            newX = data.initialX + (xOffset * roadWidth);
            // 스케일이 커질수록 더 빠르게 이동 //scale 0.1_기본속도 1, scale 1.5_기본속도 4.5, scale 최대_기본속도 8.25
            newY = position.y - (obstacleSpeed * (1f + (scale - 0.1f) * 2.5f) * Time.deltaTime);
            
            position.x = newX;  
            position.y = newY;
            obstacle.transform.position = position;
            obstacle.transform.localScale = scaleVector * scale;

            if (newY < transform.position.y + endYPosition)
            {
                ReturnToPool(obstacle);
                activeObstacles.RemoveAt(i);
            }
        }
    }
    

    public class ObstacleData : MonoBehaviour // 장애물의 초기 x위치와 방향을 저장하는 컴포넌트
    {
        public float initialX;
        public bool isLeftSide;
    }
    
    private bool IsValidSpawnPosition(Vector3 newPosition)
    {
        
        foreach (GameObject obstacle in activeObstacles)
        {
            // y축 간격이 heightCheck의 간격보다 작은지 체크
            if (Mathf.Abs(obstacle.transform.position.y - newPosition.y) < heightCheck)
            {
                // x축 간격이 최소 간격보다 작으면 생성 불가
                if (Mathf.Abs(obstacle.transform.position.x - newPosition.x) < minObstacleSpacing)
                {
                    return false;
                }
            }
        }
        return true;
    }

    private void SpawnObstacle()
    {
        if (activeObstacles.Count >= maxObstacles)
        {
            return;
        }

        //50% 확률로 장애물 왼쪽, 오른쪽으로 스폰
        bool spawnOnLeft = Random.value > 0.5f;
        HandleConsecutiveSpawns(ref spawnOnLeft);

        int typeIndex = Random.Range(0, obstaclePrefabs.Length);
        GameObject obstacle = GetObstacleFromPool(typeIndex);

        if (obstacle == null)
        {
            return;
        }

        // 초기 생성 위치를 도로 중앙 근처로 수정 //0.1 수치 증가시키면 장애물이 도로의 바깥쪽 생성,감소 >>중앙에 가깝게 생성.
        float xPos = spawnOnLeft ? -roadWidth * 0.1f : roadWidth * 0.1f;
        Vector3 newPosition = new Vector3(xPos, spawnY, 0);

        // 새로운 위치가 적절한 간격을 유지하는지 확인
        if (!IsValidSpawnPosition(newPosition))
        {
            ReturnToPool(obstacle);
            return;
        }

        SetupObstacle(obstacle, xPos, spawnOnLeft);
    }

    private void HandleConsecutiveSpawns(ref bool spawnOnLeft) //연속으로 같은 방향에 장애물이 생성되는 것을 제어
    {
        // 이전과 같은 방향으로 생성하려는 경우
        if (lastSpawnSide.HasValue && spawnOnLeft == lastSpawnSide.Value)
        {
            consecutiveSpawnCount++;
            // consecutiveNum번 연속 생성되면 반대편에 생성
            if (consecutiveSpawnCount >= consecutiveNum)
            {
                spawnOnLeft = !lastSpawnSide.Value;
                consecutiveSpawnCount = 0;
            }
        }
        else
        {
            consecutiveSpawnCount = 0;
        }
        lastSpawnSide =spawnOnLeft;
    }

    private void SetupObstacle(GameObject obstacle, float xPos, bool isLeft) // 생성된 장애물의 초기 설정을 담당
    {
        obstacle.transform.position = new Vector3(xPos, spawnY, 0);
        obstacle.transform.localScale = scaleVector * 0.1f;

        var data = obstacle.GetComponent<ObstacleData>();
        data.initialX = xPos;
        data.isLeftSide = isLeft;
        
        obstacle.SetActive(true);
        activeObstacles.Add(obstacle);
    }

    private void ReturnToPool(GameObject obstacle)
    {
        obstacle.SetActive(false);
        for (int i = 0; i < obstaclePrefabs.Length; i++)
        {
            if (obstacle.name.Contains(obstaclePrefabs[i].name))
            {
                obstaclePools[i].Enqueue(obstacle);
                break;
            }
        }
    }

    private GameObject GetObstacleFromPool(int typeIndex)
    {
        return obstaclePools[typeIndex].Count > 0 ? obstaclePools[typeIndex].Dequeue() : null;
    }
}

 

장애물은 왼쪽과 오른쪽에 생성되는 장애물에 따라 도로 벽 쪽으로 붙어서 떨어지게 작업했다. 

만들다 보니까 겹쳐서 생성될 때도 있길래 최소 x, y값의 간격을 정해서 생성되게 작업했다. 그렇게 만들었더니 간격은 마음에 드는데 뜸하게 만들어지는 것 같기도 하고 이건 수치 조절하면 될 것 같은데 그 적정한 선을 찾기가 쉽지가 않구만.

장애물이 연속해서 나오는 것도 별로길래 몇 개 같은 쪽으로 나오면 다시 만들어질 때는 다른 쪽으로 만들어지게 만들어 줬다.

주석처리를 많이 해두어서 코드를 보는 데는 문제가 없을 듯하다.

하지만 솔직하게 이 코드는 pseudo 3d game라 할 수 없다. 그래서 시간이 될 때 다시 도전해 보는 걸로 마무리 지었다. 

 

https://github.com/h8man/TurboTrack2D

 

GitHub - h8man/TurboTrack2D: Prototype of 2D arcade style racing game

Prototype of 2D arcade style racing game. Contribute to h8man/TurboTrack2D development by creating an account on GitHub.

github.com

여기 있는 프로젝트는 누군가가 만들어둔 pseudo 3d game 유니티 프로젝트이다. 

이 코드를 좀 뜯어보면서 다시 공부를 해봐야겠다.

728x90
반응형
728x90
반응형

Puran Utilities 프로그램을 알게 되었다. 복원 프로그램인데 무료로 이용이 가능하다. 현재의 내 경우에는 살리지 못했지만 언젠가 혹시 모를 참담하고도 참담할 상황인, 중요 문서를 쉬프트 딜리트로 지우는 만행을 저지를 수도 있는 나를 위해 남겨둔다. 

 

2016년 이후로 업데이트가 되고 있지 않는 프로그램이라 전부를 찾는 것은 불가능해 보여도 찾고자 하는 파일의 확장자를 추가시켜 두면 찾을 수도 있을 것 같은 부분은 발견했다. 문서나 사진, 동영상 등은 복원이 가능한 것 같다. 

 

PuranUtilitiesSetup.exe
9.63MB

 

일단 프로그램을 다운 받아서 열면 저 화면이 먼저 나오는데 ok를 눌러준다.

 

 

현재 상황이 중요 파일을 쉬프트 딜리트로 지웠거나, 휴지통에서 파일을 지웠을 때 복원시키고 싶은 거라면 File Recovery를 눌러주자.

옆에 있는 Data Recovery는 안열리는 파일을 복원시키는 기능인 것 같았다.

 

 

더블클릭하면 나오는 언어선택창에선 한국어를 지원하지 않으니 영어로  OK 해주자.

 

 

영어로 설명이 나오는데 우리는 읽어볼 필요 없이 OK 버튼 누르면 된다. 

 

 

삭제시킨 파일이 있던 장소를 선택해 주고 그냥 scan 버튼을 눌러서 스캔해도 되지만 이경우엔 다 나오지 않는 것 같다.

어차피 시간이 들여지는 거니 한 번에 한다면 Deep Scan 버튼을 클릭해서 깊게 스캔해 주자. 

 

 

그냥 스캔을 눌러도 꽤 오래 걸린다. 기다려 주자. 혹시나 기다리다 시간이 없어진다면 stop버튼을 눌러서 지금까지 훑은것들만 확인할 수도 있다. 

 

 

스캔이 끝나면 저렇게 지워졌던 파일들이 나오고 확인 버튼을 클릭해 주면 된다.

 

 

이미지를 보면 알겠지만 저기서 찾기엔 쉽지 않다. 아래쪽에 Tree View를 클릭해 주자.

 

 

그럼 간단하게 볼 수 있게 바뀐다. 여기서 원하는 파일을 선택해서 Recover버튼으로 복구시켜 주면 된다.

 

 

이미지에서 보이는 것처럼 파일 뒤에 Excellent Condition, Good Condition은 복원이 가능하지만 Poor Condition은 아쉽게도 복원되지 않는다. 

 

프로그램 사용 방법은 생각보다 쉽지만 못 찾을수도 있으니 쉬프트 딜리트는 신중하게 사용하자.

728x90
반응형
728x90
반응형

첫눈이 왔다. 그제 밤부터 하늘이 흐리기 시작하더니 어제 아침이 되자 온 세상이 눈 속에 덮여 하얀 세상이 되어있었다. 나무에는 눈꽃이 피었고, 땅과 건물들에는 추위에도 녹지 않는 눈이불이 덮였다. 말 그대로 세상이 눈 속에 있었는데 첫눈이 이렇게 커다랗게 온건 정말 오랜만이라 보고만 있어도 참 행복했다. 

이제 슬슬 크리스마스가 다가오는 구나라는 느낌이 날 더 즐겁게 만들었다. 수정볼안에 들어있는 것처럼 어느새 밖에는 또다시 눈이 내렸다.

 

"역시 눈이 내리는 날엔 핫초코지."라는 생각으로 지난번 이마트에서 사둔 미떼를 꺼냈다. 뜨거운 물에 미떼를 녹여주고 우유를 넣고 다시 저어서 전자레인지에 살짝 돌려주면 맛 좋은 핫초코가 탄생한다. 이렇게 탄 핫초코를 들고 베란다에 나가 첫눈의 흔적과 함께 계속해서 오고 있는 눈송이들을 한동안 구경했다. 여기에 이불이라도 하나 들고 오면 딱일 텐데라는 생각을 하던 중 이전에 내가 베란다에서 자보겠다고 도전했던 날이 생각이 났다.

 

그런 날들이 있다. 계절마다 해볼 수 있는 삶의 체험 현장.

유독 겨울을 좋아해서 그런지 나는 겨울에 그런 느낌이 강하게 든다. 삶의 체험 현장이라 해서 웅장한 것이 아니다. 가령 한겨울에 황토 맨발 걷기 하기나 베란다에서 자보기 같은 1박 2일에서 나올 법한 상황을 이야기하는 것이다.

 

나는 황토 맨발걷기를 할 수 있는 공원들에서 한겨울에 사람들이 걷지 않는 공통점을 발견하고 "왤 까"란 생각으로 가족들의 동의를 얻어 걸어봤다. 우리 가족들은 그런 나를 보고 "그래, 해봐라"라는 마인드를 가지고 공원 옆에 있는 그네에 앉았다. 처음엔 양말을 벗고 얼어있는 땅에 발이 닿으니 약간의 한기가 느껴졌다. 황토가 얼어있었다. 걸어봤다. 발바닥이 아픈 것도 아픈 것이지만 순식간에 내 따뜻했던 발바닥이 냉골의 상태가 되었다. 그래도 이렇게 시작을 해봤으니 한 바퀴는 돌아봐야 지란 생각을 가지고 빠른 스피드로 빠른 걷기를 시작했다. 다 걷고 난 다음 발을 물로 씻어야 됐는데 엄두가 나지 않아서 양말을 신고 집에 와서 따뜻한 물로 발을 씻었다. 집에 돌아오던 길 내내 우리 가족들은 나를 걱정하면서도 도대체 그걸 왜 해봐야 아냐고 타박을 했지만 나름 재미있었다. 그 후로 나는 겨울이 되거나 좀 추워질 때 공원을 산책하는 순간이 오면 그때의 내가 생각난다. 사람들도 아마 어쩌면 해봤다가 이건 아니란 사실을 알게 된 것일 지도 모른다. 

 

베란다에서 자보기를 한 날은 유독 뉴스에서 한파 주의를 외치던 날이었다.

겨울중에서도 가장 추운 겨울에 해보고 싶었던 마음이 컸던지라 뉴스에서 속보같이 떠있는 한파주의 단어를 보자마자 "오늘이다!"를 생각했다. 엄마, 아빠께 오늘은 밖에서 자볼 것이라 이야기하고 설레는 마음으로 밤이 될 때까지 기다렸다. 이전에 나의 형제는 고등학교 때 자신의 선교부 선배의 수능을 응원하기 위해 선배가 수능을 보는 학교 앞에서 강제로 강 추위 속에서 노숙을 한 경험이 있다. 그는 나를 보며 왜 사서 고생을 하냐는 눈빛을 보냈다. 엄마, 아빠는 그때도 어디서 구했는지 모를 냉장고 박스와 신문지, 침낭을 챙겨 나의 형제에게 집을 만들어 주었던 경력이 있다. 그때의 그 사건은 사실 학교 선배들의 강압적 태도 안에서 이뤄진 거라 자발적인 의도가 없었기 때문에 더 추위를 느꼈던 것 같았었다. 내 일이 아닌 해프닝이었기에 나 또한 얼마나 추운지 궁금했었던 마음이 컸다.

엄마, 아빠는 그날도 여기서 침낭 깔고 자다가는 얼굴이 돌아간다며 몇 가지만 해줄 테니 그 위에서 자라하셨다. 난 뭐 얼마나 달라질까를 생각했지만 부모의 사랑은 위대했다. 어째서인지 베란다 창문과 문에 김서림이 끼기 시작했다. 난 베란다에 누워 창문 밖 별을 보고자 했던 것인데 왜 저렇게 하얀 베란다가 되어있을까 싶어 나가 봤다. 바닥엔 우리 집에 있었는지도 모를 두꺼운 돗자리부터 시작해서 이불 요 매트 + 이불 + 침낭 + 파쉬 물주머니 여러 개. 원래라면 베란다에 가면 코가 시린데 그때는 집안보다 베란다가 더 따뜻했다. 뭔가 나의 의도와는 전혀 다른 잠자리였지만 그래도 베란다에서 자보기가 주 포인트였기에 설레는 마음으로 잠을 자보려 침낭 안으로 들어갔다. 얼마나 바닥이 많이 깔려있는 것인지 내 침대만큼 폭신했다. 이게 바로 가족의 사랑인가 싶었는데 가족의 사랑은 그렇게 끝나지 않았었다.

잠을 자다 너무 더워서 뒤척이다 눈이 떠졌는데 누군가가 날 보고 있는 게 아닌가. 정말 화들짝 놀라서 일어나 보니 엄마와 아빠가 베란다 안을 쳐다보고 있었다. 밖에서 자겠다는 나를 말리기엔 내 행동이 너무 완강했고, 그렇게 내버려 두자니 내가 얼어 죽을까 봐 걱정이 되셨던 것이었다. 그 새벽에 우리는 서로 놀랐지만 그만큼 웃었다. 다음날 아침 여전히 뽀얀 창문들을 보며 잠에서 깼다. 역시나 나름 재밌었던 기억이다.

 

어제는 엄마와 밤 산책을 갔었는데, 엄마가 나무들마다 한가득 눈이 쌓여있는 걸 보고 "눈이 많이 쌓여있으면 나무들이 무거울꺼야"라는 말과 함께 어디서 찾아왔는지 모를 긴 나뭇가지로 나무들을 털어주었다. 키가 닿지 않는 곳엔 점프를 하면서 털어주었는데 그 모습이 참 귀여웠다. 슬쩍 내 뒤에 와서 나에게 눈벼락을 맞게 하기 전까지 난 엄마의 따스함에 감동하고 있었다. 그렇게 눈도 맞고, 밟으면서 뽀독 뽀독 소리도 들으며 산책을 하니 슬슬 생각나는 게 있지 뭔가. 

 

한겨울에 붕어빵은 못참지.

붕어빵 파는 아줌마를 찾아 이리저리 다니다 드디어 찾아서 팥붕어빵 4개를 샀다. 아주머니께서 갓 만든 거라 아마 한입 먹으면 잊지 못할 거라 하시길래 두근거림은 배가 되었다. 무엇보다 여기 붕어빵은 아직 2개에 1000원이었다. 감사합니다의 인사와 함께 가장 맛있게 생긴 붕어빵을 엄마한테 주고 나도 하나 꺼내 먹었다. 엄마와 동시에 눈이 마주쳤고 우린 다시 왔던 길을 돌아 붕어빵 두 개를 더 사 왔다. 그 사이 붕어빵아주머니는 줄이 길게 늘어나있었다. 모두가 한마음이었던 것이다. 사실 우리가 지나가는 사람들에게 다 들리도록 너무 크게 맛있다를 외쳤던 것 같기도 하다. 

역대급으로 만족했던 붕어빵을 먹으면서 신나게 집으로 향했다. 겨울의 시작이 아주 좋다!

붕어빵은 팥이지.

728x90
반응형

+ Recent posts