목차

반응형

회사 사람들이 내가 원노트를 사용하는 것을 보고 감명을 받기도 하고 블로그에서 원노트 사용방법 관련 게시물을 조회하는 사람들이 있어서 이전 게시물에서 전부 다 올리지 못했던 내용을 보충하여 설명하고자 작성하였다.

물론 100% 활용하지는 못하겠지만 아예 처음 사용하는 사람들이 숙지하면 좋을 것 같다.

몇달정도 사용해본 소감은 쓸수록 좋은 것 같다.

요번에는 사용해보고 불편하고 큰일 날 수도 있는 점들도 같이 넣었다.

 

내용 목차

1. 나의 작업 방식

2. 다른사람에게 공유할 경우 유의할 점

3. pdf 내보내기는 깨진다.

4. 웹 페이지 공유 시 그리기는 깨진다.

5. 목록 드래그해서 순서 바꾸기

6. grid snap 기능

 

 

 

 

 

 

 

 


1. 나의 작업 방식

일단 나는 캐쥬얼 게임 개발자로서 회의가 잦고 개발자임에도 불구하고 기록이 생명이다.

내가 원노트를 어떻게 사용하는지 예시를 보이도록 하겠다.

- 프로젝트별로 화이트보드라는 페이지를 만들어서 실제 회의할 때 화이트보드처럼 개발 도중 발생하는 메모를 전부다 기록하도록 한다.

- 발생하는 큼직큼직한 이슈는 따로 페이지 만들어서 저장

 

아무래도 주로 해야하는 작업에 대하여 체크박스를 사용하여 내용 정리를 하는데 한번 만들어놓고 회의를 거칠 때마다 기존 내용을 참고하여 새롭게 써내려간다.

계속해서 새로 써 내려가지 않으면 매우 더러워지고 정신없어진다.

또한 우측에 큼직한 이슈들을 추후 다른 사람에게 공유하거나 나중에 동일 현상이 다시 발생할 경우 빠르게 찾기 위해서 따로 정리해놓았다.

 

 

2. 다른사람에게 공유할 경우 유의할 점

다른 사람에게 전자 필기장 공유를 하는 경우 원노트는 매우 조심하게 다뤄야 한다.

그 이유는 내가 페이지 하나를 공유하고 싶어도 현재 페이지가 속해있는 필기장 전체를 공유해버리기 때문이다.

 

다음과 같이 페이지를 공유하고 링크로 접속해보면

 

페이지 하나뿐만이 아니라 페이지가 속해있는 필기장 전체를 공유해버린다.

따라서 나는 페이지를 공유할 때 페이지를 다른 임시 필기장에 복사한다음 해당 필기장을 공유한다. 이 부분은 에버노트가 되게 잘 되어있다고 생각한다.

굳이 모드 필기장을 다 공유할 필요가 있을까 원노트야...

 

 

 

3. pdf 내보내기는 깨진다.

원노트에서 아쉬운 부분이다. 페이지를 내보내기 할때 무턱대고 pdf로 공유하면 페이지가 곧잘 깨진다.

 

따라서 pdf 공유는 하지 않도록 한다.

 

 

4. 웹 페이지 공유 시 그리기는 깨진다.

그렇다고 웹 페이지로 공유하는 것은 안전하느냐...

그것도 아니다 웹 페이지로 공유하면 그리기로 기재한 내용의 위치가 맞지 않는다.

따라서 그리기 사용을 자제하는 것도 하나의 방법이라고 생각하지만 원노트의 가장 큰 장점은 낙서하듯이 할 수 있는 메모라서 이 부분 또한 아쉽다.

웹 페이지 공유를 맹신하지 말자

 

 

5. 목록 드래그해서 순서 바꾸기

다음은 목록에서 순서를 쉽게 바꾸는 방법이다.

 

 

6. grid snap 기능

기본적으로 원노트는 그리드 스냅이 꺼져있어서 나같이 위치에 대한 강박증이 있는 사람에게는 좋지 못한 경험을 제공한다.

어휴 불편;;

 

그리기 > 추가 옵션 > 눈금에 맞춤을 눌러서 grid snap을 켜주도록 하자

 

이전 글을 보지 않은 사람은 이전 글의 내용도 좋으니 같이 보면 좋을 듯 하다.

OneNote 사용 팁, 에버노트와 비교

반응형
반응형

목차

목표

캐논 오브젝트 세팅

마우스 드래그 벡터 구하기

벡터와 변환된 각도 확인

각도 변환 설명

회전 각도 제한

일정 거리 이상 드래그 해야 발사 가능하도록 하기

전체 소스

 

 

목표

시연

만들고자 하는 결과물이다. 드래그 방향과 반대로 캐논 오브젝트가 가리키도록 하고 일정 거리 이상 드래그를 해야 발사가 가능하도록 한다.

 

캐논 오브젝트 세팅

다음과 같이 Circle에 Square로 포신을 만들고 Laser 오브젝트는 Square로 매우 얇고 길게 해서 만든다.

캐논 오브젝트는 단순히 플레이어에게 방향을 나타내주기만하고 게임에 영향을 주는 물리적인 요소는 전혀 없다.

 

다음과 같이 되도록 z축의 값을 차례대로 정해준다.

 

Cannon 스크립트를 생성하고 넣어준다.

맨아래 소스를 복사해서 넣는다.

 

인스펙터에서 Cannon 스크립트의 Laser에 생성한 Laser 오브젝트를 넣는다.

 

마우스 드래그 벡터 구하기

왼쪽 이미지를 보면 클릭하고 왼쪽 아래로 드래그를 하면 빨간색의 벡터만큼 크기(길이)와 방향성을 갖는다.

우리가 필요한 것은 반대방향의 벡터다. 반대방향의 벡터를 캐논 오브젝트에서 값을 가질 수 있도록 한다.

 

if (Input.GetMouseButtonDown(0))
{
originPosition = Camera.main.ScreenToWorldPoint(Input.mousePosition);
IsMouseClicked = true;
}

최초로 마우스를 클릭하면 originPosition 벡터에 현재 카메라에 클릭된 위치를 world 좌표계로 바꿔서 저장한다.


currentMousePosition = Camera.main.ScreenToWorldPoint(Input.mousePosition);
그리고 현재 마우스 위치를 currentMousePosition에 저장한다.

드래그를 하고 있다는 가정하에 계속해서 최초로 클릭한 곳의 위치와 현재 마우스의 위치의 차이를 구해야한다.


diffVector = (currentMousePosition - originPosition) * -1;
diffVectorManitude = diffVector.magnitude;
diffNormalVector = diffVector.normalized;

위에서 설명한대로 -1을 곱해서 방향을 반대로 해준다.

diffVectorManitude 는 벡터의 길이값을 환산해주는 것이고

diffNormalVector 는 캐논 오브젝트의 방향을 결정짓도록 하는 방향벡터의 단위벡터다.

 

벡터와 변환된 각도 확인

 

위와 같이 로그를 찍으면 다음과 같이 나온다.

 

차례대로

최초 마우스 클릭 위치

현재 마우스 위치

위치의 차

x,y좌표계 기준 벡터와 x축이 이루는 각도

 

 

각도 변환 설명


float rot_z = Mathf.Atan2(diffNormalVector.y, diffNormalVector.x) * Mathf.Rad2Deg;
currentDegree = Mathf.Clamp(rot_z - 90, -65, 65);

rot_z로 x축과 벡터가 이루는 각도를 계산하였는데 실제로 사용하기 위해서는 -90도를 더해줘야한다.

그 이유는 유니티에서의 벡터를 각도로 변환한 축이 위쪽으로 뻗어있기 때문이다.

 

위의 이미지를 보면 -90도가 일반적으로 우리가 생각하는 x,y축에서 x축이다.

즉 0도 = -90도 라고 대입을 해봤을때

만약에 방향벡터를 각도로 변환했을때 20도라면 -70도를 가져야 우리가 생각하는 상식선에서의 x,y 축 좌표계 방향이 나온다.

 

회전 각도 제한

드래그를 위쪽으로 하면 캐논이 아래를 가리키는데 그렇게 못하도록 최대 최소 각도를 제한해야한다.

diffVectorManitude = diffVector.magnitude;

