본문 바로가기
Godot

[GodotDocs][Your First 2D Game] 3. 플레이어 코딩(C#)

by 채식금지 2024. 2. 11.
728x90

본 게시글은 고도엔진 공식문서에 작성된 Your first 2D game를 정리하였습니다.

 

스크립트 추가

  • res://Player.tscn을 선택한다.
  • Scene 도크에서 Player 노드를 선택한다.
  • Attach Script 아이콘을 클릭한다.

 

  • Attach Node Script 팝업창이 열리면 Language를 C# 으로 선택한다.
  • C#을 선택하면 Path는 자동으로 res://Player.cs로 변경된다.
  • Create 버튼을 클릭하여 스크립트를 생성한다.

 

플레이어 이동구현

  • Player.cs 파일을 연다.
  • 클래스 안에 클래스를 프로퍼티를 선언한다.
[Export]
public int Speed
{
    get;
    set;
} = 400;

Vector2 ScreenSize
{
    get;
    set;
}
  • Export 어트리뷰트를 사용하면 해당 필드나 프로퍼티의 값을 Inspector에서 설정할 수 있게 된다.
  • 스크립트를 저장한 후, 프로젝트를 빌드한 후, Player.tscn 씬을 열고, Player 노드를 선택하면 Inspector 도크에 Speed 속성이 추가되어 있는 것을 확인 할 수 있다.

 

  • 처음 스크립트를 추가하면 _Ready() 메소드와 _Process(double delta) 메소드는 이미 추가되어 있다.
  • _Ready() 함수 안에 아래의 코드를 추가한다.
public override void _Ready()
{
    ScreenSize = GetViewportRect().Size;
}
  • _Ready() 메소드는 Player 노드 씬 트리에 등록되었을 때(초기화 되었을 때) 한 번 호출된다.
  • GetViewportRect() 메소드를 이용하여 게임 화면의 크기를 불러와 ScreenSize 프로퍼티 안에 저장한다.

 

  • _Process(double delta) 메소드 안에 아래의 코드를 추가한다.
public override void _Process(double delta)
{
    var direction = Vector2.Zero;
    if (Input.IsActionPressed("move_right"))
        direction.X += 1;
    if (Input.IsActionPressed("move_left"))
        direction.X -= 1;
    if (Input.IsActionPressed("move_down"))
        direction.Y += 1;
    if (Input.IsActionPressed("move_up"))
        direction.Y -= 1; 
}
  • move_right, move_left, move_down, move_up[GodotDocs][Your First 2D Game] 1. 프로젝트 설정에서 미리 키보드 A, D, S, W와 미리 입력 시켜 두었다.
  • Input.IsActionPressed 함수는 특정키를 누르고 있을 때 true를 반환한다.
  • Input.IsActionPressed 함수 안에 move_right, move_left, move_down, move_up을 넣어 사용하면 키보드 A, D, S, W를 누르고 있을 때 true를 반환한다.
  • 키보드를 누를 때 이동할 방향을 direction 변수에 저장한다.

 

  • 고도 엔진의 2D 좌표는 가로는 X축, 세로는 Y축이다.
  • 따로 카메라를 추가하지 않았다면 화면 기준으로 왼쪽 상단 좌표가 (0, 0)이다.
  • 오른 쪽으로 갈 수록 X축 값이 증가하고, 아래 쪽으로 내려갈 수록 Y축 값이 증가한다.

 

  • _Process(double delta) 메소드 안에 아래의 코드를 더 추가한다.
var animSprite = GetNode<AnimatedSprite2D>("AnimatedSprite2D");
var velocity = Vector2.Zero;
if (direction.Length() > 0)
{
    velocity = direction * Speed;
    animSprite.Play();
}
else
    animSprite.Stop();
  • direction.Length() 메소드는 벡터의 거리를 계산하는 함수이다. 키보드를 누를 경우 0 이 아닌 값을 반환한다.
  • velocity = direction * Speed 는 키보드를 누를 경우 direction 방향으로 speed 만큼의 속력으로 이동한다는 의미가 된다. 하지만 실제 노드의 좌표를 변경하지 않았기 때문에 이것 만으로 노드가 이동하지 않는다.
  • GetNode("AnimatedSprite2D")를 이용하여 Player 노드의 자식으로 등록된 AnimatedSprite2D 노드를 불러올 수 있다.
  • GetNode("AnimatedSprite2D").Play()는 등록된 SpriteFrame에 맞춰 스프라이트 애니메이션을 재생 시키는 함수이다.

 

  • _Process(double delta) 메소드 안에 아래의 코드를 더 추가한다.
Position += velocity * (float)delta;
Position = Position.Clamp(Vector2.Zero, ScreenSize);
  • Positionvelocity를 더해 노드가 velocity 만큼 이동하게 된다.
  • 컴퓨터의 사양과 관계없이 동일한 시간에 동일한 거리를 이동하도록 처리하기 위해 velocitydelta를 곱해준다.
  • Clamp 함수는 입력된 값의 범위를 벗어나지 않게 보정해준다.
  • Position.Clamp(Vector2.Zero, ScreenSize)를 적용하면 노드가 화면 밖으로 이동하지 않도록 보정해준다.

 

  • _Process(double delta) 함수 안에 아래의 코드를 더 추가한다.
if (direction.X != 0)
{
    animSprite.Animation = "walk";
    animSprite.FlipV = false;
    animSprite.FlipH = direction.X < 0;
}
else if (direction.Y != 0)
{
    animSprite.Animation = "up";
    animSprite.FlipV = direction.Y > 0;
}
  • walk, up[GodotDocs][Your First 2D Game] 2. 플레이어 씬 제작에서 미리 만들어둔 애니메이션이다.
  • GetNode("AnimatedSprite2D").Animation 값을 수정하여 미리 만들어둔 애니메이션을 선택할 수 있다.
  • X축으로 이동할 때는 walk, Y축으로 이동할 때는 up 애니메이션을 선택한다.
  • GetNode("AnimatedSprite2D").FlipHtrue일 경우 X축을 기준으로 이미지를 반전 시킨다.
  • GetNode("AnimatedSprite2D").FlipVtrue일 경우 Y축을 기준으로 이미지를 반전 시킨다.
  • 플레이어가 오른쪽으로 이동하면 X축을 기준으로 이미지를 반전 시키고, 아래로 내려갈 경우 Y축을 기준으로 이미지를 반전 시킨다.

 

  • 지금까지 작업한 내용은 빌드 후 에디터 오른쪽 상단의 Run Current Scene 버튼을 클릭하여 테스트할 수 있다.

 

충돌처리 준비

  • 적과 충돌하였을 때 처리를 미리 구현한다.
  • 클래스 안에 클래스를 SignalAttribute가 포함된 델리게이트를 선언한다.
[Signal]
public delegate void HitEventHandler();

 

  • Scene 도크에서 Player 노드를 선택한다.
  • 에디터 오른쪽에 있는 Node 도크를 선택한다.
  • body_entered(body: Node2D) 시그널을 더블 클릭한다.

 

  • Connect a Signal to a Method 팝압창이 열리면 Receiver Method: 에 적힌 함수의 이름을 OnBodyEntered 로 변경한다.
  • Connect 버튼을 클릭하여 body_entered(body: Node2D) 시그널과 나중에 추가할 OnBodyEntered 메소드를 연결한다.

 

  • C# 으로 개발할 경우, 시그널을 연결해도 메소드가 자동으로 추가되지 않기 때문에 시그널과 연결된 메소드를 직접 코딩으로 입력한다.
public void OnBodyEntered(Node2D body)
{
    Hide();
    EmitSignal(SignalName.Hit);
    GetNode("CollisionShape2D").SetDeferred(CollisionShape2D.PropertyName.Disabled, true);
}
  • 적과 충돌하게 되면 void OnBodyEntered(Node2D body) 메소드가 자동으로 호출된다.
  • Hide() 메소드를 호출하면 노드가 화면에 출력되지 않는다.
  • EmitSignal() 메소드를 호출하면 미리 정의해둔 Hit 시그널의 신호가 발생한다.
  • CollisionShape2D를 바로 비활성화하면 고도엔진이 충돌처리를 하는 도중에 오류가 발생할 수 있다.
  • SetDeferred 메소드드를 이용하면 안전하게 CollisionShape2D를 비활성화할 때까지 대기하도록 고도엔진에 지시하게 된다.

 

  • Start(Vector2 position) 메소드를 새로 추가한다.
  • Start(Vector2 position) 메소드는 게임을 시작할 때 플레이어를 초기화하기 위해 호출한다.
public void Start(Vector2 position)
{
    Position = position;
    Show();
    GetNode<CollisionShape2D>("CollisionShape2D").Disabled = false;
}

 

완성된 스크립트

using Godot;
using System;

public partial class Player : Area2D
{
    [Signal]
    public delegate void HitEventHandler();

    [Export]
    public int Speed
    {
        get;
        set;
    } = 400;

    Vector2 ScreenSize
    {
        get;
        set;
    }

    // Called when the node enters the scene tree for the first time.
    public override void _Ready()
    {
        ScreenSize = GetViewportRect().Size;
    }

    // Called every frame. 'delta' is the elapsed time since the previous frame.
    public override void _Process(double delta)
    {
        var direction = Vector2.Zero;
        if (Input.IsActionPressed("move_right"))
            direction.X += 1;
        if (Input.IsActionPressed("move_left"))
            direction.X -= 1;
        if (Input.IsActionPressed("move_down"))
            direction.Y += 1;
        if (Input.IsActionPressed("move_up"))
            direction.Y -= 1;

        var animSprite = GetNode<AnimatedSprite2D>("AnimatedSprite2D");
        var velocity = Vector2.Zero;
        if (direction.Length() > 0)
        {
            velocity = direction * Speed;
            GetNode<AnimatedSprite2D>("AnimatedSprite2D").Play();
        }
        else
            GetNode<AnimatedSprite2D>("AnimatedSprite2D").Stop();

        Position += velocity * (float)delta;
        Position = Position.Clamp(Vector2.Zero, ScreenSize);

        if (direction.X != 0)
        {
            animSprite.Animation = "walk";
            animSprite.FlipV = false;
            animSprite.FlipH = direction.X < 0;
        }
        else if (direction.Y != 0)
        {
            animSprite.Animation = "up";
            animSprite.FlipV = direction.Y > 0;
        }
    }

    public void OnBodyEntered(Node2D body)
    {
        Hide();
        EmitSignal(SignalName.Hit);
        GetNode("CollisionShape2D").SetDeferred(CollisionShape2D.PropertyName.Disabled, true);
    }

    public void Start(Vector2 position)
    {
        Position = position;
        Show();
        GetNode<CollisionShape2D>("CollisionShape2D").Disabled = false;
    }
}