flutter构建布局_在Flutter中构建益智游戏

flutter构建布局

介绍 (Introduction)

Flutter is a popular choice for building cross-platform apps with clean user interfaces, smooth animations, and lightning-fast performance. In this article, we will review the design and development of a simple 2D puzzle game in Flutter, utilizing several concepts and design patterns that can be applied to any kind of application that requires complex UI.

˚Flutter是建设有干净的用户界面,流畅的动画和快如闪电的性能的跨平台应用程序的普遍选择。 在本文中,我们将回顾Flutter中一个简单的2D益智游戏的设计和开发,利用几种概念和设计模式来应用于需要复杂UI的任何类型的应用程序。

To build and run this project, a Flutter environment is required. For more information about installing Flutter, see the installation page here.

要构建和运行该项目,需要Flutter环境。 有关安装Flutter的更多信息,请参见此处的安装页面。

For a copy of the example source code, check out this repo.

要获取示例源代码的副本,请查看此repo 。

概念 (Concepts)

There are several Flutter UI development concepts utilized in the design of this simple game, each of which plays an important part in the overall management of game state and graphics rendering. These include:

在此简单游戏的设计中使用了几种Flutter UI开发概念,每个概念在游戏状态和图形渲染的整体管理中都发挥着重要作用。 这些包括:

  • Basic Flutter widgets such as Container, SizedBox, Column, etc.

    基本Flutter小部件,例如Container , SizedBox , Column等。

  • Using AnimationController, AnimatedWidget, and Tween

    使用AnimationController , AnimatedWidget和Tween

  • Managing state with ChangeNotifier / ChangeNotifierProvider

    使用ChangeNotifier / ChangeNotifierProvider管理状态

  • Handling events and utilizing Stream / StreamController

    处理事件并利用Stream / StreamController

  • Capturing and de-bouncing gestures with GestureDetector

    使用GestureDetector捕获和消除反弹手势

This demo game combines implementations of these concepts to define game logic, state management, interactions, rendering, and animation behavior, in a simple and efficient way with a clean design and minimal source code.

该演示游戏结合了这些概念的实现,以简洁有效的方式,简洁的设计和最少的源代码定义了游戏逻辑,状态管理,交互,渲染和动画行为。

总览 (Overview)

The logic for the game is contained within five source files, each with specific implementations for handling various tasks. These are:

游戏的逻辑包含在五个源文件中,每个文件都有用于处理各种任务的特定实现。 这些是:

  • main.dart: app initialization and top-level UI widget rendering

    main.dart :应用程序初始化和顶级UI小部件呈现

  • game-board.dart: gesture detection and game board rendering

    game-board.dart :手势检测和游戏板渲染

  • game-piece.dart: game piece model / rendering / animations

    game-piece.dart :游戏模型/渲染/动画

  • controller.dart: process turns / update board / other game logic

    controller.dart :进程轮流/更新棋盘/其他游戏逻辑

  • score.dart: track score and refresh view when score is changed

    score.dart :更改分数时跟踪分数并刷新视图

The design of the example project is straightforward and utilizes the concepts mentioned previously to do most of the heavy lifting of state management, animations, and other tasks. The game board renders the game pieces based on their current x/y positions on the board. When a swipe gesture is detected, a turn is taken, and the board is evaluated to determine where to move pieces and when to combine them into a piece with a higher value, similar to the classic game 2048. The controller handles board calculations and will refresh the board upon each turn, updating the score as required.

示例项目的设计简单明了,并利用前面提到的概念来完成状态管理,动画和其他任务的大部分繁重工作。 游戏板根据游戏棋盘上当前的x / y位置渲染游戏棋子。 当检测到轻扫手势时,会转身,评估棋盘以确定将棋子移到哪里以及何时将它们组合成具有更高价值的棋子,类似于经典游戏2048。控制器处理棋盘计算并每回合刷新板,根据需要更新分数。

Each game piece has an x/y position on the board, along with a value from 0 to 6 corresponding to each of the seven colors in the visible spectrum. When a game piece is moved, it will animate itself to the new position on the board. When a piece collides with one of the same value, the target piece is removed, then the moving piece is promoted and moved to the target location.

