지금까지는 키보드로 플레이했습니다. 스마트폰에서 플레이할 경우 터치스크린 조작이 필요합니다.

 

스마트폰의 UI 생각해보기

이게임은 횡 스크롤 게임으로 조작은 좌우 이동과 점프입니다. 터치스크린에서 다음과 같이 구현합니다.

 

좌우 이동

화면 왼쪽 아래 버추얼 패드를표시하고 이것을 손가락으로 움직여 좌우로 이동할 수 있습니다.

점프 화면 오른쪽 점프 버튼을 배치합니다. 버튼을 누르면 점프하도록 구현합니다.

 

PlayerController스크립트 변경하기

PlayerController.cs
0.01MB

변수

isMoving이 true이면 키보드 입력처리를 무시하고 터치로 처리하도록 합니다.

// 터치 스크린 조작
bool isMoving = false;

Update() 

isMoving=false (키보드처리) 면 키보드 입력을 읽어 옵니다.

if (isMoving == false) {
// 수평 방향의 입력 확인
    axisH = Input.GetAxisRaw("Horizontal");
}

SetAxis()

버추얼 패드에서 불리는 메서드 입니다.  키보드 입력대신 axisH에 값을 입력합니다. float v는 이번에는 사용안하지만 다음을 위한 매개변수입니다.

public void SetAxis(float h, float v) {
    axisH = h;
    if (axisH == 0)  {
        isMoving = false;
    }  else {
        isMoving = true;
    }
}

 

GameManager 클래스 수정

게임이 종료되었을대 UI를 숨기는 처리를 Update메서드에 추가합니다. Jump 메서드를 추가하고 그 안에서 PalyerController의 Jump메서드를 호출합니다. GameManager에 집약시켜 이후 조작과 관련된 UI에 할당할 클래스를 GameManager로 한정해 작업을 단순화하기 위함입니다.

GameManager.cs
0.01MB

 

변수

가상패드와 연결할 변수 inputUI를 마련합니다.

// +++ 플레이어 조작 +++
public GameObject inputUI; // 조작 UI 패널

Update() 

