ساخت بازی پازل در فلاتر — از صفر تا صد

۳۶۴ بازدید
آخرین به‌روزرسانی: ۱۲ مهر ۱۴۰۲
زمان مطالعه: ۱۱ دقیقه
ساخت بازی پازل در فلاتر — از صفر تا صد

فلاتر گزینه‌ای رایج برای ساخت اپلیکیشن‌های چندپلتفرمی با اینترفیس‌های کاربری تمیز، انیمیشین‌های روان و عملکرد بسیار سریع است. در این مقاله به بررسی روش طراحی و ساخت بازی پازل در فلاتر می‌پردازیم. این بازی از چند مفهوم و الگوی طراحی بهره می‌گیرد که می‌توانید روی هر نوع اپلیکیشن که نیازمند UI پیچیده است به کار بگیرید.

برای ساخت و اجرای این پروژه به یک محیط فلاتر نیاز داریم. برای کسب اطلاعات در مورد روش راه‌اندازی محیط توسعه فلاتر به مستندات رسمی آن (+) مراجعه کنید. ریپازیتوری گیت‌هاب این پروژه را نیز می‌توانید در این لینک (+) مشاهده کنید.

مفاهیم اصلی برای ساخت بازی در فلاتر

چند مفهوم اصلی در زمینه محیط توسعه فلاتر وجود دارند که در طراحی این بازی ساده مورد استفاده قرار گرفته‌اند و هر کدام از آن‌ها نقشی مهم در مدیریت کلی حالت بازی و رندرینگ گرافیک آن دارند:

  • ویجت‌های مقدماتی فلاتر از قبیل Container ،SizedBox ،Column و غیره
  • استفاده از AnimationController ،AnimatedWidget و Tween
  • مدیریت حالت با ChangeNotifier / ChangeNotifierProvider
  • مدیریت رویدادها و استفاده از Stream / StreamController
  • دریافت ژست‌های لمسی با Stream / StreamController

در این بازی دمو ترکیبی از این مفاهیم برای تعریف منطق بازی، مدیریت حالت، تعامل‌ها، ‌رندرینگ و رفتار انیمیشن به روشی ساده و کارآمد پیاده‌سازی شده‌اند تا یک طراحی تمیز و با حداقل سورس کد داشته باشیم.

خلاصه فرایند کار

منطق بازی شامل پنج فایل سورس است که هر یک وظایف مختلفی را اداره می‌کنند. این فایل‌ها به شرح زیر هستند:

  • main.dart – مقداردهی اپلیکیشن و رندرینگ سطح بالای ویجت UI را بر عهده دارد.
  • game-board.dart – تشخیص ژست‌های لمسی و رندرینگ صفحه بازی بر عهده این فایل است.
  • game-piece.dart – مدل‌سازی، رندرینگ و انیمیشن‌های بخش بازی بر عهده این فایل است.
  • controller.dart – پردازش نوبت‌های بازی، به‌روزرسانی صفحه بازی و دیگر منطق‌های مربوط به بازی در این فایل انجام می‌شود.
  • score.dart – این فایل امتیازات را ثبت کرده و زمانی که امتیازی تغییر یابد، نما را رفرش می‌کند.

طراحی پروژه نمونه کاملاً سرراست است و از مفاهیم مورد اشاره فوق برای انجام وظایفی مانند مدیریت حالت، اجرای انیمیشن‌ها و کارهای دیگر استفاده می‌کند. صفحه بازی، مهره‌های بازی را بر اساس موقعیت‌های افقی و عمودی آن‌ها روی صفحه رندر می‌کند. زمانی که یک ژست سوایپ تشخیص داده شود، یک نوبت بازی صورت می‌گیرد و زمانی که مهره‌ها ترکیب شوند، به مهره‌ای با امتیاز بالاتر تبدیل می‌شوند که شبیه به بازی 2048 است. کنترلر محاسبات صفحه را مدیریت می‌کند و با هر نوبت بازی، صفحه را به‌روزرسانی کرده و در صورت نیاز امتیاز را نیز به‌روز می‌کند.