currentDegree = Mathf.Clamp(rot_z - 90, -65, 65);

Mathf.Clamp를 사용해서 -65도와 65도 사이의 값만 가지도록 하였다.

-65도와 65도는 임의의 값이라서 마음대로 지정해도 된다.

-65도와 65도는 수기로 각도를 바꾸면서 이쯤이면 되겠다 싶은 값이다.

 

일정 거리 이상 드래그 해야 발사 가능하도록 하기

diffVectorManitude > 5;

위에서 계산한 벡터의 크기가 특정 값 이상일때만 즉 드래그를 어느정도 해야 발사가 가능하도록 할것인데 diffVectorManitude 가 일정값 이상일때 조건식을 사용하면 된다.

 

전체 소스

Cannon.cs

using UnityEngine;

public class Cannon : MonoBehaviour
{
    //최초 클릭 위치벡터
    private Vector3 originPosition;
    //현재(나중) 클릭 위치벡터
    private Vector3 currentMousePosition;
    //마우스가 클릭되었는지 여부
    private bool IsMouseClicked;
    //마우스 드래그 벡터
    private Vector3 diffVector;
    //마우스 드래그 벡터의 방향벡터
    private Vector3 diffNormalVector;
    //마우스 드래그 세기
    private float diffVectorManitude;
    //마우스 드래그 한 벡터 방향에 대한 z축 각도
    private float currentDegree;
    //발사 가능 여부
    private bool fireConfirm;
    //레이저 오브젝트
    [SerializeField] private GameObject Laser;
    // Start is called before the first frame update
    void Start()
    {
        
    }

    // Update is called once per frame
    void Update()
    {
        if (Input.GetMouseButtonDown(0))
        {
            originPosition = Camera.main.ScreenToWorldPoint(Input.mousePosition);
            IsMouseClicked = true;
        }
        else if (Input.GetMouseButtonUp(0))
        {
            Laser.SetActive(false);
            IsMouseClicked = false;    
            if (fireConfirm)
            {
                //발사처리
                Debug.Log("발사!");
            }
        }


        if (IsMouseClicked)
        {
            //카메라 기준 마우스 클릭 벡터 계산
            currentMousePosition = Camera.main.ScreenToWorldPoint(Input.mousePosition);
            
            //마우스 드래그 한 벡터 역방향
            diffVector = (currentMousePosition - originPosition) * -1;
            diffVectorManitude = diffVector.magnitude;
            diffNormalVector = diffVector.normalized;
            
            Debug.Log(originPosition);
            Debug.Log(currentMousePosition);
            Debug.Log(diffVector);

            //방향벡터의 x,y값으로 탄젠트를 이용하여 각도로 전환
            float rot_z = Mathf.Atan2(diffNormalVector.y, diffNormalVector.x) * Mathf.Rad2Deg;
            Debug.Log(rot_z);
            
            //드래그 거리가 충분하고 드래그를 위에서 아래쪽으로 했을때
            if (CheckHasPower() && rot_z>0)
            {
                //최소 -65도 최대 65도, 0도가 위방향이기때문에 -90도를 해줘서 오른쪽 방향이 되도록 한다.
                currentDegree = Mathf.Clamp(rot_z - 90, -65, 65);
                transform.rotation = Quaternion.AngleAxis(currentDegree, Vector3.forward);
                fireConfirm = true;
                Laser.SetActive(true);
            }
            else
            {
                fireConfirm = false;
                Laser.SetActive(false);
            }
        }
    }

    private bool CheckHasPower()
    {
        return diffVectorManitude > 5;
    }
}

 

JJTAN 개발 리뷰 시리즈

1. 프로토타입 수준의 오브젝트 및 환경 설정
2. 공 발사하는 캐논 설정
3. 인풋 설정, 공 발사
4. 장애물 생성
5. 장애물 적합한 사이즈 찾기
6. 장애물 색깔 그라데이션
7. 위험한 상황 줌인
8. 사망 판정
9. 데이터 저장과 이어하기
10. 한번 더 하기

추가 개발
튕기는 반사각 계산

반응형
반응형

프로젝트의 기본 틀을 만들고자 한다. 목표는 아래와 같이 박스 콜라이더 2D와 트리거를 만드는 것 이다.

최종 목적

 

유니티 2D 프로젝트 생성

먼저 유니티 프로젝트를 2D로 생성한다.

 

해상도 변경

프로젝트를 시작하면 세로형 게임을 만들어야하는데 카메라가 자꾸 가로형으로 비춰질텐데 해상도를 9:16으로 바꿔준다.

 

 

다음은 사각형과 동그라미 스프라이트를 생성한다.

Create > Sprites > Square, Circle을 눌러 생성한다.

 

다음과 같이 생성될 것 이다.

Scene의 이름도 Game으로 바꿔준다.

 

현재 게임이 9:16 게임으로 되어 있는데 카메라의 Size는 세로 해상도의 반이라고 생각하면 된다.

즉 Size를 8로 해주면 비율 계산에 딱맞아떨어진다. (막 만들지 말고 위의 비율 계산을 하고 들어가면 정말 편해진다.)

 

다음은 뼈대를 만들 것 이다.

Square Sprite를 끌어다 놔서 오브젝트를 생성한다.

 

사각형의 스케일을 9, 16, 1로 해주면 카메라에 딱 맞아 떨어진다.

 

게임 배경이 꽉채우지는 않을테니 다음과 같이 크기와 위치를 조절한다.

 

공이 튀기는 벽을 만들건데 사각형을 3개 복사해서 W를 눌러서 대충 위치를 위와 같이 옮기고 Scale을 맞춘다.

 

그 다음 우클릭 > CreateEmpty 를 눌러 빈 게임 오브젝트를 생성한다. 오브젝트 이름은 Wall로 한다.

빈 오브젝트를 생성하면 반드시 Position을 0,0,0으로 맞추고 생성한 오브젝트들을 하위 오브젝트로 넣어준다.

 

벽에게 충돌 가능하도록 콜라이더를 넣어준다.

생성한 벽들을 선택하고 Add Component에서 Box Collider 2D를 넣어준다.

 

그리고 Sprite Renderer를 우클릭 > 삭제한다.

 

다음과 같이 위치를 맞추고 오브젝트 이름도 바꾼다.

 

위와 같이 정갈하게 될 것이다.

 

다음은 콜라이더를 만들어야하는데 똑같은 방식으로 만들어주고 Box Collider2D 컴포넌트에서 Is Trigger를 체크해준다.

 

위와 같이 세팅을 해주면 1단계는 완료다.

 

JJTAN 개발 리뷰 시리즈

1. 프로토타입 수준의 오브젝트 및 환경 설정
2. 공 발사하는 캐논 설정
3. 인풋 설정, 공 발사
4. 장애물 생성
5. 장애물 적합한 사이즈 찾기
6. 장애물 색깔 그라데이션
7. 위험한 상황 줌인
8. 사망 판정
9. 데이터 저장과 이어하기
10. 한번 더 하기

추가 개발
튕기는 반사각 계산

반응형
반응형

목차

1. 개요

2. 차이점

2.1 불러오는 이미지 차이

2.2 Sprtie Packer 비활성화 여부

2.3 카메라의 2D뷰 3D뷰

2.4 Directional Light 오브젝트

2.5 카메라 위치와 시점

2.6 환경 변경

3. 참고자료

 

 

 

1. 개요

유니티에서 프로젝트를 새로 생성할 때 2D 프로젝트와 3D 프로젝트를 고르는 옵션이 있다.

항상 어떤 차이가 있는지 정확히는 모르고 주변 사람들한테는 '아 그거 어차피 비슷하니까 뭘로 해도 상관없어요 그냥 3d로 하세요' 이런 식으로 말하곤 했는데 이번 기회에 정확하게 차이가 무엇인지 알아보고자 한다.

미리 결론을 말하자면 차이는 없으나 기본적인 세팅 정도만 달라진다.

 

 

2. 차이점

2D 프로젝트와 3D 프로젝트 차이점에 대하여 유니티에서 공식적으로 올라와 있는 내용

이미지를 불러올 때 텍스쳐로 불러올지 스프라이트로 불러올지 카메라 시점이 다르다 등의 내용

