PvZ Clone - Day 1

오늘 한거

  • ColorRect로 단색 배경 깔기 (Main 노드 하위)
  • GameBoard Scene 구현
    • 5×9 그리드 2D 배열로 관리
    • 타일 텍스처 좌표 계산해서 렌더링
    • 마우스 클릭 → 셀 좌표 변환, valid cell 체크
    • 좌클릭으로 식물 심기, 우클릭으로 제거 (queue_free)
  • Plant 베이스 씬 + Peashooter, Sunflower 상속 구조로 분리
  • enum으로 선택 식물 관리, 키 1/2로 전환
GDScript
extends Node2D
@export var cell_width: int = 80
@export var cell_height: int = 100

@export var GRID_OFFSET_X = 100
@export var GRID_OFFSET_Y = 115


enum PlantType {PEASHOOTER, SUNFLOWER}
var selected_plant = PlantType.PEASHOOTER

var PeashooterScene = preload("res://scenes/peashooter.tscn")
var SunflowerScene = preload("res://scenes/sunflower.tscn")

var TILE_SIZE: Vector2
const COLS = 9
const ROWS = 5

const TILE_TEXTURE = preload("res://assets/img/garden.png")


var grid = []
func _ready():
    TILE_SIZE = Vector2(cell_width, cell_height)
    # draw tiles
    for row in ROWS:
        for col in COLS:
            var sprite = Sprite2D.new()
            sprite.texture = TILE_TEXTURE
            sprite.position = Vector2(col * TILE_SIZE.x, row * TILE_SIZE.y) + TILE_SIZE / 2
            sprite.position.x += GRID_OFFSET_X
            sprite.position.y += GRID_OFFSET_Y
            add_child(sprite)
    # initialize grid
    for row in range(ROWS):
        grid.append([])
        for col in range(COLS):
            grid[row].append(null)
            

func _input(event):
    # key vs click separation
    
    if event is InputEventKey and event.pressed:
        if event.keycode == KEY_1:
            selected_plant = PlantType.PEASHOOTER
        elif event.keycode == KEY_2:
            selected_plant = PlantType.SUNFLOWER
    
    if event is InputEventMouseButton and event.pressed:
        var cell = get_cell(event.position)
        if not is_valid_cell(cell):
            return
        if event.button_index == MOUSE_BUTTON_LEFT:
            if grid[cell.y][cell.x] == null:
                plant_at(cell)
        elif event.button_index == MOUSE_BUTTON_RIGHT:
            if grid[cell.y][cell.x] != null:
                remove_plant(cell)

func remove_plant(cell: Vector2i) -> void:
    if grid[cell.y][cell.x] != null:
        grid[cell.y][cell.x].queue_free()
        grid[cell.y][cell.x] = null

func plant_at(cell: Vector2i):
    
    var plant
    match selected_plant:
        PlantType.PEASHOOTER:
            plant = PeashooterScene.instantiate()
        PlantType.SUNFLOWER:
            plant = SunflowerScene.instantiate()
    if plant == null:
        return
        
    plant.position = cell_to_world(cell)
    add_child(plant)
    grid[cell.y][cell.x] = plant
    
func cell_to_world(cell: Vector2i) -> Vector2:
    return Vector2(
        GRID_OFFSET_X + cell.x * cell_width + cell_width / 2,
        GRID_OFFSET_Y + cell.y * cell_height + cell_height / 2
    )

func get_cell(world_pos: Vector2) -> Vector2i:
    var col = int((world_pos.x - GRID_OFFSET_X) / cell_width)
    var row = int((world_pos.y - GRID_OFFSET_Y) / cell_height)
    return Vector2i(col, row)

func is_valid_cell(cell: Vector2i) -> bool:
    return cell.x >= 0 and cell.x < COLS and cell.y >= 0 and cell.y < ROWS

image

배운 것

Godot에서 씬이 클래스, instantiate가 new랑 같은 개념이라는 걸 알게 됨.

Plant 베이스 씬을 상속해서 Peashooter, Sunflower를 만드는 구조가 결국 OOP인듯

근데 아까 본 유튜브 영상에서 너무 OOP에만 집중하지 않고 각각의 기능에 집중하는 Composition 위주의 개발을 소개하는걸 봤는데 참고할만한 내용인듯.

다음에 할 것

  • 햇빛 시스템 (해바라기 → 햇빛 생성 → 카운터 UI)
  • 식물 선택 UI (PlantSelector scene)