paint-brush
화염 속에서 달리도록 캐릭터 교육하기~에 의해@eugene-kleshnin
3,576 판독값
3,576 판독값

화염 속에서 달리도록 캐릭터 교육하기

~에 의해 Eugene Kleshnin11m2023/02/28
Read on Terminal Reader
Read this story w/o Javascript

너무 오래; 읽다

이 기사 시리즈는 Flame(및 Flutter)을 배우고 기본 플랫폼 게임을 구축하는 나의 여정입니다. 꽤 자세하게 설명하려고 노력할 것이므로 Flame이나 게임 개발자 전반에 발을 담그고 있는 모든 사람에게 유용할 것입니다. 첫 번째 부분에서는 새로운 Flame 프로젝트를 만들고, 모든 자산을 로드하고, 플레이어 캐릭터를 추가하고, 달리는 방법을 가르칠 것입니다.

People Mentioned

Mention Thumbnail
featured image - 화염 속에서 달리도록 캐릭터 교육하기
Eugene Kleshnin HackerNoon profile picture
0-item

저는 항상 비디오 게임을 만들고 싶었습니다. 첫 직장을 구하는 데 도움이 된 첫 번째 Android 앱은 Android 보기로 만든 간단한 게임이었습니다. 그 후에도 게임 엔진을 이용하여 좀 더 정교한 게임을 만들려는 시도가 많았으나 시간 부족이나 프레임워크의 복잡성으로 인해 모두 실패했습니다. 하지만 Flutter를 기반으로 하는 Flame 엔진에 대해 처음 들었을 때 저는 그 단순성과 크로스 플랫폼 지원에 즉시 매료되어 이를 사용하여 게임을 만들어 보기로 결정했습니다.


나는 엔진의 느낌을 얻기 위해 간단하지만 여전히 어려운 것부터 시작하고 싶었습니다. 이 기사 시리즈는 Flame(및 Flutter)을 배우고 기본 플랫폼 게임을 구축하는 나의 여정입니다. 꽤 자세하게 설명하려고 노력할 것이므로 Flame이나 게임 개발자 전반에 발을 담그고 있는 모든 사람에게 유용할 것입니다.


4개의 기사를 통해 나는 다음을 포함하는 2D 횡스크롤 게임을 만들 예정입니다.

  • 달리고 점프할 수 있는 캐릭터

  • 플레이어를 따라가는 카메라

  • 지상과 플랫폼이 포함된 스크롤링 레벨 지도

  • 시차 배경

  • 플레이어가 수집할 수 있는 코인과 코인 개수를 표시하는 HUD

  • 승리 화면


완전한 게임


첫 번째 부분에서는 새로운 Flame 프로젝트를 만들고, 모든 자산을 로드하고, 플레이어 캐릭터를 추가하고, 달리는 방법을 가르칠 것입니다.


프로젝트 설정

먼저, 새로운 프로젝트를 생성해보겠습니다. 공식 Bare Flame 게임 튜토리얼은 이를 수행하는 모든 단계를 훌륭하게 설명하므로 그냥 따라가십시오.

한 가지 추가할 점: pubspec.yaml 파일을 설정할 때 라이브러리 버전을 사용 가능한 최신 버전으로 업데이트하거나 그대로 둘 수 있습니다. 버전 앞의 캐럿 기호(^)는 앱이 최신 비 버전을 사용하도록 보장하기 때문입니다. -브레이킹 버전. ( 캐럿 구문 )

모든 단계를 수행했다면 main.dart 파일은 다음과 같아야 합니다:

 import 'package:flame/game.dart'; import 'package:flutter/widgets.dart'; void main() { final game = FlameGame(); runApp(GameWidget(game: game)); }

자산

계속하기 전에 게임에 사용될 자산을 준비해야 합니다. 자산은 이미지, 애니메이션, 사운드 등입니다. 이 시리즈에서는 게임 개발에서 스프라이트라고도 불리는 이미지만 사용합니다.


플랫포머 레벨을 구축하는 가장 간단한 방법은 타일 맵과 타일 스프라이트를 사용하는 것입니다. 이는 레벨이 기본적으로 그리드이며, 각 셀은 그것이 나타내는 객체/지상/플랫폼을 나타냅니다. 나중에 게임이 실행되면 각 셀의 정보가 해당 타일 스프라이트에 매핑됩니다.


이 기술을 사용하여 제작된 게임 그래픽은 매우 정교할 수도 있고 매우 단순할 수도 있습니다. 예를 들어, 슈퍼 마리오 브라더스에서는 많은 요소가 반복되는 것을 볼 수 있습니다. 그 이유는 게임 그리드의 각 지면 타일에 대해 이를 나타내는 지면 이미지가 하나만 있기 때문입니다. 우리는 동일한 접근 방식을 따르고 우리가 가지고 있는 각 정적 개체에 대해 단일 이미지를 준비합니다.