더욱 구체적으로는 아래와 같이 차이가 발생한다고 적혀있다.

 

2D vs 3D mode settings

2D or 3D mode determines some settings for the Unity Editor. These are listed below.

In 2D Project mode:

  • Any images you import are assumed to be 2D images (Sprites
    ) and set to Sprite mode.

  • The Sprite Packer
     is enabled.

  • The Scene View
     is set to 2D.

  • The default game objects do not have real time, directional light.

  • The camera
    ’s default position is at 0,0,–10. (It is 0,1,–10 in 3D Mode.)

  • The camera is set to be Orthographic. (In 3D Mode it is Perspective.)

  • In the Lighting Window:

    • Skybox
       is disabled for new scenes
      .

    • Ambient Source is set to Color. (With the color set as a dark grey: RGB: 54, 58, 66.)

    • Precomputed Realtime GI is set to off.

    • Baked GI is set to off.

    • Auto-Building set to off.

In 3D Project mode:

  • Any images you import are NOT assumed to be 2D images (Sprites).

  • The Sprite Packer is disabled.

  • The Scene View is set to 3D.

  • The default game objects have real time, directional light.

  • The camera’s default position is at 0,1,–10. (It is 0,0,–10. in 2D Mode.)

  • The camera is set to be Perspective. (In 2D Mode it is Orthographic.)

  • In the Lighting Window:

    • Skybox is the built-in default Skybox Material.

    • Ambient Source is set to Skybox.

    • Precomputed Realtime GI is set to on.

    • Baked GI is set to on.

    • Auto-Building is set to on.

 

 

더욱 자세하게 알아보도록 하자

 

2.1 불러오는 이미지의 차이

왼쪽 2D 오른쪽 3D

두 프로젝트 똑같은 이미지를 Asset 폴더에 넣었는데 2D에서는 기본적으로 Sprite로 불러오고 3D에서는 Default로 지정된다.

2D에서는 매우 편리하게 드래그하면 2D 이미지로 사용하는 등 스프라이트로 바로 사용이 가능하다.

 

2.2 Sprtie Packer 비활성화 여부

Sprite Packer

스프라이트 팩커는 스프라이트를 효율적으로 사용할 수 있도록 해주는 것인데 2D에서는 기본적으로 활성화되어있고 3D에서는 기본적으로 위의 이미지처럼 비활성화되어 있다고 한다.

하지만 확인해보니 둘 다 비활성화 처리되어있었다.

 

2.3 카메라의 2D뷰 3D뷰

왼쪽 2D 오른쪽 3D

2D의 경우 2D버튼이 눌려있어 기본적으로 2D뷰가 활성화되어있고 3D는 꺼져있어서 기본적으로 3D뷰로 보인다.

시각적인 차이라서 큰 차이는 없어서 2D 프로젝트 3D 프로젝트 둘 다 번갈아가며 사용하는 기능이다.

 

2.4 Directional Light 오브젝트

왼쪽 2D 오른쪽 3D

3D에서는 기본적으로 Directional Light 오브젝트가 생성되어있다.

 

2.5 카메라 위치와 시점

왼쪽 2D 오른쪽 3D

2D의 카메라는 기본적으로 Y값이 0이고 3D 카메라는 1이다.

또한 2D는 Projection이 Orthographic이고 3D는 Perspective이다.

Perspective는 기본적으로 현실적으로 원근감이 반영되는 것으로 대부분의 3d 게임에서 사용하는데 위의 말대로 3D 프로젝트 환경에서 2D 게임을 만들기 위해서 카메라 시점을 Orthographic으로 바꾸어서 사용하기도 한다.

 

2.6 환경 변경

왼쪽 2D 오른쪽 3D

프로젝트를 잘못 만들었거나 설정의 변경이 필요하다면 Edit > Project Settings > Editor > Default Behavior Mode를 바꿔주도록 하자.

 

 

3. 참고자료

https://forum.unity.com/threads/difference-between-2d-and-3d-projects.554728/

 

Difference between 2d and 3d projects

hi guys i know the difference between 2d and 3d obviously but what implications does this have on creating a project for either? the space shooter...

forum.unity.com

https://unity3d.com/difference-between-2d-and-3d-games

 

Create 2D and 3D games with Unity - Unity

You have a full-featured professional toolset in Unity to create both 2D and 3D games. Get an intro to how to do it.

unity3d.com

 

반응형
반응형

맥에서 유니티의 Application.persistentDataPath 경로에 데이터를 저장하고 불러올 때 직접 들어가서 수정을 하거나 삭제를 하려는데 해당 경로로 이동하기가 쉽지 않다.

 

윈도우는 익스플로러에 경로를 넣으면 바로 들어가지지만 맥은 그렇지 않다.

 

문제

Application.persistentDataPath 로그

MAC에서 유니티의 Application.persistentDataPath 경로는 위와 같다.

위의 경로로 이동을 하려고 한다.

 

 

해결

1. 터미널 사용

터미널

맥 하단의 터미널을 켜서 접근한다.

 

  • cd ~/Library -> 라이브러리 폴더까지 바로 이동
  • 탭을 이용하면 편리함 ex) cd /U 탭 -> cd/Users/ 이렇게 알아서 자동완성시켜줌
  • Application Support의 경우 띄어쓰기를 해야 돼서 Application 치고 탭을 눌러서 자동완성시켜주는 것이 편하다.

 

이후 rm이나 vi 명령어로 삭제나 수정을 한다.

 

2. 파인더 사용

파인더를 켠다.

파인더에서 Cmd + Shift + G 를 눌러서 경로를 입력해서 이동하는 기능을 호출한다.

 

아까 유니티에서 콘솔에 찍힌 경로로 이동을 하려고 붙여 넣어서 Go를 누르면 아무 반응이 없다.

이는 Library 경로가 숨김 처리되어 있어서 그런데 아래 명령어를 통하여 숨김 처리를 해제하면 해결된다.

 

터미널에 다음과 같이 입력한다.

chflags nohidden ~/Library/

그리고 다시 로그에 나온 경로를 입력하고 Go를 누르면 아래와 같이 이동이 된다.

 

반응형
반응형

유니티에서 다른 언어를 지원하기 위해서 로컬라이제이션이 필요하다.

유니티에서 제공하는 로컬라이제이션 튜토리얼을 보고 로컬라이제이션을 숙지하게 되었다.

생각보다 어려운 내용이라서 소화하기 편하도록 자세하게 준비했다.

추가적으로 엑셀을 통하여 로컬라이제이션을 할 수 있는 방법까지 고려했다.

크... 배려심 오졌고

 

목차


1. 실제 프로젝트 시연

2. 로컬라이제이션 이미지 도식화

3. 로컬라이제이션 스크립트 핵심 부분 설명

4. 샘플 프로젝트 및 참고 링크

 

 

 

1. 실제 프로젝트 시연

 

실제로 로컬라이제이션을 사용하는 프로젝트 동영상

토글 형식으로 언어를 바꾸면 즉시 모든 텍스트 요소들이 언어에 맞추어 바뀐다.

한글 로컬라이제이션 데이터


영어 로컬라이제이션 데이터

각 데이터는 키와 밸류로 관리되고 있다.

눈여겨볼 점은 key값은 통일되어야 한다는 점이다.

입력은 엑셀로 하고 유니티에서 csv 파일을 json파일로 저장해서 json파일을 (정확히는 json형식의 txt 파일) 게임에서 불러와서 로컬라이제이션을 한다.

나중에 다시 설명을 할 것이지만 로컬라이제이션 세팅을 하고 나면 text 컴포넌트에 Localized Text 컴포넌트를 붙이고 텍스트에 해당하는 key값을 적어주면 알아서 언어 값에 따라서 바뀐다.



2. 로컬라이제이션 이미지 도식화

 

오늘따라 친절이 포텐터져서 이해하기 쉽도록 도식화까지 해놨다.

먼저 csv파일로 로컬라이제이션 데이터를 저장한다.

1. 유니티 에디터(LocalizedTextEditor)를 통하여 csv 파일을 json 형태의 txt 파일로 저장한다.

2. LocalizationManager에서 로컬라이제이션 json(txt) 데이터를 불러온다.