هر مهره بازی یک موقعیت x/y روی صفحه دارد و مقداری بین 0 تا 6 بسته به هر کدام از هفت رنگ در طیف بینایی دارد. زمانی که یک مهره جابجا می‌شود، خود را به موقعیت جدیدی روی صفحه انیمیت می‌کند. وقتی که مهره‌های هم‌امتیاز با هم برخورد کنند، مهره هدف، حذف می‌شود و سپس مهره متحرک ارتقا یافته و به مکان مهره مقصد جابجا می‌شود.

نقطه ورودی اپلیکیشن

مقداردهی اولیه بازی و ساخت چارچوب UI در فایل main.dart صورت می‌گیرد:

1import 'package:flutter/material.dart';
2import 'package:flutter/services.dart';
3import 'package:flutter_puzzle_game_demo/game-board.dart';
4import 'package:flutter_puzzle_game_demo/score.dart';
5import 'package:flutter_puzzle_game_demo/controller.dart';
6
7void main() {
8
9    WidgetsFlutterBinding.ensureInitialized();
10    SystemChrome.setPreferredOrientations([DeviceOrientation.portraitUp]);
11    runApp(Game());
12}
13
14class Game extends StatelessWidget {
15
16    final String _title = "Flutter Puzzle Game Demo";
17
18    @override
19    Widget build(BuildContext context) {
20
21        return MaterialApp(
22            title: _title,
23            theme: ThemeData.dark(),
24            home: Scaffold(
25                appBar: AppBar(title: Center(child: Text(_title))),
26                body: Column(
27                    mainAxisAlignment: MainAxisAlignment.spaceBetween,
28                    children: [
29                        ScoreView(),
30                        GameBoard(),
31                        Padding( 
32                            padding: EdgeInsets.only(bottom: 4),
33                            child: SizedBox(
34                                height: 64,
35                                width: double.infinity,
36                                child: Container(
37                                    margin: EdgeInsets.all(8),
38                                    decoration: BoxDecoration(
39                                        border: Border.all(color: Colors.white.withOpacity(0.2)),
40                                        borderRadius: BorderRadius.circular(8)
41                                    ),
42                                    child: MaterialButton(
43                                        color: Colors.grey.withOpacity(0.2),
44                                        onPressed: Controller.start,
45                                        child: Text('start')
46                                    )
47                                )
48                            )
49                        )
50                    ]
51                ),
52            )
53        );
54    }
55}

ابتدا تابع main پیش از اعمال یک قفل جهت‌گیری روی حالت افقی، مطمئن می‌شود که ویجت‌های فلاتر مقداردهی شده‌اند و سپس اپلیکیشن را اجرا می‌کند. ویجت Game یک MaterialApp استاندارد با چارچوب UI با تمِ‌ِ تاریک و دکمه آغاز بازگشت می‌دهد. نکته خاص دیگری در این فایل وجود ندارد، زیرا اغلب بخش‌های کار در کامپوننت‌های دیگر انجام می‌یابد. در ادامه نمای امتیاز و صفحه بازی را بررسی می‌کنیم تا طراحی بازی را بهتر درک کنیم.

امتیاز بازی

کلاس‌های امتیاز ساده هستند و چند مفهوم مورد استفاده در سراسر بازی را نمایش می‌‌دهند، از این رو به بررسی فایل score.dart می‌پردازیم:

1import 'package:flutter/material.dart';
2import 'package:flutter_puzzle_game_demo/controller.dart';
3import 'package:provider/provider.dart';
4
5class ScoreModel extends ChangeNotifier {
6    
7    ScoreModel();
8    int _value = 0;
9
10    int get value => _value;
11    set value(x) { _value = x; notifyListeners(); }
12}
13
14class ScoreView extends StatelessWidget {
15
16    @override
17    Widget build(BuildContext context) {
18
19        TextStyle style = Theme.of(context).textTheme.headline6.copyWith(fontWeight: FontWeight.w300);
20        
21        return ChangeNotifierProvider.value(
22            value: Controller.score,
23            child: Consumer<ScoreModel>(
24                builder: (context, model, child) { 
25                    return Column(
26                        children: [
27                            Padding(
28                                padding: EdgeInsets.only(top: 24, bottom: 12), 
29                                child: Text('score:')
30                            ),
31                            Text(model.value.toString(), style: style)
32                        ]
33                    );
34                }
35            )
36        );
37    }
38}

