1. บทนำ
Flame เป็นเอนจินเกม 2 มิติที่สร้างขึ้นจาก Flutter ในโค้ดแล็บนี้ คุณจะได้สร้างเกมที่ได้แรงบันดาลใจจากวิดีโอเกมคลาสสิกยุค 70 อย่าง Breakout ของ Steve Wozniak คุณจะใช้คอมโพเนนต์ของ Flame เพื่อวาดค้างคาว ลูกบอล และก้อนอิฐ คุณจะใช้เอฟเฟกต์ของ Flame เพื่อเคลื่อนไหวการเคลื่อนที่ของค้างคาว และดูวิธีผสานรวม Flame กับระบบการจัดการสถานะของ Flutter
เมื่อเสร็จแล้ว เกมควรมีลักษณะเหมือน GIF แบบเคลื่อนไหวนี้ แม้ว่าจะช้ากว่าเล็กน้อยก็ตาม

สิ่งที่คุณจะได้เรียนรู้
- วิธีการทำงานเบื้องต้นของ Flame โดยเริ่มจาก
GameWidget - วิธีใช้ Game Loop
- วิธีการทำงานของ
Componentใน Flame ซึ่งคล้ายกับWidgetของ Flutter - วิธีจัดการการชนกัน
- วิธีใช้
Effectเพื่อเคลื่อนไหวComponent - วิธีซ้อนทับ Flutter
Widgetบนเกม Flame - วิธีผสานรวม Flame กับการจัดการสถานะของ Flutter
สิ่งที่คุณจะสร้าง
ในโค้ดแล็บนี้ คุณจะได้สร้างเกม 2 มิติโดยใช้ Flutter และ Flame เมื่อเสร็จสมบูรณ์แล้ว เกมของคุณควรเป็นไปตามข้อกำหนดต่อไปนี้
- ทำงานบนทั้ง 6 แพลตฟอร์มที่ Flutter รองรับ ได้แก่ Android, iOS, Linux, macOS, Windows และเว็บ
- รักษาอัตราเฟรมอย่างน้อย 60 FPS โดยใช้ Game Loop ของ Flame
- ใช้ความสามารถของ Flutter เช่น แพ็กเกจ
google_fontsและflutter_animateเพื่อจำลองความรู้สึกของการเล่นเกมอาร์เคดในยุค 80
2. ตั้งค่าสภาพแวดล้อม Flutter
ผู้แก้ไข
เพื่อลดความซับซ้อนของ Codelab นี้ เราจึงถือว่า Visual Studio Code (VS Code) เป็นสภาพแวดล้อมการพัฒนาของคุณ VS Code ใช้งานได้ฟรีและทำงานได้ในแพลตฟอร์มหลักทั้งหมด เราใช้ VS Code สำหรับ Codelab นี้เนื่องจากคำสั่งต่างๆ จะใช้แป้นพิมพ์ลัดเฉพาะของ VS Code โดยค่าเริ่มต้น งานจะตรงไปตรงมายิ่งขึ้น เช่น "คลิกปุ่มนี้" หรือ "กดปุ่มนี้เพื่อทำ X" แทนที่จะเป็น "ดำเนินการที่เหมาะสมในโปรแกรมแก้ไขเพื่อทำ X"
คุณสามารถใช้โปรแกรมแก้ไขใดก็ได้ที่ต้องการ เช่น Android Studio, IntelliJ IDE อื่นๆ, Emacs, Vim หรือ Notepad++ ซึ่งทั้งหมดนี้ใช้ได้กับ Flutter

เลือกเป้าหมายการพัฒนา
Flutter สร้างแอปสำหรับหลายแพลตฟอร์ม แอปของคุณสามารถทำงานบนระบบปฏิบัติการต่อไปนี้
- iOS
- Android
- Windows
- macOS
- Linux
- เว็บ
แนวทางปฏิบัติทั่วไปคือการเลือกระบบปฏิบัติการ 1 ระบบเป็นเป้าหมายการพัฒนา นี่คือระบบปฏิบัติการที่แอปของคุณทำงานระหว่างการพัฒนา

ตัวอย่างเช่น สมมติว่าคุณใช้แล็ปท็อป Windows เพื่อพัฒนาแอป Flutter จากนั้นเลือก Android เป็นเป้าหมายการพัฒนา หากต้องการดูตัวอย่างแอป ให้เชื่อมต่ออุปกรณ์ Android กับแล็ปท็อป Windows ด้วยสาย USB แล้วแอปที่อยู่ระหว่างการพัฒนาจะทำงานบนอุปกรณ์ Android ที่เชื่อมต่อหรือในโปรแกรมจำลอง Android คุณอาจเลือก Windows เป็นเป้าหมายการพัฒนา ซึ่งจะเรียกใช้แอปที่อยู่ระหว่างการพัฒนาเป็นแอป Windows ควบคู่ไปกับโปรแกรมแก้ไข
โปรดเลือกก่อนดำเนินการต่อ คุณเรียกใช้แอปในระบบปฏิบัติการอื่นๆ ได้ทุกเมื่อ การเลือกเป้าหมายการพัฒนาจะช่วยให้ขั้นตอนถัดไปราบรื่นยิ่งขึ้น
ติดตั้ง Flutter
ดูวิธีการติดตั้ง Flutter SDK ที่อัปเดตล่าสุดได้ที่ docs.flutter.dev
วิธีการในเว็บไซต์ Flutter ครอบคลุมการติดตั้ง SDK และเครื่องมือที่เกี่ยวข้องกับเป้าหมายการพัฒนา รวมถึงปลั๊กอินของเอดิเตอร์ สำหรับ Codelab นี้ ให้ติดตั้งซอฟต์แวร์ต่อไปนี้
- Flutter SDK
- Visual Studio Code พร้อมปลั๊กอิน Flutter
- ซอฟต์แวร์คอมไพเลอร์สำหรับเป้าหมายการพัฒนาที่คุณเลือก (คุณต้องมี Visual Studio เพื่อกำหนดเป้าหมาย Windows หรือ Xcode เพื่อกำหนดเป้าหมาย macOS หรือ iOS)
ในส่วนถัดไป คุณจะสร้างโปรเจ็กต์ Flutter แรก
หากต้องการแก้ปัญหา คุณอาจพบว่าคำถามและคำตอบบางส่วนต่อไปนี้ (จาก StackOverflow) มีประโยชน์ในการแก้ปัญหา
คำถามที่พบบ่อย
- ฉันจะค้นหาเส้นทาง SDK ของ Flutter ได้อย่างไร
- ฉันควรทำอย่างไรเมื่อไม่พบคำสั่ง Flutter
- ฉันจะแก้ไขปัญหา "รอคำสั่ง Flutter อื่นให้ปลดล็อกการเริ่มต้น" ได้อย่างไร
- ฉันจะบอก Flutter ว่า Android SDK ของฉันติดตั้งอยู่ที่ใดได้อย่างไร
- ฉันจะจัดการกับข้อผิดพลาดของ Java เมื่อเรียกใช้
flutter doctor --android-licensesได้อย่างไร - ฉันควรทำอย่างไรเมื่อไม่พบเครื่องมือ Android
sdkmanager - ฉันควรจัดการกับข้อผิดพลาด "ไม่มีคอมโพเนนต์
cmdline-tools" อย่างไร - ฉันจะเรียกใช้ CocoaPods ใน Apple Silicon (M1) ได้อย่างไร
- ฉันจะปิดใช้การจัดรูปแบบอัตโนมัติเมื่อบันทึกใน VS Code ได้อย่างไร
3. สร้างโปรเจ็กต์
สร้างโปรเจ็กต์ Flutter แรก
ซึ่งเกี่ยวข้องกับการเปิด VS Code และการสร้างเทมเพลตแอป Flutter ในไดเรกทอรีที่คุณเลือก
- เปิด Visual Studio Code
- เปิด Command Palette (
F1หรือCtrl+Shift+PหรือShift+Cmd+P) แล้วพิมพ์ "flutter new" เมื่อปรากฏขึ้น ให้เลือกคำสั่ง Flutter: New Project

- เลือกล้างข้อมูลในแอปพลิเคชัน เลือกไดเรกทอรีที่จะสร้างโปรเจ็กต์ ซึ่งควรเป็นไดเรกทอรีที่ไม่ต้องใช้สิทธิ์ระดับสูงหรือมีช่องว่างในเส้นทาง เช่น ไดเรกทอรีบ้านหรือ
C:\src\

- ตั้งชื่อโปรเจ็กต์
brick_breakerส่วนที่เหลือของโค้ดแล็บนี้จะถือว่าคุณตั้งชื่อแอปเป็นbrick_breaker

ตอนนี้ Flutter จะสร้างโฟลเดอร์โปรเจ็กต์และ VS Code จะเปิดโฟลเดอร์ดังกล่าว ตอนนี้คุณจะเขียนทับเนื้อหาของ 2 ไฟล์ด้วยโครงสร้างพื้นฐานของแอป
คัดลอกและวางแอปเริ่มต้น
ซึ่งจะเพิ่มโค้ดตัวอย่างที่ระบุไว้ในโค้ดแล็บนี้ลงในแอป
- ในแผงด้านซ้ายของ VS Code ให้คลิกExplorer แล้วเปิดไฟล์
pubspec.yaml

- แทนที่เนื้อหาของไฟล์นี้ด้วยเนื้อหาต่อไปนี้
pubspec.yaml
name: brick_breaker
description: "Re-implementing Woz's Breakout"
publish_to: "none"
version: 0.1.0
environment:
sdk: ^3.8.0
dependencies:
flutter:
sdk: flutter
flame: ^1.28.1
flutter_animate: ^4.5.2
google_fonts: ^6.2.1
dev_dependencies:
flutter_test:
sdk: flutter
flutter_lints: ^5.0.0
flutter:
uses-material-design: true
ไฟล์ pubspec.yaml จะระบุข้อมูลพื้นฐานเกี่ยวกับแอป เช่น เวอร์ชันปัจจุบัน การอ้างอิง และชิ้นงานที่จะจัดส่ง
- เปิดไฟล์
main.dartในไดเรกทอรีlib/

