지난 글 - 플러터로 게임 만들어보기 01에 이서서 이번에는 배경 이미지의 움직임 효과와 드래그 액션에 따른 각 상태에 맞는 플레이어 컴포넌트 애니매이션 처리 방법에 대해 알아보겠습니다. 이 글에서 다루는 코드는 다음 Repository에서 확인할 수 있습니다. Flutter_flame_game
일단 지난글에서 main.dart에서 GameWidget을 삽입하는 부분을 Scaffold를 GestureDetector위젯으로 감싸서 처리했는데 Scaffold 구조형식의 앱이 아니기 때문에 다음과 같이 변경했습니다.
[main.dart] void main() async {
WidgetsFlutterBinding.ensureInitialized();
runApp(MaterialApp(
title: 'Flame Game',
home: GameWrapper(MyGame()),
));
}
컴포넌트 움직임 처리지난 글에 배경 Sprite이미지를 가지고 SpriteComponent 를 사용해 3개의 영역을 겹쳐서 배경 처리를 해보았는데 이렇게 만든 배경에 움직임을 더해 이동되는 효과를 처리할 수 있습니다.
SpriteComponent 로 각 영역의 컴포넌트 객체에 position을 위에서 아래로 이동되도록 하여 배경의 움직임 효과 처리를 할 수 있는데 이 처리는 SpriteComponent 의 update(double dt) 메서드에서 구현할 수 있습니다. 지난 글 에서 설명했듯이 Flame의 Game Loop 구조는 onLoad() 이후 Update() 와 Render()가 반복 호출처리 되는데 Update() 메서드는 delta ms 단위로 반복 호출 됩니다. 그렇기 때문에 update(double dt) 메서드에서 컴포넌트의 position 값 변경으로 움직임 처리를 쉽게 구현할 수 있습니다. 이 부분을 코드로 구현한다면 다음과 같이 처리할 수 있습니다. 아래 그림과 같이 position이 Vector2(0, 0) 위치에 배경 컴포넌트가 있을때
@override
void update(double dt) {
super.update(dt);
component.position.x = 0;
component.position.y += (dt * 속력);
}
이렇게 position의 y값에 변화를 주어 움직임 처리가 가능하고 중력 기준으로 가속력 처리또한 다음 처럼 처리할 수 있습니다.
// 중력값
final double gravity = 700;
// 시간
double time = 0;
@override
void update(double dt) {
time += dt;
component.position.x = 0;
component.position.y += (gravity * time * time) / 2;
}
무한 움직임 처리배경 이미지를 무한으로 움직임 처리 하는 방법은 여러가지가 있는데 그중 하나는 배경 이미지 사이즈를 스크린 사이즈의 두배 크기로 설정하고, 이미지가 스크린의 끝 부분 까지 도달 했을때 위치를 초기화 시켜서 무한으로 움직임 처리를 나타낼 수 있습니다.
위 그림과 같은 위치에서 컴포넌트의 y값을 특정 속력만큼 계속 증가 시키고 Vector2(0, 0) 위치가 된다면 다시 초기화 하고 이를 반복합니다.
위와 같은 조건일때 위치 초기화 (position: Vector2(0, 스크린.y)
지금까지 설명한 내용대로 겹쳐있는 3개의 배경 컴포넌트에 대해 각각 위치 설정이 가능하도록 하나의 배경 컴포넌트를 만들어 보겠습니다.
[components/background.dart]
import 'package:flame/components.dart';
import 'package:flame/image_composition.dart';
import 'package:flame_game/main.dart';
class Background extends PositionComponent {
late final BackgroundComponent _background01;
late final BackgroundComponent _background02;
late final BackgroundComponent _background03;
double get background01X => _background01.x;
double get background01Y => _background01.y;
double get background02X => _background02.x;
double get background02Y => _background02.y;
double get background03X => _background03.x;
double get background03Y => _background03.y;
Background(Image spriteImg, Vector2 postion) {
_background01 = BackgroundComponent(
spriteImg, Vector2(290, 0), Vector2(144, 280), postion);
_background02 = BackgroundComponent(
spriteImg, Vector2(144, 0), Vector2(144, 280), postion);
_background03 = BackgroundComponent(
spriteImg, Vector2(0, 0), Vector2(144, 280), postion);
add(_background01);
add(_background02);
add(_background03);
}
@override
void update(double dt) {
super.update(dt);
}
void setPositionBGImg01(double x, double y) {
_background01.position.x = x;
_background01.position.y = y;
}
void setPositionBGImg02(double x, double y) {
_background02.position.x = x;
_background02.position.y = y;
}
void setPositionBGImg03(double x, double y) {
_background03.position.x = x;
_background03.position.y = y;
}
}
class BackgroundComponent extends SpriteComponent {
BackgroundComponent(Image backgroundImag, Vector2 srcPosition,
Vector2 srcSize, Vector2 postion)
: super.fromImage(backgroundImag,
srcPosition: srcPosition,
srcSize: srcSize,
position: postion,
// 배경 이미지 사이즈를 전체 화면 세로 사이즈의 두배로 설정
size: Vector2(
Singleton().screenSize!.x, Singleton().screenSize!.y * 2));
@override
void render(Canvas canvas) {
super.render(canvas);
}
}
이렇게 만든 Background 컴포넌트를 사용해서 position 설정으로 움직임 처리를 다음과 같이 처리할 수 있습니다.
[components/my_world.dart]
import 'package:flame/components.dart';
import 'package:flame_game/components/background.dart';
import 'package:flame_game/game/my_game.dart';
import 'package:flame_game/main.dart';
class MyWorld extends Component with HasGameRef<MyGame> {
late Background _background;
@override
Future<void> onLoad() async {
var backgroundSprites = gameRef.images.fromCache("Backgrounds.png");
_background = Background(backgroundSprites, Vector2(0, -gameRef.size.y));
add(_background);
}
@override
void update(double dt) {
super.update(dt);
_background.setPositionBGImg01(
0, _background.background01Y + (dt * 속도));
if (_background.background01Y >= 0) {
_background.setPositionBGImg01(0, -gameRef.size.y);
}
_background.setPositionBGImg02(
0, _background.background02Y + (dt * 속도));
if (_background.background02Y >= 0) {
_background.setPositionBGImg02(0, -gameRef.size.y);
}
_background.setPositionBGImg03(
0, _background.background03Y + (dt * 속도));
if (_background.background03Y >= 0) {
_background.setPositionBGImg03(0, -gameRef.size.y);
}
}
}
[game/my_game.dart]
late MyWorld world;
@override
onLoad() async {
await super.onLoad();
world = MyWorld();
add(world);
}
[결과 화면]
SpriteAnimationGroupComponentSpriteAnimationGroupComponent 은 여러개의 SpriteAnimationComponent 를 각 상태별로 그룹 관리 할 수 있는 컴포넌트 입니다. 앞서 구현해 보았던 플레이어의 Sprite 이미지는 직진, 좌, 우 새개의 동작 상태로 애니매이션을 표현할 수 있습니다.
지난번 구현에서 SpriteAnimationComponent 를 사용해 직진 상태에 해당되는 이미지를 연속적으로 표현하여 애니매이션 처리를 하였는데 좌, 우 도 동일하게 구현해서 그룹으로 관리하고 상황에 맞게 어떤 상태를 표시할지 기능을 제공합니다. 위 플레이어 Sprite 이미지를 이용해서 다음과 같이 SpriteAnimation 객체를 생성 합니다.
var playerImage = Flame.images.fromCache("Player.png");
List<Sprite> spritesGo = [
Sprite(playerImage, srcPosition: Vector2(0, 0), srcSize: Vector2(24, 24)),
Sprite(playerImage,
srcPosition: Vector2(25, 0), srcSize: Vector2(24, 24)),
Sprite(playerImage,
srcPosition: Vector2(50, 0), srcSize: Vector2(24, 24)),
Sprite(playerImage,
srcPosition: Vector2(75, 0), srcSize: Vector2(24, 24)),
];
List<Sprite> spritesLeft = [
Sprite(playerImage,
srcPosition: Vector2(2, 50), srcSize: Vector2(24, 24)),
Sprite(playerImage,
srcPosition: Vector2(27, 50), srcSize: Vector2(24, 24)),
Sprite(playerImage,
srcPosition: Vector2(52, 50), srcSize: Vector2(24, 24)),
Sprite(playerImage,
srcPosition: Vector2(77, 50), srcSize: Vector2(24, 24)),
];
List<Sprite> spritesRight = [
Sprite(playerImage,
srcPosition: Vector2(1, 25), srcSize: Vector2(24, 24)),
Sprite(playerImage,
srcPosition: Vector2(26, 25), srcSize: Vector2(24, 24)),
Sprite(playerImage,
srcPosition: Vector2(51, 25), srcSize: Vector2(24, 24)),
Sprite(playerImage,
srcPosition: Vector2(76, 25), srcSize: Vector2(24, 24)),
];
var animatedPlayer_go =
SpriteAnimation.spriteList(spritesGo, stepTime: 0.15);
var animatedPlayer_left =
SpriteAnimation.spriteList(spritesLeft, stepTime: 0.15);
var animatedPlayer_right =
SpriteAnimation.spriteList(spritesRight, stepTime: 0.15);
참고 Flame.images.fromCache 부분은 앱 처음 시작시 main에서 Flame.images.loadAll() 메서드로 이미지 로드 처리 후 Cache로 불러왔습니다.
그리고 다음과 같이 go, left, right에 해당되는 SpriteAnimation 객체를 SpriteAnimationGroup 컴포넌트로 처리합니다.
enum PlayerDirection { go, left, right }
final player = SpriteAnimationGroupComponent<PlayerDirection>(
animations: {
PlayerDirection.go: animatedPlayer_go,
PlayerDirection.left: animatedPlayer_left,
PlayerDirection.right: animatedPlayer_right,
},
current: PlayerDirection.go,
);
[game/my_game.dart]
late MyWorld world;
@override
onLoad() async {
await super.onLoad();
world = MyWorld();
// 하단 중앙에 위치
player = Player(position: Vector2((size[0] / 2) - 40, size[1] - 70));
add(world);
add(player);
}
이렇게 여러개의 SpriteAnimation 객체를 관리해서 current속성으로 어떤 애니매이션을 표현할지 지정할 수 있습니다. HasDraggableComponentsHasDraggableComponents 컴포넌트는 드래그 액션이 필요할때 관련 이벤트를 제공해 주는 컴포넌트 입니다. 또한 반드시 FlameGame 컴포넌트에서 mixin으로 사용할 수 있습니다. HasDraggableComponents 컴포넌트가 사용되면 드래그 액션에 대한 이벤트를 통보 받을 수 있는데 드래그 이벤트관련 처리가 필요한 컴포넌트에 DragCallbacks mixin 해서 관련 이벤트 처리가 가능합니다.
여기서는 위에서 구현한 플레이어 컴포넌트가 좌/우 로 드래그 되었을때 플레이어 컴포넌트의 애니매이션 상태를 go, left, right로 처리해보겠습니다. 제일 먼저 FlameGame 를 사용하는 게임 위젯에 HasDraggableComponents 를 mixin 시킵니다. 단순히 이 처리만으로 이제 드래그 액션 사용이 가능하게 됩니다.
플레이어 컴포넌트가 드래그 대상이고 관련 이벤트 처리가 되어야 하기 때문에 플레이어 컴포넌트에 DragCallbacks 를 mixin 시킵니다. 그러면 onDragStart(DragStartEvent event), onDragUpdate(DragUpdateEvent event), onDragEnd(DragEndEvent event) 메서드들을 오버라이드해서 관련 이벤트 처리를 할 수 있습니다. 플레이어 컴포넌트가 드래그중일때 onDragUpdate(DragUpdateEvent event) 메서드에서 DragUpdateEvent 클래스로 드래그 위치 정보를 가져올 수 있기 때문에 여기서 드래그 방향에 따른 플레이어 애니메이션 표시를 처리할 수 있습니다.
@override
void onDragUpdate(DragUpdateEvent event) {
// 좌측 범위 초과 금지
if (position.x <= 150) {
position.x += 10;
return;
}
// 우측 범위 초과 금지
if (position.x >= gameRef.size[0] - 150) {
position.x -= 10;
return;
}
// 드래그 하지 않는 경우
if (event.delta.x == 0) {
_playerComponent.current = PlayerDirection.go;
}
// 우측으로 드래그중인 경우
else if (event.delta.x > 0) {
_playerComponent.current = PlayerDirection.right;
}
// 좌측으로 드래그중인 경우
else if (event.delta.x < 0) {
_playerComponent.current = PlayerDirection.left;
}
// 드래그 방향에 따라 이동
position.x += event.delta.x;
}
[결과 화면]
이렇게 해서 플레이어 컴포넌트의 상태별 애니메이션 처리와 드래그 액션 처리의 전체 코드 부분은 다음과 같이 구현 됩니다.
[components/player.dart]
import 'package:flame/components.dart';
import 'package:flame/experimental.dart';
import 'package:flame/flame.dart';
import 'package:flame_game/game/my_game.dart';
enum PlayerDirection { go, left, right }
class Player extends PositionComponent with DragCallbacks, HasGameRef<MyGame> {
late final PlayerComponent _playerComponent;
bool _isDragging = false;
Player({super.position})
: super(
size: Vector2(35, 35),
anchor: Anchor.center,
) {
var playerImage = Flame.images.fromCache("Player.png");
List<Sprite> spritesGo = [
Sprite(playerImage, srcPosition: Vector2(0, 0), srcSize: Vector2(24, 24)),
Sprite(playerImage,
srcPosition: Vector2(25, 0), srcSize: Vector2(24, 24)),
Sprite(playerImage,
srcPosition: Vector2(50, 0), srcSize: Vector2(24, 24)),
Sprite(playerImage,
srcPosition: Vector2(75, 0), srcSize: Vector2(24, 24)),
];
List<Sprite> spritesLeft = [
Sprite(playerImage,
srcPosition: Vector2(2, 50), srcSize: Vector2(24, 24)),
Sprite(playerImage,
srcPosition: Vector2(27, 50), srcSize: Vector2(24, 24)),
Sprite(playerImage,
srcPosition: Vector2(52, 50), srcSize: Vector2(24, 24)),
Sprite(playerImage,
srcPosition: Vector2(77, 50), srcSize: Vector2(24, 24)),
];
List<Sprite> spritesRight = [
Sprite(playerImage,
srcPosition: Vector2(1, 25), srcSize: Vector2(24, 24)),
Sprite(playerImage,
srcPosition: Vector2(26, 25), srcSize: Vector2(24, 24)),
Sprite(playerImage,
srcPosition: Vector2(51, 25), srcSize: Vector2(24, 24)),
Sprite(playerImage,
srcPosition: Vector2(76, 25), srcSize: Vector2(24, 24)),
];
var animatedPlayer_go =
SpriteAnimation.spriteList(spritesGo, stepTime: 0.15);
var animatedPlayer_left =
SpriteAnimation.spriteList(spritesLeft, stepTime: 0.15);
var animatedPlayer_right =
SpriteAnimation.spriteList(spritesRight, stepTime: 0.15);
_playerComponent = PlayerComponent<PlayerDirection>({
PlayerDirection.go: animatedPlayer_go,
PlayerDirection.left: animatedPlayer_left,
PlayerDirection.right: animatedPlayer_right
});
_playerComponent.current = PlayerDirection.go;
add(_playerComponent);
}
@override
void update(double dt) {
super.update(dt);
}
@override
void onDragStart(DragStartEvent event) {
_isDragging = true;
priority = 100;
}
@override
void onDragUpdate(DragUpdateEvent event) {
if (!_isDragging) {
return;
}
// 좌측 범위 초과 금지
if (position.x <= 150) {
position.x += 10;
return;
}
// 우측 범위 초과 금지
if (position.x >= gameRef.size[0] - 150) {
position.x -= 10;
return;
}
// 드래그 하지 않는 경우
if (event.delta.x == 0) {
_playerComponent.current = PlayerDirection.go;
}
// 우측으로 드래그중인 경우
else if (event.delta.x > 0) {
_playerComponent.current = PlayerDirection.right;
}
// 좌측으로 드래그중인 경우
else if (event.delta.x < 0) {
_playerComponent.current = PlayerDirection.left;
}
// 드래그 방향에 따라 이동
position.x += event.delta.x;
}
@override
void onDragEnd(DragEndEvent event) {
if (!_isDragging) {
return;
}
_isDragging = false;
_playerComponent.current = PlayerDirection.go;
}
void playerUpdate(PlayerDirection playerDirection) {
_playerComponent.playerUpdate(playerDirection);
}
void setPosition(Vector2 position) {
_playerComponent.position = position;
}
}
class PlayerComponent<T> extends SpriteAnimationGroupComponent<T> {
PlayerComponent(Map<T, SpriteAnimation> playerAnimationMap)
: super(size: Vector2(40, 40), animations: playerAnimationMap);
@override
void update(double dt) {
super.update(dt);
}
void playerUpdate(PlayerDirection playerDirection) {
current = playerDirection as T?;
}
}
지금까지 컴포넌트 애니메이션 처리와 드래그 이벤트 처리 방법에 대해 알아보았습니다. 다음에는 몹 출현 처리와 미사일 표현 그리고 컴포넌트간 충돌 감지 처리 등을 살펴보겠습니다.
위 코드는 다음 Repository에서 확인할 수 있습니다.
Flutter_flame_game
|