每个游戏棋子在棋盘上都有一个x / y位置,以及一个对应于可见光谱中七种颜色的从0到6的值。 当一个游戏棋子被移动时,它将自己动画到棋盘上的新位置。 当一个零件与相同值之一碰撞时,将目标零件移开,然后将移动的零件提升并移动到目标位置。

应用程序入口点 (App Entry Point)

Game initialization and UI scaffolding are contained in main.dart:

游戏初始化和UI支架包含在main.dart中:

import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_puzzle_game_demo/game-board.dart';
import 'package:flutter_puzzle_game_demo/score.dart';
import 'package:flutter_puzzle_game_demo/controller.dart';


void main() {


    WidgetsFlutterBinding.ensureInitialized();
    SystemChrome.setPreferredOrientations([DeviceOrientation.portraitUp]);
    runApp(Game());
}


class Game extends StatelessWidget {


    final String _title = "Flutter Puzzle Game Demo";


    @override
    Widget build(BuildContext context) {


        return MaterialApp(
            title: _title,
            theme: ThemeData.dark(),
            home: Scaffold(
                appBar: AppBar(title: Center(child: Text(_title))),
                body: Column(
                    mainAxisAlignment: MainAxisAlignment.spaceBetween,
                    children: [
                        ScoreView(),
                        GameBoard(),
                        Padding( 
                            padding: EdgeInsets.only(bottom: 4),
                            child: SizedBox(
                                height: 64,
                                width: double.infinity,
                                child: Container(
                                    margin: EdgeInsets.all(8),
                                    decoration: BoxDecoration(
                                        border: Border.all(color: Colors.white.withOpacity(0.2)),
                                        borderRadius: BorderRadius.circular(8)
                                    ),
                                    child: MaterialButton(
                                        color: Colors.grey.withOpacity(0.2),
                                        onPressed: Controller.start,
                                        child: Text('start')
                                    )
                                )
                            )
                        )
                    ]
                ),
            )
        );
    }
}

The main function first ensures Flutter widgets are initialized before applying an orientation lock to portrait mode, and then runs the app. The Game widget returns a standard MaterialApp with a dark-themed UI scaffold, consisting of a column containing the score view, game board, and start button. There’s not much in this file since most of the work is done in other components. We will examine the score view and game board next to get a feel for the design.

主要功能首先确保在将方向锁定应用于纵向模式之前初始化Flutter小部件,然后运行该应用程序。 “游戏”小部件将返回带有深色主题UI支架的标准MaterialApp,该支架由包含分数视图,游戏面板和开始按钮的列组成。 由于大多数工作是在其他组件中完成的,因此该文件中没有太多内容。 接下来,我们将检查分数视图和游戏板,以使您对设计有所了解。

游戏成绩 (Game Score)

The score classes are simple and illustrate a few concepts used throughout the game, so let’s take a look at score.dart:

分数类很简单,并说明了整个游戏中使用的一些概念,因此让我们看一下score.dart

import 'package:flutter/material.dart';
import 'package:flutter_puzzle_game_demo/controller.dart';
import 'package:provider/provider.dart';


class ScoreModel extends ChangeNotifier {
    
    ScoreModel();
    int _value = 0;


    int get value => _value;
    set value(x) { _value = x; notifyListeners(); }
}


class ScoreView extends StatelessWidget {


    @override
    Widget build(BuildContext context) {


        TextStyle style = Theme.of(context).textTheme.headline6.copyWith(fontWeight: FontWeight.w300);
        
        return ChangeNotifierProvider.value(
            value: Controller.score,
            child: Consumer(
                builder: (context, model, child) { 
                    return Column(
                        children: [
                            Padding(
                                padding: EdgeInsets.only(top: 24, bottom: 12), 
                                child: Text('score:')
                            ),
                            Text(model.value.toString(), style: style)
                        ]
                    );
                }
            )
        );
    }
}

