게임 루프(game loop)는 (주로 플레이어가 컨트롤러 또는 마우스와 상호작용하여 발생하는) 입력에 반응하여 반복적으로 실행되는 루프 코드입니다. 게임 스테이트를 업데이트하고, 플레이어가 게임 스테이트에 영향을 주는 경우(예: 플레이어가 버튼을 눌러서 불을 켜는 행동) 이를 플레이어에게 보여주는 출력을 제공합니다. 게임 스테이트를 업데이트하고, 예를 들어 플레이어가 버튼을 눌러서 불을 켜는 행동처럼 플레이어가 게임 스테이트에 영향을 주는 경우 이를 플레이어에게 보여주는 출력을 제공합니다.
타임 트라이얼: 피자 배달 게임 튜토리얼의 이번 단계를 마치면 게임 루프를 만들고 게임의 완료 및 실패 스테이트를 정의하는 방법을 배우게 됩니다.
아래에는 타임 트라이얼 피자 배달 게임에서 게임 루프를 만드는 의사코드가 나와 있습니다.
loop:
race:
loop:
SelectNextPickupZone
WaitForPlayerToCompletePickupZone
block:
WaitForFirstPickup
SelectNextDeliveryZone
WaitForPlayerToCompleteDeliveryZone
이 루프는 카운트다운 타이머가 완료되거나 게임에 예상치 못한 오류가 발생하면 종료됩니다.
코어 게임 루프 만들기
다음 단계에 따라 game_coordinator_device.verse 파일을 업데이트합니다.
private지정자와suspends지정자가 있는 새 메서드PickupDeliveryLoop()를 생성합니다. 앞서OnBegin()에 만들었던 루프를 새로운 메서드로 옮깁니다.VerseOnBegin<override>;()<suspends> : void = SetupZones() PickupDeliveryLoop() PickupDeliveryLoop<private>()<suspends> : void = var PickupLevel : int = 0 loop: if (PickupZone : base_zone = PickupZoneSelectors[PickupLevel].SelectNext[]):태그 배열의 길이로 픽업 레벨의 최대 수를 결정하고, 픽업 레벨이 최대 픽업 레벨보다 작은 동안에는 플레이어가 픽업 구역을 완료할 때마다
PickupLevel을 높입니다.VerseOnBegin<override>;()<suspends> : void = SetupZones() PickupDeliveryLoop() PickupDeliveryLoop<private>()<suspends> : void = PickupZonesTags : []pickup_zone_tag = array{pickup_zone_level_1_tag{}, pickup_zone_level_2_tag{}, pickup_zone_level_3_tag{}} MaxPickupLevel := PickupZonesTags.Length - 1 var PickupLevel : int = 0플레이어가 첫 픽업을 완료하면 배달 구역이 활성화되지만, 원한다면 플레이어가 배달 구역에 가기 전에 계속해서 아이템을 픽업할 수 있어야 합니다. 이렇게 하려면 픽업 구역 코드와 배달 구역 코드가 동시에 발생해야 합니다. 이 예시에서는 다음과 같은 이유로 race 동시성 표현식을 사용합니다.
플레이어가 배달을 마치면 배달 블록이 픽업 구역 루프를 취소해야 합니다.
픽업 루프에 문제가 있는 경우 픽업 구역 루프가 배달 블록을 취소해야 합니다.
또한 구역 비활성화를 약간 수정해야 합니다. 루프나 배달 블록이 취소되는 경우, 스크립트가 구역에서 완료를 기다리고 있다면
DeactivateZone()이 호출되지 않아야 합니다.구역 비활성화 줄이 절대 실행되지 않으므로 구역이 계속 활성 상태로 유지되며 버그를 일으킵니다.
이 문제를 고치기 위해 defer 표현식을 사용할 수 있습니다.
defer는 포함된 표현식의 실행을defer가 있는 스코프가 끝날 때까지 지연시킵니다.defer는 프로그램 컨트롤이 (함수가 끝나서) 스코프에서 일반적인 방식으로 나가거나, (return 또는 break로) 조기에 나가거나, (race같은) 비동기 표현식 또는 취소된 동시적 작업에 의해서 나가면 실행됩니다. 무슨 일이 일어나도 가장 마지막에 실행되도록 작업을 대기열로 지정하는 것과 같습니다. 각DeactivateZone호출을defer로 감싸고, 대응하는ZoneCompletedEvent.Await()앞으로 옮깁니다.VersePickupDeliveryLoop<private>()<suspends>; : void = PickupZonesTags : []pickup_zone_tag = array{pickup_zone_level_1_tag{}, pickup_zone_level_2_tag{}, pickup_zone_level_3_tag{}} MaxPickupLevel := PickupZonesTags.Length - 1 var PickupLevel : int = 0 race: loop: if (PickupZone : base_zone = PickupZoneSelectors[PickupLevel].SelectNext[]): PickupZone.ActivateZone()이전 예시에서는 배달 구역과 픽업 구역이 동시에 활성화되었지만, 배달 구역은 첫 픽업이 완료된 후에 활성화되어야 합니다. 이를 위해 이벤트를 추가하여 배달 구역이 활성화에 앞서 이벤트를 기다리게 합니다.
VerseOnBegin<override>;()<suspends> : void = SetupZones() PickupDeliveryLoop() PickupDeliveryLoop<private>()<suspends> : void = PickupZonesTags : []pickup_zone_tag = array{pickup_zone_level_1_tag{}, pickup_zone_level_2_tag{}, pickup_zone_level_3_tag{}} MaxPickupLevel := PickupZonesTags.Length - 1 FirstPickupZoneCompletedEvent := event(){}픽업 구역/배달 구역 race 표현식을 게임이 끝날 때까지 루프시켜서 플레이어가 아이템을 계속 픽업하고 배달할 수 있게 합니다.
VerseOnBegin<override>;()<suspends> : void = SetupZones() PickupDeliveryLoop() PickupDeliveryLoop<private>()<suspends> : void = PickupZonesTags : []pickup_zone_tag = array{pickup_zone_level_1_tag{}, pickup_zone_level_2_tag{}, pickup_zone_level_3_tag{}} MaxPickupLevel := PickupZonesTags.Length - 1 FirstPickupZoneCompletedEvent := event(){}작성한 game_coordinator_device.verse 파일이 다음과 같아야 합니다.
Verseusing { /Verse.org/Simulation } using { /Fortnite.com/Devices } using { /Fortnite.com/Vehicles } using { /Fortnite.com/Characters } using { /Fortnite.com/Playspaces } using { /Verse.org/Random } using { /UnrealEngine.com/Temporary/Diagnostics } using { /UnrealEngine.com/Temporary/SpatialMath } using { /UnrealEngine.com/Temporary/Curves } using { /Verse.org/Simulation/Tags }
Verse 파일을 저장하고 코드를 컴파일한 뒤 레벨을 플레이테스트합니다.
레벨을 플레이테스트하면 게임 시작 시 및 플레이어가 아이템을 픽업 시 아이템 생성 장치 중 하나가 활성화됩니다. 플레이어가 첫 아이템을 픽업하면 아이템 생성 장치가 비활성화되고 회수 영역 장치가 활성화됩니다. 이는 게임을 수동으로 종료할 때까지 계속됩니다.
게임 루프의 완료 및 실패 스테이트 정의하기
코어 게임 루프를 만들었으니, 게임 루프의 완료 및 실패 스테이트를 정의해야 합니다. 이 게임은 다음과 같은 경우에 끝납니다.
카운트다운이 종료될 때
게임 루프에 문제가 생겼을 때
다음 단계에 따라 게임의 완료 및 실패 스테이트를 구성합니다.
private지정자가 있는game_coordinator_device에서 countdown_timer 클래스의 인스턴스를 생성합니다.game_coordinator_device<public> := class(creative_device): @editable EndGame<public> : end_game_device = end_game_device{} var CountdownTimer<private> : countdown_timer = countdown_timer{}Versegame_coordinator_device<public> := class(creative_device): @editable EndGame<public> : end_game_device = end_game_device{} var CountdownTimer<private> : countdown_timer = countdown_timer{}countdown_timer의 생성자에 플레이어 레퍼런스가 필요하므로, 이 싱글 플레이어 게임의 플레이어 레퍼런스를 저장할 플레이어 변수 옵션을 추가하고 플레이어 레퍼런스를 구할FindPlayer()함수를 생성합니다. 구역을 설정하기 전에OnBegin()에서FindPlayer()를 호출합니다.Versegame_coordinator_device<public> := class(creative_device): @editable EndGame<public> : end_game_device = end_game_device{} var CountdownTimer<private> : countdown_timer = countdown_timer{} var MaybePlayer<private> : ?player = false OnBegin<override>()<suspends> : void = FindPlayer() SetupZones() FindPlayer<private>() : void = # Since this is a single player experience, the first player (at index 0) # should be the only one available. if (FirstPlayer := GetPlayspace().GetPlayers()[0]): set MaybePlayer = option{FirstPlayer} Logger.Print("Player found") else: # Log an error if we can't find a player. # This shouldn't happen because at least one player is always present. Logger.Print("Can't find valid player", ?Level := log_level.Error)카운트다운 타이머가 끝나기를 기다렸다가 게임 종료 장치를 활성화하는
HandleCountdownEnd()함수를 생성합니다.VerseHandleCountdownEnd<private>(InPlayer : agent)<suspends> : void = CountdownTimer.CountdownEndedEvent.Await() EndGame.Activate(InPlayer)StartGame()함수를 생성하고OnBegin()에서SetupZones()뒤에 이 함수를 호출합니다. 이 함수는 다음을 수행해야 합니다.카운트다운 타이머를 초기화합니다.
Versegame_coordinator_device<public> := class(creative_device): # How long the countdown timer will start counting down from. @editable InitialCountdownTime<public> : float = 30.0 @editable EndGame<public> : end_game_device = end_game_device{} OnBegin<override>()<suspends> : void = FindPlayer() SetupZones() StartGame() StartGame<private>()<suspends> : void = Logger.Print("Trying to start the game...") <# We construct a new countdown_timer that'll countdown from InitialCountdownTime once started. The countdown_timer requires a player to show their UI to. We should have a valid player by now. #> if (ValidPlayer := MaybePlayer?): Logger.Print("Valid player, starting game...") set CountdownTimer = MakeCountdownTimer(InitialCountdownTime, ValidPlayer) CountdownTimer.StartCountdown() else: Logger.Print("Can't find valid player. Aborting game start", ?Level := log_level.Error)race표현식을HandleCountdownEnd(ValidPlayer)및PickupDeliveryLoop()호출에 모두 사용하여 다음을 수행합니다.카운트다운이 끝나면 게임 루프가 중단됩니다.
또는 게임 루프가 중단되면 카운트다운이 취소됩니다.
VerseStartGame<private>()<suspends> : void = Logger.Print("Trying to start the game...") <# We construct a new countdown_timer that'll countdown from InitialCountdownTime once started. The countdown_timer requires a player to show their UI to. We should have a valid player by now. #> if (ValidPlayer := MaybePlayer?): Logger.Print("Valid player, starting game...") set CountdownTimer = MakeCountdownTimer(InitialCountdownTime, ValidPlayer) CountdownTimer.StartCountdown() # We wait for the countdown to end. # At the same time, we also run the Pickup and Delivery game loop that constitutes the core gameplay. race: HandleCountdownEnd(ValidPlayer) PickupDeliveryLoop() else: Logger.Print("Can't find valid player. Aborting game start", ?Level := log_level.Error)
이제 game_coordinate_device.verse 파일은 다음과 같은 모습이어야 합니다.
Verseusing { /Verse.org/Simulation } using { /Fortnite.com/Devices } using { /Fortnite.com/Vehicles } using { /Fortnite.com/Characters } using { /Fortnite.com/Playspaces } using { /Verse.org/Random } using { /UnrealEngine.com/Temporary/Diagnostics } using { /UnrealEngine.com/Temporary/SpatialMath } using { /UnrealEngine.com/Temporary/Curves } using { /Verse.org/Simulation/Tags }Verse 파일을 저장하고 코드를 컴파일한 뒤 레벨을 플레이테스트합니다.
레벨을 플레이테스트하면 게임은 이전 섹션과 동일하게 작동하지만 이제는 카운트다운이 끝나거나 게임 루프에 문제가 생기면 게임을 끝내는 타이머가 있습니다.