1. บทนำ
Flame เป็นเครื่องมือเกม 2D ที่ใช้ Flutter ใน Codelab นี้ คุณจะได้สร้างเกมที่ได้รับแรงบันดาลใจมาจากหนึ่งในวิดีโอเกมสุดคลาสสิกยุค 70 อย่าง Breakout ของ Steve Wozniak คุณจะใช้องค์ประกอบของเปลวเพลิงเพื่อวาดไม้ตี ลูกบอล และก้อนอิฐ คุณจะใช้เอฟเฟกต์ Flame เพื่อสร้างภาพเคลื่อนไหวของการเคลื่อนที่ของค้างคาวและดูวิธีผสานรวม Flame กับระบบการจัดการสถานะของ Flutter
เมื่อดำเนินการเสร็จแล้ว เกมควรมีลักษณะเหมือน GIF แบบเคลื่อนไหวนี้ แม้ว่าจะช้ากว่าเล็กน้อย
สิ่งที่คุณจะได้เรียนรู้
- ข้อมูลเบื้องต้นเกี่ยวกับ Flame เริ่มที่
GameWidget
- วิธีใช้ Game Loop
- วิธีการทำงานของ
Component
ของ Flame ซึ่งคล้ายกับWidget
ของ Flutter - วิธีรับมือกับการชนกัน
- วิธีใช้
Effect
เพื่อทำให้Component
เคลื่อนไหว - วิธีวางซ้อน
Widget
ของ Flutter ไว้เหนือเกม Flame - วิธีผสานรวม Flame กับการจัดการสถานะของ Flutter
สิ่งที่คุณจะสร้าง
ใน Codelab นี้ คุณจะได้สร้างเกม 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 (VS Code) เป็นสภาพแวดล้อมในการพัฒนาซอฟต์แวร์ของคุณ VS Code ไม่เสียค่าใช้จ่ายและทำงานบนแพลตฟอร์มหลักทั้งหมด เราใช้ VS Code สำหรับ Codelab นี้เนื่องจากคำแนะนำเป็นค่าเริ่มต้นของแป้นพิมพ์ลัดเฉพาะ VS งานต่างๆ จะง่ายขึ้น: "คลิกปุ่มนี้" หรือ "กดแป้นนี้เพื่อทำ X" แทนที่จะ "ดำเนินการที่เหมาะสมในตัวแก้ไขเพื่อทำ X"
คุณสามารถใช้เครื่องมือแก้ไขใดก็ได้ เช่น Android Studio, IntelliJ IDE อื่นๆ, Emacs, Vim หรือ Notepad++ ทุกเครื่องใช้กับ Flutter ได้
เลือกเป้าหมายการพัฒนา
Flutter ผลิตแอปสำหรับหลายแพลตฟอร์ม แอปของคุณสามารถทำงานในระบบปฏิบัติการใดก็ได้ต่อไปนี้
- iOS
- Android
- Windows
- macOS
- Linux
- เว็บ
แนวทางปฏิบัติทั่วไปคือการเลือกระบบปฏิบัติการเพียงระบบเดียวเป็นเป้าหมายการพัฒนา ซึ่งเป็นระบบปฏิบัติการที่แอปของคุณใช้งานในระหว่างการพัฒนา
เช่น สมมติว่าคุณใช้แล็ปท็อป Windows เพื่อพัฒนาแอป Flutter จากนั้นเลือก Android เป็นเป้าหมายการพัฒนา หากต้องการดูตัวอย่างแอป ให้ต่ออุปกรณ์ Android เข้ากับแล็ปท็อป Windows ด้วยสาย USB ส่วนการพัฒนาแอปจะทำงานในอุปกรณ์ Android ที่เชื่อมต่ออยู่หรือในโปรแกรมจำลอง Android คุณอาจเลือก Windows เป็นเป้าหมายของการพัฒนา ซึ่งจะเรียกใช้แอปที่อยู่ระหว่างการพัฒนาเป็นแอป Windows ควบคู่ไปกับตัวแก้ไข
คุณอาจอยากเลือกเว็บเป็นเป้าหมายการพัฒนา ซึ่งข้อเสียในระหว่างการพัฒนาก็คือคุณจะใช้ความสามารถ Stateful Hot Reload ของ Flutter ไม่ได้ Flutter โหลดเว็บแอปพลิเคชันซ้ำแบบ Hot-Re ไม่ได้
เลือกตัวเลือกก่อนดำเนินการต่อ คุณสามารถเรียกใช้แอปในระบบปฏิบัติการอื่นในภายหลังได้เสมอ การเลือกเป้าหมายการพัฒนาจะช่วยให้ขั้นตอนถัดไปราบรื่นขึ้น
ติดตั้ง Flutter
ดูวิธีการล่าสุดในการติดตั้ง Flutter SDK ได้ที่ docs.flutter.dev
วิธีการในเว็บไซต์ Flutter ครอบคลุมการติดตั้ง SDK และเครื่องมือที่เกี่ยวข้องกับเป้าหมายการพัฒนา และปลั๊กอินเครื่องมือแก้ไข สำหรับ Codelab นี้ ให้ติดตั้งซอฟต์แวร์ต่อไปนี้
- Flutter SDK
- โค้ด Visual Studio ที่มีปลั๊กอิน Flutter
- ซอฟต์แวร์คอมไพเลอร์สำหรับเป้าหมายการพัฒนาที่คุณเลือก (คุณต้องใช้ Visual Studio เพื่อกําหนดเป้าหมาย Windows หรือ Xcode เพื่อกําหนดเป้าหมายเป็น macOS หรือ iOS)
ในส่วนถัดไป คุณจะได้สร้างโปรเจ็กต์ Flutter แรก
หากต้องการแก้ปัญหาใดๆ คุณอาจพบว่าคำถามและคำตอบเหล่านี้ (จาก StackOverflow) มีประโยชน์สำหรับการแก้ปัญหา
คำถามที่พบบ่อย
- ฉันจะดูเส้นทาง Flutter SDK ได้อย่างไร
- ฉันควรทำอย่างไรหากไม่พบคำสั่ง 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
- เปิดพาเล็ตคำสั่ง (
F1
หรือCtrl+Shift+P
หรือShift+Cmd+P
) แล้วพิมพ์ "flutter new" เมื่อปรากฏขึ้น ให้เลือกคำสั่ง Flutter: New Project
- เลือกแอปพลิเคชันว่างเปล่า เลือกไดเรกทอรีที่จะสร้างโปรเจ็กต์ ไดเรกทอรีนี้ควรเป็นไดเรกทอรีที่ไม่ต้องใช้สิทธิ์ในระดับสูงขึ้นหรือมีพื้นที่ว่างในเส้นทาง ตัวอย่างเช่น ไดเรกทอรีหน้าแรกหรือ
C:\src\
- ตั้งชื่อโปรเจ็กต์ของคุณว่า
brick_breaker
ส่วนที่เหลือของ Codelab จะถือว่าคุณตั้งชื่อแอปว่าbrick_breaker
ตอนนี้ Flutter จะสร้างโฟลเดอร์โปรเจ็กต์และ VS Code จะเปิดขึ้น คุณจะเขียนทับเนื้อหาของไฟล์ 2 ไฟล์ด้วยโครงข่ายพื้นฐานของแอป
คัดลอกและ วางแอปเริ่มต้น
การดำเนินการนี้จะเพิ่มโค้ดตัวอย่างที่ให้ไว้ใน Codelab นี้ลงในแอปของคุณ
- คลิก Explorer ในแผงด้านซ้ายของ VS Code แล้วเปิดไฟล์
pubspec.yaml
- โดยแทนที่เนื้อหาของไฟล์นี้ด้วยข้อมูลต่อไปนี้
pubspec.yaml
name: brick_breaker
description: "Re-implementing Woz's Breakout"
publish_to: 'none'
version: 0.1.0
environment:
sdk: '>=3.3.0 <4.0.0'
dependencies:
flame: ^1.16.0
flutter:
sdk: flutter
flutter_animate: ^4.5.0
google_fonts: ^6.1.0
dev_dependencies:
flutter_test:
sdk: flutter
flutter_lints: ^3.0.1
flutter:
uses-material-design: true
ไฟล์ pubspec.yaml
ระบุข้อมูลพื้นฐานเกี่ยวกับแอป เช่น เวอร์ชันปัจจุบัน ทรัพยากร Dependency และเนื้อหาที่จะจัดส่งแอป
- เปิดไฟล์
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));
}
- เรียกใช้โค้ดนี้เพื่อยืนยันว่าทุกอย่างทำงานได้ดี จากนั้นระบบจะแสดงหน้าต่างใหม่ที่มีเฉพาะพื้นหลังว่างเปล่าสีดำ วิดีโอเกมที่แย่ที่สุดในโลกกำลังแสดงผลที่ 60fps อยู่!
4. สร้างเกม
ขยายขนาดเกม
เกมที่เล่นแบบ 2 มิติ (2 มิติ) ต้องมีพื้นที่เล่น คุณจะสร้างพื้นที่ที่มีขนาดเฉพาะเจาะจง จากนั้นใช้มิติข้อมูลเหล่านี้เพื่อปรับขนาดด้านอื่นๆ ของเกม
การวางพิกัดในพื้นที่เล่นนั้นทำได้หลายวิธี วิธีการหนึ่งคือคุณจะสามารถวัดทิศทางจากกึ่งกลางของหน้าจอโดยมีต้นทาง(0,0)
ที่กึ่งกลางหน้าจอ ค่าบวกจะย้ายรายการไปทางขวาตามแกน x และขึ้นด้านบนตามแกน y มาตรฐานนี้ใช้กับเกมปัจจุบันส่วนใหญ่ในปัจจุบัน โดยเฉพาะเมื่อเกมที่เกี่ยวข้องกับ 3 มิติ
กฎข้อเดิมเมื่อมีการสร้างเกมเบรกเอาต์เดิมขึ้นคือ การตั้งต้นทางที่มุมซ้ายบน ทิศทางแกน x ที่เป็นบวกยังคงเหมือนเดิม แต่ y กลับถูกกลับด้าน ทิศทาง x เป็นบวก ถูกต้อง และ Y ลดลง เกมนี้สร้างความแตกต่างให้กับยุคปัจจุบัน โดยเริ่มจากมุมซ้ายบน
สร้างไฟล์ชื่อ config.dart
ในไดเรกทอรีใหม่ชื่อ lib/src
ไฟล์นี้จะได้รับค่าคงที่เพิ่มขึ้นในขั้นตอนต่อไปนี้
lib/src/config.dart
const gameWidth = 820.0;
const gameHeight = 1600.0;
เกมนี้มีความกว้าง 820 พิกเซลและสูง 1,600 พิกเซล พื้นที่เกมจะปรับขนาดให้พอดีกับหน้าต่างที่แสดง แต่องค์ประกอบทั้งหมดที่เพิ่มในหน้าจอจะสอดคล้องกับความสูงและความกว้างนี้
สร้าง PlayArea
ในเกม เบรกเอาต์ ลูกบอลจะกระเด้งกระดอนไปตามกำแพงของพื้นที่เล่น คุณต้องมีคอมโพเนนต์ 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
วินาที แสงจะมี 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
โดยจะประกาศว่าไฟล์นี้แสดงฟังก์ชันใดเมื่อนำเข้าไปยังไฟล์อื่น ไฟล์นี้จะมีรายการมากขึ้นเมื่อคุณเพิ่มคอมโพเนนต์ใหม่ในขั้นตอนต่อไปนี้
สร้างเกมเปลวไฟ
หากต้องการดับลายเส้นยึกยือสีแดงจากขั้นตอนก่อนหน้า ให้ดึงคลาสย่อยใหม่สำหรับ 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 รายการ
- กำหนดค่าด้านซ้ายบนเป็น Anchor สำหรับช่องมองภาพ โดยค่าเริ่มต้น ช่องมองภาพจะใช้ตรงกลางของพื้นที่เป็นจุดยึดสำหรับ
(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
คือระยะเวลาระหว่างเฟรมก่อนหน้ากับเฟรมนี้ ซึ่งจะทำให้คุณสามารถปรับให้เข้ากับปัจจัยต่างๆ เช่น อัตราเฟรมที่แตกต่างกัน (60 hz หรือ 120 hz) หรือเฟรมที่ยาวเนื่องจากการคำนวณมากเกินไป
โปรดให้ความสำคัญกับการอัปเดต 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;
}
}
วิธีนี้จะติดตาม Hit ของคอมโพเนนต์และทริกเกอร์ Callback ของการชนในทุกเกม
หากต้องการเริ่มสร้าง Hitbox ของเกม ให้แก้ไขคอมโพเนนต์ 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
จะสร้างกล่อง Hit สำหรับการตรวจหาการชนที่ตรงกับขนาดของคอมโพเนนต์หลัก มีตัวสร้างจากโรงงานสำหรับ RectangleHitbox
ที่เรียกว่า relative
สำหรับบางครั้งเมื่อคุณต้องการ Hitbox ที่เล็กกว่าหรือใหญ่กว่าคอมโพเนนต์หลัก
เด้งบอล
จนถึงตอนนี้ การเพิ่มการตรวจจับการชนไม่ได้ส่งผลใดๆ ต่อเกมเพลย์ และจะเปลี่ยนแปลงเมื่อคุณแก้ไขคอมโพเนนต์ 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.
}
ตัวอย่างนี้ทำให้เกิดการเปลี่ยนแปลงครั้งใหญ่ด้วยการเพิ่ม Callback onCollisionStart
ระบบตรวจจับการชนที่เพิ่มลงใน BrickBreaker
ในตัวอย่างก่อนหน้านี้เรียกใช้ Callback นี้
ก่อนอื่น โค้ดจะทดสอบว่า Ball
ชนกับ PlayArea
หรือไม่ ตอนนี้ดูเหมือนจะซ้ำซ้อนกันแล้ว เนื่องจากไม่มีองค์ประกอบอื่นๆ ในโลกของเกม แล้วสิ่งนี้จะเปลี่ยนไปในขั้นตอนถัดไป เมื่อคุณเพิ่มไม้เบสบอล จากนั้นจะเพิ่มเงื่อนไข else
เพื่อจัดการเมื่อลูกบอลชนกับสิ่งที่ไม่ใช่ไม้ตี ย้ำเตือนว่าคุณนำตรรกะที่เหลือไปใช้แล้วไม่สามารถทำได้
เมื่อลูกบอลชนกับผนังด้านล่าง ลูกบอลจะหายไปจากพื้นผิวที่เล่นโดยที่ยังอยู่ในมุมมอง คุณจะจัดการกับอาร์ติแฟกต์นี้ในขั้นตอนต่อไป โดยใช้พลังของ Flame'sเอฟเฟกต์
เมื่อลูกบอลชนกับผนังเกมแล้ว การให้ไม้ตีลูกให้ตีลูกก็คงมีประโยชน์...
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
บนหน้าจอ โดยจะลบล้าง Callback render
เพื่อดำเนินการนี้
เมื่อดูการเรียก canvas.drawRRect
(วาดรูปสี่เหลี่ยมผืนผ้ามุมมน) อย่างใกล้ชิด คุณอาจสงสัยว่า "รูปสี่เหลี่ยมผืนผ้าอยู่ที่ไหน" Offset.zero & size.toSize()
จะใช้ประโยชน์จากโอเวอร์โหลด operator &
ในคลาส dart:ui
Offset
ที่สร้าง Rect
ชวเลขนี้อาจทำให้คุณสับสนในตอนแรก แต่คุณจะเห็นบ่อยครั้งในโค้ด Flutter และ Flame ระดับล่าง
อย่างที่ 2 คุณจะลากคอมโพเนนต์ Bat
นี้ได้โดยใช้นิ้วหรือเมาส์ โดยขึ้นอยู่กับแพลตฟอร์ม หากต้องการใช้ฟังก์ชันนี้ ให้เพิ่มการมิกซ์ DragCallbacks
และลบล้างเหตุการณ์ onDragUpdate
สุดท้าย คอมโพเนนต์ Bat
ต้องตอบสนองต่อการควบคุมแป้นพิมพ์ ฟังก์ชัน moveBy
ทำให้โค้ดอื่นบอกไม้เบสบอลนี้ให้เลื่อนไปทางซ้ายหรือขวาตามจำนวนพิกเซลเสมือนที่กำหนด ฟังก์ชันนี้มีขีดความสามารถใหม่ของเครื่องมือเกม Flame: Effect
s เมื่อเพิ่มออบเจ็กต์ MoveToEffect
เป็นหน่วยย่อยของคอมโพเนนต์นี้ ผู้เล่นจะเห็นค้างคาวที่ขยับไปยังตำแหน่งใหม่ Flame มีคอลเล็กชัน Effect
ให้ใช้กับเอฟเฟกต์ต่างๆ ได้
อาร์กิวเมนต์ตัวสร้างของเอฟเฟกต์มีการอ้างอิงถึง Getter 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(Bat( // Add from here...
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
และวิธี 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( // Modify from here...
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 { // To here.
debugPrint('collision with $other');
}
}
}
การเปลี่ยนแปลงโค้ดเหล่านี้จะแก้ปัญหา 2 อย่างที่แยกกัน
ประการแรก เครื่องมือนี้จะแก้ไขลูกบอลที่เด้งออกมาเมื่อแตะด้านล่างของหน้าจอ หากต้องการแก้ไขปัญหานี้ ให้ใช้ RemoveEffect
แทนการโทร removeFromParent
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
ที่ด้านบนสุดของคอมโพเนนต์ต้นไม้
แนวคิดใหม่ที่สำคัญที่สุดที่โค้ดนี้แนะนำคือวิธีที่ผู้เล่นจะบรรลุเงื่อนไขการชนะ การตรวจสอบเงื่อนไขที่ชนะจะค้นหาอิฐจากทั่วโลกและยืนยันว่ามีเพียงอิฐที่เหลืออยู่เพียงใบเดียว วิธีนี้อาจฟังดูสับสนเล็กน้อย เนื่องจากบรรทัดก่อนหน้าจะนำตัวต่อออกจากระดับบนสุด
ประเด็นสำคัญที่ต้องเข้าใจคือการนำคอมโพเนนต์ออกจะเป็นคำสั่งที่อยู่ในคิว นำอิฐออกหลังจากที่โค้ดนี้ทำงาน แต่ก่อนเวลาถัดไปในโลกของเกม
หากต้องการทำให้ BrickBreaker
เข้าถึงคอมโพเนนต์ Brick
ได้ ให้แก้ไข 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
ต้องใช้ความพยายามมาก ซึ่งจะเก็บข้อมูลตำแหน่งที่ผู้เล่นเข้า เล่น และแพ้หรือชนะ ที่ด้านบนของไฟล์ ให้ระบุการแจงนับ จากนั้นจึงสร้างอินสแตนซ์เป็นสถานะที่ซ่อนอยู่โดยมี Getters และ Setter ที่ตรงกัน Getters และ 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
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.playState = PlayState.won; // Add this line
game.world.removeAll(game.world.children.query<Ball>());
game.world.removeAll(game.world.children.query<Bat>());
}
}
}
ในทางกลับกัน หากผู้เล่นทำลายอิฐได้ทั้งหมด ก็จะได้ "ชนะในเกม" บนหน้าจอ ผู้เล่นยอดเยี่ยม เก่งมาก!
เพิ่ม Wrapper ของ Flutter
หากต้องการระบุตำแหน่งสำหรับฝังเกมและเพิ่มการวางซ้อนสถานะการเล่น ให้เพิ่ม Shell 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(
useMaterial3: true,
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
- แก้ไขคลาส
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();
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 การสร้าง UI รุ่นถัดไปใน 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(
useMaterial3: true,
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 ได้สำเร็จ!
คุณได้สร้างเกมโดยใช้เครื่องมือเกม Flame 2D และฝังเกมไว้ใน Wrapper ของ Flutter คุณใช้เอฟเฟกต์ Flame เพื่อสร้างภาพเคลื่อนไหวและนำคอมโพเนนต์ออก คุณใช้แพ็กเกจ Google Fonts และ Flutter Animate เพื่อทำให้ทั้งเกมดูออกแบบมาอย่างดี
ขั้นตอนถัดไปคือ
ลองดู Codelab เหล่านี้...
- การสร้าง UI รุ่นใหม่ใน Flutter
- เปลี่ยนแอป Flutter แต่ไม่น่าเบื่อให้กลายเป็นความสวยงาม
- การเพิ่มการซื้อในแอปไปยังแอป Flutter