paint-brush
Dạy nhân vật của bạn chạy trong ngọn lửatừ tác giả@hacker7867369
3,576 lượt đọc
3,576 lượt đọc

Dạy nhân vật của bạn chạy trong ngọn lửa

từ tác giả Eugene Kleshnin11m2023/02/28
Read on Terminal Reader

dài quá đọc không nổi

Chuỗi bài viết này là hành trình tìm hiểu Flame (và Flutter) của tôi và xây dựng một trò chơi platformer cơ bản. Tôi sẽ cố gắng trình bày khá chi tiết, vì vậy nó sẽ hữu ích cho bất kỳ ai mới bắt đầu tìm hiểu về Flame hoặc nhà phát triển trò chơi nói chung. Trong phần đầu tiên, chúng ta sẽ tạo một dự án Flame mới, tải tất cả nội dung, thêm nhân vật người chơi và dạy anh ta cách chạy.
featured image - Dạy nhân vật của bạn chạy trong ngọn lửa
Eugene Kleshnin HackerNoon profile picture
0-item

Tôi luôn muốn làm trò chơi điện tử. Ứng dụng Android đầu tiên giúp tôi có được công việc đầu tiên là một trò chơi đơn giản, được tạo bằng các chế độ xem của Android. Sau đó, đã có rất nhiều nỗ lực để tạo ra một trò chơi phức tạp hơn bằng cách sử dụng công cụ trò chơi, nhưng tất cả đều thất bại do thiếu thời gian hoặc sự phức tạp của khung. Nhưng khi tôi lần đầu tiên nghe nói về Flame engine, dựa trên Flutter, tôi đã ngay lập tức bị thu hút bởi sự đơn giản và hỗ trợ đa nền tảng của nó, vì vậy tôi quyết định thử xây dựng một trò chơi với nó.


Tôi muốn bắt đầu với một cái gì đó đơn giản, nhưng vẫn đầy thách thức, để cảm nhận về động cơ. Chuỗi bài viết này là hành trình tìm hiểu Flame (và Flutter) của tôi và xây dựng một trò chơi platformer cơ bản. Tôi sẽ cố gắng làm cho nó khá chi tiết, vì vậy nó sẽ hữu ích cho bất kỳ ai mới bắt đầu tìm hiểu về Flame hoặc nhà phát triển trò chơi nói chung.


Trong suốt 4 bài viết, tôi sẽ xây dựng một trò chơi cuộn bên 2d, bao gồm:

  • Một nhân vật có thể chạy và nhảy

  • Một camera đi theo người chơi

  • Bản đồ cấp độ cuộn, với mặt đất và nền tảng

  • Nền thị sai

  • Tiền xu mà người chơi có thể thu thập và HUD hiển thị số lượng xu

  • giành chiến thắng màn hình


Hoàn thành trò chơi


Trong phần đầu tiên, chúng ta sẽ tạo một dự án Flame mới, tải tất cả nội dung, thêm nhân vật người chơi và dạy anh ta cách chạy.


Thiết lập dự án

Đầu tiên, hãy tạo một dự án mới. Hướng dẫn trò chơi Bare Flame chính thức thực hiện rất tốt việc mô tả tất cả các bước để thực hiện điều đó, vì vậy chỉ cần làm theo nó.

Một điều cần bổ sung: khi bạn đang thiết lập tệp pubspec.yaml , bạn có thể cập nhật các phiên bản thư viện lên phiên bản mới nhất hiện có hoặc để nguyên như vậy vì dấu mũ (^) trước một phiên bản sẽ đảm bảo ứng dụng của bạn sử dụng phiên bản không mới nhất. -phá bản. ( cú pháp dấu mũ )

Nếu bạn làm theo tất cả các bước, tệp main.dart của bạn sẽ trông như thế này:

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

Tài sản

Trước khi tiếp tục, chúng ta cần chuẩn bị tài sản sẽ được sử dụng cho trò chơi. Nội dung là hình ảnh, hoạt ảnh, âm thanh, v.v. Vì mục đích của loạt bài này, chúng tôi sẽ chỉ sử dụng những hình ảnh còn được gọi là họa tiết trong nhà phát triển trò chơi.