3. 텍스트 컴포넌트에 부착된 LocalizedText 컴포넌트가 키값에 해당하는 데이터를 불러와서 텍스트 컴포넌트의 텍스트를 바꿔준다.

4. 인게임에서 바뀐 텍스트가 정상적으로 출력된다.



3. 로컬라이제이션 스크립트 핵심 부분 설명

아래 4개의 스크립트에서 핵심적인 부분만 설명을 하겠다.

사용 방법은 4번 샘플 프로젝트에서 설명하기 때문에 스크립트에 대한 이해가 필요 없어도 괜찮으니 넘어가도 좋다.

 

 

LocalizedTextEditor 

 

유니티 GUI 에디터에 대한 부분은 생략하도록 하겠다. (사실 잘 모르기도 함)

 

private void LoadCSVFile()
    {
        string filePath = EditorUtility.OpenFilePanel ("Select localization data file", Application.dataPath, "csv");
        
        if (!string.IsNullOrEmpty (filePath)) 
        {
            string dataAsJson = File.ReadAllText (filePath, Encoding.UTF8);
            string[] stringBigList = dataAsJson.Split('\n');
            localizationData = new LocalizationData();
            localizationData.items = new LocalizationItem[stringBigList.Length];
            for (var i = 1; i < stringBigList.Length; i++)
            {
                string[] stringList = stringBigList[i].Split(',');
                for (var j = 0; j < stringList.Length; j++)
                {
                    localizationData.items[i - 1] = new LocalizationItem(stringList[1], stringList[2]);
                }
            }
        }
    }

csv파일을 불러오는 함수다.

윈도 창에서 Application.dataPath 경로에 해당하는 부분으로 열어준 다음에 csv 파일 확장자를 불러온다.

만약에 파일이 공백이 아니라면 UTF8로 불러와서 \n 개행 문자열로 스플릿 해서 string 배열로 가져온다.

하지만 문제점이 있는데 이상하게 csv 파일에는 항상 마지막 부분에 개행문자가 들어가도록 되어있는지 추가적으로 공백 데이터가 인식돼버린다. 조금 거슬려서 처리해보려고 노력했으나 쉽지 않아서 나중에 예외처리가 가능하니까 내버려두었다.

읽어낸 데이터를 다시 쉼표(,)로 스플릿 하여 키와 밸류로 분리한 뒤 Serializable 클래스 LocalizationData의 Items에 LocalizationItem으로 삽입을 한다.

stringList에서 0열은 인덱스라서 사용하지 않는다.

 

private void LoadGameData()
    {
        string filePath = EditorUtility.OpenFilePanel ("Select localization data file", Application.dataPath, "txt");

        if (!string.IsNullOrEmpty (filePath)) 
        {
            string dataAsJson = File.ReadAllText (filePath);

            localizationData = JsonUtility.FromJson<LocalizationData> (dataAsJson);
        }
    }

    private void SaveGameData()
    {
        string filePath = EditorUtility.SaveFilePanel ("Save localization data file", Application.dataPath, "", "txt");

        if (!string.IsNullOrEmpty(filePath))
        {
            string dataAsJson = JsonUtility.ToJson(localizationData);
            File.WriteAllText (filePath, dataAsJson);
        }
    }

SaveGameData는 csv파일로 데이터를 불러오면 해당 데이터를 json 형태로 txt 확장자로 저장을 한다. (json 파일을 읽어 올 수 있다면 json으로 저장해도 되지만 Resources.Load<TextAsset>("Texts/"+fileName);로 텍스트 데이터를 불러올 때 json으로 잘 안 불러지길래 txt로 저장하였다.)

LoadGameData는 저장된 json 형태의 txt 파일을 조회한다.

 

 

LocalizationData 

텍스트 데이터를 들고 다니기 쉽도록 포장처리하는 데이터 형태라고 보면 된다.

 

[System.Serializable]
public class LocalizationData 
{
    public LocalizationItem[] items;
}

[System.Serializable]
public class LocalizationItem
{
    public LocalizationItem(string key, string value)
    {
        this.key = key;
        this.value = value;
    }
    public string key;
    public string value;
}

너무 간단하다.

json으로 데이터 읽고 쓰기 가능하도록 Serializable 선언

하나의 LocalizationItem의 배열 형태 items에 현재 언어에 해당하는 LocalizationItem 데이터를 전부 다 넣고 나중에 key로 조회를 한다.

 


LocalizationManager 

로컬라이제이션을 총괄하는 스크립트다.

게임의 곳곳에서 참조를 해야 하기 때문에 싱글턴 패턴으로 선언한다. 싱글턴 패턴 설명에 대해서는 생략한다.

 

missingTextString는 키값이 없어서 불러오지 못한 텍스트에 대한 기본값이다.

fileName은 불러와야 하는 파일명으로 eng.txt면 eng를 넣으면 된다. 자세한 설명은 나중에 하겠다.

 

public void LoadLocalizedText(string fileName)
    {
        localizedText = new Dictionary<string, string> ();
        TextAsset mytxtData=  Resources.Load<TextAsset>("Texts/"+fileName);
        //파일 정상적으로 읽어오는지 확인
//        Debug.Log(Resources.Load<TextAsset>("Texts/"+fileName));
//        Debug.Log(fileName);
//        Debug.Log(mytxtData);
        string txt=mytxtData.text;
        if (txt!="" && txt!= null) {
            string dataAsJson = txt;
            LocalizationData loadedData = JsonUtility.FromJson<LocalizationData> (dataAsJson);

            for (int i = 0; i < loadedData.items.Length; i++)
            {
                //불러오는 데이터 확인
                //Debug.Log(loadedData.items[i].key + ":" + loadedData.items[i].value);
                //공백데이터가 여러개 들어가면 오류발생
                if (!localizedText.ContainsKey(loadedData.items[i].key))
                    localizedText.Add(loadedData.items[i].key, loadedData.items[i].value);
            }

            Debug.Log ("Data loaded, dictionary contains: " + localizedText.Count + " entries");
        } else 
        {
            Debug.LogError ("Cannot find file!");
        }

        isReady = true;
    }

Debug.Log는 참고용으로 주석 처리해놨는데 필요시 주석 해제하면 무엇이 문제인지 확인이 쉽다.

해당 함수는 fileName에 해당하는 즉 언어에 해당하는 txt 파일을 불러와서 딕셔너리 형태의 localizedText에 언제든 꺼내 쓸 수 있도록 전부 다 넣어준다.

여기서 아까 csv파일의 특성으로 인해 생기는 공백 데이터가 문제를 일으키는데 지금 넣어놓은 

if (!localizedText.ContainsKey(loadedData.items[i].key)) 이 부분으로 예외처리를 해서 문제는 발생하지 않을 것이다.

 

만약 저렇게 예외처리를 하지 않는다면 공백 데이터가 2개 들어가 버려서 txt 저장할 때는 문제없는데 게임 실행하면 저렇게 오류가 발생한다.

 

public string GetLocalizedValue(string key)
    {
        string result = missingTextString;
        if (localizedText.ContainsKey (key)) 
        {
            result = localizedText [key].Replace("\\n","\n");
        }

        return result;

    }

나중에 텍스트 컴포넌트에 부착된 LocalizedText에서 key로 value값을 조회할 때 사용된다. \n 치환은 개인적으로 저 부분이 자꾸 문제를 일으켜서 넣어놨는데 없애도 괜찮다. (왜있는거지 ?? 나란 괴물...)

 

public void ReloadTexts(string fileName)
    {
        LoadLocalizedText(fileName);
        foreach (LocalizedText text in FindObjectsOfType<LocalizedText>())
        {
            text.ReloadText();
        }
    }

나중에 버튼과 연동시켜서 언어를 바꿀 때 쓰는 기능이다.

유의할 점은 FindObjects 함수는 게임 오브젝트가 enabled 된 컴포넌트만 가져오기 때문에 꺼져있는 텍스트는 바뀌지 않는다.

저렇게 Reload를 한다고 전부 다 바뀌는 것이 아님을 알아둬야 한다.

컴포넌트에 OnEnable을 사용하여 별도로 새로고침 하는 과정이 필요하다.

 