- แทนที่เนื้อหาของไฟล์นี้ด้วยเนื้อหาต่อไปนี้
lib/main.dart
import 'package:flame/game.dart';
import 'package:flutter/material.dart';
void main() {
final game = FlameGame();
runApp(GameWidget(game: game));
}
- เรียกใช้โค้ดนี้เพื่อยืนยันว่าทุกอย่างทำงานได้ อุปกรณ์ควรจะแสดงหน้าต่างใหม่ที่มีเฉพาะพื้นหลังสีดำเปล่า ตอนนี้วิดีโอเกมที่ห่วยที่สุดในโลกแสดงผลที่ 60 FPS แล้ว

4. สร้างเกม
ดูขนาดเกม
เกมที่เล่นใน 2 มิติ (2D) ต้องมีพื้นที่เล่น คุณจะสร้างพื้นที่ที่มีขนาดเฉพาะ แล้วใช้ขนาดเหล่านี้เพื่อกำหนดขนาดขององค์ประกอบอื่นๆ ในเกม
การวางเลย์เอาต์พิกัดในพื้นที่เล่นทำได้หลายวิธี ตามธรรมเนียมหนึ่ง คุณสามารถวัดทิศทางจากกึ่งกลางของหน้าจอโดยมีจุดเริ่มต้น (0,0)ที่กึ่งกลางของหน้าจอ ค่าบวกจะเลื่อนรายการไปทางขวาตามแกน x และขึ้นตามแกน y มาตรฐานนี้ใช้กับเกมส่วนใหญ่ในปัจจุบัน โดยเฉพาะเกมที่มี 3 มิติ
เมื่อสร้างเกม Breakout ต้นฉบับ เราได้ตั้งค่าต้นทางไว้ที่มุมซ้ายบน ทิศทาง x ที่เป็นบวกยังคงเหมือนเดิม แต่ y กลับด้าน ทิศทาง x เป็นบวกไปทางขวาและ y เป็นบวกไปทางด้านล่าง เกมนี้ตั้งค่าจุดเริ่มต้นไว้ที่มุมซ้ายบนเพื่อให้สอดคล้องกับยุคสมัย
สร้างไฟล์ชื่อ config.dart ในไดเรกทอรีใหม่ชื่อ lib/src ไฟล์นี้จะมีค่าคงที่มากขึ้นในขั้นตอนต่อไปนี้
lib/src/config.dart
const gameWidth = 820.0;
const gameHeight = 1600.0;
เกมนี้จะมีความกว้าง 820 พิกเซลและความสูง 1600 พิกเซล พื้นที่เกมจะปรับขนาดให้พอดีกับหน้าต่างที่แสดง แต่คอมโพเนนต์ทั้งหมดที่เพิ่มลงในหน้าจอจะสอดคล้องกับความสูงและความกว้างนี้
สร้าง PlayArea
ในเกม Breakout ลูกบอลจะกระดอนออกจากกำแพงของพื้นที่เล่น หากต้องการรองรับการชน คุณต้องมีPlayAreaก่อน
- สร้างไฟล์ชื่อ
play_area.dartในไดเรกทอรีใหม่ชื่อlib/src/components - เพิ่มข้อมูลต่อไปนี้ลงในไฟล์นี้
lib/src/components/play_area.dart
import 'dart:async';
import 'package:flame/components.dart';
import 'package:flutter/material.dart';
import '../brick_breaker.dart';
class PlayArea extends RectangleComponent with HasGameReference<BrickBreaker> {
PlayArea() : super(paint: Paint()..color = const Color(0xfff2e8cf));
@override
FutureOr<void> onLoad() async {
super.onLoad();
size = Vector2(game.width, game.height);
}
}
Flutter มี Widget Flame ก็มี Component ในขณะที่แอป Flutter ประกอบด้วยการสร้างโครงสร้างแบบต้นไม้ของวิดเจ็ต เกม Flame ประกอบด้วยการดูแลโครงสร้างแบบต้นไม้ของคอมโพเนนต์
ซึ่งเป็นความแตกต่างที่น่าสนใจระหว่าง Flutter กับ Flame แผนผังวิดเจ็ตของ Flutter เป็นคำอธิบายชั่วคราวที่สร้างขึ้นเพื่อใช้ในการอัปเดตRenderObjectเลเยอร์แบบถาวรและเปลี่ยนแปลงได้ คอมโพเนนต์ของ Flame จะคงอยู่และเปลี่ยนแปลงได้ โดยคาดว่านักพัฒนาซอฟต์แวร์จะใช้คอมโพเนนต์เหล่านี้เป็นส่วนหนึ่งของระบบการจำลอง
คอมโพเนนต์ของ Flame ได้รับการเพิ่มประสิทธิภาพเพื่อแสดงกลไกของเกม Codelab นี้จะเริ่มต้นด้วย Game Loop ซึ่งจะกล่าวถึงในขั้นตอนถัดไป
- หากต้องการควบคุมความรก ให้เพิ่มไฟล์ที่มีคอมโพเนนต์ทั้งหมดในโปรเจ็กต์นี้ สร้างไฟล์
components.dartในlib/src/componentsแล้วเพิ่มเนื้อหาต่อไปนี้
lib/src/components/components.dart
export 'play_area.dart';
คําสั่ง export มีบทบาทตรงกันข้ามกับ import ซึ่งจะประกาศฟังก์ชันการทำงานที่ไฟล์นี้แสดงเมื่อนำเข้าไปยังไฟล์อื่น ไฟล์นี้จะมีรายการมากขึ้นเมื่อคุณเพิ่มคอมโพเนนต์ใหม่ในขั้นตอนต่อไปนี้
สร้างเกม Flame
หากต้องการลบเส้นหยักสีแดงจากขั้นตอนก่อนหน้า ให้สร้างคลาสย่อยใหม่สำหรับ FlameGame ของ Flame
- สร้างไฟล์ชื่อ
brick_breaker.dartในlib/srcแล้วเพิ่มโค้ดต่อไปนี้
lib/src/brick_breaker.dart
import 'dart:async';
import 'package:flame/components.dart';
import 'package:flame/game.dart';
import 'components/components.dart';
import 'config.dart';
class BrickBreaker extends FlameGame {
BrickBreaker()
: super(
camera: CameraComponent.withFixedResolution(
width: gameWidth,
height: gameHeight,
),
);
double get width => size.x;
double get height => size.y;
@override
FutureOr<void> onLoad() async {
super.onLoad();
camera.viewfinder.anchor = Anchor.topLeft;
world.add(PlayArea());
}
}
ไฟล์นี้จะประสานงานการดำเนินการของเกม ในระหว่างการสร้างอินสแตนซ์ของเกม โค้ดนี้จะกำหนดค่าเกมให้ใช้การแสดงผลความละเอียดคงที่ เกมจะปรับขนาดให้เต็มหน้าจอที่มีเกมนั้นอยู่ และเพิ่มแถบดำบนและล่างตามที่จำเป็น
คุณแสดงความกว้างและความสูงของเกมเพื่อให้คอมโพเนนต์ย่อย เช่น PlayArea สามารถตั้งค่าขนาดที่เหมาะสมได้
ในonLoadเมธอดที่ลบล้าง โค้ดจะดำเนินการ 2 อย่าง
- กำหนดค่าด้านซ้ายบนเป็นจุดยึดสำหรับช่องมองภาพ โดยค่าเริ่มต้น
viewfinderจะใช้กึ่งกลางของพื้นที่เป็นจุดยึดสำหรับ(0,0) - เพิ่ม
PlayAreaไปยังworldโลกแสดงถึงโลกของเกม โดยจะฉายภาพลูกๆ ทั้งหมดผ่านCameraComponentการเปลี่ยนมุมมอง
ดูการแข่งขันบนหน้าจอ
หากต้องการดูการเปลี่ยนแปลงทั้งหมดที่คุณทำในขั้นตอนนี้ ให้อัปเดตไฟล์ lib/main.dart ด้วยการเปลี่ยนแปลงต่อไปนี้
lib/main.dart
import 'package:flame/game.dart';
import 'package:flutter/material.dart';
import 'src/brick_breaker.dart'; // Add this import
void main() {
final game = BrickBreaker(); // Modify this line
runApp(GameWidget(game: game));
}
หลังจากทำการเปลี่ยนแปลงเหล่านี้แล้ว ให้รีสตาร์ทเกม เกมควรมีลักษณะคล้ายกับรูปต่อไปนี้