Cách đơn giản nhất để xây dựng cấp độ platformer là sử dụng bản đồ ô xếp và họa tiết ô xếp. Nó có nghĩa là cấp độ về cơ bản là một lưới, trong đó mỗi ô cho biết đối tượng/mặt đất/nền tảng mà nó đại diện. Sau đó, khi trò chơi đang chạy, thông tin từ mỗi ô được ánh xạ tới ô xếp hình tương ứng.


Đồ họa trò chơi được xây dựng bằng kỹ thuật này có thể thực sự phức tạp hoặc rất đơn giản. Ví dụ, trong Super Mario bros, bạn thấy rằng rất nhiều yếu tố đang lặp lại. Đó là bởi vì, đối với mỗi ô nền trong lưới trò chơi, chỉ có một hình ảnh nền đại diện cho nó. Chúng tôi sẽ làm theo cách tiếp cận tương tự và chuẩn bị một hình ảnh duy nhất cho từng đối tượng tĩnh mà chúng tôi có.


Cấp độ được xây dựng bằng gạch lặp lại


Chúng tôi cũng muốn một số đối tượng, chẳng hạn như nhân vật người chơi và đồng xu được làm động. Hoạt hình thường được lưu trữ dưới dạng một loạt ảnh tĩnh, mỗi ảnh đại diện cho một khung hình. Khi hoạt ảnh đang phát, các khung hình nối tiếp nhau, tạo ảo giác về đối tượng đang chuyển động.


Bây giờ câu hỏi quan trọng nhất là lấy tài sản ở đâu. Tất nhiên, bạn có thể tự vẽ chúng hoặc giao chúng cho một nghệ sĩ. Ngoài ra, có rất nhiều nghệ sĩ tuyệt vời đã đóng góp nội dung trò chơi cho mã nguồn mở. Tôi sẽ sử dụng gói Tài sản Arcade Platformer của GrafxKid .


Thông thường, nội dung hình ảnh có hai dạng: trang sprite và sprite đơn. Cái trước là một hình ảnh lớn, chứa tất cả nội dung trò chơi trong một. Sau đó, các nhà phát triển trò chơi chỉ định vị trí chính xác của nhân vật được yêu cầu và công cụ trò chơi sẽ cắt nó khỏi trang tính. Đối với trò chơi này, tôi sẽ sử dụng các họa tiết đơn lẻ (ngoại trừ hoạt ảnh, việc giữ chúng dưới dạng một hình ảnh sẽ dễ dàng hơn) vì tôi không cần tất cả nội dung được cung cấp trong bảng họa tiết.



Một sprite duy nhất đại diện cho mặt đất


Bảng Sprite với 6 sprite cho hoạt ảnh của trình phát


Chạy hoạt hình


Cho dù bạn đang tự tạo các hình vẽ hoặc nhận chúng từ một nghệ sĩ, bạn có thể cần phải cắt chúng để làm cho chúng phù hợp hơn với công cụ trò chơi. Bạn có thể sử dụng các công cụ được tạo riêng cho mục đích đó (như trình đóng gói kết cấu) hoặc bất kỳ trình chỉnh sửa đồ họa nào. Tôi đã sử dụng Adobe Photoshop, bởi vì, trong bảng sprite này, các sprite có khoảng cách không bằng nhau giữa chúng, điều này khiến các công cụ tự động khó trích xuất hình ảnh, vì vậy tôi phải thực hiện thủ công.


Bạn cũng có thể muốn tăng kích thước của nội dung, nhưng nếu đó không phải là hình ảnh véc-tơ, hình ảnh kết quả có thể bị mờ. Một giải pháp thay thế mà tôi thấy rất hiệu quả đối với nghệ thuật pixel là sử dụng phương pháp thay đổi kích Nearest Neighbour (hard edges) trong Photoshop (hoặc Nội suy được đặt thành Không có trong Gimp). Nhưng nếu tài sản của bạn chi tiết hơn, nó có thể sẽ không hoạt động.