LocalizedText 

Text 및 TextMeshPro에 붙어서 로컬라이제이션 값을 가져와 교체해주는 컴포넌트

using System.Collections;
using System.Collections.Generic;
using TMPro;
using UnityEngine;
using UnityEngine.UI;

public class LocalizedText : MonoBehaviour {

    public string key;
    private Text Text;
    private TextMeshProUGUI TextPro;
    // Use this for initialization
    void Start () 
    {
        
    }

    private void OnEnable()
    {
        Text = GetComponent<Text> ();
        TextPro = GetComponent<TextMeshProUGUI> ();
        ReloadText();
    }

    public void ReloadText()
    {
        if (Text != null)
        {
            Text.text = LocalizationManager.Instance.GetLocalizedValue (key);
        }
        if (TextPro != null)
        {
            TextPro.text = LocalizationManager.Instance.GetLocalizedValue (key);
        }
    }
}

아까 말한 대로 OnEnable을 통하여 활성화되었을 때 새로고침을 하도록 한다.

ReloadText 함수는 텍스트 컴포넌트가 존재하면 로컬라이제이션 매니저에다가 값을 질의하여 텍스트를 교체해준다.

 

여기까지 핵심 스크립트에 대한 설명은 끝났다. 자세한 사용 방법을 알아보도록 하자.



4. 샘플 프로젝트 및 참고 링크

먼저 영상을 보고 무슨 샘플 프로젝트인지 보자. 음식 이름에 대해서 고찰해보는 프로젝트다.

 

 

한글
영어

먼저 csv 파일을 2개 준비한다.

각각 영어 데이터와 한글 데이터를 넣어둔다.

key값은 동일해야 한다.

 

Window > Localized Text Editor > LoadCSVFile

아까 만들었던 csv 파일을 불러온다.

 

한글 데이터부터 먼저 가져온다.

 

정상적으로 가져온 것 같지만 2가지 문제가 있다.

한글 데이터 인코딩 문제와 4, 5번 엘리먼트에는 공백이 담겨있다.

공백 데이터에 대해서는 우리는 예외처리를 했으니까 별 상관은 없지만 엘리먼트 숫자를 줄여서 없애도 되고 마음대로 하자

 

인코딩 문제는 다음과 같이 불러온 데이터가 깨져서 나온다.

웹 쪽 일을 조금 해봤다면 이럴 때 어떻게 해야 하는지 알 것이다.

그렇다 있는 척 좀 해봤다.

C# 내에서 인코딩 건드려서 불러와도 되고 나는 메모장에서 인코딩을 utf-8로 바꿨다.

 

요렇게 해서 덮어쓰기로 저장한다.

 

그럼 이제 잘 나온다.

 

그러면 Save data를 눌러서 위의 경로에 맞추어 저장하도록 하자

파일명은 kor eng로 하겠다.

 

영어도 똑같이 한다.

 

그리고 텍스트 컴포넌트가 붙어있는 텍스트 오브젝트에 Localized Text의 Key에 교체되길 원하는 key값을 적어 넣는다. 우리의 경우 food1이나 food2다.

 

Localization Manager 오브젝트도 넣고 인스펙터의 File Name에는 최초로 활성화하고 싶은 언어 파일명을 넣는다.

 

한글 버튼에다가 다음과 같이 온 클릭 이벤트를 걸어놓는다.

영어 버튼에다가는 eng를 넣으면 되겠다.

 

요런 식으로 사용하면 된다.

이하 참고 링크와 샘플 프로젝트를 올리며 마무리하겠다. (ㅃㅇ~)

사실 해당 소스는 유니티에서 친절하게 제공하는 튜토리얼을 보고 따라 만든 것이다.

아래 링크를 보면 자세하게 설명이 나와있다.

https://unity3d.com/kr/learn/tutorials/topics/scripting/localization-manager

 

김치와 불고기의 로컬라이제이션에 대하여 고찰하는 샘플 프로젝트다.

localization sample.zip
0.52MB

반응형
반응형

목차

1. 개요

2. 프로토타입 게임

3. 핵심 소스코드 및 설명

4. 유니티 세팅 방법

 

1. 개요

게임 중에 음성을 인식하여 진행하는 방식의 하이퍼 캐주얼 게임이 있다.

주목할만한 점은 게임성만을 놓고 봤을때 그렇게 중독성이 있거나 하지는 않지만 유튜버들이 이목을 끌기 좋은 콘텐츠라서 유튜버들이 올리면 사람들이 많이 본다.

인플루언서들(influncer) 마케팅을 통하여 해당 게임이 성장하였다고 추측된다.

 

유튜버들이 해당 게임을 플레이하는 영상 시청률이 적지 않다.
음성인식 게임, 1000만 다운로드 이상에 반응도 매우 좋다.

 

이번 글에서는 음성인식 게임의 핵심 부분을 다뤄보고자 한다.

 

2. 프로토타입 게임

 

음성인식 프로토타입 게임

캐릭터는 계속해서 이동하고 소리를 내면(어디 아픈 사람 같지만 아니다.) 내 공의 색이 바뀐다. 공의 색이 바뀐동안 상대방 공을 때린다고 생각하면 된다.

소리를 내서 펀치나 발차기를 하는 게임이라고 상상하자

 

음성인식 프로토타입 게임 2

엄마는 게임기를 숨겼다. 느낌의 게임이라고 생각하면 된다.

소리를 내서 불량배들을 쫓아내는 게임이다.

 

3. 핵심 소스코드 및 설명

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Audio;
using UnityEngine.UI;

public class MicrophoneListener : MonoBehaviour
{
    [SerializeField] private Image _imageSound;
    [SerializeField] private Text TextVol;
    public float sensitivity = 100;
    public float loudness = 0;
    public float pitch = 0;
    AudioSource _audio;

    public float RmsValue;
    public float DbValue;
    public float PitchValue;
 
    private const int QSamples = 1024;
    private const float RefValue = 0.1f;
    private const float Threshold = 0.02f;
 
    float[] _samples;
    private float[] _spectrum;
    private float _fSample;

   //Written in part by Benjamin Outram
 
     //option to toggle the microphone listenter on startup or not
     public bool startMicOnStartup = true;
 
     //allows start and stop of listener at run time within the unity editor
     public bool stopMicrophoneListener = false;
     public bool startMicrophoneListener = false;
 
     private bool microphoneListenerOn = false;
 
     //public to allow temporary listening over the speakers if you want of the mic output
     //but internally it toggles the output sound to the speakers of the audiosource depending
     //on if the microphone listener is on or off
     public bool disableOutputSound = false; 
 
     //an audio source also attached to the same object as this script is
     AudioSource src;
 
     //make an audio mixer from the "create" menu, then drag it into the public field on this script.
     //double click the audio mixer and next to the "groups" section, click the "+" icon to add a 
     //child to the master group, rename it to "microphone".  Then in the audio source, in the "output" option, 
     //select this child of the master you have just created.
     //go back to the audiomixer inspector window, and click the "microphone" you just created, then in the 
     //inspector window, right click "Volume" and select "Expose Volume (of Microphone)" to script,
     //then back in the audiomixer window, in the corner click "Exposed Parameters", click on the "MyExposedParameter"
     //and rename it to "Volume"
     public AudioMixer masterMixer;
 
 
     float timeSinceRestart = 0;
 
     void Start() {
         //start the microphone listener
         if (startMicOnStartup) {
             RestartMicrophoneListener ();
             StartMicrophoneListener ();
             
             _audio = GetComponent<AudioSource> ();
             _audio.clip = Microphone.Start (null, true, 10, 44100);
             _audio.loop = true;
             while (!(Microphone.GetPosition(null) > 0))  {}
             _audio.Play();
             _samples = new float[QSamples];
             _spectrum = new float[QSamples];
             _fSample = AudioSettings.outputSampleRate;
             //유니티 5.x 부터는 audio source에서 mute를 하면 정상적으로 음성이 안나온다.
             //audio mixer에서 master volume의 db를 -80으로 하여 소리 출력만 안되도록 하면 된다.
             //_audio.mute = true;
         }
     }
 
