双达特里斯(译文)
By robot-v1.0
本文链接 https://www.kyfws.com/games/double-dartris-zh/
版权声明 本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!
- 19 分钟阅读 - 9292 个词 阅读量 0双达特里斯(译文)
原文地址:https://www.codeproject.com/Articles/764120/Double-Dartris
原文作者:Fredrik Bornander
译文由本站 robot-v1.0 翻译
前言
Implementing a Tetris-like game for the web using Dart
使用Dart在网络上实现类似俄罗斯方块的游戏
介绍(Introduction)
在本文中,我将介绍一种使用Dart来实现HTML5的方法(In this article I’ll cover a way to use Dart in order to implement a HTML5) 俄罗斯方块(Tetris) 类似的游戏.正如我所介绍的(-like game. As I’ve covered) 使用Dart开发网络游戏的基础(the basics of web game development using Dart) 在此之前,本文的主要重点是实现类似俄罗斯方块的游戏逻辑和动作.(before, the main focus of this article will be implementing Tetris-like game logic and actions.) 有(There’s) 编译为JavaScript的早期版本(an early version compiled to JavaScript) 如果您想了解一下实际情况.(if you want to have a look at what it looks like in action.)
使用代码(Using the code)
zip存档包含一个Dart Editor项目,只需解压缩并导入即可.(The zip-archive contains a Dart Editor project, just unzip and import.)
要求(Requirements)
我为Tetris实现定义了一组要求:(I defined a set of requirements for my Tetris implementation:)
- 某种形式的效果(Some form of effects);我希望在清除一行时发生一些额外的事情,而不仅仅是消失的块.(; I wanted something extra to happen when a row was cleared rather than just the blocks dissappearing.)
- 墙踢(Wall kick);某些类似俄罗斯方块的游戏的一个特点是,如果玩家试图在一块棋子太靠近比赛场地的边缘时旋转它,它将被向内"踢"向比赛场地的中心(前提是那里有空间) .(; A feature of some Tetris-like games is that if the player tries to rotate a piece while it’s too close to the edge of the playing field, it will be “kicked” inwards towards the center of the playing field (provided there’s room there).)
- 声音(Sound);游戏应在发生不同事件时播放声音.(; The game should play sounds as different events take place.)
- 可配置键(Configurable keys);游戏开发中最困难(我发现)的部分之一是完成所有非游戏性的内容,例如简介和菜单,因此为此,我将不得不至少添加一种配置键的基本方法.(; One of the hardest (I find) part of game development is to finish everything that’s not game-play specific, such as intros and menus, so for this one I’m going to have to add at least a basic way of configuring the keys.)
- 捻(Twist);该游戏不应是俄罗斯方块的大容量标准版本,但应对此有所改进.(; The game should not be a bulk-standard version of Tetris, but should have some twist to it.) 为了(For the)**捻(Twist)**第一部分,我完成了双重设计,即玩家一次控制两个游戏,一个倒置,另一个控制正确的方式.(part I ended up doing the double-thingy, where the player controls two games at once, one upside-down and the other the right way up.)
基本原理(Fundamentals)
在俄罗斯方块中,玩家旋转并移动所有由四个正方形组成的棋子.块的外观或构造规则是,每个正方形必须与另一个正方形相邻,并且它们必须与至少一个邻居共享至少一个完整的侧面.(In Tetris the players rotates and moves pieces all made up of four squares. The rule for how pieces look or are constructed is that each square must be adjecant to another square, and they must be sharing at least one full side with at least one neighbour.) 这些正方形可以占据七个不同的配置,如果有三个正方形而不是四个,那么本来应该是三个,对于五个正方形,总共存在*某种配置.(*There are seven distinct configurations that the squares can occupy, had there been three squares in stead of four it would have been three and for five squares a total of something configurations exists.) 由于有简单的规则,因此可以生成所有可能的块,虽然这是我将采用的方法(如果块由五个或更多正方形组成),但只有七个可能的块时则不需要. (我认为)手动定义这七个部分更快,更整洁.(Because of the simple rule it is possible to generate all possible pieces and while this would be the approach I would have taken if the pieces were made up of five or more squares it is not required when there are only seven possible pieces. It is quicker and tidier (I think) to manually define the seven pieces.) 在Double-Dartris中,有3个基本类别.(In Double-Dartris there are three classes that are fundamental;)
Block
;代表一个正方形或下降件的一部分.(; which represents a single square or part of a falling piece.)Piece
;这是一组下降的代表(; which represents a falling piece by being a set of)Block
s.(s.)PlayingField
;代表(; which represents the area where the)Piece
的秋天,哪里(s fall and where the)Block
堆叠起来.(s stack up.)
代码(The Code)
块(Block)
的(The) Block
类代表一个正方形,可以作为下降的一部分(class represents a single square, either as part of a falling) Piece
或作为(or as a) Block
已经定居在(that has settled somewhere in the) PlayingField
.我都需要的原因(. The reason I need both) Block
和(s and) Piece
s是a的特征(s is that the characteristics of a) Piece
一旦解决,将发生巨大的变化,并且清除一行可以清除部分(changes drastically as soon as it’s settled, and clearing a row can clear part of a) Piece
.(.)
因为(Because the) Block
是这样一个基本事物的抽象,它实质上只包含两个事物;(is an abstraction of such a basic thing it essentially contains only two things;)
- 位置(Position)
- 颜色(Color)
除了保存以上数据外,它还知道如何渲染自身.(In addition to holding the above data, it also knows how to render itself.)
请注意,它没有尺寸的概念,这是渲染所必需的.这是因为块的隐式大小为(Notice that it does not have a concept of size, something that would be required for rendering. This is because the block has an implicit size of)1x1(1x1)和(, and the)
render
方法知道如何将其转换并缩放到正确的屏幕位置.它通过投影一个(method knows how to translate and scale that into the correct onscreen position. It does this by projecting a)**游戏区(game-area)**到一个(onto a)屏幕(screen):(:)
void render(final CanvasRenderingContext2D context, final Rect screen, final Rect gameArea) {
final int w = screen.width ~/ gameArea.width;
final int h = screen.height ~/ gameArea.height;
final Rect block = new Rect(w * (gameArea.left + position.x), h * (gameArea.top + position.y ), w, h);
fillBlock(context, block, _color);
}
在上面(In the above the) screen
是一个(is a) Rect
以像素为单位,(in pixels, and the) gameArea
是在野外单位中进行的,即如果游戏的宽度为十格,高为二十格(is in field units, i.e. if the game is ten blocks wide and twenty blocks tall) gameArea
是(is)10x20(10x20).(.)
这样做可以将游戏逻辑与渲染逻辑解耦,(Doing it this way decouples the game-play logic from the rendering logic, the) Block
可以在小区域或全屏模式下渲染,并且宽度仍然看起来正确(can be rendered in a small area or fullscreen and it would still look correct as the width) w
和高度(and height) h
计算为您将获得的距离(are calculated as the distances you’d get if) gameArea
被拉伸到(was stretched onto) screen
.(.)
的(The) ~/
运算符是一个(operator is a)镖(Dart)方便的运算符,它给出除法的整数结果,它是正常除法的更快,更短的版本以及显式的int转换:(convenience operator that gives the integer result of a division, it is a faster, shorter version of normal division plus an explicit int conversion:)
// Do this
final int w = screen.width ~/ gameArea.width;
// Do not do this
final int w = (screen.width / gameArea.width).toInt();
的完整清单(The full listing for) Block
看起来像这样:(looks like this:)
class Block {
final Position position;
final ColorPair _color;
Block(this.position, this._color);
String toString() => "B@$position";
get hashCode => position.hashCode;
operator ==(final Block other) {
return position == other.position;
}
void render(final CanvasRenderingContext2D context, final Rect screen, final Rect gameArea) {
final int w = screen.width ~/ gameArea.width;
final int h = screen.height ~/ gameArea.height;
final Rect block = new Rect(w * (gameArea.left + position.x), h * (gameArea.top + position.y ), w, h);
fillBlock(context, block, _color);
}
}
这两个似乎很奇怪(It might seem peculiar that two) Block
如果s的位置相同而与颜色无关,则s被认为是相同的,但是s中的位置(s are considered the same if their positions are the same regardless of their color, but the position in the) PlayingField
是什么使一个块变得独特(当我介绍(is what makes a block unique (as will be apparent when I cover) PlayingField
后来).(later).)
片(Piece)
的(The) Piece
类代表四人一组(class represents the group of four) Block
目前落在(s that is currently falling across the) PlayingField
.它将渲染委托给(. It delegates the rendering to the) render
方法开启(method on) Block
.(.)
作品的形状由它的(The shape of the piece is defined by it’s) final List<Block> _positions;
组成块的成员(member that hold the blocks making up the)**默认(default)**的配置(configuration of the) Piece
.在属性中使用位置和旋转属性(. Using properties for position and rotation in the) PlayingField
的(the) Piece
转换(transforms the)默认(default) Block
s表示每个渲染帧上的当前表示形式.这显然不是很有效,但是对于像俄罗斯方块这样简单的游戏来说,这并不重要.(s to the current representation on every rendered frame. This is obviously not very efficient but it shouldn’t really matter for a game as simple as a Tetris.)
List<Block%gt; _getTransformed() {
final List<Block%gt; transformed = new List<Block%gt;();
for(int i = 0; i < _positions.length; ++i) {
Block block = _positions[i];
for(int r = 0; r < _rotation; ++r) {
block = new Block(new Position(-block.position.y, block.position.x), block._color);
}
transformed.add(new Block(_position + block.position, block._color));
}
return transformed;
}
void render(final CanvasRenderingContext2D context, final Rect screen, final Rect gameArea) {
final List<Block%gt; transformed = _getTransformed();
for(int i = 0; i < transformed.length; ++i) {
final Block block = transformed[i];
block.render(context, screen, gameArea);
}
}
的另一责任(Another responsibility of the) Piece
类是接受来自控制器的移动和旋转请求.而不是在移动之前检查移动是否有效(有效,我的意思是不要移动到移动范围之外)(class is to accept move and rotate requests from the controller. Instead of checking if a move is valid before the move (by valid I mean not moving outside the bounds of the) PlayingField
或成(or into a) Block
),所有移动都将被接受,并且完成后,当前状态的有效性(), all moves are accepted and upon completion the validity of the current state of the) PlayingField
被检查,如果发现无效,则会回滚到以前的状态.(is checked and if found to be invalid it’s rolled back to the previous state.)
使用这种try-rollback方法,用于移动(水平移动,软放和旋转)的各个方法变得非常简单,并且基本上仅将很小的有效载荷委派给称为(Using this try-rollback method the individual methods for moving (move horizontally, soft drop and rotate) become fairly simple and essentially only delegate a very small payload to a method called) _tryAction
.(.)
/// This method runs the transaction delegate, then verifies that the piece is still in a valid position
/// if the field, if it is not valid the rollback delegate is run to undo the effect.
bool _tryAction(void transaction(), void rollback(), final Set<Block%gt; field, final Rect gameArea) {
transaction();
final Iterable<Block%gt; transformed = _getTransformed();
final int lastLine = falling ? gameArea.bottom : gameArea.top - 1;
if (transformed.any((b) =%gt; b.position.y == lastLine || b.position.x < gameArea.left || b.position.x %gt;= gameArea.right || field.any((fp) =%gt; fp == b))) {
rollback();
return true;
}
else {
return false;
}
}
使用两个委托进行操作和回滚,首先(Two delegates are used for the action and the rollback, first) transaction
执行(这会改变位置和/或旋转),并在使用转换后的版本检查状态,使用新参数进行转换后,将其保留该状态,或者如果发现无效则回滚.回滚(is executed (and this mutates the position and/or the rotation) and after the state has been checked using the transformed version, transformed using the new parameters, it is either left that way or rolled back if found to be invalid. To rollback the) rollback
委托被调用.(delegate is invoked.)
由于采用了"尝试回滚"方法,因此,(Because of the try-rollback approach the code for moving a) Piece
变成包含两个相等且相反的代表的单行.(becomes a single line with two delegates that are equal and opposite.)
bool move(final Position delta, final Set<Block> field, final Rect gameArea) {
return _tryAction(() => _position += delta, () => _position -= delta, field, gameArea);
}
调整(Adjust the) _position
由一个(by a) _delta
数量,如果没有产生有效状态,则通过负数进行调整以进行回滚(amount, and if it doesn’t yield a valid state then rollback by adjusting by the negative) _delta
.如果不是为了旋转,旋转一块也将同样简单.(. Rotating a piece would be similarly simple if it wasn’t for the)**墙踢(wall-kick)**如果旋转被墙壁阻挡,则该功能可将工件向内移动.至(feature that moves the piece inwards if the rotation is blocked by the walls. To)**墙踢(wall-kick)**尝试多次尝试回滚操作的次数(the try-rollback action is attempted as many times as there are) Block
中的(s in the) Piece
,这可以确保已经尝试了所有必要的脚踢位置,因为没有任何必要的脚踢可以找到一个更大的空间.(, that makes sure that all required positions of kick have been tried as there’s never any need to kick more than that to find a clear space.)
对于每次迭代,将应用旋转,并且脚踢距离从(For each iteration the rotation is applied and the kick distance is increased from)0(0)至(to)数量(number of) Block
s(s),如果旋转有效,它将保持原状,否则将回滚并以更高的反冲距离值再次尝试.(, if the rotation was valid it stays, otherwise it’s rolled back and tried again with a higher value for the kick distance.)
/// Helper method for the rotate method
void _wallKickRotate(final Position kickDirection, final bool rollback) {
_rotation += rollback ? -1 : 1;
if (rollback)
_position -= kickDirection;
else
_position += kickDirection;
}
bool rotate(final Set<Block> field, final Rect gameArea) {
final int originalX = _position.x;
for(int i = 0; i < _positions.length; ++i) {
final Position kickDirection = new Position(_position.x < gameArea.horizontalCenter ? i : -i, 0);
if (!_tryAction(() => _wallKickRotate(kickDirection, false), () => _wallKickRotate(kickDirection, true), field, gameArea)) {
if (_position.x != originalX)
_audioManager.play(_position.y % 2 == 0 ? "wallKickA" : "wallKickB");
return true;
}
}
return false;
// This is rotating without wall kicking
//return _tryAction(() => ++_rotation, () => --_rotation, field, gameArea);
}
同样,这根本不是针对性能进行优化,并且显然有更有效的方法可以做到这一点.(Again, this is not optimizing for performance at all and there are obviously much more efficient ways of doing this.)
比赛场地(PlayingField)
的(The) PlayingField
类代表区域(class represents the area the) Piece
属于(s fall in and) Block
占据.它接受移动,旋转和放下电流的请求(s occupy. It accepts requests to move, rotate and drop the current) Piece
并使用工厂生成下一个(and uses a factory to generate the next) Piece
当当前的人定居到(when the current one settles in the field into) Block
s.(s.)
该字段还负责检查是否有任何行完全被(The field is also responsible for checking if any rows are fully occupied by) Block
s,应该折叠.该类的主要目的是游戏逻辑,因此当一行折叠时,它不会计算新的分数,而只是返回(s and should be collapsed. The main purpose of the class is game-logic so it doesn’t calculate the new score when a row is collapsed, it simply returns the) Block
被折叠的.它返回块的原因,而不仅仅是折叠的数量的原因是,特殊效果需要折叠的位置(s that were collapsed. The reason it returns the blocks and not just a number of how many collapsed is that the special effects require the position of the collapsed) Block
s,我不要(s and I don’t want the) PlayingField
要了解特殊效果,这不是游戏逻辑.(to know about the special effects, that’s not game-logic.)
此外,(Further, the) PlayingField
负责检测游戏结束状态,它也知道如何进行渲染,但该操作已委派给(is in charge of detecting the game over state, it also knows how to render itself but that action is delegated to the) render
方法(methods on) Piece
和(and) Block
.(.)
储存积木(Storing the Blocks)
我决定使用一些不同的方法,而不是用二维数组表示该字段.的(Instead of a two-dimensional array to represent the field I decided to try something a little bit different; the) Block
中的(s in the) PlayingField
放在一个集合中.(are kept in a set.)
final Set<Block> _field = new HashSet<Block>();
这就是为什么等于重载(This is why the equals overload on) Block
只考虑位置而不考虑颜色.(only consider position and not color.)
我这样做是为了尝试使用(I did it this way to try to use the)LINQ(LINQ)类似的功能(-like functions on) Iterable
而不是访问类似网格的结构.这种方法没有什么特别聪明的,我只是想看看以这种方式实现它的样子.(instead of accessing a grid-like structure. There isn’t anything particular clever about that approach, I just wanted to see what it would look like implementing it that way.)
倒塌(Collapsing a row)
要折叠一行,该字段将从顶部(或底部为反面)遍历所有行,并且(To collapse a row the field will iterate through all rows from the top (or bottom for the flipped side) and if the number of) Block
一行上的s等于(s on one row equals the width of the) _gameArea
(即,可能的水平((i.e the number of possible horizontal) Block
s),然后清除该行,并将上方的任何内容向下移动一级.(s) then the row is cleared and anything above is moved down one step.)
由于我不在乎性能(Since I don’t care about the performance the) Block
清除的行上方的s实际上并没有移动,而是将它们删除并读取,这是(s above the cleared row aren’t actually moved, they’re deleted and readded, that’s an effect of the) Block
类是不可变的.(class being immutable.)
Iterable<Block> checkCollapse() {
final int rowDirection = _isFalling ? 1 : -1;
final Position gridDelta = new Position(0, rowDirection);
final int firstRow = _isFalling ? _gameArea.top : _gameArea.bottom;
final int lastRow = _isFalling ? _gameArea.bottom : _gameArea.top - 1;
final Set<Block> collapsed = new HashSet<Block>();
for(int r = firstRow; r != lastRow; r += rowDirection) {
final Iterable<Block> row = _field.where((block) => block.position.y == r).toList();
if (row.length == _gameArea.width) {
collapsed.addAll(row);
_field.removeAll(row);
final Iterable<Block> blocksToDrop = _field.where((block) => _isFalling ? block.position.y < r : block.position.y > r).toList();
_field.removeAll(blocksToDrop);
_field.addAll(blocksToDrop.map((block) => new Block(block.position + gridDelta, block._color)));
}
}
return collapsed;
}
检查游戏结束状态的过程就像检查是否有游戏一样简单(The process of checking for the game over state is as simple as checking if any) Block
s被放置在比赛场地的第一行.(s are settled on the first row of the playing field.)
现场控制器(FieldController)
该类是(The class that is the)**胶(glue)**游戏状态之间((between the game state () StateGame
)和游戏逻辑(() and the game logic () PlayingField
) 是个() is the) FieldController
.(.)
的(The) FieldController
负责读取用户的输入并将其中继到(is responsible for reading and relaying user input to the) PlayingField
以及创建和维护与游戏逻辑不直接相关的图形效果(在(as well as creating and maintaining the graphical effects that are not directly related to game logic (in)双达特里斯(Double-Dartris)这些效果是一滴清除了多行时产生动画效果的消息.(those effects are messages that animate when multiple rows have been cleared in one drop).)
拥有控制器可以在游戏的抽象和与用户的界面之间提供巧妙的去耦,并允许添加AI玩家之类的事情.自从(Having a controller provides a neat de-coupling between the abstraction of the game and the interface with the user, and it allows for things like adding an AI-player. Since the) PlayingField
班级关心游戏中可以做什么(向左移动,向右移动,放下,旋转,清除行或输掉游戏),它不应该对引发这些事情的原因有任何深入的了解,这就是控制器的职责.即使掉落(class cares about what can be done in the game (move left, move right, drop, rotate, clear row or lose the game) it shouldn’t have any intimate knowledge about what initiates those things, that’s the controller’s job. Even the dropping of a) Piece
是控制器的工作,该字段仅知道如何删除以及是否(is the job of the controller, the field only knows how to drop and whether or not the) Piece
已经解决(has settled into) Block
s.(s.)
作为游戏(As the game “)滴答声(ticks)“只要浏览器提供了(” whenever the browser provides an)**动画框架(animation frame)**控制器需要跟踪经过的时间,仅在经过足够的时间(由当前级别决定)后才请求下降.(the controller needs to keep track of elapsed time and only request a drop when enough time (as dictated by the current level) has passed.)
有多种辅助方法(There are various helper methods in) FieldController
但是相关的方法是(but the relevant method is) control
看起来像这样:(which looks like this:)
Iterable<Block> control(final double elapsed, final Rect screen) {
if (isGameOver)
return new List<Block>();
_accumulatedElapsed += elapsed;
_checkRotating();
_checkMoving();
// This way it's getting very difficult, very fast
_dropInterval = (1.0 / level);
double dropTime = _dropInterval * (isDropping ? 0.25 : 1.0);
dropTime = min(dropTime, isDropping ? 0.05 : dropTime);
if (_accumulatedElapsed > dropTime) {
_accumulatedElapsed = 0.0;
score += level;
if (_field.dropCurrent()) {
// Piece has settled, check collapsed and then generate a new Piece
final Iterable<Block> collapsed = _field.checkCollapse();
score += collapsed.length * collapsed.length * level;
_field.next();
final numberOfRowsCleared = collapsed.length ~/ _field._gameArea.width;
switch(numberOfRowsCleared) {
case 0: _audioManager.play("drop"); break; // 0 means no rows cleared by the piece came to rest
case 2: _audioManager.play("double"); break;
case 3: _audioManager.play("triple"); break;
case 4: _audioManager.play("quadruple"); break;
}
final TextEffect effect = _buildTextEffect(numberOfRowsCleared, screen);
if (effect != null)
_textEffects.add(effect);
return collapsed;
}
}
return new List<Block>();
}
该方法的本质是:(Essentially what the method does is:)
Process Horizontal Move
Process Rotate
If it is time for Piece to Drop Then
Drop Piece
If dropped piece settled then
Collapse rows // If any
Play some sounds
End If
If applicable Then Create Text Effect
Generate Next Piece
End if
控制器由(其中包括)用于控制(The controller is constructed with (amongst other things) the keys that will control the) Piece
,因此可以轻松配置游戏中使用的按键,这是我要介绍的要求之一.(, that makes it easy to have the keys used in the game configurable which was one of the requirements I set out to cover.)
的(The) FieldController
(或者实际上是一对)被主状态使用((or actually a pair of them) are used by the main state) StateGame
.(.)
状态游戏(StateGame)
游戏使用状态机非常像(The game uses a state machine much like) 我在上一篇有关Dart游戏开发的文章中描述的(the one I described in my previous article on Dart game development) ,而(, and while the) StateGame
是最有趣的状态,完整的状态机如下所示:(is the most interesting state, the full state machine looks like this:)
主要状态包两个(The main state wraps up two) FieldController
s(一个下降和一个上升的游戏)并控制清除一行或游戏结束时发生的动画.(s (one falling and one rising game) and controls the animation that takes place when a row is cleared or the game is over.)
这也是游戏与(It is also where the game interacts with the)的HTML(HTML)用于显示游戏大部分文字的元素.我想要这两个(elements used to display most of the game’s text. I wanted that portion out of both the) PlayingField
和(and the) FieldController
当代码耦合到DOM树时,编写单元测试变得更加困难.诚然,我以一种可以扩展为允许嘲笑的方式结束了设置和清除文本的调用,但是我认为,一路走到像<俄罗斯方块>游戏一样简单的东西上,实在是太过分了.(as writing unit tests becomes harder when the code couples to the DOM-tree. Granted, I have wrapped up the calls to set and clear the text in a way that could be extended to allow mocking but I thought it overkill to go all the way for something as simple as a Tetris game.)
文字效果(Text Effects)
当在一滴中清除两行或更多行时,一条文字消息” Double"," Triple"或" Quadruple"会快速淡入和淡出,同时也具有动画效果.这些效果归归(When two or more rows are cleared in one drop a text message saying “Double”, “Triple” or “Quadruple” fades in and out quickly whilst also being animated. These effects are owned by the) PlayingField
文本效果实现由两个类组成.(and there are two classes that make up the text effect implementation.)
文本(Text)
的(The) Text
类表示处于单一状态的文本,并且能够在该状态下呈现文本.所谓状态,是指诸如以下属性:(class represents the text in a single state, and is able to render the text in that state. By state I mean properties such as:)
- 字体大小(Font size)
- 位置(Position)
- 回转(Rotation)
- 阿尔法混合(Alpha blend)
除了上面的字符串外,文本和颜色也可以在(In addition to the above the string that is the text and the color is also available on the)
Text
类,但不允许对其进行动画处理.(class, but they’re not allowed to be animated.) 由于可以为以下内容设置转换(位置和旋转共同构成转换)(As the transform can be set (position and rotation together make up the transform) for the)Text
由于该转换是全局设置的,因此e.对于随后的所有平局(and since the transform is set globally, i. e. for all subsequent draws on a)CanvasRenderingContext2D
,(, the)Text
的(’s)render
方法在应用文本属性之前保存当前的渲染变换.然后,当其呈现其自己的当前状态时,它将还原先前的变换.(method saves the current render transform before applying the properties of the text. Then, when it has rendered it’s own current state it restores the previous transform.) 的(The)render
方法采用(method takes the properties of the)Text
并将它们应用于(and applies them to the)CanvasRenderingContext2D
,所以(, so)_position
成为翻译,(becomes the translation,)_fontSize
和(and)_font
成为字体等(become the font, etc.)
void render(final CanvasRenderingContext2D context, final Rect screen) {
context.save();
context.translate(_x, _y);
context.rotate(_angle);
context.globalAlpha = _alpha;
context.fillStyle = _color.primary.toString();
context.shadowColor = _color.secondary.toString();
context.shadowOffsetX = 2;
context.shadowOffsetY = 2;
context.font = "${_fontSize}px ${_font}";
context.textAlign = _align;
context.textBaseline = "middle";
context.fillText(_text, 0, 0);
context.restore();
}
所以虽然(So while the) Text
类保存并呈现单个"状态",还有另一个类可以改变该状态,(class holds and renders a single “state”, there’s another class that mutates that state, the) TextEffect
类.(class.)
文字效果(TextEffect)
的(The) TextEffect
课堂有两个非常简单的职责;(class has two very simple responsibilities;)
- 跟踪效果持续多长时间,持续时间.(Keep track of how long the effect lasts for, the duration.)
- 改变它的状态(Mutate the state of it’s)
Text
使用动画师.(using animators.) 持续时间(以秒为单位)作为(The duration (in seconds) is passed as a)double
到(to the)TextEffect
构造函数,并在每次更新时将经过的时间添加到累积(constructor and on every update the elapsed time is added to a cummulative)_elapsed
领域.什么时候(field. When)_elapsed
大于(is greater than)_duration
效果是完整的,可以从拥有的收藏中删除(the effect is complete and can be removed from the collection owned by the)FieldController
.(.) 对于每一帧,都会计算经过时间和持续时间之间的分数,并将该分数作为输入提供给动画师,进而为动画效果产生新的值.(For each frame the fraction between elapsed and duration is calculated, and it is this fraction that is fed as input to the animators that in turn yield new values for the properties of the)Text
.(.) 动画师是简单的函数指针(The animators are simple function pointers)
typedef double DoubleAnimator(final double _elapsed);
class TextEffect {
Text _text;
double _elapsed = 0.0;
double _duration;
DoubleAnimator _sizeAnimator;
DoubleAnimator _xAnimator;
DoubleAnimator _yAnimator;
DoubleAnimator _angleAnimator;
DoubleAnimator _alphaAnimator;
...
}
这样(This way the) update
的方法(method of) TextEffect
将会更新,例如(will update, for example, the)X(X)的一部分(part of the) Text
的位置是这样的:(’s position like this:)
void update(final double elapsed) {
_elapsed += elapsed;
final double fraction = _elapsed / _duration;
_text._x = _xAnimator(fraction).toInt();
...
}
由于传入动画器的值是(As the value passed in to the animator is the fraction between) _elapsed
和(and) _duration
动画师不应回答以下问题:(the animators are not supposed to answer the question “)N秒后的状态是什么?(what is the state after N seconds?)“而是回答”(” but rather answer “)持续时间的P%过去后的状态是什么?(what is the state after P% of the duration has passed?)”.(".)
用这种方法可以很容易地操作文本的属性.(Doing it this way makes it easy to manipulate the properties of the text.)
举个例子;播放器在一滴中清除四行时播放的文本效果如下所示:(As an example; the text effect played when the player clears four rows in one drop looks like this:)
final TextEffect effect = new TextEffect(new Text("QUADRUPLE!", "pressstart", "center", color), 1.0);
effect._sizeAnimator = (f) => 10.0 + f * 30;
effect._xAnimator = (f) => screen.horizontalCenter;
effect._yAnimator = (f) => screen.verticalCenter;
effect._angleAnimator = (f) => sin(f * 2 * PI);
effect._alphaAnimator = (f) => 1.0 - f;
这会设置一个从(This sets up a text that grows from)10.0(10.0)至(to)30.0(30.0)指向并摆动,同时以线性方式逐渐消失.(points and wobbles a bit whilst fading out in a linear fashion.)
兴趣点(Points of Interest)
概要(Summary)
我认为我已经完成了我要实施的要求,但这只是我第二次尝试(I think I managed to complete the requirements I set out to implement but as this is only my second attempt at a)镖(Dart)程序/游戏的代码比我想要的更混乱.我不认为这很糟糕,但是当我走时,我发现做适合自己的事情的方法(program/game the code got messier than I wanted. I don’t think it’s aweful but as I go I discover ways of doing things that suits)镖(Dart)更好.第二个项目对我来说再次确认的是,由于(better. What this second project has reconfirmed for me though is that because of the similarities between)镖(Dart)和我通常每天使用的语言((and languages I normally use on a daily basis ()爪哇(Java),(,)C#(C#))与使用JavaScript相比,在为Web生成内容时效率更高.但是仍然,正如我在上一期中所讨论的() I am alot more efficient when producing stuff for the web than I would be in JavaScript. But still, as I discussed in my previous)镖(Dart)文章,我发现缺少资源和工具(article, I find the lack of resources and tooling for)镖(Dart)不足.(lacking.)
后视总是20-20(Hindsight is always 20-20)
如(As in)双达特里斯(Double-Dartris)一局下跌,另一局上升(one game falls and the other rises my) Piece
,(,) PlayingField
和(and) FieldController
所有人都在关注上下的方向以及方向(all care about what is up and down and what direction a) Piece
**下降(falls)**我应该做的是不要这样做,因为它会使代码回旋,几乎没有什么好处.明智的做法是只在渲染中处理一个游戏的翻转,并使两个游戏的逻辑保持相同.从本质上讲,这将使单元测试工作减少一半.(in. What I should have done was to not do that as it convolutes the code for very little upside. The smarter thing would have been to take care of the flipping of one game exclusively in the rendering and kept the game logic the same for both. That would have essentially cut down unit-testing effort by half.)
历史(History)
- 2014-04-24;第一版.(2014-04-24; First version.)
许可
本文以及所有相关的源代码和文件均已获得The Code Project Open License (CPOL)的许可。
HTML5 Dart HTML web game 新闻 翻译