Với những lời giải thích rõ ràng, hãy tải xuống nội dung tôi đã chuẩn bị hoặc chuẩn bị nội dung của riêng bạn và thêm chúng vào thư mục assets/images trong dự án của bạn.


Bất cứ khi nào bạn thêm nội dung mới, bạn cần đăng ký chúng trong tệp pubspec.yaml như sau:

 flutter: assets: - assets/images/

Và mẹo cho tương lai: nếu bạn đang cập nhật nội dung đã đăng ký, bạn cần khởi động lại trò chơi để xem các thay đổi.


Bây giờ, hãy thực sự tải nội dung vào trò chơi. Tôi muốn có tất cả các tên nội dung ở một nơi, điều này rất phù hợp với một trò chơi nhỏ, vì việc theo dõi mọi thứ và sửa đổi nếu cần sẽ dễ dàng hơn. Vì vậy, hãy tạo một tệp mới trong thư mục 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];


Và sau đó tạo một tệp khác, tệp này sẽ chứa tất cả logic trò chơi trong tương lai: 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 là lớp chính đại diện cho trò chơi của chúng tôi, nó mở rộng FlameGame , lớp trò chơi cơ sở được sử dụng trong công cụ Flame. Lần lượt mở rộng Component - khối xây dựng cơ bản của Flame. Mọi thứ trong trò chơi của bạn, bao gồm hình ảnh, giao diện hay hiệu ứng đều là Thành phần. Mỗi Component có một phương thức không đồng bộ onLoad , được gọi khi khởi tạo thành phần. Thông thường, tất cả logic thiết lập thành phần đều ở đó.


Cuối cùng, chúng tôi đã nhập tệp assets.dart mà chúng tôi đã tạo trước đó và thêm as Assets để khai báo rõ ràng nguồn gốc của các hằng số tài sản của chúng tôi. Và đã sử dụng phương thức images.loadAll để tải tất cả nội dung được liệt kê trong danh sách SPRITES vào bộ đệm hình ảnh trò chơi.


Sau đó, chúng ta cần tạo PlatformerGame mới từ main.dart . Sửa đổi tập tin như sau:

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

Mọi sự chuẩn bị đã xong, và phần thú vị bắt đầu.


Thêm nhân vật người chơi

Tạo một thư mục mới lib/actors/ và một tệp mới theboy.dart bên trong nó. Đây sẽ là thành phần đại diện cho nhân vật người chơi: Cậu bé.

 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 ), ); } }

Lớp mở rộng SpriteAnimationComponent là một thành phần được sử dụng cho các nhân vật hoạt hình và có một mixin HasGameRef cho phép chúng ta tham chiếu đối tượng trò chơi để tải hình ảnh từ bộ đệm của trò chơi hoặc nhận các biến toàn cầu sau này.


Trong phương thức onLoad của chúng tôi, chúng tôi tạo một SpriteAnimation mới từ bảng ma THE_BOY mà chúng tôi đã khai báo trong tệp assets.dart .


Bây giờ hãy thêm người chơi của chúng tôi vào trò chơi! Quay trở lại tệp game.dart và thêm phần sau vào cuối phương thức onLoad :

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

Nếu bạn chạy trò chơi bây giờ, chúng ta sẽ có thể gặp The Boy!


Gặp Chàng Trai

chuyển động của người chơi

Đầu tiên, chúng ta cần thêm khả năng điều khiển The Boy từ bàn phím. Hãy thêm mixin HasKeyboardHandlerComponents vào tệp game.dart .

 class PlatformerGame extends FlameGame with HasKeyboardHandlerComponents


Tiếp theo, hãy quay lại mixin theboy.dartKeyboardHandler :

 class TheBoy extends SpriteAnimationComponent with KeyboardHandler, HasGameRef<PlatformerGame>


Sau đó, thêm một số biến lớp mới vào thành phần 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


Cuối cùng, hãy ghi đè phương thức onKeyEvent cho phép nghe đầu vào bàn phím:

 @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; }