The game score is implemented with a separate model and view, with the model extending ChangeNotifier and the view consuming this model, which will allow the view to automatically update when the model is changed. This is accomplished by calling notifyListeners() within the score value setter, which will propagate the notification to ChangeNotifierProvider, causing the widget to rebuild itself with the new score value.

游戏分数是通过单独的模型和视图实现的,模型扩展了ChangeNotifier且视图使用了该模型,这将允许视图在模型更改时自动更新。 这是通过在得分值设置器中调用notifyListeners()来完成的,它将通知传播到ChangeNotifierProvider,从而使小部件使用新的得分值重建自身。

This illustrates a basic implementation of state management with provider and provides a clean way to update a view when the model changes state.

这说明了使用提供程序进行状态管理的基本实现,并提供了一种在模型更改状态时更新视图的干净方法。

Flutter doesn’t allow mutable properties on StatelessWidget or direct access to the state of a StatefulWidget, so this is a good solution for updating state on an object and having its widget re-build with any desired conditions.

Flutter不允许StatelessWidget上具有可变属性,也不允许直接访问StatefulWidget的状态,因此,这是更新对象上的状态并在任何所需条件下重新构建其小部件的好方法。

游戏板 (Game Board)

Gesture handling and board rendering are located in game-board.dart:

手势处理和棋盘渲染位于game-board.dart中

import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter_puzzle_game_demo/controller.dart';
import 'package:flutter_puzzle_game_demo/game-piece.dart';


class GameBoard extends StatefulWidget {


    GameBoard({Key key}) : super(key: key);


    @override
    _GameBoardState createState() => _GameBoardState();
}


class _GameBoardState extends State {


    StreamSubscription _eventStream;
    Offset dragOffset = Offset(0, 0);


    List pieces = [];


    void onTurn(dynamic data) => setState(() { pieces = Controller.pieces; });


    void onGesture(DragUpdateDetails ev) => 
        dragOffset = Offset((dragOffset.dx + ev.delta.dx) / 2, (dragOffset.dy + ev.delta.dy) / 2);
    
    void onPanEnd(DragEndDetails ev) { 
        Controller.on(dragOffset);
        dragOffset = Offset(0, 0);
    }


    @override
    void initState() {


        super.initState();
        _eventStream = Controller.listen(onTurn);
    }


    @override
    void dispose() {


        super.dispose();
        _eventStream.cancel();
    }


    @override
    Widget build(BuildContext context) {


        Size size = MediaQuery.of(context).size;
        double root = size.width;


        return GestureDetector(
            onPanUpdate: onGesture,
            onPanEnd: onPanEnd,
            child: Expanded(
                child: Center(
                    child: Container(
                        margin: EdgeInsets.all(8),
                        decoration: BoxDecoration(
                            border: Border.all(color: Colors.cyan.withOpacity(0.4), width: 1),
                            borderRadius: BorderRadius.circular(24)
                        ),
                        width: root,
                        height: root,
                        child: Container(
                            child: Stack(
                                key: UniqueKey(),
                                children: pieces
                            )
                        )
                    )
                )
            )
        );
    }
}

The game board defines several properties and methods for receiving user input and rendering the game content. During initState() an event listener is set up to receive update events from the controller to re-draw the UI. Since the Controller class (which we will examine in a moment) has only static properties and doesn’t require an instance, it uses streams to notify listeners manually rather than requiring an instance of ChangeNotifier.

游戏板定义了几种用于接收用户输入并呈现游戏内容的属性和方法。 在initState()期间,将设置事件侦听器以从控制器接收更新事件以initState() UI。 由于Controller类(我们将在稍后进行检查)仅具有静态属性,并且不需要实例,因此它使用流来手动通知侦听器,而不是需要ChangeNotifier实例。

GestureDetector is used to capture input, which is averaged over the course of the entire pan event and submitted to the controller when the swipe action is complete, then reset for the next incoming gesture. This de-bounces the input, making it easier for the controller to interpret the intended direction.

GestureDetector用于捕获输入,该输入在整个平移事件的过程中进行平均,并在滑动动作完成后提交给控制器,然后针对下一个传入手势进行重置。 这样可以消除输入的反跳,使控制器更容易解释预期的方向。