레벨은 반복되는 타일로 구성됩니다.


또한 플레이어 캐릭터나 동전과 같은 일부 개체에 애니메이션이 적용되기를 원합니다. 애니메이션은 일반적으로 각각 단일 프레임을 나타내는 일련의 정지 이미지로 저장됩니다. 애니메이션이 재생되면 프레임이 차례로 이동하여 객체가 움직이는 듯한 착각을 불러일으킵니다.


이제 가장 중요한 질문은 자산을 어디서 얻을 수 있는가입니다. 물론 직접 그릴 수도 있고 아티스트에게 의뢰할 수도 있습니다. 또한 게임 자산을 오픈 소스에 기여한 멋진 아티스트가 많이 있습니다. 나는 GrafxKidArcade Platformer Assets 팩을 사용할 것입니다.


일반적으로 이미지 자산은 스프라이트 시트와 단일 스프라이트의 두 가지 형태로 제공됩니다. 전자는 모든 게임 자산을 하나로 포함하는 큰 이미지입니다. 그런 다음 게임 개발자가 필요한 스프라이트의 정확한 위치를 지정하면 게임 엔진이 이를 시트에서 잘라냅니다. 이 게임에서는 스프라이트 시트에 제공된 모든 자산이 필요하지 않기 때문에 단일 스프라이트를 사용합니다(애니메이션 제외, 하나의 이미지로 유지하는 것이 더 쉽습니다).



땅을 나타내는 단일 스프라이트


플레이어 애니메이션을 위한 6개의 스프라이트가 포함된 스프라이트 시트


애니메이션 실행


스프라이트를 직접 만들거나 아티스트로부터 가져오거나 관계없이 게임 엔진에 더 적합하도록 스프라이트를 슬라이스해야 할 수도 있습니다. 해당 목적을 위해 특별히 제작된 도구(예: 텍스처 패커) 나 그래픽 편집기를 사용할 수 있습니다. 저는 Adobe Photoshop을 사용했습니다. 왜냐하면 이 스프라이트 시트에서 스프라이트 사이의 공간이 동일하지 않아 자동 도구로 이미지를 추출하기 어려워 수동으로 수행해야 했기 때문입니다.


자산의 크기를 늘리고 싶을 수도 있지만 벡터 이미지가 아닌 경우 결과 스프라이트가 흐려질 수 있습니다. 픽셀 아트에 매우 효과적인 한 가지 해결 방법은 Photoshop에서 Nearest Neighbour (hard edges) 크기 조정 방법(또는 Gimp에서 보간을 없음으로 설정)을 사용하는 것입니다. 그러나 자산이 더 자세하다면 아마도 작동하지 않을 것입니다.


설명이 방해가 되지 않는데, 제가 준비한 에셋을 다운로드 받거나 직접 준비한 후 프로젝트의 assets/images 폴더에 추가하세요.


새 자산을 추가할 때마다 다음과 같이 pubspec.yaml 파일에 등록해야 합니다.

 flutter: assets: - assets/images/

그리고 미래를 위한 팁: 이미 등록된 자산을 업데이트하는 경우 변경 사항을 보려면 게임을 다시 시작해야 합니다.


이제 실제로 자산을 게임에 로드해 보겠습니다. 저는 모든 자산 이름을 한 곳에 모아 두는 것을 좋아합니다. 이는 모든 것을 추적하고 필요한 경우 수정하기가 더 쉽기 때문에 소규모 게임에 적합합니다. 이제 lib 디렉터리에 assets.dart 라는 새 파일을 만들어 보겠습니다.

 const String THE_BOY = "theboy.png"; const String GROUND = "ground.png"; const String PLATFORM = "platform.png"; const String MIST = "mist.png"; const String CLOUDS = "clouds.png"; const String HILLS = "hills.png"; const String COIN = "coin.png"; const String HUD = "hud.png"; const List<String> SPRITES = [THE_BOY, GROUND, PLATFORM, MIST, CLOUDS, HILLS, COIN, HUD];


그런 다음 향후 모든 게임 로직을 포함할 또 다른 파일인 game.dart 를 만듭니다.

 import 'package:flame/game.dart'; import 'assets.dart' as Assets; class PlatformerGame extends FlameGame { @override Future<void> onLoad() async { await images.loadAll(Assets.SPRITES); } }