ในขั้นตอนถัดไป คุณจะเพิ่มลูกบอลลงในโลกและทำให้ลูกบอลเคลื่อนที่
5. แสดงลูกบอล
สร้างคอมโพเนนต์ลูกบอล
การวางลูกบอลที่เคลื่อนไหวบนหน้าจอต้องสร้างคอมโพเนนต์อีกรายการหนึ่งและเพิ่มลงในโลกของเกม
- แก้ไขเนื้อหาของไฟล์
lib/src/config.dartดังนี้
lib/src/config.dart
const gameWidth = 820.0;
const gameHeight = 1600.0;
const ballRadius = gameWidth * 0.02; // Add this constant
รูปแบบการออกแบบของการกำหนดค่าคงที่ที่มีชื่อเป็นค่าที่ได้จะปรากฏหลายครั้งใน Codelab นี้ ซึ่งจะช่วยให้คุณแก้ไขระดับบนสุดของ gameWidth และ gameHeight เพื่อดูว่ารูปลักษณ์ของเกมเปลี่ยนแปลงไปอย่างไร
- สร้างคอมโพเนนต์
Ballในไฟล์ชื่อball.dartในlib/src/components
lib/src/components/ball.dart
import 'package:flame/components.dart';
import 'package:flutter/material.dart';
class Ball extends CircleComponent {
Ball({
required this.velocity,
required super.position,
required double radius,
}) : super(
radius: radius,
anchor: Anchor.center,
paint: Paint()
..color = const Color(0xff1e6091)
..style = PaintingStyle.fill,
);
final Vector2 velocity;
@override
void update(double dt) {
super.update(dt);
position += velocity * dt;
}
}
ก่อนหน้านี้คุณได้กำหนด PlayArea โดยใช้ RectangleComponent ดังนั้นจึงเป็นเหตุผลที่ว่ามีรูปร่างอื่นๆ อยู่ CircleComponent เช่น RectangleComponent มาจาก PositionedComponent คุณจึงวางลูกบอลบนหน้าจอได้ และที่สำคัญกว่านั้นคือคุณสามารถอัปเดตตำแหน่งของส่วนนี้ได้
คอมโพเนนต์นี้จะแนะนำแนวคิดของvelocityหรือการเปลี่ยนแปลงตำแหน่งเมื่อเวลาผ่านไป ความเร็วเป็นออบเจ็กต์ Vector2 เนื่องจากความเร็วคือทั้งอัตราเร็วและทิศทาง หากต้องการอัปเดตตำแหน่ง ให้ลบล้างเมธอด update ซึ่งเกมเอนจินจะเรียกใช้สำหรับทุกเฟรม dt คือระยะเวลาระหว่างเฟรมก่อนหน้ากับเฟรมนี้ ซึ่งจะช่วยให้คุณปรับตัวตามปัจจัยต่างๆ ได้ เช่น อัตราเฟรมที่แตกต่างกัน (60Hz หรือ 120Hz) หรือเฟรมยาวเนื่องจากการคำนวณมากเกินไป
โปรดอ่านposition += velocity * dtอย่างละเอียด นี่คือวิธีอัปเดตการจำลองการเคลื่อนไหวแบบไม่ต่อเนื่องเมื่อเวลาผ่านไป
- หากต้องการรวมคอมโพเนนต์
Ballไว้ในรายการคอมโพเนนต์ ให้แก้ไขไฟล์lib/src/components/components.dartดังนี้
lib/src/components/components.dart
export 'ball.dart'; // Add this export
export 'play_area.dart';
เพิ่มลูกบอลลงในโลก
คุณมีลูกบอล วางไว้ในโลกและตั้งค่าให้เคลื่อนที่ไปรอบๆ พื้นที่เล่น
แก้ไขไฟล์ lib/src/brick_breaker.dart ดังนี้
lib/src/brick_breaker.dart
import 'dart:async';
import 'dart:math' as math; // Add this import
import 'package:flame/components.dart';
import 'package:flame/game.dart';
import 'components/components.dart';
import 'config.dart';
class BrickBreaker extends FlameGame {
BrickBreaker()
: super(
camera: CameraComponent.withFixedResolution(
width: gameWidth,
height: gameHeight,
),
);
final rand = math.Random(); // Add this variable
double get width => size.x;
double get height => size.y;
@override
FutureOr<void> onLoad() async {
super.onLoad();
camera.viewfinder.anchor = Anchor.topLeft;
world.add(PlayArea());
world.add(
Ball( // Add from here...
radius: ballRadius,
position: size / 2,
velocity: Vector2(
(rand.nextDouble() - 0.5) * width,
height * 0.2,
).normalized()..scale(height / 4),
),
);
debugMode = true; // To here.
}
}
การเปลี่ยนแปลงนี้จะเพิ่มคอมโพเนนต์ Ball ลงใน world หากต้องการตั้งค่า position ของลูกบอลให้อยู่ตรงกลางพื้นที่แสดงผล โค้ดจะลดขนาดของเกมลงครึ่งหนึ่งก่อน เนื่องจาก Vector2 มีการโอเวอร์โหลดโอเปอเรเตอร์ (* และ /) เพื่อปรับขนาด Vector2 ตามค่าสเกลาร์
การตั้งค่าvelocityของลูกบอลมีความซับซ้อนมากขึ้น โดยมีจุดประสงค์เพื่อย้ายลูกบอลลงมาที่ด้านล่างของหน้าจอในทิศทางแบบสุ่มด้วยความเร็วที่เหมาะสม การเรียกใช้เมธอด normalized จะสร้างออบเจ็กต์ Vector2 ที่ตั้งค่าให้มีทิศทางเดียวกับ Vector2 เดิม แต่ลดขนาดลงเหลือระยะทาง 1 ซึ่งจะทำให้ความเร็วของลูกบอลคงที่ ไม่ว่าลูกบอลจะไปในทิศทางใดก็ตาม จากนั้นจะปรับความเร็วของลูกบอลให้เป็น 1/4 ของความสูงของเกม
การกำหนดค่าต่างๆ เหล่านี้ให้ถูกต้องต้องมีการทำซ้ำ ซึ่งในอุตสาหกรรมนี้เรียกว่าการทดสอบเกม
บรรทัดสุดท้ายจะเปิดการแสดงผลการแก้ไขข้อบกพร่อง ซึ่งจะเพิ่มข้อมูลเพิ่มเติมลงในการแสดงผลเพื่อช่วยในการแก้ไขข้อบกพร่อง
เมื่อคุณเรียกใช้เกมในตอนนี้ เกมควรมีลักษณะคล้ายกับการแสดงผลต่อไปนี้