A square game board is defined with height and width equal to the width of the device, and the game pieces are rendered as children of a Stack, which will stack them on top of each other on the z-axis. The x/y rendering position of each game piece is handled internally within the game piece classes using alignment properties, which we will examine next.

定义了方形游戏板,其高度和宽度等于设备的宽度,并且游戏棋子被渲染为Stack的子代,它将在z轴上彼此堆叠。 每个游戏棋子的x / y渲染位置是在游戏棋子类中使用对齐属性在内部处理的,我们接下来将对其进行检查。

游戏件 (Game Pieces)

The classes for handling game piece operations and widget rendering are located in game-piece.dart:

用于处理游戏作品操作和小部件渲染的类位于game-piece.dart中

import 'package:flutter_puzzle_game_demo/controller.dart';
import 'package:provider/provider.dart';
import 'package:flutter/material.dart';
import 'dart:math';


class GamePieceModel extends ChangeNotifier {


    GamePieceModel({ this.value, this.position }) {
        prev = initialPoint(this.initialDirection);
    }


    int value;
    Point position;
    Point prev;


    Direction get initialDirection => Controller.lastDirection;


    Point initialPoint(Direction direction) {


        switch (initialDirection) {


            case Direction.UP:
                return Point(this.position.x, 6);


            case Direction.DOWN:
                return Point(this.position.x, 0);


            case Direction.LEFT:
                return Point(6, this.position.y);
            
            case Direction.RIGHT:
                return Point(0, this.position.y);


            case Direction.NONE:
                break;
        }


        return Point(0, 0);
    }


    void move(Point to) {


        this.prev = position;
        this.position = to;
        notifyListeners();
    }
}


class GamePieceView extends AnimatedWidget {


    GamePieceView({Key key, this.model, controller}) :
    
         x = Tween( begin: model.prev.x.toDouble(),  end: model.position.x.toDouble(), )
             .animate( CurvedAnimation( parent: controller, curve: Interval( 0.0, 0.100,  curve: Curves.ease, ))),


        y = Tween( begin: model.prev.y.toDouble(),  end: model.position.y.toDouble(), )
            .animate( CurvedAnimation( parent: controller, curve: Interval( 0.0, 0.100,  curve: Curves.ease, ))),


        super(key: key, listenable: controller);


    final GamePieceModel model;
    AnimationController get controller => listenable;


    final Animation x;
    final Animation y;


    final List colors = const [
        Colors.red,
        Colors.orange,
        Colors.yellow,
        Colors.green,
        Colors.blue,
        Colors.indigo,
        Colors.purple
    ];


    Widget build(BuildContext context) {


        model.prev = model.position;


        Size size = MediaQuery.of(context).size;
        double itemSize = size.width / 7;


        return Align(
            alignment: FractionalOffset(x.value/6, y.value/6),
            child: Container(
                constraints: BoxConstraints(maxHeight: itemSize, maxWidth: itemSize),
                height: itemSize,
                width: itemSize,
                child: Align( 
                    alignment: Alignment.center,
                    child: Container(
                        height: itemSize * .7,
                        width: itemSize * .7,
                        padding: EdgeInsets.all(3),
                        decoration: BoxDecoration(
                            color: colors[model.value].withOpacity(0.1),
                            border: Border.all(color: colors[model.value], width: 1),
                            borderRadius: BorderRadius.circular(itemSize / 2)
                        )
                    )
                )
            )
        );
    }
}


class GamePiece extends StatefulWidget {


    GamePiece({ Key key, @required this.model }) : super(key: key);


    final GamePieceModel model;


    int get value => model.value;
    Point get position => model.position;
    void move(Point to) => model.move(to);


    @override
    _GamePieceState createState() => _GamePieceState();
}


class _GamePieceState extends State with TickerProviderStateMixin {


    _GamePieceState();


    AnimationController _controller;


    @override
    void initState() {


        super.initState();
        _controller = AnimationController(vsync: this, duration: const Duration(milliseconds: 1400));
    }


    @override
    void dispose() {


        super.dispose();
        _controller.dispose();
    }