PlatformerGame 은 게임을 나타내는 기본 클래스이며 Flame 엔진에서 사용되는 기본 게임 클래스 FlameGame 확장합니다. 이는 결국 Flame의 기본 구성 요소인 Component 확장합니다. 이미지, 인터페이스, 효과를 포함한 게임의 모든 것은 구성 요소입니다. 각 Component 에는 구성 요소 초기화 시 호출되는 비동기 메서드 onLoad 있습니다. 일반적으로 모든 구성 요소 설정 논리가 여기에 있습니다.


마지막으로, 이전에 생성하고 as Assets 추가한 assets.dart 파일을 가져와서 자산 상수의 출처를 명시적으로 선언했습니다. 그리고 images.loadAll 메소드를 사용하여 SPRITES 목록에 나열된 모든 자산을 게임 이미지 캐시에 로드했습니다.


그런 다음 main.dart 에서 새 PlatformerGame 만들어야 합니다. 파일을 다음과 같이 수정합니다.

 import 'package:flame/game.dart'; import 'package:flutter/widgets.dart'; import 'game.dart'; void main() { runApp( const GameWidget<PlatformerGame>.controlled( gameFactory: PlatformerGame.new, ), ); }

모든 준비가 완료되고 재미있는 부분이 시작됩니다.


플레이어 캐릭터 추가

