Hướng dẫn xây dựng trò chơi 2D với Flutter – Sự xuất hiện và lớn mạnh của Flutter đã tạo đòn bẩy cho sự phát triển của thiết kế trò chơi đa nền tảng; Trò chơi Flutter có thể được tạo chỉ với một vài dòng mã cho thiết kế và logic, trong khi vẫn duy trì giao diện người dùng / UX tuyệt vời.


Flutter có khả năng hiển thị ở tốc độ lên đến 60FPS. Bạn có thể khai thác khả năng đó để xây dựng một trò chơi 2D hoặc thậm chí 3D đơn giản. Hãy nhớ rằng các trò chơi phức tạp hơn sẽ không phải là một ý tưởng hay để phát triển trong Flutter, vì hầu hết các nhà phát triển sẽ hướng tới phát triển bản địa cho các ứng dụng phức tạp.
Trong hướng dẫn này, chúng tôi sẽ tạo lại một trong những trò chơi máy tính đầu tiên từng được tạo ra: Pong. Pong là một trò chơi đơn giản, vì vậy đây là một nơi tuyệt vời để bắt đầu. Bài viết này được chia thành hai phần chính: logic trò chơi và giao diện người dùng, để làm cho việc xây dựng rõ ràng hơn một chút bằng cách tập trung vào các phần quan trọng một cách riêng biệt.
Trước khi bắt đầu xây dựng, chúng ta hãy xem xét các điều kiện tiên quyết và thiết lập.
Nội dung
Điều kiện tiên quyết
Để hiểu và viết mã cùng với bài học này, bạn sẽ cần những thứ sau:
- Flutter được cài đặt trên máy của bạn
- Kiến thức làm việc của Dart và Flutter
- Một trình soạn thảo văn bản
Bắt đầu
Trong bài đăng này, chúng tôi sẽ sử dụng làm đại diện cho vị trí của trục X và Y của màn hình, điều này sẽ giúp phát triển vật lý của trò chơi. Chúng tôi cũng sẽ tạo các widget không trạng thái cho một số biến của chúng tôi và khai báo chúng trong tệp để làm cho mã bớt cồng kềnh và dễ hiểu.Alignment(x,y)
Vector(x,y)
homepage.dart
Đầu tiên, tạo một dự án Flutter. Xóa mã mặc định trong tệp và nhập gói để bao gồm các tiện ích Vật liệu trong ứng dụng.main.dart
material.dart
Tiếp theo, tạo một lớp được gọi và trả về , sau đó tạo một và chuyển nó vào tham số như hình dưới đây:MyApp()
MaterialApp()
statefulWidget
HomePage()
home
MaterialApp()
//player variations
double playerX = -0.2;double playerX = -0.2;
double brickWidth = 0.4;double brickWidth = 0.4;
int playerScore = 0;int playerScore = 0;
// enemy variable// enemy variable
double enemyX = -0.2;double enemyX = -0.2;
int enemyScore = 0;int enemyScore = 0;
//ball//ball
double ballx = 0;double ballx = 0;
double bally = 0;double bally = 0;
var ballYDirection = direction.DOWN;var ballYDirection = direction.DOWN;
var ballXDirection = direction.RIGHT;var ballXDirection = direction.RIGHT;
bool gameStarted = false;bool gameStarted = false;
......
Sau đó, chúng tôi cung cấp một bảng liệt kê cho các hướng di chuyển của quả bóng và viên gạch:
enum direction { UP, DOWN, LEFT, RIGHT } direction { UP, DOWN, LEFT, RIGHT }
......
Để trò chơi này hoạt động, chúng ta cần tạo ra trọng lực nhân tạo để khi quả bóng chạm vào viên gạch trên cùng (0.9) hoặc viên gạch dưới (-0.9), nó sẽ đi theo hướng ngược lại. Ngược lại, nếu nó không trúng một trong hai viên gạch và đi lên đầu (1) hoặc dưới cùng (-1) của sân chơi, nó sẽ ghi lại là người chơi thua.
Khi quả bóng chạm vào bức tường bên trái (1) hoặc bên phải (-1), nó sẽ đi theo hướng ngược lại:
void startGame() { startGame() {
gameStarted = true;= true;
Timer.periodic(Duration(milliseconds: 1), (timer) {Timer.periodic(Duration(milliseconds: 1), (timer) {
updatedDirection();();
moveBall();();
moveEnemy();();
if (isPlayerDead()) {if (isPlayerDead()) {
enemyScore++;++;
timer.cancel();.cancel();
_showDialog(false);(false);
// resetGame();// resetGame();
}}
if (isEnemyDead()) {if (isEnemyDead()) {
playerScore++;++;
timer.cancel();.cancel();
_showDialog(true);(true);
// resetGame();// resetGame();
}}
});});
}}
......
Trong đoạn mã trên, chúng ta đã bắt đầu với một hàm thay đổi boolean thành , sau đó chúng ta gọi a với thời lượng là một giây.startGame()
gameStarted
true
Timer()
Trong bộ đếm thời gian, các chức năng như , và được chuyển cùng với một câu lệnh để kiểm tra xem một trong hai người chơi có bị lỗi hay không. Nếu vậy, điểm số được tích lũy, bộ đếm thời gian bị hủy và một hộp thoại sẽ hiển thị.updatedDirection()
moveBall()
moveEnemy()
if
Các chức năng sau đây đảm bảo rằng quả bóng không đi quá 0.9
thẳng hàng và quả bóng sẽ chỉ đi theo hướng ngược lại khi nó tiếp xúc với viên gạch:
void updatedDirection() { updatedDirection() {
setState(() {(() {
//update vertical dirction//update vertical dirction
if (bally >= 0.9 && playerX + brickWidth>= ballx && playerX <= ballx) {if (bally >= 0.9 && playerX + brickWidth>= ballx && playerX <= ballx) {
ballYDirection = direction.UP;= direction.UP;
} else if (bally <= -0.9) {} else if (bally <= -0.9) {
ballYDirection = direction.DOWN;= direction.DOWN;
}}
// update horizontal directions// update horizontal directions
if (ballx >= 1) {if (ballx >= 1) {
ballXDirection = direction.LEFT;= direction.LEFT;
} else if (ballx <= -1) {} else if (ballx <= -1) {
ballXDirection = direction.RIGHT;= direction.RIGHT;
}}
});});
}}
void moveBall() {void moveBall() {
//vertical movement//vertical movement
setState(() {(() {
if (ballYDirection == direction.DOWN) {if (ballYDirection == direction.DOWN) {
bally += 0.01;+= 0.01;
} else if (ballYDirection == direction.UP) {} else if (ballYDirection == direction.UP) {
bally -= 0.01;-= 0.01;
}}
});});
//horizontal movement//horizontal movement
setState(() {(() {
if (ballXDirection == direction.LEFT) {if (ballXDirection == direction.LEFT) {
ballx -= 0.01;-= 0.01;
} else if (ballXDirection == direction.RIGHT) {} else if (ballXDirection == direction.RIGHT) {
ballx += 0.01;+= 0.01;
}}
});});
}}
......
Also, if the ball hits the left or right of the field, it goes in the opposite direction:
void moveLeft() { moveLeft() {
setState(() {(() {
if (!(playerX - 0.1 <= -1)) {if (!(playerX - 0.1 <= -1)) {
playerX -= 0.1;-= 0.1;
}}
});});
}}
void moveRight() {void moveRight() {
if (!(playerX + brickWidth >= 1)) {if (!(playerX + brickWidth >= 1)) {
playerX += 0.1;+= 0.1;
}}
}}
......
Các và chức năng giúp điều khiển chuyển động của các viên gạch của chúng ta từ trái sang phải bằng cách sử dụng mũi tên trên bàn phím. Chúng hoạt động với một câu lệnh để đảm bảo các viên gạch không vượt ra ngoài chiều rộng của cả hai trục của trường.moveLeft()
moveRight()
if
Hàm trả về các cầu thủ và bóng về vị trí mặc định của họ:resetGame()
void resetGame() { resetGame() {
Navigator.pop(context);Navigator.pop(context);
setState(() {(() {
gameStarted = false;= false;
ballx = 0;= 0;
bally = 0;= 0;
playerX = -0.2;= -0.2;
enemyX =- 0.2;=- 0.2;
});});
}}
......
Tiếp theo, chúng ta tạo hai hàm và trả về giá trị boolean. Họ kiểm tra xem một trong hai người chơi có bị thua hay không (nếu bóng đã chạm vào phần thẳng đứng phía sau viên gạch):isEnemyDead()
isPlayerDead()
bool isEnemyDead(){ isEnemyDead(){
if (bally <= -1) {if (bally <= -1) {
return true;return true;
}}
return false;return false;
}}
bool isPlayerDead() {bool isPlayerDead() {
if (bally >= 1) {if (bally >= 1) {
return true;return true;
}}
return false;return false;
}}
......
Cuối cùng, chức năng _showDialog
hiển thị hộp thoại khi một trong hai người chơi thắng. Nó chuyển một boolean, enemyDied
để phân biệt khi nào người chơi thua. Sau đó, nó tuyên bố người chơi không thua đã thắng vòng và sử dụng màu của người chơi chiến thắng cho văn bản hiển thị “chơi lại:”
void _showDialog(bool enemyDied) { _showDialog(bool enemyDied) {
showDialog((
context: context,: context,
barrierDismissible: false,: false,
builder: (BuildContext context) {: (BuildContext context) {
// return object of type Dialog// return object of type Dialog
return AlertDialog(return AlertDialog(
elevation: 0.0,: 0.0,
shape: RoundedRectangleBorder(: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(10.0)),: BorderRadius.circular(10.0)),
backgroundColor: Colors.purple,: Colors.purple,
title: Center(: Center(
child: Text(: Text(
enemyDied?"Pink Wins": "Purple Wins",?"Pink Wins": "Purple Wins",
style: TextStyle(color: Colors.white),: TextStyle(color: Colors.white),
),),
),),
actions: [: [
GestureDetector(GestureDetector(
onTap: resetGame,: resetGame,
child: ClipRRect(: ClipRRect(
borderRadius: BorderRadius.circular(5),: BorderRadius.circular(5),
child: Container(: Container(
padding: EdgeInsets.all(7),: EdgeInsets.all(7),
color: Colors.purple[100],: Colors.purple[100],
child: Text(: Text(
"Play Again","Play Again",
style: TextStyle(color:enemyDied?Colors.pink[300]: Colors.purple[000]),: TextStyle(color:enemyDied?Colors.pink[300]: Colors.purple[000]),
)),)),
),),
))
],],
););
});});
}}
Giao diện người dùng
Bây giờ, chúng ta sẽ bắt đầu phát triển giao diện người dùng .
Bên trong tiện ích con build
trong tệp, hãy thêm mã bên dưới:homePage.dart
return RawKeyboardListener( RawKeyboardListener(
focusNode: FocusNode(),: FocusNode(),
autofocus: false,: false,
onKey: (event) {: (event) {
if (event.isKeyPressed(LogicalKeyboardKey.arrowLeft)) {if (event.isKeyPressed(LogicalKeyboardKey.arrowLeft)) {
moveLeft();();
} else if (event.isKeyPressed(LogicalKeyboardKey.arrowRight)) { } else if (event.isKeyPressed(LogicalKeyboardKey.arrowRight)) {
moveRight();();
}}
},},
child: GestureDetector(: GestureDetector(
onTap: startGame,: startGame,
child: Scaffold(: Scaffold(
backgroundColor: Colors.grey[900],: Colors.grey[900],
body: Center(: Center(
child: Stack(: Stack(
children: [: [
Welcome(gameStarted),Welcome(gameStarted),
//top brick//top brick
Brick(enemyX, -0.9, brickWidth, true),Brick(enemyX, -0.9, brickWidth, true),
//scoreboard//scoreboard
Score(gameStarted,enemyScore,playerScore),Score(gameStarted,enemyScore,playerScore),
// ball// ball
Ball(ballx, bally),Ball(ballx, bally),
// //bottom brick// //bottom brick
Brick(enemyX, 0.9, brickWidth, false)Brick(enemyX, 0.9, brickWidth, false)
],],
))),))),
),),
););
Trong mã, chúng tôi quay trở lại , sẽ cung cấp chuyển động từ trái sang phải khi chúng tôi đang xây dựng trên web. Điều này cũng có thể được sao chép cho một thiết bị màn hình cảm ứng.RawKeyboardListener()
Tiện ích cung cấp chức năng được sử dụng để gọi hàm được viết ở trên trong logic. Một phần tử con, cũng được viết để chỉ định màu nền và phần thân của ứng dụng.GestureDetector()
onTap
startGame
Scaffold()
Tiếp theo, tạo một lớp được gọi Welcome
và chuyển vào một boolean để kiểm tra xem trò chơi đã bắt đầu hay chưa. Nếu trò chơi chưa bắt đầu, dòng chữ “nhấn để chơi” sẽ hiển thị:
class Welcome extends StatelessWidget { Welcome extends StatelessWidget {
final bool gameStarted;final bool gameStarted;
Welcome(this.gameStarted);Welcome(this.gameStarted);
@override@override
Widget build(BuildContext context) {Widget build(BuildContext context) {
return Container(return Container(
alignment: Alignment(0, -0.2),: Alignment(0, -0.2),
child: Text(: Text(
gameStarted ? "": "T A P T O P L A Y",? "": "T A P T O P L A Y",
style: TextStyle(color: Colors.white),: TextStyle(color: Colors.white),
));));
}}
}}
Bây giờ chúng ta có thể tạo một lớp khác Ball
, để xử lý thiết kế quả bóng và vị trí của nó tại mọi điểm trên sân sử dụng . Chúng tôi chuyển các tham số này thông qua một phương thức khởi tạo để di chuyển, như sau:Alignment(x,y)
class Ball extends StatelessWidget { Ball extends StatelessWidget {
final x;final x;
final y;final y;
Ball(this.x, this.y);Ball(this.x, this.y);
@override@override
Widget build(BuildContext context) {Widget build(BuildContext context) {
return Container(return Container(
alignment: Alignment(x, y),: Alignment(x, y),
child: Container(: Container(
decoration: BoxDecoration(shape: BoxShape.circle, color: Colors.white),: BoxDecoration(shape: BoxShape.circle, color: Colors.white),
width: 20,: 20,
height: 20,: 20,
),),
););
}}
}}
Bây giờ chúng ta hãy thiết kế Brick
lớp để xử lý thiết kế gạch, màu sắc, vị trí và loại trình phát.
Ở đây, chúng tôi sử dụng một phương trình toán học ( ) để chuyển vị trí cho trục x và y:Alignment((2* x +brickWidth)/(2-brickWidth), y)
class Brick extends StatelessWidget { Brick extends StatelessWidget {
final x;final x;
final y;final y;
final brickWidth;final brickWidth;
final isEnemy;final isEnemy;
Brick( this.x, this.y, this.brickWidth, this.isEnemy);Brick( this.x, this.y, this.brickWidth, this.isEnemy);
@override@override
Widget build(BuildContext context) {Widget build(BuildContext context) {
return Container(return Container(
alignment: Alignment((2* x +brickWidth)/(2-brickWidth), y),: Alignment((2* x +brickWidth)/(2-brickWidth), y),
child: ClipRRect(: ClipRRect(
borderRadius: BorderRadius.circular(10),: BorderRadius.circular(10),
child: Container(: Container(
alignment: Alignment(0, 0),: Alignment(0, 0),
color: isEnemy?Colors.purple[500]: Colors.pink[300],: isEnemy?Colors.purple[500]: Colors.pink[300],
height: 20,: 20,
width:MediaQuery.of(context).size.width * brickWidth/ 2,:MediaQuery.of(context).size.width * brickWidth/ 2,
),),
));));
}}
}}
Cuối cùng, Score
lớp nên được đặt ngay bên dưới build
tiện ích con trong tệp; nó hiển thị điểm của mỗi người chơi.homepage.dart
Tạo một hàm tạo cho các biến enemyScore
và playerScore
để xử lý điểm số của từng người chơi và gameStarted
để kiểm tra xem trò chơi đã bắt đầu chưa. Điều này sẽ hiển thị nội dung của , hoặc trống :Stack()
Container()
class Score extends StatelessWidget { Score extends StatelessWidget {
final gameStarted;final gameStarted;
final enemyScore;final enemyScore;
final playerScore;final playerScore;
Score(this.gameStarted, this.enemyScore,this.playerScore, );Score(this.gameStarted, this.enemyScore,this.playerScore, );
@override@override
Widget build(BuildContext context) {Widget build(BuildContext context) {
return gameStarted? Stack(children: [return gameStarted? Stack(children: [
Container(Container(
alignment: Alignment(0, 0),: Alignment(0, 0),
child: Container(: Container(
height: 1,: 1,
width: MediaQuery.of(context).size.width / 3,: MediaQuery.of(context).size.width / 3,
color: Colors.grey[800],: Colors.grey[800],
)),)),
Container(Container(
alignment: Alignment(0, -0.3),: Alignment(0, -0.3),
child: Text(: Text(
enemyScore.toString(),.toString(),
style: TextStyle(color: Colors.grey[800], fontSize: 100),: TextStyle(color: Colors.grey[800], fontSize: 100),
)),)),
Container(Container(
alignment: Alignment(0, 0.3),: Alignment(0, 0.3),
child: Text(: Text(
playerScore.toString(),.toString(),
style: TextStyle(color: Colors.grey[800], fontSize: 100),: TextStyle(color: Colors.grey[800], fontSize: 100),
)),)),
]): Container();]): Container();
}}
}}
Sự kết luận
Trong bài đăng này, chúng tôi đã đề cập đến alignment
, RawKeyboardListener
widget, boolean, ClipRect cho vùng chứa và các hàm toán học trong mã của chúng tôi, tất cả đều được sử dụng để tạo lại trò chơi Pong. Trò chơi cũng có thể được cải thiện bằng cách tăng số lượng bóng hoặc giảm chiều dài viên gạch, làm cho nó phức tạp hơn.
Tôi hy vọng bài đăng này hữu ích và thú vị như nó đã được xây dựng và ghi lại nó. Vui lòng sử dụng các nguyên tắc trong bài viết để tạo lại các trò chơi cổ điển khác hoặc phát minh ra trò chơi mới. Bạn có thể tìm thấy liên kết đến mã từ bài viết này trên GitHub .