امتیاز بازی با یک مدل و نمای مجزا به همراه ChangeNotifier که مدل را بسط داده است و نمای مصرف‌کننده مدل پیاده‌سازی می‌شود که به نما امکان می‌دهد تا به صورت خودکار در زمان تغییر یافت مدل به‌روزرسانی شود. این کار از طریق فراخوانی ()notifyListeners درون بخش تعیین امتیاز صورت می‌گیرد که نوتیفکیشنی را روی ChangeNotifierProvider منتشر می‌سازد و موجب می‌شود که ویجت خود را از نو و با مقدار جدید امتیاز بسازد.

این وضعیت نمایانگر یک پیاده‌سازی مقدماتی از مدیریت حالت با provider است و یک روش تمیز برای به‌روزرسانی نما در زمان تغییر یافتن حالت ارائه می‌کند.

فلاتر امکان استفاده از مشخصه‌های «تغییرپذیر» (mutable) را روی StatelessWidget نمی‌دهد و همچنین دسترسی مستقیمی به حالت StatelessWidget ارائه نکرده است. از این رو این یک راه‌حل مناسب برای به‌روزرسانی حالت روی یک شیء و واداشتن ویجت به ساخت مجدد با شرایط مطلوب است.

صفحه بازی

مدیریت ژست‌ها و رندرینگ صفحه در فایل game-board.dart صورت می‌گیرد:

1import 'dart:async';
2import 'package:flutter/material.dart';
3import 'package:flutter_puzzle_game_demo/controller.dart';
4import 'package:flutter_puzzle_game_demo/game-piece.dart';
5
6class GameBoard extends StatefulWidget {
7
8    GameBoard({Key key}) : super(key: key);
9
10    @override
11    _GameBoardState createState() => _GameBoardState();
12}
13
14class _GameBoardState extends State<GameBoard> {
15
16    StreamSubscription _eventStream;
17    Offset dragOffset = Offset(0, 0);
18
19    List<GamePiece> pieces = [];
20
21    void onTurn(dynamic data) => setState(() { pieces = Controller.pieces; });
22
23    void onGesture(DragUpdateDetails ev) => 
24        dragOffset = Offset((dragOffset.dx + ev.delta.dx) / 2, (dragOffset.dy + ev.delta.dy) / 2);
25    
26    void onPanEnd(DragEndDetails ev) { 
27        Controller.on(dragOffset);
28        dragOffset = Offset(0, 0);
29    }
30
31    @override
32    void initState() {
33
34        super.initState();
35        _eventStream = Controller.listen(onTurn);
36    }
37
38    @override
39    void dispose() {
40
41        super.dispose();
42        _eventStream.cancel();
43    }
44
45    @override
46    Widget build(BuildContext context) {
47
48        Size size = MediaQuery.of(context).size;
49        double root = size.width;
50
51        return GestureDetector(
52            onPanUpdate: onGesture,
53            onPanEnd: onPanEnd,
54            child: Expanded(
55                child: Center(
56                    child: Container(
57                        margin: EdgeInsets.all(8),
58                        decoration: BoxDecoration(
59                            border: Border.all(color: Colors.cyan.withOpacity(0.4), width: 1),
60                            borderRadius: BorderRadius.circular(24)
61                        ),
62                        width: root,
63                        height: root,
64                        child: Container(
65                            child: Stack(
66                                key: UniqueKey(),
67                                children: pieces
68                            )
69                        )
70                    )
71                )
72            )
73        );
74    }
75}

صفحه بازی چند مشخصه و متد برای دریافت ورودی کاربر و رندر کردن محتوای بازی دریافت می‌کند. در طی اجرای ()initState یک شنونده رویداد برای دریافت رویداد‌های به‌روزرسانی از کنترلر و رسم مجدد UI راه‌اندازی می‌شود. از آنجا که کلاس Controller (که در ادامه بررسی خواهیم کرد) تنها مشخصه‌های استاتیک دارد و به یک وهله نیاز ندارد، از استریم‌ها برای اطلاع‌رسانی دستی به شونده‌ها به جای الزام به ساخت یک وهله از ChangeNotifier استفاده می‌کند.

GestureDetector برای دریافت ورودی استفاده می‌شود که مقدار میانگین آن در طی کل رویداد pan دریافت می‌شود و در زمان کامل شدن عمل سوایپ به کنترل تحویل داده می‌شود. سپس برای ژست ورودی بعدی ریست می‌شود. این کار موجب حذف شدن ورودی می‌شود و کنترلر به روش آسان‌تری می‌تواند جهت مورد نظر را تفسیر کند.