ทั้งคอมโพเนนต์ PlayArea และคอมโพเนนต์ Ball มีข้อมูลการแก้ไขข้อบกพร่อง แต่แมตต์พื้นหลังจะครอบตัดตัวเลขของ PlayArea สาเหตุที่ทุกอย่างแสดงข้อมูลการแก้ไขข้อบกพร่องเป็นเพราะคุณเปิดdebugModeสำหรับทั้งแผนผังคอมโพเนนต์ นอกจากนี้ คุณยังเปิดการแก้ไขข้อบกพร่องสำหรับคอมโพเนนต์ที่เลือกเท่านั้นได้ด้วย หากเป็นประโยชน์มากกว่า
หากรีสตาร์ทเกม 2-3 ครั้ง คุณอาจสังเกตเห็นว่าลูกบอลไม่โต้ตอบกับกำแพงตามที่คาดไว้ หากต้องการให้เกิดเอฟเฟกต์ดังกล่าว คุณต้องเพิ่มการตรวจหาการชน ซึ่งคุณจะทำในขั้นตอนถัดไป
6. ตีกลับ
เพิ่มการตรวจจับการชน
การตรวจหาการชนจะเพิ่มลักษณะการทำงานที่เกมจะรับรู้เมื่อวัตถุ 2 ชิ้นสัมผัสกัน
หากต้องการเพิ่มการตรวจหาการชนลงในเกม ให้เพิ่ม HasCollisionDetection มิกซ์อินลงใน BrickBreaker เกมตามที่แสดงในโค้ดต่อไปนี้
lib/src/brick_breaker.dart
import 'dart:async';
import 'dart:math' as math;
import 'package:flame/components.dart';
import 'package:flame/game.dart';
import 'components/components.dart';
import 'config.dart';
class BrickBreaker extends FlameGame with HasCollisionDetection { // Modify this line
BrickBreaker()
: super(
camera: CameraComponent.withFixedResolution(
width: gameWidth,
height: gameHeight,
),
);
final rand = math.Random();
double get width => size.x;
double get height => size.y;
@override
FutureOr<void> onLoad() async {
super.onLoad();
camera.viewfinder.anchor = Anchor.topLeft;
world.add(PlayArea());
world.add(
Ball(
radius: ballRadius,
position: size / 2,
velocity: Vector2(
(rand.nextDouble() - 0.5) * width,
height * 0.2,
).normalized()..scale(height / 4),
),
);
debugMode = true;
}
}
ซึ่งจะติดตามฮิตบ็อกซ์ของคอมโพเนนต์และเรียกใช้การเรียกกลับการชนกันในทุกๆ การอัปเดตเกม
หากต้องการเริ่มป้อนข้อมูลฮิตบ็อกซ์ของเกม ให้แก้ไขคอมโพเนนต์ PlayArea ดังที่แสดง
lib/src/components/play_area.dart
import 'dart:async';
import 'package:flame/collisions.dart'; // Add this import
import 'package:flame/components.dart';
import 'package:flutter/material.dart';
import '../brick_breaker.dart';
class PlayArea extends RectangleComponent with HasGameReference<BrickBreaker> {
PlayArea()
: super(
paint: Paint()..color = const Color(0xfff2e8cf),
children: [RectangleHitbox()], // Add this parameter
);
@override
FutureOr<void> onLoad() async {
super.onLoad();
size = Vector2(game.width, game.height);
}
}
การเพิ่มคอมโพเนนต์ RectangleHitbox เป็นองค์ประกอบย่อยของ RectangleComponent จะสร้างช่องการตรวจหาการชนที่มีขนาดตรงกับขนาดของคอมโพเนนต์หลัก มีตัวสร้างจากโรงงานสำหรับ RectangleHitbox ที่ชื่อ relative ในกรณีที่คุณต้องการฮิตบ็อกซ์ที่มีขนาดเล็กหรือใหญ่กว่าคอมโพเนนต์หลัก
โยนลูกบอล
จนถึงตอนนี้ การเพิ่มการตรวจหาการชนไม่ได้ทำให้เกมเพลย์แตกต่างออกไป แต่จะเปลี่ยนเมื่อคุณแก้ไขคอมโพเนนต์ Ball พฤติกรรมของลูกบอลจะต้องเปลี่ยนเมื่อชนกับPlayArea
แก้ไขคอมโพเนนต์ Ball ดังนี้
lib/src/components/ball.dart
import 'package:flame/collisions.dart'; // Add this import
import 'package:flame/components.dart';
import 'package:flutter/material.dart';
import '../brick_breaker.dart'; // And this import
import 'play_area.dart'; // And this one too
class Ball extends CircleComponent
with CollisionCallbacks, HasGameReference<BrickBreaker> { // Add these mixins
Ball({
required this.velocity,
required super.position,
required double radius,
}) : super(
radius: radius,
anchor: Anchor.center,
paint: Paint()
..color = const Color(0xff1e6091)
..style = PaintingStyle.fill,
children: [CircleHitbox()], // Add this parameter
);
final Vector2 velocity;
@override
void update(double dt) {
super.update(dt);
position += velocity * dt;
}
@override // Add from here...
void onCollisionStart(
Set<Vector2> intersectionPoints,
PositionComponent other,
) {
super.onCollisionStart(intersectionPoints, other);
if (other is PlayArea) {
if (intersectionPoints.first.y <= 0) {
velocity.y = -velocity.y;
} else if (intersectionPoints.first.x <= 0) {
velocity.x = -velocity.x;
} else if (intersectionPoints.first.x >= game.width) {
velocity.x = -velocity.x;
} else if (intersectionPoints.first.y >= game.height) {
removeFromParent();
}
} else {
debugPrint('collision with $other');
}
} // To here.
}
ตัวอย่างนี้ทำการเปลี่ยนแปลงครั้งใหญ่ด้วยการเพิ่มonCollisionStartการเรียกกลับ ระบบตรวจหาการชนที่เพิ่มลงใน BrickBreaker ในตัวอย่างก่อนหน้าจะเรียกใช้การเรียกกลับนี้
ก่อนอื่น โค้ดจะทดสอบว่า Ball ชนกับ PlayArea หรือไม่ ซึ่งดูเหมือนจะซ้ำซ้อนในตอนนี้ เนื่องจากไม่มีคอมโพเนนต์อื่นๆ ในโลกของเกม ซึ่งจะเปลี่ยนไปในขั้นตอนถัดไปเมื่อคุณเพิ่มค้างคาวลงในโลก จากนั้นยังเพิ่มเงื่อนไข else เพื่อจัดการเมื่อลูกบอลชนกับสิ่งที่ไม่ใช่ไม้ ช่วยเตือนให้ใช้ตรรกะที่เหลืออยู่ หากคุณต้องการ
เมื่อลูกบอลชนกับกำแพงด้านล่าง ลูกบอลจะหายไปจากพื้นผิวการเล่น แต่ยังคงมองเห็นได้ชัดเจน คุณจะจัดการอาร์ติแฟกต์นี้ในขั้นตอนถัดไปโดยใช้พลังของเอฟเฟกต์ของ Flame
ตอนนี้คุณมีลูกบอลที่ชนกับกำแพงของเกมแล้ว การให้ผู้เล่นมีไม้ตีเพื่อตีลูกบอลก็คงจะเป็นประโยชน์...
7. ตีลูกให้โดน
สร้างค้างคาว
หากต้องการเพิ่มไม้ตีเพื่อตีลูกให้อยู่ในการเล่นภายในเกม ให้ทำดังนี้
- แทรกค่าคงที่บางอย่างในไฟล์
lib/src/config.dartดังนี้
lib/src/config.dart
const gameWidth = 820.0;
const gameHeight = 1600.0;
const ballRadius = gameWidth * 0.02;
const batWidth = gameWidth * 0.2; // Add from here...
const batHeight = ballRadius * 2;
const batStep = gameWidth * 0.05; // To here.
ค่าคงที่ batHeight และ batWidth มีความชัดเจนในตัวอยู่แล้ว ส่วนbatStepค่าคงที่นั้นต้องมีคำอธิบายเพิ่มเติม หากต้องการโต้ตอบกับลูกบอลในเกมนี้ ผู้เล่นสามารถลากไม้ด้วยเมาส์หรือนิ้ว (ขึ้นอยู่กับแพลตฟอร์ม) หรือใช้แป้นพิมพ์ batStep ค่าคงที่จะกำหนดระยะทางที่ค้างคาวจะก้าวเมื่อกดปุ่มลูกศรซ้ายหรือขวาแต่ละครั้ง
- กำหนดคลาสคอมโพเนนต์
Batดังนี้
lib/src/components/bat.dart
import 'package:flame/collisions.dart';
import 'package:flame/components.dart';
import 'package:flame/effects.dart';
import 'package:flame/events.dart';
import 'package:flutter/material.dart';
import '../brick_breaker.dart';
class Bat extends PositionComponent
with DragCallbacks, HasGameReference<BrickBreaker> {
Bat({
required this.cornerRadius,
required super.position,
required super.size,
}) : super(anchor: Anchor.center, children: [RectangleHitbox()]);
final Radius cornerRadius;
final _paint = Paint()
..color = const Color(0xff1e6091)
..style = PaintingStyle.fill;
@override
void render(Canvas canvas) {
super.render(canvas);
canvas.drawRRect(
RRect.fromRectAndRadius(Offset.zero & size.toSize(), cornerRadius),
_paint,
);
}
@override
void onDragUpdate(DragUpdateEvent event) {
super.onDragUpdate(event);
position.x = (position.x + event.localDelta.x).clamp(0, game.width);
}
void moveBy(double dx) {
add(
MoveToEffect(
Vector2((position.x + dx).clamp(0, game.width), position.y),
EffectController(duration: 0.1),
),
);
}
}
คอมโพเนนต์นี้มาพร้อมความสามารถใหม่ๆ
ก่อนอื่น คอมโพเนนต์ Bat คือ PositionComponent ไม่ใช่ RectangleComponent หรือ CircleComponent ซึ่งหมายความว่าโค้ดนี้ต้องแสดง Bat บนหน้าจอ โดยจะลบล้างrender Callback เพื่อให้บรรลุเป้าหมายนี้
เมื่อดูการเรียก canvas.drawRRect (วาดสี่เหลี่ยมผืนผ้ามน) อย่างละเอียด คุณอาจสงสัยว่า "สี่เหลี่ยมผืนผ้าอยู่ตรงไหน" Offset.zero & size.toSize()ใช้operator &โอเวอร์โหลดในคลาส dart:ui Offset ที่สร้างRect ตัวย่อนี้อาจทำให้คุณสับสนในตอนแรก แต่คุณจะเห็นตัวย่อนี้บ่อยๆ ในโค้ด Flutter และ Flame ระดับล่าง
ประการที่สอง Bat คอมโพเนนต์นี้ลากได้โดยใช้นิ้วหรือเมาส์ ทั้งนี้ขึ้นอยู่กับแพลตฟอร์ม หากต้องการใช้ฟังก์ชันนี้ ให้เพิ่มมิกซ์อิน DragCallbacks และลบล้างเหตุการณ์ onDragUpdate
สุดท้าย คอมโพเนนต์ Bat ต้องตอบสนองต่อการควบคุมด้วยแป้นพิมพ์ ฟังก์ชัน moveBy ช่วยให้โค้ดอื่นๆ บอกให้ค้างคาวตัวนี้เคลื่อนที่ไปทางซ้ายหรือขวาตามจำนวนพิกเซลเสมือนที่กำหนด ฟังก์ชันนี้จะเปิดตัวความสามารถใหม่ของเอนจินเกม Flame นั่นคือ Effects การเพิ่มออบเจ็กต์ MoveToEffect เป็นองค์ประกอบย่อยของคอมโพเนนต์นี้จะทำให้ผู้เล่นเห็นค้างคาวเคลื่อนไหวไปยังตำแหน่งใหม่ Flame มีคอลเล็กชันEffectให้ใช้งานเพื่อสร้างเอฟเฟกต์ต่างๆ
อาร์กิวเมนต์ของตัวสร้างเอฟเฟกต์มีการอ้างอิงถึงตัวดึงข้อมูล game ด้วยเหตุนี้คุณจึงรวมมิกซ์อิน HasGameReference ไว้ในคลาสนี้ มิกซ์อินนี้จะเพิ่มตัวช่วยเข้าถึง game ที่ปลอดภัยต่อประเภทให้กับคอมโพเนนต์นี้เพื่อเข้าถึงอินสแตนซ์ BrickBreaker ที่ด้านบนของแผนผังคอมโพเนนต์
- หากต้องการให้
Batพร้อมใช้งานสำหรับBrickBreakerให้อัปเดตไฟล์lib/src/components/components.dartดังนี้
lib/src/components/components.dart
export 'ball.dart';
export 'bat.dart'; // Add this export
export 'play_area.dart';
เพิ่มค้างคาวลงในโลก
หากต้องการเพิ่มคอมโพเนนต์ Bat ลงในโลกของเกม ให้อัปเดต BrickBreaker ดังนี้
lib/src/brick_breaker.dart
import 'dart:async';
import 'dart:math' as math;
import 'package:flame/components.dart';
import 'package:flame/events.dart'; // Add this import
import 'package:flame/game.dart';
import 'package:flutter/material.dart'; // And this import
import 'package:flutter/services.dart'; // And this
import 'components/components.dart';
import 'config.dart';
class BrickBreaker extends FlameGame
with HasCollisionDetection, KeyboardEvents { // Modify this line
BrickBreaker()
: super(
camera: CameraComponent.withFixedResolution(
width: gameWidth,
height: gameHeight,
),
);
final rand = math.Random();
double get width => size.x;
double get height => size.y;
@override
FutureOr<void> onLoad() async {
super.onLoad();
camera.viewfinder.anchor = Anchor.topLeft;
world.add(PlayArea());
world.add(
Ball(
radius: ballRadius,
position: size / 2,
velocity: Vector2(
(rand.nextDouble() - 0.5) * width,
height * 0.2,
).normalized()..scale(height / 4),
),
);
world.add( // Add from here...
Bat(
size: Vector2(batWidth, batHeight),
cornerRadius: const Radius.circular(ballRadius / 2),
position: Vector2(width / 2, height * 0.95),
),
); // To here.
debugMode = true;
}
@override // Add from here...
KeyEventResult onKeyEvent(
KeyEvent event,
Set<LogicalKeyboardKey> keysPressed,
) {
super.onKeyEvent(event, keysPressed);
switch (event.logicalKey) {
case LogicalKeyboardKey.arrowLeft:
world.children.query<Bat>().first.moveBy(-batStep);
case LogicalKeyboardKey.arrowRight:
world.children.query<Bat>().first.moveBy(batStep);
}
return KeyEventResult.handled;
} // To here.
}
การเพิ่ม KeyboardEvents mixin และเมธอด onKeyEvent ที่ลบล้างจะจัดการอินพุตจากแป้นพิมพ์ เรียกคืนโค้ดที่คุณเพิ่มไว้ก่อนหน้านี้เพื่อย้ายค้างคาวตามจำนวนก้าวที่เหมาะสม
โค้ดที่เหลือที่เพิ่มเข้ามาจะเพิ่มค้างคาวลงในโลกของเกมในตำแหน่งที่เหมาะสมและมีสัดส่วนที่ถูกต้อง การแสดงการตั้งค่าทั้งหมดเหล่านี้ในไฟล์นี้ช่วยให้คุณปรับขนาดสัมพัทธ์ของไม้และลูกบอลได้ง่ายขึ้นเพื่อให้ได้ความรู้สึกที่เหมาะสมกับเกม
หากเล่นเกมในตอนนี้ คุณจะเห็นว่าสามารถขยับไม้เพื่อสกัดลูกบอลได้ แต่จะไม่มีการตอบสนองที่มองเห็นได้ นอกเหนือจากการบันทึกการแก้ไขข้อบกพร่องที่คุณทิ้งไว้ในโค้ดการตรวจหาการชนของ Ball
ถึงเวลาแก้ไขแล้ว แก้ไขคอมโพเนนต์ Ball ดังนี้
lib/src/components/ball.dart
import 'package:flame/collisions.dart';
import 'package:flame/components.dart';
import 'package:flame/effects.dart'; // Add this import
import 'package:flutter/material.dart';
import '../brick_breaker.dart';
import 'bat.dart'; // And this import
import 'play_area.dart';
class Ball extends CircleComponent
with CollisionCallbacks, HasGameReference<BrickBreaker> {
Ball({
required this.velocity,
required super.position,
required double radius,
}) : super(
radius: radius,
anchor: Anchor.center,
paint: Paint()
..color = const Color(0xff1e6091)
..style = PaintingStyle.fill,
children: [CircleHitbox()],
);
final Vector2 velocity;
@override
void update(double dt) {
super.update(dt);
position += velocity * dt;
}
@override
void onCollisionStart(
Set<Vector2> intersectionPoints,
PositionComponent other,
) {
super.onCollisionStart(intersectionPoints, other);
if (other is PlayArea) {
if (intersectionPoints.first.y <= 0) {
velocity.y = -velocity.y;
} else if (intersectionPoints.first.x <= 0) {
velocity.x = -velocity.x;
} else if (intersectionPoints.first.x >= game.width) {
velocity.x = -velocity.x;
} else if (intersectionPoints.first.y >= game.height) {
add(RemoveEffect(delay: 0.35)); // Modify from here...
}
} else if (other is Bat) {
velocity.y = -velocity.y;
velocity.x =
velocity.x +
(position.x - other.position.x) / other.size.x * game.width * 0.3;
} else { // To here.
debugPrint('collision with $other');
}
}
}
การเปลี่ยนแปลงโค้ดเหล่านี้จะแก้ไขปัญหา 2 อย่างที่แยกกัน
ก่อนอื่น ฟีเจอร์นี้จะแก้ไขปัญหาลูกบอลหายไปทันทีที่สัมผัสด้านล่างของหน้าจอ หากต้องการแก้ไขปัญหานี้ ให้แทนที่การเรียกใช้ removeFromParent ด้วย RemoveEffect RemoveEffect จะนำลูกบอลออกจากโลกของเกมหลังจากปล่อยให้ลูกบอลออกนอกพื้นที่เล่นที่มองเห็นได้
ประการที่สอง การเปลี่ยนแปลงเหล่านี้แก้ไขการจัดการการชนกันระหว่างไม้กับลูกบอล ซึ่งโค้ดการจัดการนี้จะเอื้อประโยชน์ต่อผู้เล่นเป็นอย่างมาก ตราบใดที่ผู้เล่นสัมผัสลูกบอลด้วยไม้ ลูกบอลจะกลับไปที่ด้านบนของหน้าจอ หากรู้สึกว่าการจัดการนี้ง่ายเกินไปและต้องการให้สมจริงมากขึ้น ให้เปลี่ยนการจัดการนี้เพื่อให้เหมาะกับความรู้สึกที่คุณต้องการให้เกมเป็น
เราควรชี้ให้เห็นถึงความซับซ้อนของการอัปเดต velocity โดยไม่ได้เพียงแค่กลับทิศทางyของความเร็วเท่านั้น เหมือนกับการชนกำแพง นอกจากนี้ ยังอัปเดตxในลักษณะที่ขึ้นอยู่กับตำแหน่งสัมพัทธ์ของไม้และลูกบอลในขณะที่สัมผัสกัน ซึ่งจะช่วยให้ผู้เล่นควบคุมสิ่งที่ลูกบอลทำได้มากขึ้น แต่ระบบจะไม่แจ้งให้ผู้เล่นทราบถึงวิธีการที่แน่นอนในทุกวิถีทาง ยกเว้นผ่านการเล่น
ตอนนี้คุณมีไม้ตีลูกบอลแล้ว ก็ควรจะมีอิฐให้ลูกบอลทุบด้วย
8. ทลายกำแพง
สร้างอิฐ
วิธีเพิ่มอิฐลงในเกม
- แทรกค่าคงที่บางอย่างในไฟล์
lib/src/config.dartดังนี้
lib/src/config.dart
import 'package:flutter/material.dart'; // Add this import
const brickColors = [ // Add this const
Color(0xfff94144),
Color(0xfff3722c),
Color(0xfff8961e),
Color(0xfff9844a),
Color(0xfff9c74f),
Color(0xff90be6d),
Color(0xff43aa8b),
Color(0xff4d908e),
Color(0xff277da1),
Color(0xff577590),
];
const gameWidth = 820.0;
const gameHeight = 1600.0;
const ballRadius = gameWidth * 0.02;
const batWidth = gameWidth * 0.2;
const batHeight = ballRadius * 2;
const batStep = gameWidth * 0.05;
const brickGutter = gameWidth * 0.015; // Add from here...
final brickWidth =
(gameWidth - (brickGutter * (brickColors.length + 1))) / brickColors.length;
const brickHeight = gameHeight * 0.03;
const difficultyModifier = 1.03; // To here.
- แทรกคอมโพเนนต์
Brickดังนี้
lib/src/components/brick.dart
import 'package:flame/collisions.dart';
import 'package:flame/components.dart';
import 'package:flutter/material.dart';
import '../brick_breaker.dart';
import '../config.dart';
import 'ball.dart';
import 'bat.dart';
class Brick extends RectangleComponent
with CollisionCallbacks, HasGameReference<BrickBreaker> {
Brick({required super.position, required Color color})
: super(
size: Vector2(brickWidth, brickHeight),
anchor: Anchor.center,
paint: Paint()
..color = color
..style = PaintingStyle.fill,
children: [RectangleHitbox()],
);
@override
void onCollisionStart(
Set<Vector2> intersectionPoints,
PositionComponent other,
) {
super.onCollisionStart(intersectionPoints, other);
removeFromParent();
if (game.world.children.query<Brick>().length == 1) {
game.world.removeAll(game.world.children.query<Ball>());
game.world.removeAll(game.world.children.query<Bat>());
}
}
}
ตอนนี้คุณน่าจะคุ้นเคยกับโค้ดส่วนใหญ่แล้ว โค้ดนี้ใช้ RectangleComponent ทั้งการตรวจหาการชนกันและการอ้างอิงที่ปลอดภัยตามประเภทไปยังเกม BrickBreaker ที่ด้านบนของแผนผังคอมโพเนนต์
แนวคิดใหม่ที่สำคัญที่สุดที่โค้ดนี้แนะนำคือวิธีที่ผู้เล่นบรรลุเงื่อนไขการชนะ การตรวจสอบเงื่อนไขการชนะจะค้นหาอิฐทั่วโลกและยืนยันว่าเหลือเพียงก้อนเดียว ซึ่งอาจทำให้สับสนเล็กน้อยเนื่องจากบรรทัดก่อนหน้าจะนำอิฐนี้ออกจากอิฐหลัก
ประเด็นสำคัญที่ควรทราบคือการนำคอมโพเนนต์ออกเป็นคำสั่งที่อยู่ในคิว โดยจะนำบล็อกออกหลังจากโค้ดนี้ทำงาน แต่ก่อนที่โลกของเกมจะอัปเดตครั้งถัดไป
หากต้องการทำให้Brickเข้าถึงได้ใน BrickBreaker ให้แก้ไข lib/src/components/components.dart ดังนี้
lib/src/components/components.dart
export 'ball.dart';
export 'bat.dart';
export 'brick.dart'; // Add this export
export 'play_area.dart';
เพิ่มอิฐลงในโลก
อัปเดตคอมโพเนนต์ Ball ดังนี้
lib/src/components/ball.dart
import 'package:flame/collisions.dart';
import 'package:flame/components.dart';
import 'package:flame/effects.dart';
import 'package:flutter/material.dart';
import '../brick_breaker.dart';
import 'bat.dart';
import 'brick.dart'; // Add this import
import 'play_area.dart';
class Ball extends CircleComponent
with CollisionCallbacks, HasGameReference<BrickBreaker> {
Ball({
required this.velocity,
required super.position,
required double radius,
required this.difficultyModifier, // Add this parameter
}) : super(
radius: radius,
anchor: Anchor.center,
paint: Paint()
..color = const Color(0xff1e6091)
..style = PaintingStyle.fill,
children: [CircleHitbox()],
);
final Vector2 velocity;
final double difficultyModifier; // Add this member
@override
void update(double dt) {
super.update(dt);
position += velocity * dt;
}
@override
void onCollisionStart(
Set<Vector2> intersectionPoints,
PositionComponent other,
) {
super.onCollisionStart(intersectionPoints, other);
if (other is PlayArea) {
if (intersectionPoints.first.y <= 0) {
velocity.y = -velocity.y;
} else if (intersectionPoints.first.x <= 0) {
velocity.x = -velocity.x;
} else if (intersectionPoints.first.x >= game.width) {
velocity.x = -velocity.x;
} else if (intersectionPoints.first.y >= game.height) {
add(RemoveEffect(delay: 0.35));
}
} else if (other is Bat) {
velocity.y = -velocity.y;
velocity.x =
velocity.x +
(position.x - other.position.x) / other.size.x * game.width * 0.3;
} else if (other is Brick) { // Modify from here...
if (position.y < other.position.y - other.size.y / 2) {
velocity.y = -velocity.y;
} else if (position.y > other.position.y + other.size.y / 2) {
velocity.y = -velocity.y;
} else if (position.x < other.position.x) {
velocity.x = -velocity.x;
} else if (position.x > other.position.x) {
velocity.x = -velocity.x;
}
velocity.setFrom(velocity * difficultyModifier); // To here.
}
}
}
ซึ่งเป็นองค์ประกอบใหม่เพียงอย่างเดียว นั่นคือตัวปรับความยากที่จะเพิ่มความเร็วของลูกบอลหลังจากชนบล็อกแต่ละครั้ง พารามิเตอร์ที่ปรับได้นี้ต้องได้รับการทดสอบการเล่นเพื่อค้นหาเส้นโค้งความยากที่เหมาะสมกับเกมของคุณ
แก้ไขเกม BrickBreaker ดังนี้
lib/src/brick_breaker.dart
import 'dart:async';
import 'dart:math' as math;
import 'package:flame/components.dart';
import 'package:flame/events.dart';
import 'package:flame/game.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'components/components.dart';
import 'config.dart';
class BrickBreaker extends FlameGame
with HasCollisionDetection, KeyboardEvents {
BrickBreaker()
: super(
camera: CameraComponent.withFixedResolution(
width: gameWidth,
height: gameHeight,
),
);
final rand = math.Random();
double get width => size.x;
double get height => size.y;
@override
FutureOr<void> onLoad() async {
super.onLoad();
camera.viewfinder.anchor = Anchor.topLeft;
world.add(PlayArea());
world.add(
Ball(
difficultyModifier: difficultyModifier, // Add this argument
radius: ballRadius,
position: size / 2,
velocity: Vector2(
(rand.nextDouble() - 0.5) * width,
height * 0.2,
).normalized()..scale(height / 4),
),
);
world.add(
Bat(
size: Vector2(batWidth, batHeight),
cornerRadius: const Radius.circular(ballRadius / 2),
position: Vector2(width / 2, height * 0.95),
),
);
await world.addAll([ // Add from here...
for (var i = 0; i < brickColors.length; i++)
for (var j = 1; j <= 5; j++)
Brick(
position: Vector2(
(i + 0.5) * brickWidth + (i + 1) * brickGutter,
(j + 2.0) * brickHeight + j * brickGutter,
),
color: brickColors[i],
),
]); // To here.
debugMode = true;
}
@override
KeyEventResult onKeyEvent(
KeyEvent event,
Set<LogicalKeyboardKey> keysPressed,
) {
super.onKeyEvent(event, keysPressed);
switch (event.logicalKey) {
case LogicalKeyboardKey.arrowLeft:
world.children.query<Bat>().first.moveBy(-batStep);
case LogicalKeyboardKey.arrowRight:
world.children.query<Bat>().first.moveBy(batStep);
}
return KeyEventResult.handled;
}
}
หากคุณเรียกใช้เกม ระบบจะแสดงกลไกหลักทั้งหมดของเกม คุณอาจปิดการแก้ไขข้อบกพร่องและถือว่าเสร็จสิ้นแล้ว แต่ก็ยังรู้สึกว่าขาดอะไรไป