     void Update(){    
         //can use these variables that appear in the inspector, or can call the public functions directly from other scripts
         if (stopMicrophoneListener) {
             StopMicrophoneListener ();
         }
         if (startMicrophoneListener) {
             StartMicrophoneListener ();
         }
         //reset paramters to false because only want to execute once
         stopMicrophoneListener = false;
         startMicrophoneListener = false;
 
         //must run in update otherwise it doesnt seem to work
         MicrophoneIntoAudioSource (microphoneListenerOn);
 
         //can choose to unmute sound from inspector if desired
         DisableSound (!disableOutputSound);
 
         loudness = GetAveragedVolume() * sensitivity;
         GetPitch();
         
         //아래는 커스텀 한 소스
         //소리가 
         if (loudness > 5f)
             _imageSound.fillAmount = 1f;
         else
         {
             _imageSound.fillAmount = 0.65f;             
         }

         FindObjectOfType<GameManager>().currentLoud = loudness;
         //TextVol.text = "vol:" + loudness;
     }
 

    float GetAveragedVolume() {
        float[] data = new float[256];
        float a = 0;
        _audio.GetOutputData (data, 0);
        foreach(float s in data) 
        {
            a+=Mathf.Abs(s);
        }
        return a/256;
    }

    void GetPitch() {
        GetComponent<AudioSource>().GetOutputData(_samples, 0); // fill array with samples
        int i;
        float sum = 0;
        for (i = 0; i < QSamples; i++)
        {
            sum += _samples[i] * _samples[i]; // sum squared samples
        }
        RmsValue = Mathf.Sqrt(sum / QSamples); // rms = square root of average
        DbValue = 20 * Mathf.Log10(RmsValue / RefValue); // calculate dB
        if (DbValue < -160) DbValue = -160; // clamp it to -160dB min
        // get sound spectrum
        GetComponent<AudioSource>().GetSpectrumData(_spectrum, 0, FFTWindow.BlackmanHarris);
        float maxV = 0;
        var maxN = 0;
        for (i = 0; i < QSamples; i++)
        { // find max 
            if (!(_spectrum[i] > maxV) || !(_spectrum[i] > Threshold))
                continue;
            maxV = _spectrum[i];
            maxN = i; // maxN is the index of max
        }
        float freqN = maxN; // pass the index to a float variable
        if (maxN > 0 && maxN < QSamples - 1)
        { // interpolate index using neighbours
            var dL = _spectrum[maxN - 1] / _spectrum[maxN];
            var dR = _spectrum[maxN + 1] / _spectrum[maxN];
            freqN += 0.5f * (dR * dR - dL * dL);
        }
        PitchValue = freqN * (_fSample / 2) / QSamples; // convert index to frequency
    }
 
     //stops everything and returns audioclip to null
     public void StopMicrophoneListener(){
         //stop the microphone listener
         microphoneListenerOn = false;
         //reenable the master sound in mixer
         disableOutputSound = false;
         //remove mic from audiosource clip
         src.Stop ();
         src.clip = null;
 
         Microphone.End (null);
     }
 
 
     public void StartMicrophoneListener(){
         //start the microphone listener
         microphoneListenerOn = true;
         //disable sound output (dont want to hear mic input on the output!)
         disableOutputSound = true;
         //reset the audiosource
         RestartMicrophoneListener ();
     }
     
     
     //controls whether the volume is on or off, use "off" for mic input (dont want to hear your own voice input!) 
     //and "on" for music input
     public void DisableSound(bool SoundOn){
         
         float volume = 0;
         
         if (SoundOn) {
             volume = 0.0f;
         } else {
             volume = -80.0f;
         }
         
         masterMixer.SetFloat ("MasterVolume", volume);
     }
 
 
 
     // restart microphone removes the clip from the audiosource
     public void RestartMicrophoneListener(){
 
         src = GetComponent<AudioSource>();
         
         //remove any soundfile in the audiosource
         src.clip = null;
 
         timeSinceRestart = Time.time;
 
     }
 
     //puts the mic into the audiosource
     void MicrophoneIntoAudioSource (bool MicrophoneListenerOn){
 
         if(MicrophoneListenerOn){
             //pause a little before setting clip to avoid lag and bugginess
             if (Time.time - timeSinceRestart > 0.5f && !Microphone.IsRecording (null)) {
                 src.clip = Microphone.Start (null, true, 10, 44100);
                 
                 //wait until microphone position is found (?)
                 while (!(Microphone.GetPosition (null) > 0)) {
                 }
                 
                 src.Play (); // Play the audio source
             }
         }
     }}

여기 업데이트 부분을 보면 loudness가 소리 크기를 받아오는 부분이고 GetPitch 함수를 통해 PitchValue에 소리의 높낮이 값을 저장한다.

 

4. 유니티 세팅 방법

위의 소스코드만 넣는다고 해서 바로 적용이 되지는 않는다.

인터넷에 돌아다니는 소스 코드를 적용해보았더니 내가 내는 소리가 출력이 되어야지 작동을 했었다.

(audio source에 mute를 넣으면 작동을 안함)

 

프로젝트 우클릭 > Create > Audio Mixer 생성

 

생성된 Audio Mixer 더블클릭 > Groups 새로 생성

 

새로 생성한 그룹의 이름을 microphone이라고 지정

 

새로 생성한 그룹 클릭 > 우측에 Volume이 보이는데 우클릭

 

첫 번째 메뉴 선택

 

우측 상단에 Exposed Parameters 생성됨 > 클릭

 

이미 이름이 지정되어 있는데 Volume으로 이름 변경

 

Master 그룹의 dB를 -80으로 변경

 

유니티 인스펙터에서 MicrophoneListener 컴포넌트에 다음과 같이 등록

반응형
반응형

공을 쏴서 장애물로 바뀌고 장애물을 제거하면서 버텨나가는 하이퍼 캐쥬얼 장르의 게임을 출시하였는데 구현하는데 생각보다  어려웠고 구현하기 힘들었던 부분들을 정리하고자 한다.

어째서인지 국내 안드로이드 마켓에서는 검색이 되지 않는다...

 

 

https://play.google.com/store/apps/details?id=com.percent.jjtan

불러오는 중입니다...

https://itunes.apple.com/app/id1459980112

불러오는 중입니다...

 

목차는 아래와 같다.

JJTAN 개발 리뷰 시리즈

1. 프로토타입 수준의 오브젝트 및 환경 설정
2. 공 발사하는 캐논 설정
3. 인풋 설정, 공 발사
4. 장애물 생성
5. 장애물 적합한 사이즈 찾기
6. 장애물 색깔 그라데이션
7. 위험한 상황 줌인
8. 사망 판정
9. 데이터 저장과 이어하기
10. 한번 더 하기

추가 개발
튕기는 반사각 계산

 

원래 게임의 주인공이었으나 밀려난 비운의 주인공...

반응형
반응형

Problem

Attched collider 2d script, physical object loose bouncy after few bounce and slide along surface.

It would be easy to understand if you have a look at the video below.

 

 

reproduce project setting

 

 

 

reproduce project setting 

Test project looks like above, has 4 2d box collider and a circle collider which bounces inside a box.

 

 

 

attched script of a circle

The script just give a force on start of the project.

 

 

 

 

Physics Material 2D applied to walls and a circle.

This Physics Material 2D make the circle loose it's force gradually.

 

 

Cause

Theres is a setting in Unity Physics 2D which consider collision to inelastic collision, called velocity threshold.

 

 

 

 

 

Edit > Project Settings > Pysics (2D) > Velocity Threshold

When a collision has lower velocity than the threshold preset. It is considered as an inelastic collision.

 

 

 

Solution

Lower the Velocity Threshold value.

 

 

Result

The circle bounces well though has slow velocity

 

반응형
반응형

개요

 

2019년 4월 30일 ~ 5월 4일 필리핀의 세부, 막탄에 위치한 오션 플레이어라는 숙소를 예약하고 스쿠버 다이빙 교육을 받으며 수집한 정보, 느낀 점들을 간단하게 적어본다.

 

본인의 성향은 매우 느긋하고 위생관념에 관대하기 때문에 실제 방문 시 본인의 주관과 다를 수 있다.

 

정보성향의 글은 글씨 색을 파란색으로 하겠다.

 

 

목차

 