    @override
    Widget build(BuildContext context) {
        
        return ChangeNotifierProvider.value(
            value: widget.model,
            child: Consumer(
                builder: (context, model, child) {


                    try {
                        _controller.reset();
                        _controller.forward();
                    } 
                    on TickerCanceled {}
                
                    return GamePieceView(model: model, controller: _controller);
                }
            )
        );
    }
}

The three main components within this file are:

该文件中的三个主要组件是:

  • GamePieceModel: manages data and notifies listeners when changed

    GamePieceModel:更改数据时管理数据并通知侦听器
  • GamePieceView: renders a circle that animates itself when moved

    GamePieceView:渲染一个圆形,使其在移动时具有动画效果
  • GamePiece: wraps GamePieceModel and GamePieceView in a widget

    GamePiece:将GamePieceModel和GamePieceView包装在小部件中

The game board renders a list of GamePiece objects, each of which is backed by a GamePieceModel, and also drives the rendering of GamePieceView.

游戏板呈现GamePiece对象的列表,每个对象均由GamePieceModel支持,并且还驱动GamePieceView的呈现。

Each GamePiece renders a GamePieceView widget straight through to the parent Stack on the game board, and provides get accessors to expose the underlying model’s properties and methods as necessary. This allows the GamePiece widget to serve as the interface for the game piece to the rest of the program, simplifying the task of performing operations on each.

每个GamePiece都将GamePieceView小部件直接呈现到游戏板上的父堆栈,并提供get访问器以根据需要公开底层模型的属性和方法。 这允许GamePiece小部件充当游戏片断与程序其余部分的接口,从而简化了在每个片断上执行操作的任务。

Positioning of each piece within the parent Stack is accomplished within GamePieceView by using Align and FractionalOffset, to offset each piece by a factor of one-seventh the total board size on both the x and y axes.

通过使用Align和FractionalOffset在GamePieceView中完成每个堆叠在父堆栈中的定位,以使每个堆叠在x轴和y轴上的偏移为板总大小的七分之一。

When a game piece is created, an instance of GamePieceModel is passed in to store the position and value of the piece, and the previous swipe direction is retrieved from the game controller to determine which side of the board the new game piece will slide in from.

创建游戏棋子时,将传入GamePieceModel的实例以存储棋子的位置和值,并从游戏控制器中检索以前的滑动方向,以确定新游戏棋子将从棋盘的哪一侧滑入。

When the piece is moved to a new position, the ChangeNotifierProvider within GamePiece picks up the change, rebuilding the widget and firing off the controller that drives the animation of moving the widget across the screen. The animation is driven by an AnimationController on the game piece state, using TickerProviderStateMixin to synchronize itself with the animation controller, which is passed to the GamePieceView and ultimately to the AnimatedWidget that it extends. The constructor for GamePieceView creates animated values for x and y using Tween and CurvedAnimation, which will create an animation path from previous to current position. Once the GamePieceView object has been rendered, the previous position is set to the current one, to prevent the animation from running again until the next time the piece needs to be moved.

当棋子移动到新位置时,GamePiece中的ChangeNotifierProvider拾取更改,重新构建小部件并触发控制器,该控制器驱动在屏幕上移动小部件的动画。 动画由位于游戏块状态的AnimationController驱动,使用TickerProviderStateMixin将自身与动画控制器同步,该动画控制器传递给GamePieceView并最终传递给它扩展的AnimatedWidget。 GamePieceView的构造函数使用Tween和CurvedAnimation为x和y创建动画值,这将创建从上一个位置到当前位置的动画路径。 渲染完GamePieceView对象后,会将先前的位置设置为当前位置,以防止动画再次运行,直到下次需要移动该作品为止。

控制者 (Controller)

Putting it all together is controller.dart:

放在一起就是controller.dart

import 'dart:async';
import 'dart:math';
import 'package:flutter/material.dart';
import 'package:flutter_puzzle_game_demo/game-piece.dart';
import 'package:flutter_puzzle_game_demo/score.dart';


enum Direction { UP, DOWN, LEFT, RIGHT, NONE }


class Controller {


