Adds hit tests and transformations between coordinate systems in sprites

Sprite nodes use Point instead of Vector2

Updates sprite test app

Refactors accounting for pivot points in sprites

Adds abstract NodeWithSize class in Sprites.

Refactors SpriteNode to Sprite

Refactors TransformNode to Node (may need to find another name as it conflicts with Sky's Node).

Sprite system now uses and caches transformation matrices.

R=abarth@chromium.org

Review URL: https://codereview.chromium.org/1180703002.
This commit is contained in:
Viktor Lidholt 2015-06-11 08:44:22 -07:00
parent 42877fd114
commit dcb0fa4312
9 changed files with 260 additions and 118 deletions

View file

@ -5,6 +5,7 @@ import 'dart:math' as Math;
import 'package:vector_math/vector_math_64.dart';
import 'sprites.dart';
import 'package:box2d/box2d.dart';
import 'package:sky/framework/rendering/box.dart';
part 'game_world.dart';
part 'game_box.dart';

View file

@ -2,7 +2,59 @@ part of game;
Math.Random _rand;
class GameTests extends TransformNode{
class GameTestsBox extends SpriteBox {
GameTestsBox(Node tests, SpriteBoxTransformMode mode) : super(tests, mode);
void handleEvent(Event event, BoxHitTestEntry entry) {
if (event is PointerEvent) {
Point pointerPos = new Point(event.x, event.y);
int pointer = event.pointer;
switch (event.type) {
case 'pointerdown':
List sprites = findNodesAtPosition(pointerPos);
for (Node node in sprites) {
if (node is Sprite) {
Sprite sprt = node;
sprt.colorOverlay = new Color(0x66ff0000);
}
}
break;
}
}
}
}
class GameTestsSimple extends Node {
Image _imgAsteroid;
Image _imgBg;
Image _imgShip;
GameTestsSimple(ImageMap images) {
// Setup random number generator
_rand = new Math.Random();
// Fetch images
_imgBg = images["https://raw.githubusercontent.com/slembcke/GalacticGuardian.spritebuilder/GDC/Packages/SpriteBuilder%20Resources.sbpack/resources-auto/BurnTexture.png"];
_imgAsteroid = images["https://raw.githubusercontent.com/slembcke/GalacticGuardian.spritebuilder/GDC/Packages/SpriteBuilder%20Resources.sbpack/Sprites/resources-auto/asteroid_big_002.png"];
_imgShip = images["https://raw.githubusercontent.com/slembcke/GalacticGuardian.spritebuilder/GDC/Packages/SpriteBuilder%20Resources.sbpack/Sprites/resources-auto/GG_blueship_Lv3.png"];
Sprite sprt0 = new Sprite.withImage(_imgBg);
sprt0.size = new Size(100.0, 100.0);
sprt0.pivot = new Point(0.0, 0.0);
sprt0.position = new Point(100.0, 100.0);
this.addChild(sprt0);
Sprite sprt1 = new Sprite.withImage(_imgBg);
sprt1.size = new Size(100.0, 100.0);
sprt1.pivot = new Point(0.0, 0.0);
sprt1.position = new Point(100.0, 100.0);
sprt0.addChild(sprt1);
}
}
class GameTests extends Node{
Image _imgAsteroid;
Image _imgBg;
@ -24,8 +76,7 @@ class GameTests extends TransformNode{
void addSprite([double scale = null]) {
TestAsteroidSprite sprt = new TestAsteroidSprite.withImage(_imgAsteroid);
sprt.width = 64.0;
sprt.height = 64.0;
sprt.size = new Size(64.0, 64.0);
if (scale == null) {
scale = _rand.nextDouble();
@ -33,19 +84,29 @@ class GameTests extends TransformNode{
sprt.zPosition = scale;
sprt.scale = scale;
sprt.position = new Vector2(_rand.nextDouble()*1024.0, _rand.nextDouble()*1024.0);
sprt.position = new Point(_rand.nextDouble()*1024.0, _rand.nextDouble()*1024.0);
this.addChild(sprt);
Sprite sprtMoon0 = new Sprite.withImage(_imgAsteroid);
sprtMoon0.size = new Size(32.0, 32.0);
sprtMoon0.position = new Point(32.0, 0.0);
sprt.addChild(sprtMoon0);
Sprite sprtMoon1 = new Sprite.withImage(_imgAsteroid);
sprtMoon1.size = new Size(32.0, 32.0);
sprtMoon1.position = new Point(-32.0, 0.0);
sprt.addChild(sprtMoon1);
}
void update(double dt) {
for (TransformNode child in children) {
for (Node child in children) {
child.update(dt);
}
}
}
class TestAsteroidSprite extends SpriteNode {
class TestAsteroidSprite extends Sprite {
Vector2 _movementVector;
double _rotationalSpeed;
@ -56,11 +117,14 @@ class TestAsteroidSprite extends SpriteNode {
}
void update(double dt) {
position = position + _movementVector * scale;
Vector2 vPos = new Vector2(position.x, position.y);
Vector2 vNewPos = vPos + _movementVector * scale;
position = new Point(vNewPos[0], vNewPos[1]);
// Bounce at edges
if (position[0] < 0 || position[0] > 1024.0) _movementVector[0] = -_movementVector[0];
if (position[1] < 0 || position[1] > 1024.0) _movementVector[1] = -_movementVector[1];
if (position.x < 0 || position.x > 1024.0) _movementVector[0] = -_movementVector[0];
if (position.y < 0 || position.y > 1024.0) _movementVector[1] = -_movementVector[1];
rotation += _rotationalSpeed;
}

View file

@ -1,6 +1,6 @@
part of game;
class GameWorld extends TransformNode {
class GameWorld extends Node {
World world;
List<Body> bodies = [];
@ -42,7 +42,7 @@ class GameWorld extends TransformNode {
}
void addBackground() {
SpriteNode sprtBg = new SpriteNode.withImage(_imgBg);
Sprite sprtBg = new Sprite.withImage(_imgBg);
sprtBg.width = width;
sprtBg.height = height;
sprtBg.pivot = new Vector2(0.0, 0.0);
@ -81,7 +81,7 @@ class GameWorld extends TransformNode {
bodies.add(body);
// Create sprite
SpriteNode sprt = new SpriteNode.withImage(_imgAsteroid);
Sprite sprt = new Sprite.withImage(_imgAsteroid);
sprt.width = radius*2;
sprt.height = radius*2;
// sprt.colorOverlay = new Color(0x33ff0000);
@ -122,7 +122,7 @@ class GameWorld extends TransformNode {
_bodyShip = body;
// Create sprite
SpriteNode sprt = new SpriteNode.withImage(_imgShip);
Sprite sprt = new Sprite.withImage(_imgShip);
sprt.width = radius*2;
sprt.height = radius*2;
sprt.position = new Vector2(width/2.0, height/2.0);
@ -155,7 +155,7 @@ class GameWorld extends TransformNode {
}
void updateBody(Body body) {
SpriteNode sprt = body.userData;
Sprite sprt = body.userData;
double rot = 0.0; //body.getRotation();
// Check bounds and warp objects

View file

@ -4,27 +4,22 @@ double degrees2radians(double degrees) => degrees * Math.PI/180.8;
double radians2degrees(double radians) => radians * 180.0/Math.PI;
class TransformNode {
class Node {
// Member variables
SpriteBox _spriteBox;
TransformNode _parent;
Node _parent;
Vector2 _position;
Point _position;
double _rotation;
bool _isMatrixDirty;
Matrix3 _transform;
Matrix3 _pivotTransform;
double _width;
double _height;
Matrix4 _transformMatrix;
Matrix4 _transformMatrixFromWorld;
double _scaleX;
double _scaleY;
Vector2 _pivot;
bool visible;
@ -33,20 +28,16 @@ class TransformNode {
int _childrenLastAddedOrder;
bool _childrenNeedSorting;
List<TransformNode>_children;
List<Node>_children;
// Constructors
TransformNode() {
_width = 0.0;
_height = 0.0;
Node() {
_rotation = 0.0;
_pivot = new Vector2(0.0, 0.0);
_position = new Vector2(0.0, 0.0);
_position = new Point(0.0, 0.0);
_scaleX = _scaleY = 1.0;
_isMatrixDirty = false;
_transform = new Matrix3.identity();
_pivotTransform = new Matrix3.identity();
_transformMatrix = new Matrix4.identity();
_children = [];
_childrenNeedSorting = false;
_childrenLastAddedOrder = 0;
@ -57,7 +48,7 @@ class TransformNode {
SpriteBox get spriteBox => _spriteBox;
TransformNode get parent => _parent;
Node get parent => _parent;
double get rotation => _rotation;
@ -66,33 +57,12 @@ class TransformNode {
_isMatrixDirty = true;
}
Vector2 get position => _position;
Point get position => _position;
void set position(Vector2 position) {
void set position(Point position) {
_position = position;
_isMatrixDirty = true;
}
double get width => _width;
void set width(double width) {
_width = width;
_isMatrixDirty = true;
}
double get height => _height;
void set height(double height) {
_height = height;
_isMatrixDirty = true;
}
Vector2 get pivot => _pivot;
void set pivot(Vector2 pivot) {
_pivot = pivot;
_isMatrixDirty = true;
}
double get zPosition => _zPosition;
@ -113,11 +83,11 @@ class TransformNode {
_isMatrixDirty = true;
}
List<TransformNode> get children => _children;
List<Node> get children => _children;
// Adding and removing children
void addChild(TransformNode child) {
void addChild(Node child) {
assert(child._parent == null);
_childrenNeedSorting = true;
@ -128,7 +98,7 @@ class TransformNode {
child._addedOrder = _childrenLastAddedOrder;
}
void removeChild(TransformNode child) {
void removeChild(Node child) {
if (_children.remove(child)) {
child._parent = null;
child._spriteBox = null;
@ -141,7 +111,7 @@ class TransformNode {
}
void removeAllChildren() {
for (TransformNode child in _children) {
for (Node child in _children) {
child._parent = null;
child._spriteBox = null;
}
@ -151,16 +121,14 @@ class TransformNode {
// Calculating the transformation matrix
Matrix3 get transformMatrix {
Matrix4 get transformMatrix {
if (!_isMatrixDirty) {
return _transform;
return _transformMatrix;
}
Vector2 pivotInPoints = new Vector2(_width * _pivot[0], _height * _pivot[1]);
double cx, sx, cy, sy;
if (_rotation == 0) {
if (_rotation == 0.0) {
cx = 1.0;
sx = 0.0;
cy = 1.0;
@ -175,22 +143,70 @@ class TransformNode {
cy = Math.cos(radiansY);
sy = Math.sin(radiansY);
}
// TODO: Add support for scale
double scaleX = 1.0;
double scaleY = 1.0;
// Create transformation matrix for scale, position and rotation
_transform.setValues(cy * scaleX, sy * scaleX, 0.0,
-sx * scaleY, cx * scaleY, 0.0,
_position[0], _position[1], 1.0);
_transformMatrix.setValues(cy * _scaleX, sy * _scaleX, 0.0, 0.0,
-sx * _scaleY, cx * _scaleY, 0.0, 0.0,
0.0, 0.0, 1.0, 0.0,
_position.x, _position.y, 0.0, 1.0
);
if (_pivot.x != 0 || _pivot.y != 0) {
_pivotTransform.setValues(1.0, 0.0, 0.0, 0.0, 1.0, 0.0, pivotInPoints[0], pivotInPoints[1], 1.0);
_transform.multiply(_pivotTransform);
return _transformMatrix;
}
// Transforms to other nodes
Matrix4 _nodeToBoxMatrix() {
Matrix4 t = transformMatrix;
Node p = this.parent;
while (p != null) {
t = new Matrix4.copy(p.transformMatrix).multiply(t);
p = p.parent;
}
return _transform;
return t;
}
Matrix4 _boxToNodeMatrix() {
Matrix4 t = _nodeToBoxMatrix();
t.invert();
return t;
}
Point convertPointToNodeSpace(Point boxPoint) {
assert(boxPoint != null);
assert(_spriteBox != null);
Vector4 v =_boxToNodeMatrix().transform(new Vector4(boxPoint.x, boxPoint.y, 0.0, 1.0));
return new Point(v[0], v[1]);
}
Point convertPointToBoxSpace(Point nodePoint) {
assert(nodePoint != null);
assert(_spriteBox != null);
Vector4 v =_nodeToBoxMatrix().transform(new Vector4(nodePoint.x, nodePoint.y, 0.0, 1.0));
return new Point(v[0], v[1]);
}
Point convertPointFromNode(Point point, Node node) {
assert(node != null);
assert(point != null);
assert(_spriteBox != null);
assert(_spriteBox == node._spriteBox);
Point boxPoint = node.convertPointToBoxSpace(point);
Point localPoint = convertPointToNodeSpace(boxPoint);
return localPoint;
}
// Hit test
bool hitTest(Point nodePoint) {
assert(nodePoint != null);
return false;
}
// Rendering
@ -206,16 +222,13 @@ class TransformNode {
void prePaint(PictureRecorder canvas) {
canvas.save();
canvas.translate(_position[0], _position[1]);
canvas.rotate(degrees2radians(_rotation));
canvas.scale(_scaleX, _scaleY);
canvas.translate(-_width*_pivot[0], -_height*_pivot[1]);
// TODO: Use transformation matrix instead of individual calls
// List<double> matrix = [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0];
// this.transformMatrix.copyIntoArray(matrix);
// canvas.concat(matrix);
// TODO: Can this be done more efficiently?
// Get the transformation matrix and apply transform
List<double> matrix = [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0];
this.transformMatrix.copyIntoArray(matrix);
Float32List list32 = new Float32List.fromList(matrix);
canvas.concat(list32);
}
void paint(PictureRecorder canvas) {
@ -225,7 +238,7 @@ class TransformNode {
void visitChildren(PictureRecorder canvas) {
// Sort children primarily by zPosition, secondarily by added order
if (_childrenNeedSorting) {
_children.sort((TransformNode a, TransformNode b) {
_children.sort((Node a, Node b) {
if (a._zPosition == b._zPosition) {
return b._addedOrder - a._addedOrder;
}

View file

@ -0,0 +1,31 @@
part of sprites;
abstract class NodeWithSize extends Node {
Size size;
Point pivot;
NodeWithSize() {
size = new Size(0.0, 0.0);
pivot = new Point(0.0, 0.0);
}
NodeWithSize.withSize(Size size, [Point pivot]);
void applyTransformForPivot(PictureRecorder canvas) {
if (pivot.x != 0 || pivot.y != 0) {
double pivotInPointsX = size.width * pivot.x;
double pivotInPointsY = size.height * pivot.y;
canvas.translate(-pivotInPointsX, -pivotInPointsY);
}
}
bool hitTest (Point nodePoint) {
double minX = -size.width * pivot.x;
double minY = -size.height * pivot.y;
double maxX = minX + size.width;
double maxY = minY + size.height;
return (nodePoint.x >= minX && nodePoint.x < maxX &&
nodePoint.y >= minY && nodePoint.y < maxY);
}
}

View file

@ -2,7 +2,7 @@ part of sprites;
// TODO: Actually draw images
class SpriteNode extends TransformNode {
class Sprite extends NodeWithSize {
Image _image;
bool constrainProportions = false;
@ -10,12 +10,12 @@ class SpriteNode extends TransformNode {
Color colorOverlay;
TransferMode transferMode;
SpriteNode() {
this.pivot = new Vector2(0.5, 0.5);
Sprite() {
}
SpriteNode.withImage(Image image) : super() {
this.pivot = new Vector2(0.5, 0.5);
Sprite.withImage(Image image) {
this.pivot = new Point(0.5, 0.5);
this.size = new Size(image.width.toDouble(), image.height.toDouble());
_image = image;
}
@ -27,21 +27,24 @@ class SpriteNode extends TransformNode {
}
void paint(PictureRecorder canvas) {
canvas.save();
// Account for pivot point
applyTransformForPivot(canvas);
if (_image != null && _image.width > 0 && _image.height > 0) {
canvas.save();
double scaleX = _width/_image.width;
double scaleY = _height/_image.height;
double scaleX = size.width/_image.width;
double scaleY = size.height/_image.height;
if (constrainProportions) {
// Constrain proportions, using the smallest scale and by centering the image
if (scaleX < scaleY) {
canvas.translate(0.0, (_height - scaleX * _image.height)/2.0);
canvas.translate(0.0, (size.height - scaleX * _image.height)/2.0);
scaleY = scaleX;
}
else {
canvas.translate((_width - scaleY * _image.width)/2.0, 0.0);
canvas.translate((size.width - scaleY * _image.width)/2.0, 0.0);
scaleX = scaleY;
}
}
@ -59,13 +62,12 @@ class SpriteNode extends TransformNode {
}
canvas.drawImage(_image, 0.0, 0.0, paint);
canvas.restore();
}
else {
// Paint a red square for missing texture
canvas.drawRect(new Rect.fromLTRB(0.0, 0.0, this.width, this.height),
canvas.drawRect(new Rect.fromLTRB(0.0, 0.0, size.width, size.height),
new Paint()..setARGB(255, 255, 0, 0));
}
canvas.restore();
}
}

View file

@ -11,8 +11,10 @@ enum SpriteBoxTransformMode {
class SpriteBox extends RenderBox {
// Member variables
// Root node for drawing
TransformNode _rootNode;
Node _rootNode;
// Tracking of frame rate and updates
double _lastTimeStamp;
@ -22,7 +24,9 @@ class SpriteBox extends RenderBox {
double _systemWidth;
double _systemHeight;
SpriteBox(TransformNode rootNode, [SpriteBoxTransformMode mode = SpriteBoxTransformMode.nativePoints, double width=1024.0, double height=1024.0]) {
// Setup
SpriteBox(Node rootNode, [SpriteBoxTransformMode mode = SpriteBoxTransformMode.nativePoints, double width=1024.0, double height=1024.0]) {
assert(rootNode != null);
assert(rootNode._spriteBox == null);
@ -40,30 +44,31 @@ class SpriteBox extends RenderBox {
_scheduleTick();
}
void _addSpriteBoxReference(TransformNode node) {
void _addSpriteBoxReference(Node node) {
node._spriteBox = this;
for (TransformNode child in node._children) {
for (Node child in node._children) {
_addSpriteBoxReference(child);
}
}
// Properties
double get systemWidth => _systemWidth;
double get systemHeight => _systemHeight;
TransformNode get rootNode => _rootNode;
Node get rootNode => _rootNode;
void performLayout() {
size = constraints.constrain(Size.infinite);
}
// Event handling
void handleEvent(Event event, BoxHitTestEntry entry) {
switch (event.type) {
case 'pointerdown':
print("pointerdown");
break;
}
}
// Rendering
void paint(RenderObjectDisplayList canvas) {
// Move to correct coordinate space before drawing
double scaleX = 1.0;
@ -104,7 +109,6 @@ class SpriteBox extends RenderBox {
scaleX = size.width/_systemWidth;
scaleY = scaleX;
_systemHeight = size.height/scaleX;
print("systemHeight: $_systemHeight");
break;
case SpriteBoxTransformMode.fixedHeight:
scaleY = size.height/_systemHeight;
@ -129,6 +133,8 @@ class SpriteBox extends RenderBox {
canvas.restore();
}
// Updates
int _animationId = 0;
void _scheduleTick() {
@ -151,4 +157,29 @@ class SpriteBox extends RenderBox {
_rootNode.update(delta);
_scheduleTick();
}
// Hit tests
List<Node> findNodesAtPosition(Point position) {
assert(position != null);
List<Node> nodes = [];
// Traverse the render tree and find objects at the position
_addNodesAtPosition(_rootNode, position, nodes);
return nodes;
}
_addNodesAtPosition(Node node, Point position, List<Node> list) {
// Visit children first
for (Node child in node.children) {
_addNodesAtPosition(child, position, list);
}
// Do the hit test
Point posInNodeSpace = node.convertPointToNodeSpace(position);
if (node.hitTest(posInNodeSpace)) {
list.add(node);
}
}
}

View file

@ -3,14 +3,14 @@ library sprites;
import 'dart:sky';
import 'dart:math' as Math;
import 'package:vector_math/vector_math_64.dart';
import 'package:sky/framework/app.dart';
import 'dart:typed_data';
import 'package:sky/framework/rendering/box.dart';
//import 'package:sky/framework/rendering/node.dart';
import 'package:sky/framework/rendering/object.dart';
import 'package:sky/framework/scheduler.dart' as scheduler;
import 'package:sky/framework/net/image_cache.dart' as image_cache;
part 'sprite_box.dart';
part 'transform_node.dart';
part 'sprite_node.dart';
part 'node.dart';
part 'node_with_size.dart';
part 'sprite.dart';
part 'image_map.dart';

View file

@ -19,5 +19,5 @@ void allLoaded(ImageMap loader) {
// Create a new app with the sprite box that contains our game world
//app = new AppView(new GameBox(new GameWorld(loader)));
//print("hello");
app = new AppView((new SpriteBox(new GameTests(loader), SpriteBoxTransformMode.letterbox)));
app = new AppView((new GameTestsBox(new GameTests(loader), SpriteBoxTransformMode.nativePoints)));
}