1. 세부 도착, 오션 플레이어 숙소 도착

 

2. 숙소 환경

 

3. 필요한 마음가짐

 

4. 주변에서 할 것

 

5. 술

 

6. 스쿠버 다이빙 교육

 

7. 출국

 

8. 요약

 

 

1. 세부 도착, 오션플레이어 숙소 도착

 

에어아시아 항공 전화번호 : 에어아시아 대한민국 콜센터 0504-0920-0525

전화하고 나면 필리핀 국가 관련 문의는 아예 따로 나뉘어있어서 따로 번호 눌러줘야 함

 

비행기는 4시간 정도 걸렸고 비행기 안에서 물도 사 먹어야 하니까 챙겨가자.

비행기가 엄청 흔들리고 기름 냄새도 나긴 했다.

아래와 같이 항공편을 예약하여 세부에 도착하니 새벽 1시였다.

 

일단 매우 더웠다. 어차피 공항에서 나오고 나면 엄청 덥고 습하니까 비행기에서 내리자마자 옷을 반팔, 반바지 이런 것으로 갈아입자.

 

5월 경의 필리핀은 30도, 매우 습하다.

 

 

공항에서 나오고 조금 앞으로 걸어가 보면 앞에서 유심칩을 판다. 본인이 머무르는 날짜에 맞춰서 구매해서 사용하자

 

속도는 유튜브 정도는 시청 가능하였고 나는 5일 사용한다고 하니까 하루에 3기가 데이터로  정확한 가격은 기억 안 나지만 8달러(415페소) 한다. 쓸만하니까 사용하자. 사람들 몰려나와서 정신없어서 빨리 가서 구매하는 게 편할 것 같다. 정신없어서 돈 제대로 안 받아내면 돈 뜯기니까 알아서 잘 계산하자.

 

거기서 구매한 유심칩 구매시 받는 메시지

 

그러고 나서 오션 플레이어 측에서 공항으로 일행을 데리러 왔다.

아래와 같이 오션 플레이어 숙소로 이동하였다.

 

오션 플레이어에서 다이빙 일정을 계획할 것이라면 무조건 숙소는 오션 플레이어 본관 숙소를 잡도록 하자.

삶이 편해진다. (본관에서 식사, 본관에서 모여서 수업, 본관에서 출발, 본관에서 장비 정비... )

본관 숙소 중에서도 3층이 매우 좋다.(이유는 나중에 나옴)

 

 

환율은 아래와 같다.

1페소 -> 22원

1달러 -> 50페소라고 생각하면 좋다.

수강료는 달러로 지불해도 괜찮고 숙소에서 달러를 페소로 환전해주는데 환율을 좋게 쳐주니까 50~100달러 정도 페소로 바꿨더니 4박 5일 정도 머무르는데 돈은 충분했다.

 

2. 숙소 환경

 

숙소 환경은 덥고 습한 것 빼고는 대체로 만족스러웠다. 3층이었다.

 

3층에서 바닷가를 본 사진
해변가로 나갔을때 사진 (지금 내가 있는 해변가가 재밌는 곳이다. 나중에 이야기가 나온다.)
해변에 놀러온 사람들
머물렀던 방 외부
방 내부(4인실)
화장실 겸용 샤워실
오전에 바닷가 들어갔다가 이렇게 의자 놓고 쉬면 짱이다. 3층이라면 꼭 해보자
밤에는 도마뱀이 빛을 향해 몰려든다. 얘네 시선 의식하니까 지나가면서 쳐다보지말자
식당

 

오전 8시, 오후 12시 30분, 오후 6시 식사시간이다.

 

저녁에 숙소 식당에서 식사를 하게 되면 칠판에 이름을 적거나 강사님께 말씀을 드리자

 

어차피 숙소 인근에는 맛있는 거 없으니까 그냥 무난한 숙소 밥 먹자. 배고프면 맛있다.

 

밤에 10시~12시까지 한국인 사람들이 모여서 술을 마신다. 모르는 사람들도 호의적으로 껴주면서 같이 술 마시니까 기회가 되면 가보자 재밌는 사람을 많이 만났다.

 

밤에찍은 사진

 

그 외에

 

- 비데 없음

 

- 모기 거의 없음(나는 안 물림)

 

- 저녁에는 시원한 바람이 솔솔 불었음

 

3. 필요한 마음가짐

 

모든 여행이 다 그렇겠지만 이 곳에서 본인의 마음가짐과 행동에 따라 여행이 더 재미있어질 수 있다.

이곳 필리핀에서는 나의 적극적이고 이것저것 해보려는 시도가 많은 즐거움을 가져다주었다.

특히 인근의 필리핀 사람들이 한국인한테 친절해서 영어로 의사소통 시 즐거운 대화가 가능하다.

몇 가지 일화를 적어보겠다.

 

- 오션 플레이어 밖의 해변가를 걷다 보니 현지인 여자 두 분이 나보고 잘생겼다고 같이 사진 찍어 달라고 하였음

 

- 오션 플레이어 밖의 해변가에서 사람들이 고기 구워 먹는데 나보고 먹어보라고 하고 술도 나눠줌

 

- 오션 플레이어 밖의 해변가에서 밤에 술 마시는데 나한테 포도주 나눠줌

 

- private 해수욕장 아저씨가 과자 나눠주면서 이것저것 얘기함

 

- 많은 사람들이 눈 마주치면 먼저 인사해줌, 한국어로도 인사 많이 해줌

 

- 외국인 아저씨랑 좀 친해져서 밥 먹여줌

 

가능하면 현지인들 술집 가서 술도 마셔보고 밖에서 술 마실 때 술 들고 가서 같이 마셔보면서 놀아보고 싶었는데 하지는 못했고 다음에 또 오게 된다면 해보고 싶다.

 

새벽에 오션플레이어에서 세븐일레븐 이동하기 위해서 밖에서 대기중이던 오토바이 운전하시는 분과 이야기

현지인 아저씨 분께 이것저것 물어봤는데 오션 플레이어라는 비즈니스가 지역 경제 상권에 많은 도움을 줬기 때문에 한국인들을 좋게 생각하고 고마워한다고 말씀하셨다.

 

 

4. 주변에서 할 것

 

오션 플레이어 본관 숙소 바로 옆의 가게

 

사진에서 처럼 가게가 문을 닫은 것 같지만 오른쪽에 불이 켜져 있는데 거기 사람이 있으면 아직 장사를 한다.

아주머니가 9시에 문을 닫는다고 했다.

비위생적이라서 red horse 맥주만 사 먹어봤다. 병당 110 페소(대략 2300원) 병 겁나 큼!

 

숙소 주변 지도

 

1 : 숙소 위치

2 : 가장 가까운 편의점 세븐일레븐 (툭툭이 타고 15분, 50페소)

3 : 큰 마트 그랜드 몰 (툭툭이 타고 25분, 70~100페소)

4 : 마사지 샵, 음식점, 가게 모든 것이 다 있다고 한다. 가보지는 않음 (툭툭이 타고 25분, 100페소)

 

툭툭이(자주 타게 되는 교통수단)

 

여기 오신 분들은 분명 마사지 샵을 갈 것인데

일행과 버블 스파를 갔는데 픽업 서비스가 있긴 하지만 마사지해주시는 분들이 전문성이 부족해서 그렇게 좋지는 않았다. 별로 안시원 방문하더라도 마사지 강도 세게 해 달라고 해야 함(비추천)

1시간 대략 500~700 페소(팁 미포함)

 

가격표

 

 

아래는 가보지 못했지만 가격대는 보통 마사지 샵과 비슷하지만 좋다고 한 곳

https://goo.gl/maps/FU7aTSXfpk4f3rHz7

 

Retreat Spa Maribago Branch

★★★★★ · 데이 스파 · 2FL Jinju Square, M.L Quezon Highway, Maribago

www.google.com

 

 

아래는 가격 엄청 비싸지만 가격 값은 한다고 한 곳(시간당 4~6만 원 꼴)

https://goo.gl/maps/NBC7LuicaLowSR979

 

골드문스파

★★★★☆ · 스파 · Agus Gamay

www.google.com

 

5. 술, 과자

술이 빠질 수가 없다.

 