    static ScoreModel score = ScoreModel();


    static Random rnd = Random();


    static List _pieces = [];
    static Map index = {};


    static get pieces => _pieces;


    static StreamController bus = StreamController.broadcast();
    static StreamSubscription listen(Function handler) => bus.stream.listen(handler);


    static dispose() => bus.close();


    static Direction lastDirection = Direction.RIGHT;


    static Direction parse(Offset offset) {


        if (offset.dx < 0 && offset.dx.abs() > offset.dy.abs()) return Direction.LEFT;        
        if (offset.dx > 0 && offset.dx.abs() > offset.dy.abs()) return Direction.RIGHT;
        if (offset.dy < 0 && offset.dy.abs() > offset.dx.abs()) return Direction.UP;
        if (offset.dy > 0 && offset.dy.abs() > offset.dx.abs()) return Direction.DOWN;
        return Direction.NONE;
    }


    static addPiece(GamePiece piece) {


        _pieces.add(piece);
        index[piece.position] = piece;
    }


    static removePiece(GamePiece piece) {


        _pieces.remove(piece);
        index[piece.position] = null;
    }


    static void on(Offset offset) {


        lastDirection = parse(offset);
        process(lastDirection);


        bus.add(null);
        if (_pieces.length > 48) { start(); } // Game Over :/


        Point p;
        while (p == null || index.containsKey(p)) { p = Point(rnd.nextInt(6), rnd.nextInt(6)); }
        
        addPiece(GamePiece(model: GamePieceModel(position: p, value: 0)));
    }


    static void process(Direction direction) {
        
        switch (direction) {


            case (Direction.UP):
                return scan(0, 7, 1, Axis.vertical);
                
            case (Direction.DOWN):
                return scan(6, -1, -1, Axis.vertical);


            case (Direction.LEFT):
                return scan(0, 7, 1, Axis.horizontal);


            case (Direction.RIGHT):
                return scan(6, -1, -1, Axis.horizontal);


            default:
                break;
        }
    }
    
    static scan(int start, int end, int op, Axis axis) {


        for (int j = start; j != end; j += op) {
            for (int k = 0; k != 7; k++) {
                
                Point p = axis == Axis.vertical ? Point(k, j) : Point(j, k);
                if (index.containsKey(p)) { check(start, op, axis, index[p]); }
            }
        }
    }


    static void check(int start, int op, Axis axis, GamePiece piece) {


        int target = (axis == Axis.vertical) ? piece.position.y : piece.position.x;
        for (var n = target - op; n != start - op; n -= op) {


            Point lookup = (axis == Axis.vertical) 
                ? Point(piece.position.x, n) 
                : Point(n, piece.position.y);


            if (!index.containsKey(lookup)) { target -= op; }
            else if (index[lookup].value == piece.value) { return merge(piece, index[lookup]); }
            else { break; }
        }


        Point destination = (axis == Axis.vertical) 
            ? Point(piece.position.x, target) 
            : Point(target, piece.position.y);


        if (destination != piece.position) { relocate(piece, destination); }
    }


    static void merge(GamePiece source, GamePiece target) {


        if (source.value == 6) {


            index.remove(source.position);
            index.remove(target.position);
            _pieces.remove(source);
            _pieces.remove(target);
            score.value += source.model.value * 100;
            return;
        }


        source.model.value += 1;
        index.remove(target.position);
        _pieces.remove(target);
        relocate(source, target.position);
        score.value += source.model.value * 10;
    }


    static void relocate(GamePiece piece, Point destination) {


        index.remove(piece.position);
        piece.move(destination);
        index[piece.position] = piece;
    }


    static void start() {


        _pieces = [];
        index = {};
        on(Offset(1,0));
    }
}

The Controller class performs the bulk of in-game logic, including game initialization, input handling, and turn processing and evaluation. A random number generator is created along with a List to store the pieces in play and Map to serve as an x/y lookup index for convenience. The bus property provides an implementation of Stream and StreamController to broadcast an event when a turn is complete.