ลองทำหน้าจอต้อนรับ หน้าจอเกมโอเวอร์ และอาจมีคะแนนด้วยไหม Flutter สามารถเพิ่มฟีเจอร์เหล่านี้ลงในเกมได้ และนั่นคือสิ่งที่คุณจะให้ความสนใจต่อไป
9. ชนะเกม
เพิ่มสถานะการเล่น
ในขั้นตอนนี้ คุณจะฝังเกม Flame ไว้ใน Wrapper ของ Flutter จากนั้นเพิ่มการซ้อนทับ Flutter สำหรับหน้าจอต้อนรับ เกมจบ และชนะ
ก่อนอื่น ให้แก้ไขไฟล์เกมและคอมโพเนนต์เพื่อใช้สถานะการเล่นที่ระบุว่าจะแสดงภาพซ้อนหรือไม่ และหากแสดง จะแสดงภาพซ้อนใด
- แก้ไขเกม
BrickBreakerดังนี้
lib/src/brick_breaker.dart
import 'dart:async';
import 'dart:math' as math;
import 'package:flame/components.dart';
import 'package:flame/events.dart';
import 'package:flame/game.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'components/components.dart';
import 'config.dart';
enum PlayState { welcome, playing, gameOver, won } // Add this enumeration
class BrickBreaker extends FlameGame
with HasCollisionDetection, KeyboardEvents, TapDetector { // Modify this line
BrickBreaker()
: super(
camera: CameraComponent.withFixedResolution(
width: gameWidth,
height: gameHeight,
),
);
final rand = math.Random();
double get width => size.x;
double get height => size.y;
late PlayState _playState; // Add from here...
PlayState get playState => _playState;
set playState(PlayState playState) {
_playState = playState;
switch (playState) {
case PlayState.welcome:
case PlayState.gameOver:
case PlayState.won:
overlays.add(playState.name);
case PlayState.playing:
overlays.remove(PlayState.welcome.name);
overlays.remove(PlayState.gameOver.name);
overlays.remove(PlayState.won.name);
}
} // To here.
@override
FutureOr<void> onLoad() async {
super.onLoad();
camera.viewfinder.anchor = Anchor.topLeft;
world.add(PlayArea());
playState = PlayState.welcome; // Add from here...
}
void startGame() {
if (playState == PlayState.playing) return;
world.removeAll(world.children.query<Ball>());
world.removeAll(world.children.query<Bat>());
world.removeAll(world.children.query<Brick>());
playState = PlayState.playing; // To here.
world.add(
Ball(
difficultyModifier: difficultyModifier,
radius: ballRadius,
position: size / 2,
velocity: Vector2(
(rand.nextDouble() - 0.5) * width,
height * 0.2,
).normalized()..scale(height / 4),
),
);
world.add(
Bat(
size: Vector2(batWidth, batHeight),
cornerRadius: const Radius.circular(ballRadius / 2),
position: Vector2(width / 2, height * 0.95),
),
);
world.addAll([ // Drop the await
for (var i = 0; i < brickColors.length; i++)
for (var j = 1; j <= 5; j++)
Brick(
position: Vector2(
(i + 0.5) * brickWidth + (i + 1) * brickGutter,
(j + 2.0) * brickHeight + j * brickGutter,
),
color: brickColors[i],
),
]);
} // Drop the debugMode
@override // Add from here...
void onTap() {
super.onTap();
startGame();
} // To here.
@override
KeyEventResult onKeyEvent(
KeyEvent event,
Set<LogicalKeyboardKey> keysPressed,
) {
super.onKeyEvent(event, keysPressed);
switch (event.logicalKey) {
case LogicalKeyboardKey.arrowLeft:
world.children.query<Bat>().first.moveBy(-batStep);
case LogicalKeyboardKey.arrowRight:
world.children.query<Bat>().first.moveBy(batStep);
case LogicalKeyboardKey.space: // Add from here...
case LogicalKeyboardKey.enter:
startGame(); // To here.
}
return KeyEventResult.handled;
}
@override
Color backgroundColor() => const Color(0xfff2e8cf); // Add this override
}
โค้ดนี้จะเปลี่ยนแปลงเกม BrickBreaker ไปมาก การเพิ่มการแจงนับ playState ต้องใช้เวลามาก ซึ่งจะบันทึกตำแหน่งที่ผู้เล่นเข้า เล่น และแพ้หรือชนะเกม ที่ด้านบนของไฟล์ คุณจะกำหนดการแจงนับ จากนั้นสร้างอินสแตนซ์เป็นสถานะที่ซ่อนอยู่พร้อมกับตัวรับและตัวตั้งค่าที่ตรงกัน Getter และ Setter เหล่านี้ช่วยให้แก้ไขการวางซ้อนได้เมื่อส่วนต่างๆ ของเกมทริกเกอร์การเปลี่ยนสถานะการเล่น
จากนั้นแยกโค้ดใน onLoad ออกเป็น onLoad และเมธอด startGame ใหม่ ก่อนการเปลี่ยนแปลงนี้ คุณจะเริ่มเกมใหม่ได้โดยการรีสตาร์ทเกมเท่านั้น การเพิ่มฟีเจอร์ใหม่เหล่านี้จะช่วยให้ผู้เล่นเริ่มเกมใหม่ได้โดยไม่ต้องใช้มาตรการที่รุนแรงเช่นนี้
คุณได้กำหนดค่าแฮนเดิลใหม่ 2 รายการสำหรับเกมเพื่อให้ผู้เล่นเริ่มเกมใหม่ได้ คุณได้เพิ่มตัวแฮนเดิลการแตะและขยายตัวแฮนเดิลแป้นพิมพ์เพื่อให้ผู้ใช้เริ่มเกมใหม่ในหลายรูปแบบได้ เมื่อใช้สถานะการเล่นที่จำลองแล้ว การอัปเดตคอมโพเนนต์เพื่อทริกเกอร์การเปลี่ยนสถานะการเล่นเมื่อผู้เล่นชนะหรือแพ้ก็จะเป็นเรื่องสมเหตุสมผล
- แก้ไขคอมโพเนนต์
Ballดังนี้
lib/src/components/ball.dart
import 'package:flame/collisions.dart';
import 'package:flame/components.dart';
import 'package:flame/effects.dart';
import 'package:flutter/material.dart';
import '../brick_breaker.dart';
import 'bat.dart';
import 'brick.dart';
import 'play_area.dart';
class Ball extends CircleComponent
with CollisionCallbacks, HasGameReference<BrickBreaker> {
Ball({
required this.velocity,
required super.position,
required double radius,
required this.difficultyModifier,
}) : super(
radius: radius,
anchor: Anchor.center,
paint: Paint()
..color = const Color(0xff1e6091)
..style = PaintingStyle.fill,
children: [CircleHitbox()],
);
final Vector2 velocity;
final double difficultyModifier;
@override
void update(double dt) {
super.update(dt);
position += velocity * dt;
}
@override
void onCollisionStart(
Set<Vector2> intersectionPoints,
PositionComponent other,
) {
super.onCollisionStart(intersectionPoints, other);
if (other is PlayArea) {
if (intersectionPoints.first.y <= 0) {
velocity.y = -velocity.y;
} else if (intersectionPoints.first.x <= 0) {
velocity.x = -velocity.x;
} else if (intersectionPoints.first.x >= game.width) {
velocity.x = -velocity.x;
} else if (intersectionPoints.first.y >= game.height) {
add(
RemoveEffect(
delay: 0.35,
onComplete: () { // Modify from here
game.playState = PlayState.gameOver;
},
),
); // To here.
}
} else if (other is Bat) {
velocity.y = -velocity.y;
velocity.x =
velocity.x +
(position.x - other.position.x) / other.size.x * game.width * 0.3;
} else if (other is Brick) {
if (position.y < other.position.y - other.size.y / 2) {
velocity.y = -velocity.y;
} else if (position.y > other.position.y + other.size.y / 2) {
velocity.y = -velocity.y;
} else if (position.x < other.position.x) {
velocity.x = -velocity.x;
} else if (position.x > other.position.x) {
velocity.x = -velocity.x;
}
velocity.setFrom(velocity * difficultyModifier);
}
}
}
การเปลี่ยนแปลงเล็กๆ นี้จะเพิ่มonCompleteการเรียกกลับไปยัง RemoveEffect ซึ่งจะทริกเกอร์gameOverสถานะการเล่น ซึ่งควรจะรู้สึกว่าถูกต้องหากผู้เล่นปล่อยให้ลูกบอลหลุดออกจากด้านล่างของหน้าจอ
- แก้ไขคอมโพเนนต์
Brickดังนี้
lib/src/components/brick.dart
impimport 'package:flame/collisions.dart';
import 'package:flame/components.dart';
import 'package:flutter/material.dart';
import '../brick_breaker.dart';
import '../config.dart';
import 'ball.dart';
import 'bat.dart';
class Brick extends RectangleComponent
with CollisionCallbacks, HasGameReference<BrickBreaker> {
Brick({required super.position, required Color color})
: super(
size: Vector2(brickWidth, brickHeight),
anchor: Anchor.center,
paint: Paint()
..color = color
..style = PaintingStyle.fill,
children: [RectangleHitbox()],
);
@override
void onCollisionStart(
Set<Vector2> intersectionPoints,
PositionComponent other,
) {
super.onCollisionStart(intersectionPoints, other);
removeFromParent();
if (game.world.children.query<Brick>().length == 1) {
game.playState = PlayState.won; // Add this line
game.world.removeAll(game.world.children.query<Ball>());
game.world.removeAll(game.world.children.query<Bat>());
}
}
}
ในทางกลับกัน หากผู้เล่นทำลายบล็อกกำแพงได้ทั้งหมด ก็จะได้รับหน้าจอ "ชนะเกม" ยอดเยี่ยมมาก
เพิ่ม Wrapper ของ Flutter
เพิ่มเชลล์ Flutter เพื่อให้มีที่ฝังเกมและเพิ่มภาพซ้อนทับสถานะการเล่น
- สร้างไดเรกทอรี
widgetsภายใต้lib/src - เพิ่มไฟล์
game_app.dartแล้วแทรกเนื้อหาต่อไปนี้ลงในไฟล์นั้น
lib/src/widgets/game_app.dart
import 'package:flame/game.dart';
import 'package:flutter/material.dart';
import 'package:google_fonts/google_fonts.dart';
import '../brick_breaker.dart';
import '../config.dart';
class GameApp extends StatelessWidget {
const GameApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
debugShowCheckedModeBanner: false,
theme: ThemeData(
textTheme: GoogleFonts.pressStart2pTextTheme().apply(
bodyColor: const Color(0xff184e77),
displayColor: const Color(0xff184e77),
),
),
home: Scaffold(
body: Container(
decoration: const BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: [Color(0xffa9d6e5), Color(0xfff2e8cf)],
),
),
child: SafeArea(
child: Padding(
padding: const EdgeInsets.all(16),
child: Center(
child: FittedBox(
child: SizedBox(
width: gameWidth,
height: gameHeight,
child: GameWidget.controlled(
gameFactory: BrickBreaker.new,
overlayBuilderMap: {
PlayState.welcome.name: (context, game) => Center(
child: Text(
'TAP TO PLAY',
style: Theme.of(context).textTheme.headlineLarge,
),
),
PlayState.gameOver.name: (context, game) => Center(
child: Text(
'G A M E O V E R',
style: Theme.of(context).textTheme.headlineLarge,
),
),
PlayState.won.name: (context, game) => Center(
child: Text(
'Y O U W O N ! ! !',
style: Theme.of(context).textTheme.headlineLarge,
),
),
},
),
),
),
),
),
),
),
),
);
}
}
เนื้อหาส่วนใหญ่ในไฟล์นี้เป็นไปตามการสร้างแผนผังวิดเจ็ต Flutter มาตรฐาน ส่วนที่เฉพาะเจาะจงสำหรับ Flame ได้แก่ การใช้ GameWidget.controlled เพื่อสร้างและจัดการอินสแตนซ์เกม BrickBreaker และอาร์กิวเมนต์ overlayBuilderMap ใหม่สำหรับ GameWidget
คีย์ของ overlayBuilderMap นี้ต้องสอดคล้องกับภาพซ้อนทับที่ผู้ตั้งค่า playState ใน BrickBreaker เพิ่มหรือนำออก การพยายามตั้งค่าการซ้อนทับที่ไม่ได้อยู่ในแผนที่นี้จะทำให้เกิดหน้าเศร้าไปทั่ว
- หากต้องการให้ฟังก์ชันการทำงานใหม่นี้ปรากฏบนหน้าจอ ให้แทนที่ไฟล์
lib/main.dartด้วยเนื้อหาต่อไปนี้
lib/main.dart
import 'package:flutter/material.dart';
import 'src/widgets/game_app.dart';
void main() {
runApp(const GameApp());
}
หากคุณเรียกใช้โค้ดนี้ใน iOS, Linux, Windows หรือเว็บ เอาต์พุตที่ต้องการจะแสดงในเกม หากกำหนดเป้าหมายเป็น macOS หรือ Android คุณต้องทำการปรับแต่งครั้งสุดท้ายเพื่อเปิดใช้ google_fonts ให้แสดง
เปิดใช้การเข้าถึงแบบอักษร
เพิ่มสิทธิ์อินเทอร์เน็ตสำหรับ Android
สำหรับ Android คุณต้องเพิ่มสิทธิ์เข้าถึงอินเทอร์เน็ต แก้ไข AndroidManifest.xml ดังนี้
android/app/src/main/AndroidManifest.xml
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<!-- Add the following line -->
<uses-permission android:name="android.permission.INTERNET" />
<application
android:label="brick_breaker"
android:name="${applicationName}"
android:icon="@mipmap/ic_launcher">
<activity
android:name=".MainActivity"
android:exported="true"
android:launchMode="singleTop"
android:taskAffinity=""
android:theme="@style/LaunchTheme"
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
android:hardwareAccelerated="true"
android:windowSoftInputMode="adjustResize">
<!-- Specifies an Android theme to apply to this Activity as soon as
the Android process has started. This theme is visible to the user
while the Flutter UI initializes. After that, this theme continues
to determine the Window background behind the Flutter UI. -->
<meta-data
android:name="io.flutter.embedding.android.NormalTheme"
android:resource="@style/NormalTheme"
/>
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
</activity>
<!-- Don't delete the meta-data below.
This is used by the Flutter tool to generate GeneratedPluginRegistrant.java -->
<meta-data
android:name="flutterEmbedding"
android:value="2" />
</application>
<!-- Required to query activities that can process text, see:
https://developer.android.com/training/package-visibility and
https://developer.android.com/reference/android/content/Intent#ACTION_PROCESS_TEXT.
In particular, this is used by the Flutter engine in io.flutter.plugin.text.ProcessTextPlugin. -->
<queries>
<intent>
<action android:name="android.intent.action.PROCESS_TEXT"/>
<data android:mimeType="text/plain"/>
</intent>
</queries>
</manifest>
แก้ไขไฟล์การให้สิทธิ์สำหรับ macOS
สำหรับ macOS คุณมีไฟล์ 2 ไฟล์ที่ต้องแก้ไข
- แก้ไขไฟล์
DebugProfile.entitlementsให้ตรงกับโค้ดต่อไปนี้
macos/Runner/DebugProfile.entitlements
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>com.apple.security.app-sandbox</key>
<true/>
<key>com.apple.security.cs.allow-jit</key>
<true/>
<key>com.apple.security.network.server</key>
<true/>
<!-- Add from here... -->
<key>com.apple.security.network.client</key>
<true/>
<!-- to here. -->
</dict>
</plist>
- แก้ไขไฟล์
Release.entitlementsให้ตรงกับโค้ดต่อไปนี้
macos/Runner/Release.entitlements
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>com.apple.security.app-sandbox</key>
<true/>
<!-- Add from here... -->
<key>com.apple.security.network.client</key>
<true/>
<!-- to here. -->
</dict>
</plist>
การเรียกใช้โค้ดนี้ตามที่ระบุควรแสดงหน้าจอต้อนรับและหน้าจอเกมโอเวอร์หรือหน้าจอชนะในทุกแพลตฟอร์ม หน้าจอเหล่านั้นอาจดูเรียบง่ายไปหน่อย และคงจะดีหากมีคะแนน ดังนั้น คุณจะทำอะไรในขั้นตอนถัดไป
10. จดคะแนน
เพิ่มคะแนนลงในเกม
ในขั้นตอนนี้ คุณจะแสดงคะแนนเกมต่อบริบท Flutter โดยรอบ ในขั้นตอนนี้ คุณจะเปิดเผยสถานะจากเกม Flame ไปยังการจัดการสถานะ Flutter โดยรอบ ซึ่งจะช่วยให้โค้ดเกมอัปเดตคะแนนทุกครั้งที่ผู้เล่นทำลายอิฐ
- แก้ไขเกม
BrickBreakerดังนี้
lib/src/brick_breaker.dart
import 'dart:async';
import 'dart:math' as math;
import 'package:flame/components.dart';
import 'package:flame/events.dart';
import 'package:flame/game.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'components/components.dart';
import 'config.dart';
enum PlayState { welcome, playing, gameOver, won }
class BrickBreaker extends FlameGame
with HasCollisionDetection, KeyboardEvents, TapDetector {
BrickBreaker()
: super(
camera: CameraComponent.withFixedResolution(
width: gameWidth,
height: gameHeight,
),
);
final ValueNotifier<int> score = ValueNotifier(0); // Add this line
final rand = math.Random();
double get width => size.x;
double get height => size.y;
late PlayState _playState;
PlayState get playState => _playState;
set playState(PlayState playState) {
_playState = playState;
switch (playState) {
case PlayState.welcome:
case PlayState.gameOver:
case PlayState.won:
overlays.add(playState.name);
case PlayState.playing:
overlays.remove(PlayState.welcome.name);
overlays.remove(PlayState.gameOver.name);
overlays.remove(PlayState.won.name);
}
}
@override
FutureOr<void> onLoad() async {
super.onLoad();
camera.viewfinder.anchor = Anchor.topLeft;
world.add(PlayArea());
playState = PlayState.welcome;
}
void startGame() {
if (playState == PlayState.playing) return;
world.removeAll(world.children.query<Ball>());
world.removeAll(world.children.query<Bat>());
world.removeAll(world.children.query<Brick>());
playState = PlayState.playing;
score.value = 0; // Add this line
world.add(
Ball(
difficultyModifier: difficultyModifier,
radius: ballRadius,
position: size / 2,
velocity: Vector2(
(rand.nextDouble() - 0.5) * width,
height * 0.2,
).normalized()..scale(height / 4),
),
);
world.add(
Bat(
size: Vector2(batWidth, batHeight),
cornerRadius: const Radius.circular(ballRadius / 2),
position: Vector2(width / 2, height * 0.95),
),
);
world.addAll([
for (var i = 0; i < brickColors.length; i++)
for (var j = 1; j <= 5; j++)
Brick(
position: Vector2(
(i + 0.5) * brickWidth + (i + 1) * brickGutter,
(j + 2.0) * brickHeight + j * brickGutter,
),
color: brickColors[i],
),
]);
}
@override
void onTap() {
super.onTap();
startGame();
}
@override
KeyEventResult onKeyEvent(
KeyEvent event,
Set<LogicalKeyboardKey> keysPressed,
) {
super.onKeyEvent(event, keysPressed);
switch (event.logicalKey) {
case LogicalKeyboardKey.arrowLeft:
world.children.query<Bat>().first.moveBy(-batStep);
case LogicalKeyboardKey.arrowRight:
world.children.query<Bat>().first.moveBy(batStep);
case LogicalKeyboardKey.space:
case LogicalKeyboardKey.enter:
startGame();
}
return KeyEventResult.handled;
}
@override
Color backgroundColor() => const Color(0xfff2e8cf);
}
การเพิ่ม score ลงในเกมจะทำให้สถานะของเกมเชื่อมโยงกับการจัดการสถานะของ Flutter
- แก้ไข
Brickclass เพื่อเพิ่มคะแนนเมื่อผู้เล่นทำลายอิฐ
lib/src/components/brick.dart
import 'package:flame/collisions.dart';
import 'package:flame/components.dart';
import 'package:flutter/material.dart';
import '../brick_breaker.dart';
import '../config.dart';
import 'ball.dart';
import 'bat.dart';
class Brick extends RectangleComponent
with CollisionCallbacks, HasGameReference<BrickBreaker> {
Brick({required super.position, required Color color})
: super(
size: Vector2(brickWidth, brickHeight),
anchor: Anchor.center,
paint: Paint()
..color = color
..style = PaintingStyle.fill,
children: [RectangleHitbox()],
);
@override
void onCollisionStart(
Set<Vector2> intersectionPoints,
PositionComponent other,
) {
super.onCollisionStart(intersectionPoints, other);
removeFromParent();
game.score.value++; // Add this line
if (game.world.children.query<Brick>().length == 1) {
game.playState = PlayState.won;
game.world.removeAll(game.world.children.query<Ball>());
game.world.removeAll(game.world.children.query<Bat>());
}
}
}
สร้างเกมที่ดูดี
ตอนนี้คุณสามารถบันทึกคะแนนใน Flutter ได้แล้ว ก็ถึงเวลาประกอบวิดเจ็ตเพื่อให้ดูดี
- สร้าง
score_card.dartในlib/src/widgetsแล้วเพิ่มข้อมูลต่อไปนี้
lib/src/widgets/score_card.dart
import 'package:flutter/material.dart';
class ScoreCard extends StatelessWidget {
const ScoreCard({super.key, required this.score});
final ValueNotifier<int> score;
@override
Widget build(BuildContext context) {
return ValueListenableBuilder<int>(
valueListenable: score,
builder: (context, score, child) {
return Padding(
padding: const EdgeInsets.fromLTRB(12, 6, 12, 18),
child: Text(
'Score: $score'.toUpperCase(),
style: Theme.of(context).textTheme.titleLarge!,
),
);
},
);
}
}
- สร้าง
overlay_screen.dartในlib/src/widgetsแล้วเพิ่มโค้ดต่อไปนี้
ซึ่งจะช่วยให้การวางซ้อนดูดีขึ้นด้วยการใช้พลังของflutter_animateแพ็กเกจเพื่อเพิ่มการเคลื่อนไหวและสไตล์ให้กับหน้าจอการวางซ้อน
lib/src/widgets/overlay_screen.dart
import 'package:flutter/material.dart';
import 'package:flutter_animate/flutter_animate.dart';
class OverlayScreen extends StatelessWidget {
const OverlayScreen({super.key, required this.title, required this.subtitle});
final String title;
final String subtitle;
@override
Widget build(BuildContext context) {
return Container(
alignment: const Alignment(0, -0.15),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Text(
title,
style: Theme.of(context).textTheme.headlineLarge,
).animate().slideY(duration: 750.ms, begin: -3, end: 0),
const SizedBox(height: 16),
Text(subtitle, style: Theme.of(context).textTheme.headlineSmall)
.animate(onPlay: (controller) => controller.repeat())
.fadeIn(duration: 1.seconds)
.then()
.fadeOut(duration: 1.seconds),
],
),
);
}
}
หากต้องการดูรายละเอียดเพิ่มเติมเกี่ยวกับประสิทธิภาพของ flutter_animate โปรดดู Codelab Building next generation UIs in Flutter
โค้ดนี้มีการเปลี่ยนแปลงมากมายในคอมโพเนนต์ GameApp ก่อนอื่น หากต้องการให้ ScoreCard เข้าถึง score ได้ คุณต้องแปลงจาก StatelessWidget เป็น StatefulWidget การเพิ่มตารางสรุปสถิติจะต้องเพิ่ม Column เพื่อซ้อนคะแนนเหนือเกม
ประการที่ 2 คุณได้เพิ่มวิดเจ็ต OverlayScreen ใหม่เพื่อปรับปรุงประสบการณ์การต้อนรับ เกมจบ และชนะ
lib/src/widgets/game_app.dart
import 'package:flame/game.dart';
import 'package:flutter/material.dart';
import 'package:google_fonts/google_fonts.dart';
import '../brick_breaker.dart';
import '../config.dart';
import 'overlay_screen.dart'; // Add this import
import 'score_card.dart'; // And this one too
class GameApp extends StatefulWidget { // Modify this line
const GameApp({super.key});
@override // Add from here...
State<GameApp> createState() => _GameAppState();
}
class _GameAppState extends State<GameApp> {
late final BrickBreaker game;
@override
void initState() {
super.initState();
game = BrickBreaker();
} // To here.
@override
Widget build(BuildContext context) {
return MaterialApp(
debugShowCheckedModeBanner: false,
theme: ThemeData(
textTheme: GoogleFonts.pressStart2pTextTheme().apply(
bodyColor: const Color(0xff184e77),
displayColor: const Color(0xff184e77),
),
),
home: Scaffold(
body: Container(
decoration: const BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: [Color(0xffa9d6e5), Color(0xfff2e8cf)],
),
),
child: SafeArea(
child: Padding(
padding: const EdgeInsets.all(16),
child: Center(
child: Column( // Modify from here...
children: [
ScoreCard(score: game.score),
Expanded(
child: FittedBox(
child: SizedBox(
width: gameWidth,
height: gameHeight,
child: GameWidget(
game: game,
overlayBuilderMap: {
PlayState.welcome.name: (context, game) =>
const OverlayScreen(
title: 'TAP TO PLAY',
subtitle: 'Use arrow keys or swipe',
),
PlayState.gameOver.name: (context, game) =>
const OverlayScreen(
title: 'G A M E O V E R',
subtitle: 'Tap to Play Again',
),
PlayState.won.name: (context, game) =>
const OverlayScreen(
title: 'Y O U W O N ! ! !',
subtitle: 'Tap to Play Again',
),
},
),
),
),
),
],
), // To here.
),
),
),
),
),
);
}
}
เมื่อตั้งค่าทุกอย่างเรียบร้อยแล้ว คุณควรจะเรียกใช้เกมนี้บนแพลตฟอร์มเป้าหมาย Flutter ทั้ง 6 แพลตฟอร์มได้ เกมควรมีลักษณะคล้ายกับตัวอย่างต่อไปนี้
|
|
11. ขอแสดงความยินดี
ขอแสดงความยินดี คุณสร้างเกมด้วย Flutter และ Flame ได้สำเร็จแล้ว
คุณสร้างเกมโดยใช้เครื่องมือเกม 2 มิติของ Flame และฝังไว้ใน Wrapper ของ Flutter คุณใช้เอฟเฟกต์ของ Flame เพื่อสร้างภาพเคลื่อนไหวและนำคอมโพเนนต์ออก คุณใช้แพ็กเกจ Google Fonts และ Flutter Animate เพื่อให้เกมทั้งเกมดูออกแบบมาอย่างดี
ขั้นตอนต่อไปคืออะไร
ลองดู Codelab เหล่านี้
- การสร้าง UI รุ่นถัดไปใน Flutter
- เปลี่ยนแอป Flutter จากน่าเบื่อเป็นสวยงาม
- การเพิ่มการซื้อในแอปไปยังแอป Flutter