یک صفحه بازی مربعی با ارتفاع و عرض برابر با صفحه دستگاه تعریف می‌شود و مهره‌های بازی به عنوان فرزندان یک پشته تعریف می‌شوند که آن‌ها را روی محور Z بر روی هم انباشت می‌کند. موقعیت رندرینگ X/Y هر مهره بازی به صورت داخلی درون کلاس‌های مهره بازی با استفاده از مشخصه‌های هم‌راستایی مدیریت می‌شود که در ادامه بررسی خواهیم کرد.

مهره‌های بازی

کلاس‌های مدیریت عملیات مهره بازی و رندرینگ ویجت درون فایل game-piece.dart قرار دارند:

1import 'package:flutter_puzzle_game_demo/controller.dart';
2import 'package:provider/provider.dart';
3import 'package:flutter/material.dart';
4import 'dart:math';
5
6class GamePieceModel extends ChangeNotifier {
7
8    GamePieceModel({ this.value, this.position }) {
9        prev = initialPoint(this.initialDirection);
10    }
11
12    int value;
13    Point position;
14    Point prev;
15
16    Direction get initialDirection => Controller.lastDirection;
17
18    Point initialPoint(Direction direction) {
19
20        switch (initialDirection) {
21
22            case Direction.UP:
23                return Point(this.position.x, 6);
24
25            case Direction.DOWN:
26                return Point(this.position.x, 0);
27
28            case Direction.LEFT:
29                return Point(6, this.position.y);
30            
31            case Direction.RIGHT:
32                return Point(0, this.position.y);
33
34            case Direction.NONE:
35                break;
36        }
37
38        return Point(0, 0);
39    }
40
41    void move(Point to) {
42
43        this.prev = position;
44        this.position = to;
45        notifyListeners();
46    }
47}
48
49class GamePieceView extends AnimatedWidget {
50
51    GamePieceView({Key key, this.model, controller}) :
52    
53         x = Tween<double>( begin: model.prev.x.toDouble(),  end: model.position.x.toDouble(), )
54             .animate( CurvedAnimation( parent: controller, curve: Interval( 0.0, 0.100,  curve: Curves.ease, ))),
55
56        y = Tween<double>( begin: model.prev.y.toDouble(),  end: model.position.y.toDouble(), )
57            .animate( CurvedAnimation( parent: controller, curve: Interval( 0.0, 0.100,  curve: Curves.ease, ))),
58
59        super(key: key, listenable: controller);
60
61    final GamePieceModel model;
62    AnimationController get controller => listenable;
63
64    final Animation<double> x;
65    final Animation<double> y;
66
67    final List<Color> colors = const [
68        Colors.red,
69        Colors.orange,
70        Colors.yellow,
71        Colors.green,
72        Colors.blue,
73        Colors.indigo,
74        Colors.purple
75    ];
76
77    Widget build(BuildContext context) {
78
79        model.prev = model.position;
80
81        Size size = MediaQuery.of(context).size;
82        double itemSize = size.width / 7;
83
84        return Align(
85            alignment: FractionalOffset(x.value/6, y.value/6),
86            child: Container(
87                constraints: BoxConstraints(maxHeight: itemSize, maxWidth: itemSize),
88                height: itemSize,
89                width: itemSize,
90                child: Align( 
91                    alignment: Alignment.center,
92                    child: Container(
93                        height: itemSize * .7,
94                        width: itemSize * .7,
95                        padding: EdgeInsets.all(3),
96                        decoration: BoxDecoration(
97                            color: colors[model.value].withOpacity(0.1),
98                            border: Border.all(color: colors[model.value], width: 1),
99                            borderRadius: BorderRadius.circular(itemSize / 2)
100                        )
101                    )
102                )
103            )
104        );
105    }
106}
107
108class GamePiece extends StatefulWidget {
109
110    GamePiece({ Key key, @required this.model }) : super(key: key);
111
112    final GamePieceModel model;
113
114    int get value => model.value;
115    Point get position => model.position;
116    void move(Point to) => model.move(to);
117
118    @override
119    _GamePieceState createState() => _GamePieceState();
120}
121
122class _GamePieceState extends State<GamePiece> with TickerProviderStateMixin {
123
124    _GamePieceState();
125
126    AnimationController _controller;
127
128    @override
129    void initState() {
130
131        super.initState();
132        _controller = AnimationController(vsync: this, duration: const Duration(milliseconds: 1400));
133    }
134
135    @override
136    void dispose() {
137
138        super.dispose();
139        _controller.dispose();
140    }
141
142    @override
143    Widget build(BuildContext context) {
144        
145        return ChangeNotifierProvider.value(
146            value: widget.model,
147            child: Consumer<GamePieceModel>(
148                builder: (context, model, child) {
149
150                    try {
151                        _controller.reset();
152                        _controller.forward();
153                    } 
154                    on TickerCanceled {}
155                
156                    return GamePieceView(model: model, controller: _controller);
157                }
158            )
159        );
160    }
161}