새 폴더 lib/actors/ 와 그 안에 새 파일 theboy.dart 만듭니다. 이것은 플레이어 캐릭터인 The Boy를 나타내는 구성 요소가 될 것입니다.

 import '../game.dart'; import '../assets.dart' as Assets; import 'package:flame/components.dart'; class TheBoy extends SpriteAnimationComponent with HasGameRef<PlatformerGame> { TheBoy({ required super.position, // Position on the screen }) : super( size: Vector2.all(48), // Size of the component anchor: Anchor.bottomCenter // ); @override Future<void> onLoad() async { animation = SpriteAnimation.fromFrameData( game.images.fromCache(Assets.THE_BOY), SpriteAnimationData.sequenced( amount: 1, // For now we only need idle animation, so we load only 1 frame textureSize: Vector2.all(20), // Size of a single sprite in the sprite sheet stepTime: 0.12, // Time between frames, since it's a single frame not that important ), ); } }

이 클래스는 애니메이션 스프라이트에 사용되는 구성 요소인 SpriteAnimationComponent 확장하고 게임 개체를 참조하여 게임 캐시에서 이미지를 로드하거나 나중에 전역 변수를 가져올 수 있는 HasGameRef 믹스인을 포함합니다.


onLoad 메소드에서는 assets.dart 파일에 선언한 THE_BOY 스프라이트 시트에서 새로운 SpriteAnimation 생성합니다.


이제 게임에 플레이어를 추가해 보겠습니다! game.dart 파일로 돌아가서 onLoad 메서드 하단에 다음을 추가합니다.

 final theBoy = TheBoy(position: Vector2(size.x / 2, size.y / 2)); add(theBoy);

지금 게임을 실행하면 더보이를 만날 수 있을 텐데요!


소년을 만나다

플레이어의 움직임

먼저, 키보드에서 The Boy를 제어하는 기능을 추가해야 합니다. game.dart 파일에 HasKeyboardHandlerComponents 믹스인을 추가해 보겠습니다.

 class PlatformerGame extends FlameGame with HasKeyboardHandlerComponents


다음으로 theboy.dartKeyboardHandler 믹스인으로 돌아가겠습니다.

 class TheBoy extends SpriteAnimationComponent with KeyboardHandler, HasGameRef<PlatformerGame>


그런 다음 TheBoy 구성 요소에 몇 가지 새로운 클래스 변수를 추가합니다.

 final double _moveSpeed = 300; // Max player's move speed int _horizontalDirection = 0; // Current direction the player is facing final Vector2 _velocity = Vector2.zero(); // Current player's speed


마지막으로 키보드 입력을 수신할 수 있는 onKeyEvent 메서드를 재정의해 보겠습니다.

 @override bool onKeyEvent(RawKeyEvent event, Set<LogicalKeyboardKey> keysPressed) { _horizontalDirection = 0; _horizontalDirection += (keysPressed.contains(LogicalKeyboardKey.keyA) || keysPressed.contains(LogicalKeyboardKey.arrowLeft)) ? -1 : 0; _horizontalDirection += (keysPressed.contains(LogicalKeyboardKey.keyD) || keysPressed.contains(LogicalKeyboardKey.arrowRight)) ? 1 : 0; return true; }

이제 _horizontalDirection 플레이어가 오른쪽으로 이동하면 1, 플레이어가 왼쪽으로 이동하면 -1, 플레이어가 움직이지 않으면 0이 됩니다. 하지만 아직 플레이어의 위치가 변경되지 않았기 때문에 화면에서는 볼 수 없습니다. update 메소드를 추가하여 이 문제를 해결해 보겠습니다.


이제 게임 루프가 무엇인지 설명해야 합니다. 기본적으로 이는 게임이 무한 루프로 실행되고 있음을 의미합니다. 각 반복에서 현재 상태는 Component's render 메서드에서 렌더링된 다음 update 메서드에서 새 상태가 계산됩니다. 메서드 시그니처의 dt 매개변수는 마지막 상태 업데이트 이후의 시간(밀리초)입니다. 이를 염두에 두고 theboy.dart 에 다음을 추가하세요.

 @override void update(double dt) { super.update(dt); _velocity.x = _horizontalDirection * _moveSpeed; position += _velocity * dt; }

각 게임 루프 주기마다 현재 방향과 최대 속도를 사용하여 수평 속도를 업데이트합니다. 그런 다음 업데이트된 값에 dt 곱하여 스프라이트 위치를 변경합니다.


왜 마지막 부분이 필요한가요? 음, 속도만으로 위치를 업데이트하면 스프라이트는 우주로 날아갈 것입니다. 하지만 더 작은 속도 값을 사용해도 될까요? 가능하지만 플레이어가 움직이는 방식은 FPS(초당 프레임 수) 속도에 따라 달라집니다. 초당 프레임(또는 게임 루프) 수는 게임 성능과 게임이 실행되는 하드웨어에 따라 다릅니다. 장치 성능이 좋을수록 FPS가 높아지고 플레이어가 더 빠르게 움직입니다. 이를 방지하기 위해 마지막 프레임에서 경과된 시간에 따라 속도를 결정합니다. 이렇게 하면 스프라이트가 모든 FPS에서 비슷하게 움직일 것입니다.


좋습니다. 지금 게임을 실행하면 다음과 같이 표시됩니다.


소년은 X축을 따라 위치를 변경합니다.


좋습니다. 이제 소년이 왼쪽으로 갈 때 돌아서도록 하겠습니다. update 메소드의 맨 아래에 다음을 추가하십시오.

 if ((_horizontalDirection < 0 && scale.x > 0) || (_horizontalDirection > 0 && scale.x < 0)) { flipHorizontally(); }


매우 쉬운 논리: 현재 방향(사용자가 누르고 있는 화살표)이 스프라이트의 방향과 다른지 확인한 다음 수평 축을 따라 스프라이트를 뒤집습니다.


이제 실행 중인 애니메이션도 추가해 보겠습니다. 먼저 두 개의 새로운 클래스 변수를 정의합니다.

 late final SpriteAnimation _runAnimation; late final SpriteAnimation _idleAnimation;


그런 다음 onLoad 다음과 같이 업데이트합니다.

 @override Future<void> onLoad() async { _idleAnimation = SpriteAnimation.fromFrameData( game.images.fromCache(Assets.THE_BOY), SpriteAnimationData.sequenced( amount: 1, textureSize: Vector2.all(20), stepTime: 0.12, ), ); _runAnimation = SpriteAnimation.fromFrameData( game.images.fromCache(Assets.THE_BOY), SpriteAnimationData.sequenced( amount: 4, textureSize: Vector2.all(20), stepTime: 0.12, ), ); animation = _idleAnimation; }

여기서는 이전에 클래스 변수에 추가된 대기 애니메이션을 추출하고 새로운 실행 애니메이션 변수를 정의했습니다.


다음으로, 새로운 updateAnimation 메소드를 추가해 보겠습니다.

 void updateAnimation() { if (_horizontalDirection == 0) { animation = _idleAnimation; } else { animation = _runAnimation; } }


그리고 마지막으로 update 메소드 하단에 이 메소드를 호출하여 게임을 실행합니다.

애니메이션 실행 및 스프라이트 뒤집기


결론

이것이 첫 번째 부분입니다. 우리는 Flame 게임을 설정하는 방법, 자산을 찾을 수 있는 위치, 자산을 게임에 로드하는 방법, 멋진 애니메이션 캐릭터를 만들고 키보드 입력에 따라 움직이게 만드는 방법을 배웠습니다. 이 부분의 코드는 내 github에서 찾을 수 있습니다.


다음 기사에서는 Tiled를 사용하여 게임 레벨을 만드는 방법, Flame 카메라를 제어하는 방법, 시차 배경을 추가하는 방법을 다루겠습니다. 계속 지켜봐 주시기 바랍니다!

자원

각 파트의 마지막에는 제가 배운 멋진 제작자와 리소스 목록을 추가하겠습니다.