맥주 레드홀스 국민 술이다. 가성비 최고다
GINEBRA 진 양주인데 가성비 최고
위의 탄산 음료와 숙소 본관에서 얼음 받아서 컵에다가 먹으면 최고

 

술은 저렇게 맥주와 양주를 음료수와 섞어마시면 가성비가 너무 좋다.

400페소도 안되는데 술을 한가득 사서 마실 수 있다.

숙소 본관 10시~12시에 가서 외부 일행들과 같이 마시면 스쿠버 다이빙에 대한 정보와 재밌는 이야깃거리들과 함께 더 즐겁게 마실 수 있다.

 

 

본관에서 팔고 필리핀 곳곳에서 파는 바나나 과자 맛있음
현지인이 추천해준 매운 새우깡 맛 나는 과자(더 짜고 매움)
현지인이 추천해준 과자 맛은 잘 기억 안나는데 맛있었다.

 

 

6. 스쿠버 다이빙 교육

 

사실 이게 포스팅의 주목적인데 왜 6번으로 내려가 있는지 모르겠다. 마음속의 우선순위에서 밀려났나 보다.

가르쳐주신 선생님은 명쌤(채명록)이었다.

너무 FM대로 가르쳐주셔서 수업받는도중 '와 이렇게 제대로 가르쳐주는 곳이 세상에 또 있을까 싶었다'

 

PADI 자격증 안내도

 

PADI라는 세계에서 인정받을 수 있는 스쿠버 다이빙 협회가 있는데 PADI 협회에서 지정한 수업을 이수하면 자격증을 취득할 수 있다.

취득한 자격증을 통하여 세계 곳곳에서 스쿠버 다이빙이 가능하도록 해준다.(자격증 없으면 스쿠버 다이빙 업체 측에서 허락 안 해줌)

왼쪽 박스들은 큰 범주에 속하는 자격증이라고 생각하면 된다.

오른쪽 박스들은 왼쪽 자격증 취득 후 추가적인 교육을 통하여 취득이 가능한 전문 자격증이라고 보면 된다.(스페셜티라고 부른다)

 

 

위 사진에서 빨간색 박스를 쳐놓은 부분은 내가 취득한 자격증이다.

Open water diver와 Enriched air diver인데

Open water diver는 공개된 바다(진짜 바다)에서 훈련을 통하여 스쿠버 다이빙의 기본적인 부분을 배운다는 의미에서 Open water diver라는 자격증명을 가지고 있다.

Enriched air diver는 Open water diver 자격증이 있으면 취득 가능한 공기통의 질소 비율 조정에 관하여 배우는 스페셜티이다.

 

Open water diver 교재

 


Open Water Diver 수업은 3일에 걸쳐 진행된다.

 

1일 차

 

- 아침식사(08:00 ~ 09:00)

아침은 8시부터다. 아침을 든든하게 먹어야 한다.

 

- 이론수업(09:00 ~ 12:00)

숙소 내부에서 Open water diver의 5장 내용 중 3장 분량의 내용을 영상으로 공부한다.

그리고 중요한 부분을 선생님이 추가 설명을 해주신다.

만일 완전히 못 알아들었다고 하더라도 계속해서 실전 수업과 설명을 통해서 알려주신다.

 

- 수영장에서 실전 연습(13:00 ~ 17:00)

숙소 내부에 있는 수영장에서 이론 수업에서 들었던 장비에 대해서 설명을 듣고 실제로 사용해본다.

 

수업을 받고 있는 모습

수업 도중 화장실을 가면 장비 벗고 슈트 벗고 하느라 시간이 많이 걸리니까 화장실을 미리 갔다 와야 한다. (도중에 참느라 죽는 줄 알았다.)

훈련 끝나면 다들 힘들어서 일찍 잠들었다.

나는 12시까지 사람들하고 놀다가 잤다.

 

2일 차

 

- 바다에서 실전 연습(9:00 ~ 12:30)

오전에 슈트를 전부다 입고 나갈 준비를 한다.

명쌤에게 간단한 브리핑을 받고 출발해서 2번의 다이빙을 했다.

다이빙을 해서 전날 수영장에서 배웠던 부분을 다시 한번 더 복습하는 과정을 하였다.

그리고 수영장에서는 연습하기 힘들었던 하강, 호버링 수중 기술을 연습하였다.

근데 이날 전날 술을 좀 마셔서 배가 아팠는데 지옥을 맛보니까 이 부분도 염두에 두자...

 

- 이론수업, 시험 (14:00 ~ 17:00)

남은 이론 수업 분량 4장~5장까지 마저 이론 수업을 하고 간단한 시험을 봤다.

간단하다고는 하나 대략 100문제 정도는 풀어야 해서 한두 시간 정도 걸린다.

 

- 로그 작성, 피드백 (17:00~)

작성한 로그

다이빙을 하고 나면 항상 다이빙을 했던 내용에 대해서 로그를 작성해야 한다.

추후 본인의 다이빙 내역을 보여줘서 본인의 수준과 필요 장비에 대해서 쉽게 전달하기 위해서 라고 한다.

영어로 작성하면 좋을 듯 하나 일단 단어를 몰라서 못썼다.

선생님께서 다이빙하면서 수행했던 내용을 요약정리해주시고 피드백도 해주었다.(Open water diver 강의하면서 작성한 로그 내용은 참고용으로 아래에다가 올리도록 하겠다.)

 

3일 차

 

- 바다에서 실전 연습, 펀 다이브 (10:00 ~ 12:30)

어제 연습했던 내용들을 이번에는 선생님께서 거의 도와주지 않는 수준에서 다시 한번 더 연습을 하였다. 돌발상황이 여러 번 발생하였으나 다들 침착하게 대처하였다.

그러고 나서 두 번째 다이빙은 동료와 같이 펀 다이빙이 어떤 느낌인지 알려주는 다이빙 시간을 가졌다.

 

- 로그 작성, 동영상 피드백, 마무리 (14:00 ~ )

마찬가지로 로그 작성과 이번에는 선생님께서 우리의 모습을 동영상으로 남겨서 피드백을 해주시고 수업을 마무리하였다. 아쉽...

 

Open water diver를 하면서 작성한 로그(글씨 ㅈㅅ;;)

 

7. 출국

 

마지막으로 출국을 하는 날이다.

스쿠버 다이빙을 하고 나면 18시간 정도는 휴식을 취해야 비행기를 탈 수 있기 때문에 아침에 일어나서 주로 쉬는 시간을 가졌다.

본인은 17:30분에 이륙하는 비행기였는데 숙소에서 셔틀버스로 공항까지 데려다주었고 15:00에 출발하여도 비행기를 타는 데에는 충분하였다.

 

세부 공항

나의 캐리어는 7.7kg이었는데 용량 초과로 1500페소를 지불하였다.

미리 온라인으로 수하물을 예약하면 가격과 기내식도 싼 가격으로 이용이 가능하다고 하다.

실제로 일행은 1500페소 안 되는 돈으로 나보다 훨씬 무거운 짐을 부쳤고 기내식까지 이용하였다.

또한 비행기를 타기 전에 850페소를 추가적으로 지불해야 하니 참고하자.

 

8. 요약

스쿠버 다이빙을 중심적으로 한 여행이라서 그런지 처음에는 매우 힘들었지만 시간이 갈수록 귀국하지 말고 남아서 더 하고 싶어 질 정도로 재미있는 여행이었다.

친한 친구들하고 오면 얼마나 재미있었을까 상상이 되지 않는다.

전반적으로 친절했던 필리핀 현지 사람들, 조금 힘들지만 정석으로 알려주는 스쿠버 다이빙 레슨, 대체로 만족할만한 오션 플레이어 서비스

새로운 것을 하고 싶은 사람에게 강력히 추천한다. 같이 갈 동료가 있으면 훨씬 좋을 것 같다.

반응형

'일상' 카테고리의 다른 글

산업기능요원 훈련소 A to Z  (4) 2019.10.18
일학습병행제 실태 고발  (4) 2019.07.31
산업기능요원 국외여행 신청하기  (0) 2019.04.13
산업기능요원 전직 후기  (12) 2019.03.22
생각의 전환  (0) 2018.12.30