게임클리어나 게임오버시 가상패드 UI를 숨깁니다.

 void Update()
    {
        if (PlayerController.gameState == "gameclear")
        {
            // 게임 클리어
            // +++ 점수 추가 +++
            // 사운드 재생
            // +++ 플레어이 조작 +++
            inputUI.SetActive(false);   // 조작 UI 숨기기
        }
        else if (PlayerController.gameState == "gameover")
        {
            // 게임 오버
             // +++ 시간 제한 추가 +++
            // +++ 사운드 재생 추가 +++
            // +++ 플레어이 조작 +++
            inputUI.SetActive(false);   // 조작 UI 숨기기
        }
        else if (PlayerController.gameState == "playing")
        {
			~생략

Jump()

Player오브젝트의 PlayerController컴포넌트의 Jump()메서드를 실행합니다.

    // +++ 플레이어 조작 +++ 
    // 점프
      public void Jump() {
        GameObject player = GameObject.FindGameObjectWithTag("Player");
        PlayerController playerCnt = player.GetComponent<PlayerController>();
        playerCnt.Jump();
    }

 

점프버튼 배치하기

이제 Canvas에 점프 버튼을 배치하고 버튼이 눌리면 플레이어가 점프하도록 해봅시다.

Canvas 프리팹을 편집모드로 엽니다.

하이라키 +>UI>Legacy>Button을 선택한 후 Canvas의 프리팹에 새로운 버튼 오브젝트를 배치하고 이름을 jumpbutton으로 합니다. Images폴더의 JumpButton Sprite를 끌어 JumpButton 오브젝트의 Image컴포넌트의 Source Image로 끌어 넣고

preserve Aspect 체크 SetNativeSize 클릭후 Canvas 우하로 배치시킵니다 .Ankor Preset +ALT 배치할수 있습니다.

이미지만 표시할 것이므로 하이라키 JumpButton을 선택한후 자식 Text는 제거합니다.

점프버튼의 이벤트 설정하기

버튼이 눌리면 플레이어 캐릭터가 점프할 수 있도록 이벤트를 연결해 보겠습니다.

Button의 OnClikc() 컴포넌트는 버튼이 눌렸다 떨어질때 반응하는데 점프버튼은 눌렸을때 바로 반응해야 하므로 Event Trigger라는 컴포넌트를 추가하여 사용합니다.  추가하면 Add New Event Type을 눌러 Pointer Down을 선택합니다.

OnClick()처럼 +를 눌러 리스트를 추가하고 None Object에 GameManager를 끌어다 연결합니다. GameManager는 Canvas오브젝트에 어태치되어 있으므로 저는 Canvas를 끌어다 연결했습니다. 이후 No Function을 클릭해 GameManager의 Jump()를 선택해줍니다.

이제 버튼을 누르면 누르면 캐릭터가 점프합니다.

 

버추얼 패드 만들기

이제 캐릭터를 좌우로 이동시킬 버추얼 패드를 만들겠습니다. 계층뷰의 +>UI>Image를 선택후 Canvas에 새로운 이미지 오브젝트를 배치합니다. 이름은 VirtualPadBase로 한 후 자식으로 Image 오브젝트를 하나 더 비치하고 이름을 VirtualPadBaseTab으로 합니다.

Images폴더에서 VirtualPad2D는  VirtualPadBase로 VirtualPadTab이미지는 자식인 VirtualPadBaseTab의 Image컴포넌트 Image Source에 끌어 놓고 Preserve Perspective 체크, Set Native Size 클릭해줍니다.

하이라키에서 +>UI>Panel을 선택해 Canvas에 Panel을 배치하고 이름을 InputUI로 변경합니다. 사이즈를 가상패드와 점프버튼이 들어갈수 있게 그림처럼 줄입니다.

 JumpButton과 VirtualPadBase를 끌어다 패널의 자식으로 배치합니다.InputUI는 Color값을 변경해 투명하게 만듭니다.

버추얼 패드의 스크립트 만들기

VirtualPad.cs 스크립트를 만들고  VirtualPadTab에 어태치합니다. 책에서는 원형이미지에 어태치하라는데 Image폴더에 어태치해봤는데 안되서 VirtualPadBasdTab에 어태치했습니다.

 

VirtualPad.cs
0.00MB

변수

public float MaxLength = 70;    // 탭이 움직이는 최대 거리
public bool is4DPad = false;    // 상하좌우로 움직이는지 여부
GameObject player;              // 조작 할 플레이어 GameObject
Vector2 defPos;                 // 탭의 초기 좌표
Vector2 downPos;                // 터치 위치

 

Start()

void Start(){
    // 플레이어 캐릭터 가져오기
    player = GameObject.FindGameObjectWithTag("Player");
    // 탭의 초기 좌표
    defPos = GetComponent<RectTransform>().localPosition;
}

PadDown()

마우스 클릭시 스크린좌표를 저장합니다.

public void PadDown()  {
    // 마우스 포인트의 스크린 좌표
    downPos = Input.mousePosition;
}

PadDrag()

마우스를 드래그하면 localPosition을 이동시킵니다. 플레이어 캐릭터도 이동시킵니다. MaxLength를 넘기지 못하게 합니다.

public void PadDrag()  {
    // 마우스 포인트의 스크린 좌표
    Vector2 mousePosition = Input.mousePosition;
    // 새로운 탭 위치 구하기
    Vector2 newTabPos = mousePosition - downPos;// 마우스 다운 위치로 부터의 이동 거리
    if (is4DPad == false)  {
        newTabPos.y = 0; // 횡스크롤 일 때는  Y 값을 0 으로 한다.
    }
    // 이동 벡터 계산하기
    Vector2 axis = newTabPos.normalized; // 벡터를 정규화
    // 두 점의 거리 구하기
    float len = Vector2.Distance(defPos, newTabPos);
    if (len > MaxLength)  {
        // 한계거리를 넘겼기 때문에 한계 좌표로 설정
        newTabPos.x = axis.x * MaxLength;
        newTabPos.y = axis.y * MaxLength;
    }
    // 탭 이동 시키기
    GetComponent<RectTransform>().localPosition = newTabPos;
    // 플레이어 캐릭터 이동 시키기
    PlayerController plcnt = player.GetComponent<PlayerController>();
    plcnt.SetAxis(axis.x, axis.y);
}

PlayerController의 SetAxis()

매개변수를 받아 axisH를 설정합니다.

public void SetAxis(float h, float v) {
    axisH = h;
    if (axisH == 0)  {
        isMoving = false;
    }  else {
        isMoving = true;
    }
}

 

PadUp()

마우스 클릭이 업되면 패드의 위치를 초기화하고 캐릭터를 정지시킵니다.

public void PadUp() {
    // 탭 위치의 최기화 
    GetComponent<RectTransform>().localPosition = defPos;
    // 플레이어 캐릭터 정지 시키기
    PlayerController plcnt = player.GetComponent<PlayerController>();
    plcnt.SetAxis(0, 0);
}

컴포넌트 어태치

VirtualPadBase의 자식으로 배치했던 VirtualPadBaseTab에 컴포넌트를 몇가지 어태치합니다.

Event>Evnt Trigger를 선택해 Event Trigger컴포넌트를 추가합니다.

Add New Event Type 버튼을 클릭한 후 Pointer Down, Drag, Point Up 세가지를 추가합니다.

각각의  Event의 +버튼을 클릭해 리스트를 추가하고 이벤트를 추가합니다. None(Object)에 하이라키의 VirtualPadBaseTab을 끌어다 놓고 None Function 풀다운 메뉴에서 VirtualPad> PadDown(), PadDrag(), PadUP()을 선택합니다.

추가 스테이지는 여러분이 여태까지의 과정을 복습하면서 만들어 보시기 바랍니다. 

BGM 재생하기

Cavas 프리팸을 편집모드로 연후 인스펙트뷰의 Audio source 컴포넌트를 추가합니다.

Audio Source 폴더의 AudioClip에 프로젝트 뷰의 BGM_game_00을 드래그 앤 드롭합니다. 이때 Play On Awake와  Loop를 체크합니다.  이제 게임을 시작함과 동시에 BGM_game_00이 반복해서 재생됩니다.

Sounds.zip
2.45MB

 

스크립트로 사운드 재생및 정지시키기

게임을 클리어 했을때와 게임 오버일때 game_BGM_00을 멈추고 다른 사운드를 플레이합니다.

GameManager스크립트에 해당기능을 추가합니다.

 

변수

게임오버 클리어용 오디오클립 참조 변수를 마련합니다.

// +++ 사운드 재생 추가 +++
public AudioClip meGameOver;        // 게임 오버
public AudioClip meGameClear;       // 게임 클리어

Update()

오디오 컨트롤을 위해서 Audiosource컴포넌트를 이용합니다.

AudioSource soundPlayer = GetComponent<AudioSource>();


정지를 위해서 Stop(), 한번 플레이를 위해서 PlayOneShot()메서드를 이용합니다.

soundPlayer.Stop(); 
soundPlayer.PlayOneShot(meGameClear);

Update()변경부분

 void Update()
    {
        if (PlayerController.gameState == "gameclear")
        {
            // 게임 클리어
            ~중략

             // +++ 사운드 재생 추가 +++
            // 사운드 재생
            AudioSource soundPlayer = GetComponent<AudioSource>();
            if (soundPlayer != null)
            {
                // BGM 정지
                soundPlayer.Stop(); soundPlayer.PlayOneShot(meGameClear);
            }
            // +++ 플레어이 조작 +++
            inputUI.SetActive(false);   // 조작 UI 숨기기
        }
         else if (PlayerController.gameState == "gameover")
        {
            // 게임 오버
           ~중략
            // +++ 사운드 재생 추가 +++
            // 사운드 재생
            AudioSource soundPlayer = GetComponent<AudioSource>();
            if (soundPlayer != null)
            {
                // BGM 정지
                soundPlayer.Stop();
                soundPlayer.PlayOneShot(meGameOver);
            }
            // +++ 플레어이 조작 +++
            inputUI.SetActive(false);   // 조작 UI 숨기기
        }

GameManager.cs
0.01MB

스크립트를 캔버스 프리팹에 어태치하고 오디오 클립2개를 연결해줍니다.

 

이제 적 캐릭터를 만듭니다. 움직이는 데미지 블록과 비슷합니다.

적 캐릭터는 다음과 같이 동작합니다.

플레이어가 접촉하면 게임오버가 됩니다. (Dead Tag)

일정 범위를 왔다갔다한다. (MovingBlock)

벽에 접촉하면 180도 방향을 바꾼다.

이번 예제에서 만들 적캐릭터 데이터는 Empty폴더를 만들어 저장합니다.

 

적 캐릭터 만들기

적 캐릭터는 4장에서 애니메이션이 되도록 이미지를 준비했습니다.

images폴더에서 enemy1~4를 한꺼번에 선택해 씬뷰에 끌어다 애니메이션을 만듭니다. 게임오브젝트와 애니메이션 이름을 Enemy로 변경합니다.

태그를 Dead로 설정합니다. Sprite Renderer 컴포넌트의 Order in Layer는 2로 합니다. 어태치할 컴포넌트는 RigidBody 2D, Circle Collider 2D, Box Collider 2D입니다.  어태치한 후 Rigidbody 2D의 Freeze Rotation에서 Z를 체크해 회전하지 않도록 합니다.

Circle Collider 2D와 Box Collider 2D의 위치는 다음 그림과 같이 설정합니다. Collider영역은 Edit Collider를 누르면 나타나는 영역과 점을 이용해 조정합니다.

Box Collider가 Circle Collider보다 약간 크고 isTrigger를 체크해서 충돌을 담당합니다.

Player라 접촉하기면 죽일수 있게 Enemy game object는 Dead Tag를 지정하였습니다. Player가 충돌하면 죽는 DeadZone이 함정, 니들, 적으로 늘어났습니다. Circle Collider2D는 지면과의 물리적 충돌을 처리하기위해 약간 아래로 내립니다. 원이 박스보다 접촉이 적어 저항이 적습니다. 

 

적 캐릭터의 스크립트 만들기

스크립트를 하나 만들고 EnemyController라고 이름을 바꿉니다.  스크립트를 만들고 어태치합니다. 플레이어 캐릭터를 움직이는 PlayerController의 간소한 버전입니다.

 

변수

public float speed = 3.0f;          // 이동 속도
public string direction = "left";   // 방향 right or left 
public float range = 0.0f;          // 움직이는 범위
Vector3 defPos;                     // 시작 위치

Start()

변수 direction이 "right"일 경우 스프라이트 localScale을  x축으로 반전시켜 줍니다. 현재위치를 디폴트위치로 합니다.

void Start()  {
    if (direction == "right") {
        transform.localScale = new Vector2(-1, 1);// 방향 변경
    }
    defPos = transform.position;  // 시작 위
}

Update()

적캐릭터가 이동범위를 벗어나면 direction="right"<->"left"로 방향을 바꿔준다. range가 0일 경우 범위 체크는 안하고 벽을 부딪쳐야만 반전한다.

void Update() {
    if (range > 0.0f)  {
        if (transform.position.x < defPos.x - (range / 2)) {
            direction = "right";
            transform.localScale = new Vector2(-1, 1);// 방향 변경
            if (transform.position.x > defPos.x + (range / 2)){
                direction = "left";
                transform.localScale = new Vector2(1, 1);// 방향 변경
            }
        }
    }
}

FixedUpdate()

direction방향에 따라 velocity를 가속해준다

void FixedUpdate()   {
    // 속도 갱신
    // Rigidbody2D 가져오기
    Rigidbody2D rbody = GetComponent<Rigidbody2D>();
    if (direction == "right")  {
        rbody.velocity = new Vector2(speed, rbody.velocity.y);
    } else  {
        rbody.velocity = new Vector2(-speed, rbody.velocity.y);
    }
}

OnTriggerEnter2d()

벽과 충돌하면 direction="right"<-> "left"를 반전시켜 방향을 바꿔준다. 충돌체크를 위해서는  BoxCollider 2D isTrigger가 체크되어 있어야 합니다.

private void OnTriggerEnter2D(Collider2D collision) {
    if (direction == "right")  {
        direction = "left";
        transform.localScale = new Vector2(1, 1); // 방향 변경
    }  else {
        direction = "right";
        transform.localScale = new Vector2(-1, 1); // 방향 변경
    }
}

EnemyController.cs
0.00MB

스크립트를 적캐릭터에 어태치하고 플레이한다.

Range가 0일 경우 무대 전체를 왔다갔다하면서 벽에 부딪치면 반대방향으로 달립니다. Range를 4로 하면 4만큼의 범위내에서만 움직입니다.

 

고정 포대 게임 오브젝트 만들기

images폴더의 cannon을 씬 뷰에 드래그 앤 드롭해 게임오브젝트를 만듭니다. Order in Layer를 2로 하고 Box Collider 2D를 추가합니다. 자식오브젝트로 Create Empty를 추가하고 이름을 gate로 합니다. 위치를 포대 왼쪽으로 설정합니다. Empty라 보이지 않으므로 컬러 아이콘을 추가합니다

 

고정 포대 스크립트 만들기

스크립트를 하나 만들고 이름을 CanonController라고 합니다. cannon에 어태치 합니다.

 

변수

public GameObject objPrefab;        // 발생시키는 Prefab 데이터 
public float delayTime = 3.0f;      // 지연시간
public float fireSpeedX = -4.0f;    // 발사 벡터  X
public float fireSpeedY = 0.0f;     // 발사 벡터 Y
public float length = 8.0f;

GameObject player;                  // 플레이어
GameObject gateObj;                 // 발사구
float passedTimes = 0;              // 경과 시간

Start()

cannon의 자식오브젝트 gate와 Player를 찾아 연결해 줍니다.

void Start()
{
    // 발사구 오브젝트 얻기
    Transform tr = transform.Find("gate");  //자식오브젝트 transform 찾기
    gateObj = tr.gameObject;  //자식오브젝트 가져오기
    // 플레이어
    player = GameObject.FindGameObjectWithTag("Player");
}

Update()

passedTimes를 갱신하면서 delayTime보다 크면 포탄을 발사해줍니다. 발사위치는 gate의 위치인 gateObj이 됩니다. Instatiate(objPrefab)으로 포탄을 만듭니다. 스크립트로 런타임으로 프리팹을 이용 게임오브젝트를 만들어 줍니다.  rigidbody.AddForce()를 이용해 포탄을 발사하게 만듭니다.

 void Update()
{
    // 발사 시간 판정
    passedTimes += Time.deltaTime;
    // 거리 확인
    if (CheckLength(player.transform.position))
    {
        if (passedTimes > delayTime)
        {
            // 발사!!
            passedTimes = 0;
            // 발사 위치
            Vector3 pos = new Vector3(gateObj.transform.position.x,
                                      gateObj.transform.position.y,
                                      transform.position.z);
            // Prefab 으로 GameObject 만들기
            GameObject obj = Instantiate(objPrefab, pos, Quaternion.identity);
            // 발사 방향
            Rigidbody2D rbody = obj.GetComponent<Rigidbody2D>();
            Vector2 v = new Vector2(fireSpeedX, fireSpeedY);
            rbody.AddForce(v, ForceMode2D.Impulse);
        }
    }
}

CheckLength()

자신과 targetPos를 측정해 length 보다 크면 true 작으면 false를 리턴하는 함수입니다.

bool CheckLength(Vector2 targetPos)
{
    bool ret = false;
    float d = Vector2.Distance(transform.position, targetPos);
    if (length >= d)
    {
        ret = true;
    }
    return ret;
}

포탄 만들기

Images폴더의 shell을 끌어다 씬뷰에 놓습니다. 

Tag는 Dead를 선택하고 Layer를 Shell을 새로 만들어 지정합니다.

Circle Collider 2D를 추가하고 IsTrigger를 체크합니다.

Rigidbody2D를 추가하고 Gravity Scale를 0로 설정합니다.

포탄의 스크립트 만들기

ShellController 라는 이름의 스크립트를 만든후  shell 게임오브젝트에 추가합니다.

포탄이 발사되면 일정시간(deleteTime=3)후 제거되며 무언가 접촉해도 제거됩니다.

 

변수

public float deleteTime = 3.0f; // 제거할 시간 지정

Start()

void Start()  {
    Destroy(gameObject, deleteTime); // 제거 설정
}

OnTriggerEnter2D()

void OnTriggerEnter2D(Collider2D collision) {
    Destroy(gameObject); // 무언가에 접촉하면 제거
}

레이어의 접촉 설정 수정하기

포탄은 무언가 접촉하면 사라지기 때문에 포탑과 접촉해도 사라집니다. Edit>ProjectSettings>Physics2D 에서 Ground-Shell간의 매트릭스를 꺼준다.

이제 씬뷰에서 shell을  프로젝트뷰로 끌어다 프리팹을 만들어주고 씬뷰의 shell을 지워준다

cannon오브젝트의 스크립트 컴포넌트에 shell을 끌어다 적용하고 cannon역시 프로젝트뷰로 끌어다 프리팹으로 만든다.

플레이어가 cannon에 접근하면 포탄이 발사 된다.

스위치 게임 오브젝트 만들기

Lever_off 이미지 에셋을 씬 뷰에 배치해 게임 오브젝트를 만들고 이름을 Switch로 합니다. 구분을 위해 Switch 태그를 추가하고 설정합니다. Order in Layer를 2로 하고 Box Collider2D를 추가하고 isTrigger를 체크합니다.

스위치 스크립트 작성하기

스크립트명은 SwitchAction으로 하고 Switch 게임오브젝트에 어태치합니다.

변수

public GameObject targetMoveBlock;
public Sprite imageOn;
public Sprite imageOff;
public bool on = false; // 스위치의 상태 (true:눌린 상태 false:눌리지 않은 상태)

Start()

스크립트 글로벌 변수 on이 true면 imageOn, off면 imageOff를 지정합니다.

void Start()  {
    if (on) {
        GetComponent<SpriteRenderer>().sprite = imageOn;
    } else{
        GetComponent<SpriteRenderer>().sprite = imageOff;
    }
}

OnTriggerEnter2D()

플레이어가 switch와 접촉하면 on<->off 토글시켜줍니다. movBlock도 Stop() Move()시켜줍니다.

void OnTriggerEnter2D(Collider2D col){
    if (col.gameObject.tag == "Player"){
        if (on) {
            on = false;
            GetComponent<SpriteRenderer>().sprite = imageOff;
            MovingBlock movBlock = targetMoveBlock.GetComponent<MovingBlock>();
            movBlock.Stop();
        } else{
            on = true;
            GetComponent<SpriteRenderer>().sprite = imageOn;
            MovingBlock movBlock = targetMoveBlock.GetComponent<MovingBlock>();
            movBlock.Move();
        }
    }
}

SwitchAction.cs
0.00MB

스크립트를 Switch게임오브젝트에 어태치해주고 변수를 설정해줍니다. 

Image폴더의 ImageOn ImageOff스프라이트를 끌어다 연결해줍니다. TargetMoveBlock은 프리팹을 만든후 설정하겠습니다.

여기까지 만들고 프로젝트뷰로 끌어 프리팹을 만들어 줍니다.

MovingBlock Prefab을 끌어다 씬뷰에 배치하고 변수를 다음과 같이 설정합니다.

하이라키의 MovingBlock을 끌어다 Switch 스크립트컴포넌트 변수에 연결해줍니다.

 

'유니티2D게임 > 게임에 장치 추가' 카테고리의 다른 글

7.5 돌아다니는 적 캐릭터 만들기  (0) 2023.05.20
고정 포대 만들기  (0) 2023.05.19
이동 블록 만들기  (0) 2023.05.19
7.1 대미지 블록 만들기  (0) 2023.05.17
결과 화면 추가하기  (2) 2023.05.17

캐릭터가 올라갈 수 있는 움직이는 블록을 만들겠습니다. 다음같은 속성을 갖습니다.

  • 이동방향
  • 거리
  • 시간
  • 플레이어가 올라가면 움직이느냐?

움직이는 블록 게임 오브젝트 만들기

Assets>Images폴더내 block_move을 프로젝트 뷰에서 씬뷰로 배치해 게임 오브젝트를 만들고 이름은 MovingBlock으로 합니다.

인스펙트뷰에서 Layer를 Ground로 하고 Sprite Rendere의 Order in Layer의 값을  2로 설정하고 Box Collider 2D를 어태치합니다. 이동 블록은 물리동작을 따르지 않아 Rigidbody는 필요없습니다.

 

블록을 움직이는 스크립트 작성

MovingBlock 스크립트를 만들고 MovingBlock 게임오브젝트에 어태치해줍니다.

변수

public float moveX = 0.0f;          //X이동거리
public float moveY = 0.0f;          //Y이동거리
public float times = 0.0f;          //단방향 이동시간
public float weight = 0.0f;         //정지 시간
public bool isMoveWhenOn = false;   //올라 탓을 때 움직이기

public bool isCanMove = true;       //움직임
float perDX;                        //1프레임 당 X이동 값, 이동속도
float perDY;                        //1프레임 당 Y이동 값, 이동속도
Vector3 defPos;                     //초기 위치, 왕복시 돌아올 위치
bool isReverse = false;             //정방향 반대반향 여부

Start()

perDX = moveX / (1.0f / timestep * times);  //1 프레임의 X 이동 값 이다.

float timestep = Time.fixedDeltaTime; //1프레임에 이동하는 시간

1/timestep = 1초당 프레임수

moveX/프레임수 = 1초동안 인동할 때 한프레임당 이동거리

한프레임당 이동거리 / times = times초 이동시 한프레임 이동거리

결론은 perDX는 tiems초 동안 블록이 moveX거리를 이동할때 프레임당 이동거리이다. 간단한걸 엄청 복잡하게 써놨다.

그냥 perDX = moveX * Time.fixedDeltaTime * speed(1/ times)와 같다. ㅠㅠㅠ

만일 블록 특성이 isMoveWhenOn(올라타야 움직임) = true라면 isCanMove = false로 만들어 블록이 이동못하게 한다.

void Start(){
    defPos = transform.position;         //초기 위치
    float timestep = Time.fixedDeltaTime; //1프레임에 이동하는 시간 
    perDX = moveX / (1.0f / timestep * times);  //1 프레임의 X 이동 값
    perDY = moveY / (1.0f / timestep * times); //1 프레임의 Y 이동 값

    if (isMoveWhenOn){
        //처음엔 움직이지 않고 올라 타면 움직이기 시작
        isCanMove = false;
    }
}

FixedUpdate()

코드가 긴것 같은데 별것 없다. 위에서 정한 perDX값만큼 매 프레임 transform.Translate()만큼 움직여주면 된다. 그러다 moveX만큼 움직이거나 다시 원위치로 오면 잠시 쉬었다가 방향을 반전해 주면 된다.

 Vector3 v = new Vector3(perDX, perDY, defPos.z);
                transform.Translate(v);

원점에 도착하면 현재위치 transfor.position = defPos로 보정해줘 실수연산오차를 방지해준다.

private void FixedUpdate(){
        if (isCanMove) {
            //이동중
            float x = transform.position.x;
            float y = transform.position.y;
            bool endX = false;
            bool endY = false;
            if (isReverse){
                //반대 방향 이동
                //이동량이 양수고 이동 위치가 초기 위치보다 작거나
                //이동량이 음수고 이동 위치가 초기 위치보다 큰경우
                if ((perDX >= 0.0f && x <= defPos.x) || (perDX < 0.0f && x >= defPos.x)) {
                    //이동량이 +
                    endX = true;    //X 방향 이동 종료
                }
                if ((perDY >= 0.0f && y <= defPos.y) || (perDY < 0.0f && y >= defPos.y)) {
                    endY = true;    //Y 방향 이동 종료
                }
                //블록 이동
                transform.Translate(new Vector3(-perDX, -perDY, defPos.z));
            }else{
                //정방향 이동
                //이동량이 양수고 이동 위치가 초기 위치보다 큰거나
                //이동량이 음수고 이동 위치가 초기 + 이동거리 보다 작은 경우
                if ((perDX >= 0.0f && x >= defPos.x + moveX) || (perDX < 0.0f && x <= defPos.x + moveX)){
                    endX = true;    //X 방향 이동 종료
                }
                if ((perDY >= 0.0f && y >= defPos.y + moveY) || (perDY < 0.0f && y <= defPos.y + moveY)){
                    endY = true;    //Y 방향 이동 종료
                }
                //블록 이동
                Vector3 v = new Vector3(perDX, perDY, defPos.z);
                transform.Translate(v);
            }

            if (endX && endY){
                //이동 종료
                if (isReverse){
                    //위치가 어긋나는것을 방지하기 위해 정면 방향이동으로 돌아가기 전에 초기 위치로 돌림
                    transform.position = defPos;
                }
                isReverse = !isReverse; //값을 반전 시킴
                isCanMove = false;      //이동 가능 값을 false
                if (isMoveWhenOn == false){
                    //올라 탓을 때 움직이는 값이 꺼져있는 경우
                    Invoke("Move", weight);  // weight 만큼 지연 후 다시 이동
                }
            }
        }
    }

OnCollisionenter2D()

캐릭터가 블록위에 올라타면 블록의 자식으로 해서 같이 움직이게 해준다 캐릭터가 블록위에서 벗어나면 풀어준다. OnCollisionEnter와 Exit를 이용해서 구현한다.

private void OnCollisionEnter2D(Collision2D collision)  {
    if (collision.gameObject.tag == "Player") {
        //접촉한것이 플레이어라면 이동 블록의 자식으로 만들기
        collision.transform.SetParent(transform);
        if (isMoveWhenOn) {
            //올라탔을 때 움직이는 경우면 
            isCanMove = true;   //이동하게 만들기
        }
    }
}
//접촉 종료
private void OnCollisionExit2D(Collision2D collision) {
    if (collision.gameObject.tag == "Player") {
        //접촉한것이 플레이어라면 이동 블록의 자식에서 제외시킴
        collision.transform.SetParent(null);
    }
}

블록에 스크립트를 어태치합니다

블록을 프리팹으로 만들고 프리팹을 끌어 씬뷰에 하나더 배치합니다. 스크립트의 변수 설정이 둘다 비슷한데 하나는 IsMoveWhenOn이 체크되어 있어 게임 실행후는 멈춰있지만 올라타면 움직이고 내리면 멈춥니다.

MoveX는 자동으로 움직일 거리입니다.

Times는 자동으로 움직일 편도 시간입니다. 3이면 3초동안 진행합니다.

 

MovingBlock.cs
0.00MB

밝으면 죽는 대미지 블록을 만듭니다. 새로운 게임오브젝트를 만들어 "Dead" Tag를 붙여주면 됩니다.

  • 대미지 블록의 외관은 바늘
  • 양 옆에서 접축하면 대미지 없이 충돌
  • 위에서 접촉시만 대미지 (게임오버)

 

바늘 대미지 블록 만들기

대미지 블록을 위한 바늘 이미지는 에셋인 needle을 씬뷰나 하이라키고 드래그 드롭으로 추가합니다.

Sprite Render컴포넌트의 Order in Layer값을 2로 합니다.

인스펙터 뷰에서 Add Component>Physics 2D를 선택한 후 Box Collider 2D를 두개 어태치해 양옆으로 벽처럼 배치합니다.  플레이어의 진행을 벽처럼 막을 목적이므로 isTrigger는 체크하지 않습니다.(체크하면 통과해버립니다.)

하이라키뷰에서 needle의 자식으로 Create Empty한후 이름을 DeadZone으로 하고 BoxCollider 2D를 추가하고 isTrigger를 체크합니다. 범위는 가운데쪽으로 해서 위에서 떨어지는 접촉을 감지합니다.

needle 을 Prefab폴더로 끌어다 prefab으로 만듭니다.

 

낙하하는 대미지 블록 만들기

images폴더에서 block1x1을 씬뷰에 끌어다 이름을 GimmickBlock으로 변경합니다. Sprite Renderer컴포넌트의 Order in Layuers는 2로 변경, Layer값은 Ground로 해서 캐릭터가 올라탈수 있도록 합니다.

Box Collider 2D를 추가하고 아래쪽을 살짝 올려줍니다.

지면과의 접촉은 CircleCollider2D를 추가합니다. 지면과의 마찰을 줄여 매끄럽게 이동하기 위해서입니다.   플레이어가 밀수 있게 Rigidbody2D를 추가하고 넘어지지 않게 Freeze Rotation Z를 체크하고 25로 설정합니다.

떨어지다가 플레이어와의 접촉을 담당할 새로운 빈오브젝트를 만들어 이름을 DeadZone으로 바꾸고 GimmickBlock의 자식으로 만들고 Tag를 Dead로 설정합니다.  Box Collider 2D를 추가하고 위치를 아래로 약간 내리고 IsTrigger를 체크해서 플레이어의 검출을 담당합니다.

장애물 블록과 식별을 용이하게 하기 위해 컬러아이콘을 붙입니다. 인스펙터뷰 맨위좌측을 눌러보면 아이콘을 선택할 수 있습니다. 부탁후 씬쀼에서 이름을 확인할 수 있습니다. 게임중에는 표시되지 않습니다.

공중에 배치하면 낙하하고 플레이어와 충돌하면 게임 오버가 되면 지면에 배치하면 플레이어가 밀어서 움직일 수 있고 올라탈수 있는 블록이 완성되었습니다.

 

대미지 블록을 낙하시키는 스크립트 만들기

GimmickBlock에 다음 기능을 추가하는 스크립트를 추가해봅니다.

플레이어가 일정 거리 안에 들어가면 떨어진다.

낙하 후에는 점점 사라진다.(변수로 시간을 정의할 수 있음)

GimmickBlock.cs의 코드 입니다.

변수 : 낙하와 페이드아웃 시간을 정의하고 있습니다.

public float length = 0.0f;     // 자동 낙하 탐지 거리
public bool isDelete = false;   // 낙하 후 제거할지 여부

bool isFell = false;            // 낙하 플래그
float fadeTime = 0.5f;          // 페이드 아웃 시간

Start()  : 게임 시작시 낙하할 블록을 Static으로 만들어 못 움직이게 만듭니다.

void Start() {
    // Rigidbody2D 물리연동 정지
    Rigidbody2D rbody = GetComponent<Rigidbody2D>();
    rbody.bodyType = RigidbodyType2D.Static;
}

Update() : 플레이어와의 거리를 계산해서 bodyType을 Dynamic으로 바꿔 떨어지게 만듭니다.

블록이 바닥에 떨어져 있는 상태라면 faceTiem을 카운트다운하면서 서서히 투명도를 바꿔줍니다. 0이 되면 제거합니다.

void Update(){
        GameObject player = GameObject.FindGameObjectWithTag("Player"); // 플레이어 찾기
        if (player != null) {
            // 플레이어와의 거리 계산
            float d = Vector2.Distance(transform.position, player.transform.position);
            if (length >= d) {
                Rigidbody2D rbody = GetComponent<Rigidbody2D>();
                if (rbody.bodyType == RigidbodyType2D.Static) {
                    // Rigidbody2D물리 연동 시작
                    rbody.bodyType = RigidbodyType2D.Dynamic;
                }
            }
        }
        if (isFell){
            // 낙하
            // 투명도를 변경해여 페이드 아웃 효과
            fadeTime -= Time.deltaTime; // 이전 프레이과의 차이만큼 시간 차감
            Color col = GetComponent<SpriteRenderer>().color;   // 컬러 값 가져오기
            col.a = fadeTime;   // 투명도 변경
            GetComponent<SpriteRenderer>().color = col; // 컬러 값을 재설정
            if (fadeTime <= 0.0f){            
                Destroy(gameObject); // 0보다 작으면(투명)제거
            }
        }
    }

OnCollisionEnter2D() 충돌은 바닥과 생기므로 오브젝트 검사는 하지 않습니다. 바닥에 떨어지면 isFell이 true로 바꿔줍니다.

void OnCollisionEnter2D(Collision2D collision) {
    if (isDelete){
        isFell = true; // 낙하 플래그 true
    }
}

스크립트를 GimmickBlock에 어태치하고 Lengh를 2정도로 해줍니다. 플레이어와 블록의 거리가 2이하이면 떨어지게 만들어 줍니다. isDelete도 체크해줘 땅에 떨어지면 사라지게도 해봅니다. 이걸 안하면 화면이 스크롤 되면서 블록이 왼쪽에 부디치면 어색하게 밀립니다.

블록이 2이상 높이 배치되어 있을 경우 블록이 떨어지지 않습니다. 거리d를 x만 계산해서 높이가 높아도 x축으로 다가가면  떨어지게 만들어 봤습니다. 이건 게임의 상황에 따라 적절히 조절해보시면 됩니다.

float d = Mathf.Abs( transform.position.x- player.transform.position.x);

완성된 GimmickBlock을 프리팹으로 만들어 씬에 몇개더 배치해 봅니다. 

게임 스테이지를 전부 클리어했을때 표시할 엔딩장면을 만들어 봅시다. Result 라는 이름의 씬을 만들어 봅니다.

결과화면  UI 만들기 

배경 이미지 배치

+UI>Image를 추가하고 자동추가된 Canvas 컴포넌트의 Render Mode값을 ScreenSpace-Camera로 설정하고 Render Camara Main Camera를 선택해 줍니다.  tile_back이미지를 Image컴포넌트에 끌어다 놓습니다. 이미지 크기를 화면에 맞게 조정합니다. 하이라키에서 image를 선택하고Anchor Presets를 열어 ALT를 누른상태에서 stetch를 선택해도 됩니다.

 

타이틀로 돌아가는 버튼 배치

하이라키의+를 클릭해 UI>Legacy>Button을 추가합니다. 프로젝트뷰 Image폴더의 Button스프라이트를 Image컴포넌트의  Source에 끌어다 놓습니다. Preserve Aspect / SetNativeSize후 적당히 위치를 잡습니다.

자식으로 Text도 추가하고 타이틀로 돌아가기를 입력해줍니다. 사이즈는 48정도 입니다.

Button에 ChangeScene 스크립트를 끌어다 추가해주고 Scene Name은 Title로 설정해줍니다.

OnClick()에 +를 눌러 리스트를 하나 만들고 하이라키의 Button을 끌어다 연결후 None Function을 눌러 ChangeScene>Load()를 선택해줍니다.

 

점수표시용 UI만들기

하이라키의+를 클릭해 UI>Image을 추가합니다. 프로젝트뷰 Image폴더의 ScoreBoard스프라이트를 Image컴포넌트의  Source에 끌어다 놓습니다. Preserve Aspect / SetNativeSize후 적당히 위치를 잡습니다. 이름을 ScoreBoard 로 바꿉니다.

자식으로 Text도 추가하고 타이틀로 돌아가기를 입력해줍니다. 사이즈는 48정도 입니다. 이름을 ScoreText로 바꿉니다.

전체 점수를 표시할 스크립트 만들기

ResultManager스크립트를 만들어 Canvas에 어태치합니다.

ResultManager.cs
0.00MB

실행되면 GameManager의 totalScore를 ScoreBoard자식인 scoreText에 표시하는게 다입니다.

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

public class ResultManager : MonoBehaviour
{
    public GameObject scoreText;

    // Start is called before the first frame update
    void Start() {
        scoreText.GetComponent<Text>().text = GameManager.totalScore.ToString();
    }
}

 Canvas에 어태치된 ResultManager스크립트 컴포넌트의  scoreText변수에 ScoreText를 끌어다 스크립트에 연결해 줍니다.

+ Recent posts