Bây giờ _horizontalDirection bằng 1 nếu người chơi di chuyển sang phải, -1 nếu người chơi di chuyển sang trái và 0 nếu người chơi không di chuyển. Tuy nhiên, chúng tôi chưa thể nhìn thấy nó trên màn hình vì vị trí của người chơi vẫn chưa thay đổi. Hãy khắc phục điều đó bằng cách thêm phương thức update .


Bây giờ tôi cần giải thích vòng lặp trò chơi là gì. Về cơ bản, nó có nghĩa là trò chơi đang chạy trong một vòng lặp vô tận. Trong mỗi lần lặp lại, trạng thái hiện tại được hiển thị trong phương thức Component's render và sau đó một trạng thái mới được tính toán trong phương thức update . Tham số dt trong chữ ký của phương thức là thời gian tính bằng mili giây kể từ lần cập nhật trạng thái cuối cùng. Với ý nghĩ đó, hãy thêm phần sau vào theboy.dart :

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

Đối với mỗi chu kỳ vòng lặp trò chơi, chúng tôi cập nhật vận tốc ngang, sử dụng hướng hiện tại và tốc độ tối đa. Sau đó, chúng tôi thay đổi vị trí sprite với giá trị được cập nhật nhân với dt .


Tại sao chúng ta cần phần cuối cùng? Chà, nếu bạn cập nhật vị trí chỉ với vận tốc, thì sprite sẽ bay vào không gian. Nhưng bạn có thể hỏi chúng ta có thể chỉ sử dụng giá trị tốc độ nhỏ hơn không? Chúng tôi có thể, nhưng cách người chơi di chuyển sẽ khác với tốc độ khung hình trên giây (FPS) khác nhau. Số khung hình (hoặc vòng lặp trò chơi) mỗi giây tùy thuộc vào hiệu suất trò chơi và phần cứng mà trò chơi chạy trên đó. Hiệu suất thiết bị càng tốt, FPS càng cao và người chơi di chuyển càng nhanh. Để tránh điều đó, chúng tôi làm cho tốc độ phụ thuộc vào thời gian trôi qua từ khung hình cuối cùng. Bằng cách đó, sprite sẽ di chuyển tương tự trên bất kỳ FPS nào.


Được rồi, nếu chúng ta chạy trò chơi bây giờ, chúng ta sẽ thấy điều này:


Cậu bé thay đổi vị trí của mình dọc theo trục X


Tuyệt vời, bây giờ hãy làm cho cậu bé quay lại khi đi sang trái. Thêm phần này vào cuối phương thức update :

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


Logic khá đơn giản: chúng tôi kiểm tra xem hướng hiện tại (mũi tên người dùng đang nhấn) có khác với hướng của sprite hay không, sau đó chúng tôi lật sprite dọc theo trục ngang.


Bây giờ, hãy thêm hoạt ảnh đang chạy. Đầu tiên xác định hai biến lớp mới:

 late final SpriteAnimation _runAnimation; late final SpriteAnimation _idleAnimation;


Sau đó cập nhật onLoad như thế này:

 @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; }

Ở đây, chúng tôi đã trích xuất hoạt ảnh nhàn rỗi đã thêm trước đó vào biến lớp và xác định một biến hoạt ảnh chạy mới.


Tiếp theo, hãy thêm một phương thức updateAnimation mới:

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


Và cuối cùng, gọi phương thức này ở cuối phương thức update và chạy trò chơi.

Chạy hoạt hình và lật sprite


Phần kết luận

Đó là nó cho phần đầu tiên. Chúng tôi đã học cách thiết lập trò chơi Flame, nơi tìm nội dung, cách tải chúng vào trò chơi của bạn và cách tạo một nhân vật hoạt hình tuyệt vời và làm cho nhân vật đó di chuyển dựa trên đầu vào bàn phím. Mã cho phần này có thể được tìm thấy trên github của tôi .


Trong bài viết tiếp theo, tôi sẽ đề cập đến cách tạo cấp độ trò chơi bằng cách sử dụng Lát xếp, cách điều khiển camera Flame và thêm nền thị sai. Giữ nguyên!

Tài nguyên

Ở cuối mỗi phần, tôi sẽ thêm danh sách những người sáng tạo tuyệt vời và tài nguyên mà tôi đã học được.