سه کامپوننت عمده‌ی درون این فایل به شرح زیر هستند:

  • GamePieceModel – داده‌ها را مدیریت کرده و در زمان تغییر یافتن به شنونده‌ها اطلاع می‌‌دهد.
  • GamePieceModel – یک دایره را رندر می‌کند که در زمان جابجایی خود را انیمیت می‌کند.
  • GamePiece – کامپوننت‌های GamePieceModel و GamePieceView را در یک ویجت قرار می‌دهد.

صفحه بازی یک فهرست از اشیای GamePiece رندر می‌کند که هر یک از آن‌ها از سوی یک GamePieceModel پشتیبانی می‌شوند و همچنین رندرینگ GamePieceModel را موجب می‌شود.

هر GamePiece یک ویجت GamePieceView را مستقیماً درون پشته والد روی صفحه بازی رندر می‌کند و accessors-های get را برای افشای مشخصه‌ها و متدهای مورد نیاز مدل عرضه می‌کند. بدین ترتیب ویجت GamePiece به عنوان یک اینترفیس برای مهره بازی و بقیه برنامه عمل می‌کند و وظیفه اجرای عملیات روی هر یک از آن‌ها را تسهیل می‌سازد.

موقعیت‌یابی هر مهره درون پشته والد درون GamePieceView و با استفاده از دو مشخصه Align و FractionalOffset انجام می‌گیرد تا هر مهره به میزان ضریبی از یک-هفتم کل اندازه صفحه در هر دو محور x و y جابجا شود.

زمانی که یک مهره ایجاد ‌می‌شود، یک وهله از GamePieceModel ارسال می‌شود تا موقعیت و مقدار مهره را ذخیره کند و جهت سوایپ قبلی از کنترلر گیم دریافت می‌شود تا مشخص شود که مهره جدید باید از کدام جهت صفحه به داخل بلغزد.

زمانی که یک مهره به موقعیت جدید جابجا شود، ChangeNotifierProvider درون ChangeNotifierProvider یک تغییر را دریافت می‌کند و ویجت را از نو ساخته و کنترلری که انیمیشن را بر عهده دارد آغاز کرده و ویجت را در روی صفحه حرکت می‌دهد. انیمیشن از سوی یک AnimationController روی حالت مهره اجرا می‌شود و از یک piece برای همگام‌سازی خود با کنترلر انیمیشن استفاده می‌کند که به GamePieceView ارسال می‌شود و در نهایت به AnimatedWidget می‌رود که آن را بسط می‌دهد. سازنده مربوط به GamePieceView، مقادیر انیمیت‌شده را برای x و y با استفاده از Tween و CurvedAnimation ایجاد می‌کند که یک مسیر انیمیشن از موقعیت قبلی به جدید می‌سازد. زمانی که شیء GamePieceView رندر شود، موقعیت قبلی روی موقعیت جاری تنظیم می‌شود تا از اجرای مجدد انیمیشن تا دفعه بعد که نیاز به جابجایی مهره باشد، جلوگیری کند.

کنترلر

با در نظر گرفتن همه مواردی که مطرح شد، فایل controller.dart به صورت زیر است:

1import 'dart:async';
2import 'dart:math';
3import 'package:flutter/material.dart';
4import 'package:flutter_puzzle_game_demo/game-piece.dart';
5import 'package:flutter_puzzle_game_demo/score.dart';
6
7enum Direction { UP, DOWN, LEFT, RIGHT, NONE }
8
9class Controller {
10
11    static ScoreModel score = ScoreModel();
12
13    static Random rnd = Random();
14
15    static List<GamePiece> _pieces = [];
16    static Map<Point, GamePiece> index = {};
17
18    static get pieces => _pieces;
19
20    static StreamController bus = StreamController.broadcast();
21    static StreamSubscription listen(Function handler) => bus.stream.listen(handler);
22
23    static dispose() => bus.close();
24
25    static Direction lastDirection = Direction.RIGHT;
26
27    static Direction parse(Offset offset) {
28
29        if (offset.dx < 0 && offset.dx.abs() > offset.dy.abs()) return Direction.LEFT;        
30        if (offset.dx > 0 && offset.dx.abs() > offset.dy.abs()) return Direction.RIGHT;
31        if (offset.dy < 0 && offset.dy.abs() > offset.dx.abs()) return Direction.UP;
32        if (offset.dy > 0 && offset.dy.abs() > offset.dx.abs()) return Direction.DOWN;
33        return Direction.NONE;
34    }
35
36    static addPiece(GamePiece piece) {
37
38        _pieces.add(piece);
39        index[piece.position] = piece;
40    }
41
42    static removePiece(GamePiece piece) {
43
44        _pieces.remove(piece);
45        index[piece.position] = null;
46    }
47
48    static void on(Offset offset) {
49
50        lastDirection = parse(offset);
51        process(lastDirection);
52
53        bus.add(null);
54        if (_pieces.length > 48) { start(); } // Game Over :/
55
56        Point p;
57        while (p == null || index.containsKey(p)) { p = Point(rnd.nextInt(6), rnd.nextInt(6)); }
58        
59        addPiece(GamePiece(model: GamePieceModel(position: p, value: 0)));
60    }
61
62    static void process(Direction direction) {
63        
64        switch (direction) {
65
66            case (Direction.UP):
67                return scan(0, 7, 1, Axis.vertical);
68                
69            case (Direction.DOWN):
70                return scan(6, -1, -1, Axis.vertical);
71
72            case (Direction.LEFT):
73                return scan(0, 7, 1, Axis.horizontal);
74
75            case (Direction.RIGHT):
76                return scan(6, -1, -1, Axis.horizontal);
77
78            default:
79                break;
80        }
81    }
82    
83    static scan(int start, int end, int op, Axis axis) {
84
85        for (int j = start; j != end; j += op) {
86            for (int k = 0; k != 7; k++) {
87                
88                Point p = axis == Axis.vertical ? Point(k, j) : Point(j, k);
89                if (index.containsKey(p)) { check(start, op, axis, index[p]); }
90            }
91        }
92    }
93
94    static void check(int start, int op, Axis axis, GamePiece piece) {
95
96        int target = (axis == Axis.vertical) ? piece.position.y : piece.position.x;
97        for (var n = target - op; n != start - op; n -= op) {
98
99            Point lookup = (axis == Axis.vertical) 
100                ? Point(piece.position.x, n) 
101                : Point(n, piece.position.y);
102
103            if (!index.containsKey(lookup)) { target -= op; }
104            else if (index[lookup].value == piece.value) { return merge(piece, index[lookup]); }
105            else { break; }
106        }
107
108        Point destination = (axis == Axis.vertical) 
109            ? Point(piece.position.x, target) 
110            : Point(target, piece.position.y);
111
112        if (destination != piece.position) { relocate(piece, destination); }
113    }
114
115    static void merge(GamePiece source, GamePiece target) {
116
117        if (source.value == 6) {
118
119            index.remove(source.position);
120            index.remove(target.position);
121            _pieces.remove(source);
122            _pieces.remove(target);
123            score.value += source.model.value * 100;
124            return;
125        }
126
127        source.model.value += 1;
128        index.remove(target.position);
129        _pieces.remove(target);
130        relocate(source, target.position);
131        score.value += source.model.value * 10;
132    }
133
134    static void relocate(GamePiece piece, Point destination) {
135
136        index.remove(piece.position);
137        piece.move(destination);
138        index[piece.position] = piece;
139    }
140
141    static void start() {
142
143        _pieces = [];
144        index = {};
145        on(Offset(1,0));
146    }
147}