Controller类执行大量的游戏内逻辑,包括游戏初始化,输入处理以及回合处理和评估。 创建一个随机数生成器以及一个List来存储游戏中的棋子,以及Map以便作为x / y查找索引,以方便使用。 bus属性提供Stream和StreamController的实现,以在转弯完成时广播事件。

Input is received by the on method, and turned into a Direction by calling parse on the Offset received from the event. The turn is then evaluated and an update event is fired on the bus. If all 49 spaces on the board are occupied, the game is restarted, otherwise a new piece is added to the board and the game is ready for the next move.

输入由on方法接收,并通过对从事件接收到的Offset调用parse来将其转换为Direction 。 然后评估转弯,并在总线上触发更新事件。 如果棋盘上的所有49个空间都被占用,则重新开始游戏,否则将新棋子添加到棋盘上,并且游戏为下一个动作做好了准备。

Turn evaluation begins with the process method, in which the Direction is received and scan is called with corresponding loop parameters to evaluate the board in the opposite direction of the swipe. This allows the furthest items along the target vector to be handled first, merging pieces as necessary and clearing a path across the board for the remaining pieces under evaluation.

转弯评估从process方法开始,在该方法中,将接收“ Direction ,并使用相应的循环参数调用scan ,以沿滑动的相反方向评估电路板。 这样可以首先处理沿着目标向量的最远的项目,根据需要合并碎片,并为待评估的其余碎片清除整个路径。

The scan method takes a start, end, increment operation, and axis. A loop is constructed with these parameters to begin scanning the board and looking for pieces in play. Each piece is passed into check which takes the starting point, increment operation, axis, and game piece as input, and checks the intended path of the piece, merging or relocating each as required.

scan方法采用开始,结束,增量操作和轴。 使用这些参数构建一个循环,开始扫描棋盘并寻找游戏中的棋子。 每个棋子都经过check ,该棋子以起点,增量操作,轴和游戏棋子为输入,并检查棋子的预期路径,并根据需要合并或重新放置它们。

Merge operations are handled by first checking whether the pieces being merged are at the highest possible value, and if so they are both removed and a bonus score is awarded, otherwise the target piece is removed and the incoming piece is promoted and moved to the target location.

合并操作是通过首先检查要合并的片段是否在最高可能值上来进行的,如果这样,则将它们都移除并授予奖励分数,否则将移除目标片段,并将传入的片段提升并移至目标位置。

Relocate operations simply remove the piece from the index at its current key, call move on the piece to update it and trigger the animation, and finally place the item back in the index at the updated x/y position key.

重新定位操作只需将片段从其当前关键点的索引中删除,调用片段上的move即可更新它并触发动画,最后将项目放回到更新的x / y位置关键点的索引中。

flutter构建布局_在Flutter中构建益智游戏_第1张图片

结论 (Conclusion)

This demo illustrates some of the power and flexibility available within Flutter and showcases how simple 2D games can be made using only the Flutter SDK and provider package, creating everything else from scratch.

该演示演示了Flutter中可用的一些功能和灵活性,并展示了仅使用Flutter SDK和提供程序包如何制作简单的2D游戏,并从头开始创建其他所有内容。

The concepts used within can be directly transferred to any kind of Flutter application requiring simple state management, highly interactive UI, efficient animations and render cycles, and overall clean architecture requiring low maintenance overhead in the future.

所使用的概念可以直接转移到任何需要简单状态管理,高度交互的UI,高效动画和渲染周期以及将来需要较少维护开销的整洁体系结构的Flutter应用程序中。

Follow me here on Medium for more tech resources on modern software technologies ranging from front-end and mobile to cloud, virtualization, and API development tools.

在Medium上关注我,以获取有关现代软件技术的更多技术资源,从前端,移动到云,虚拟化和API开发工具,一应俱全。

Thanks for reading and good luck with your next Flutter project!

感谢您的阅读并祝您下一个Flutter项目顺利!

~ 8_bit_hacker

〜8_bit_hacker

翻译自: https://itnext.io/building-a-puzzle-game-in-flutter-41c6c1eee65a

flutter构建布局

你可能感兴趣的:(游戏,python,dart,unity,人工智能)