کلاس Controller بخش عمده منطق درون بازی را اجرا می‌کند که شامل مقداردهی بازی مدیریت ورودی و پردازش و ارزیابی نوبت بازی‌ها است. یک مولد عدد تصادفی همراه با یک List<GamePiece>‎ برای ذخیره مهره‌ها در بازی استفاده می‌شود و Map<Point, GamePiece>‎ به عنوان یک اندیس گشتن به دنبال موقعیت X/Y عمل می‌کند. مشخصه bus یک پیاده‌سازی از Stream و StreamController برای انتشار یک رویداد در زمان پایان یافتن نوبت بازی استفاده می‌شود.

ورودی از طریق متد on دریافت می‌شود و با فراخوانی parse روی Offset دریافتی از رویداد به یک Direction تبدیل می‌شود. سپس این نوبت بازی ارزیابی ‌می‌شود و رویداد pdate روی این بأس اجرا خواهد شد. اگر هر 49 فضای روی صفحه اشغال شوند، بازی ری‌استارت می‌شود، در غیر این صورت یک مهره جدید به صفحه اضافه می‌شود و بازی آماده حرکت بعدی خواهد بود.

ارزیابی نوبت بازی با متد process صورت می‌گیرد که در آن process دریافت می‌شود و scan به همراه پارامترهای متناظر حلقه جهت ارزیابی صفحه در جهت عکس سوایپ فراخوانی می‌شود. این کار به دورترین آیتم در راستای محور هدف امکان می‌دهد که ابتدا مدیریت شود و به این ترتیب مهره‌ها در صورت ضرورت ادغام شده و یک مسیر در راستای صفحه برای بقیه مهره‌ها که باید ارزیابی شوند باز می‌شود.

متد scan یک آغاز، انتها، عملیات افزایش و محور می‌گیرد. یک حلقه با استفاده از این پارامترها برای آغاز اسکن کردن صفحه و گشتن به دنبال مهره‌های بازی ساخته می‌شود. هر مهره که به check ارسال می‌شود، نقطه آغازین، عملیات افزایش، محور، مهره بازی به عنوان ورودی را دریافت می‌کند و شروع به برسی مسیر مورد نظر مهره می‌کند و در صورت نیاز آن‌ها را ادغام کرده یا از نو ارائه می‌کند.

عملیات مختلف ادغام ابتدا از طریق بررسی این که مهره‌هایی که ادغام می‌شوند در بالاترین مقدار ممکن هستند یا نه آغاز می‌شود. اگر چنین باشد، هر دوی آن‌ها حذف شده و یک مهره جایزه اضافه می‌شود. در غیر این صورت مهره هدف حذف می‌شود و مهره ورودی ارتقا یافته و به مکان مقصد جابجا می‌شود.

عملیات تخصیص مجدد به سادگی مهره را از اندیس به عنوان کلید کنونی حذف می‌کند و move را روی مهره فراخوانی می‌کند تا آن را به‌روزرسانی کرده و یک انیمیشن را آغاز کند. در نهایت آیتم مجدداً در اندیس در کلید موقعیت z/y به‌روزرسانی شده قرار می‌گیرد.

سخن پایانی

در این دمو بخشی از قدرت و انعطاف‌پذیری موجود درون فلاتر را نشان دادیم و همچنین شیوه ساخت بازی‌های 2 بعدی ساده با استفاده صرف از Flutter SDK و پکیج provider را تبیین کرده و همه موارد دیگر را از صفر تا صد، خودمان ساختیم.

مفاهیم مورد استفاده در این دمو می‌توانند مستقیماً به هر نوع اپلیکیشن دیگر فلاتر که نیازمند مدیریت ساده حالت، UI با قابلیت‌های تعاملی بالا، انیمیشن‌های کارآمد و چرخه‌های رندر و همچنین معماری کلی تمیز که نیازمند تلاش اندکی برای نگهداری در آینده باشد، انتقال داد.

بر اساس رای ۰ نفر
آیا این مطلب برای شما مفید بود؟
اگر بازخوردی درباره این مطلب دارید یا پرسشی دارید که بدون پاسخ مانده است، آن را از طریق بخش نظرات مطرح کنید.
منابع:
itnext.io
نظر شما چیست؟

نشانی ایمیل شما منتشر نخواهد شد. بخش‌های موردنیاز علامت‌گذاری شده‌اند *