part of game; final double _gameSizeWidth = 320.0; double _gameSizeHeight = 320.0; final bool _drawDebug = false; class GameDemoNode extends NodeWithSize { GameDemoNode( this._images, this._spritesGame, this._spritesUI, this._sounds, this._gameOverCallback ): super(new Size(320.0, 320.0)) { // Add background _background = new RepeatedImage(_images["assets/starfield.png"]); addChild(_background); // Create starfield _starField = new StarField(_spritesGame, 200); addChild(_starField); // Add nebula _nebula = new RepeatedImage(_images["assets/nebula.png"], sky.TransferMode.plus); addChild(_nebula); // Setup game screen, it will always be anchored to the bottom of the screen _gameScreen = new Node(); addChild(_gameScreen); // Setup the level and add it to the screen, the level is the node where // all our game objects live. It is moved to scroll the game _level = new Level(); _gameScreen.addChild(_level); _objectFactory = new GameObjectFactory(_spritesGame, _sounds, _level); _level.ship = new Ship(_objectFactory); _level.addChild(_level.ship); // Add the joystick _joystick = new VirtualJoystick(); _gameScreen.addChild(_joystick); // Add HUD _hud = new Hud(_spritesUI); addChild(_hud); // Add initial game objects addObjects(); } // Resources ImageMap _images; Map _sounds; SpriteSheet _spritesGame; SpriteSheet _spritesUI; // Sounds SoundEffectPlayer _effectPlayer = SoundEffectPlayer.sharedInstance(); // Callback Function _gameOverCallback; // Game screen nodes Node _gameScreen; VirtualJoystick _joystick; GameObjectFactory _objectFactory; Level _level; StarField _starField; RepeatedImage _background; RepeatedImage _nebula; Hud _hud; // Game properties double _scrollSpeed = 2.0; double _scroll = 0.0; int _framesToFire = 0; int _framesBetweenShots = 20; bool _gameOver = false; void spriteBoxPerformedLayout() { _gameSizeHeight = spriteBox.visibleArea.height; _gameScreen.position = new Point(0.0, _gameSizeHeight); } void update(double dt) { // Scroll the level _scroll = _level.scroll(_scrollSpeed); _starField.move(0.0, _scrollSpeed); _background.move(_scrollSpeed * 0.1); _nebula.move(_scrollSpeed); // Add objects addObjects(); // Move the ship if (!_gameOver) { _level.ship.applyThrust(_joystick.value, _scroll); } // Add shots if (_framesToFire == 0 && _joystick.isDown && !_gameOver) { fire(); _framesToFire = _framesBetweenShots; } if (_framesToFire > 0) _framesToFire--; // Move game objects for (Node node in _level.children) { if (node is GameObject) { node.move(); } } // Remove offscreen game objects for (int i = _level.children.length - 1; i >= 0; i--) { Node node = _level.children[i]; if (node is GameObject) { node.removeIfOffscreen(_scroll); } } if (_gameOver) return; // Check for collisions between lasers and objects that can take damage List lasers = []; for (Node node in _level.children) { if (node is Laser) lasers.add(node); } List damageables = []; for (Node node in _level.children) { if (node is GameObject && node.canBeDamaged) damageables.add(node); } for (Laser laser in lasers) { for (GameObject damageable in damageables) { if (laser.collidingWith(damageable)) { // Hit something that can take damage _hud.score += damageable.addDamage(laser.impact); laser.destroy(); } } } // Check for collsions between ship and objects that can damage the ship List nodes = new List.from(_level.children); for (Node node in nodes) { if (node is GameObject && node.canDamageShip) { if (node.collidingWith(_level.ship)) { // The ship was hit :( killShip(); _level.ship.visible = false; } } } } int _chunk = 0; double _chunkSpacing = 640.0; void addObjects() { while (_scroll + _chunkSpacing >= _chunk * _chunkSpacing) { addLevelChunk( _chunk, -_chunk * _chunkSpacing - _chunkSpacing); _chunk += 1; } } void addLevelChunk(int chunk, double yPos) { if (chunk == 0) { // Leave the first chunk empty return; } else if (chunk == 1) { addLevelAsteroids(10, yPos, 0.0); } else { addLevelAsteroids(9 + chunk, yPos, 0.5); } } void addLevelAsteroids(int numAsteroids, double yPos, double distribution) { for (int i = 0; i < numAsteroids; i++) { GameObjectType type = (randomDouble() < distribution) ? GameObjectType.asteroidBig : GameObjectType.asteroidSmall; Point pos = new Point(randomSignedDouble() * 160.0, yPos + _chunkSpacing * randomDouble()); _objectFactory.addGameObject(type, pos); } _objectFactory.addGameObject(GameObjectType.movingEnemy, new Point(0.0, yPos + 160.0)); } void fire() { Laser shot0 = new Laser(_objectFactory); shot0.position = _level.ship.position + new Offset(17.0, -10.0); _level.addChild(shot0); Laser shot1 = new Laser(_objectFactory); shot1.position = _level.ship.position + new Offset(-17.0, -10.0); _level.addChild(shot1); _effectPlayer.play(_sounds["laser"]); } void killShip() { // Hide ship _level.ship.visible = false; _effectPlayer.play(_sounds["explosion"]); // Add explosion Explosion explo = new Explosion(_spritesGame); explo.scale = 1.5; explo.position = _level.ship.position; _level.addChild(explo); // Add flash Flash flash = new Flash(size, 1.0); addChild(flash); // Set the state to game over _gameOver = true; // Return to main scene and report the score back in 2 seconds new Timer(new Duration(seconds: 2), () { _gameOverCallback(_hud.score); }); } } class Level extends Node { Level() { position = new Point(160.0, 0.0); } Ship ship; double scroll(double scrollSpeed) { position += new Offset(0.0, scrollSpeed); return position.y; } } abstract class GameObject extends Node { double radius = 0.0; double removeLimit = 1280.0; bool canDamageShip = true; bool canBeDamaged = true; double maxDamage = 3.0; double damage = 0.0; Paint _paintDebug = new Paint() ..color=new Color(0xffff0000) ..strokeWidth = 1.0 ..setStyle(sky.PaintingStyle.stroke); bool collidingWith(GameObject obj) { return (GameMath.pointQuickDist(position, obj.position) < radius + obj.radius); } void move() { } void removeIfOffscreen(double scroll) { ; if (-position.y > scroll + removeLimit || -position.y < scroll - 50.0) { removeFromParent(); } } void destroy() { if (parent != null) { Explosion explo = createExplosion(); if (explo != null) { explo.position = position; parent.addChild(explo); } removeFromParent(); } } int addDamage(double d) { if (!canBeDamaged) return 0; damage += d; if (damage >= maxDamage) { destroy(); return (maxDamage * 10).ceil(); } return 10; } Explosion createExplosion() { return null; } void paint(PaintingCanvas canvas) { if (_drawDebug) { canvas.drawCircle(Point.origin, radius, _paintDebug); } super.paint(canvas); } void setupActions() { } } class Ship extends GameObject { Ship(GameObjectFactory f) { // Add main ship sprite _sprt = new Sprite(f.sheet["ship.png"]); _sprt.scale = 0.3; _sprt.rotation = -90.0; addChild(_sprt); radius = 20.0; canBeDamaged = false; canDamageShip = false; // Set start position position = new Point(0.0, 50.0); } Sprite _sprt; void applyThrust(Point joystickValue, double scroll) { Point oldPos = position; Point target = new Point(joystickValue.x * 160.0, joystickValue.y * 220.0 - 250.0 - scroll); double filterFactor = 0.2; position = new Point( GameMath.filter(oldPos.x, target.x, filterFactor), GameMath.filter(oldPos.y, target.y, filterFactor)); } } class Laser extends GameObject { double impact = 1.0; Laser(GameObjectFactory f) { // Add sprite _sprt = new Sprite(f.sheet["laser.png"]); _sprt.scale = 0.3; _sprt.transferMode = sky.TransferMode.plus; addChild(_sprt); radius = 10.0; removeLimit = 640.0; canDamageShip = false; canBeDamaged = false; } Sprite _sprt; void move() { position += new Offset(0.0, -10.0); } } abstract class Obstacle extends GameObject { Obstacle(this._f); double explosionScale = 1.0; GameObjectFactory _f; Explosion createExplosion() { SoundEffectPlayer.sharedInstance().play(_f.sounds["explosion"]); Explosion explo = new Explosion(_f.sheet); explo.scale = explosionScale; return explo; } } abstract class Asteroid extends Obstacle { Asteroid(GameObjectFactory f) : super(f); Sprite _sprt; void setupActions() { // Rotate obstacle int direction = 1; if (randomDouble() < 0.5) direction = -1; ActionTween rotate = new ActionTween( (a) => _sprt.rotation = a, 0.0, 360.0 * direction, 5.0 + 5.0 * randomDouble()); _sprt.actions.run(new ActionRepeatForever(rotate)); } set damage(double d) { super.damage = d; int alpha = ((200.0 * d) ~/ maxDamage).clamp(0, 200); _sprt.colorOverlay = new Color.fromARGB(alpha, 255, 3, 86); } } class AsteroidBig extends Asteroid { AsteroidBig(GameObjectFactory f) : super(f) { _sprt = new Sprite(f.sheet["asteroid_big_${randomInt(3)}.png"]); _sprt.scale = 0.3; radius = 25.0; maxDamage = 5.0; addChild(_sprt); } } class AsteroidSmall extends Asteroid { AsteroidSmall(GameObjectFactory f) : super(f) { _sprt = new Sprite(f.sheet["asteroid_small_${randomInt(3)}.png"]); _sprt.scale = 0.3; radius = 12.0; maxDamage = 3.0; addChild(_sprt); } } class MovingEnemy extends Obstacle { MovingEnemy(GameObjectFactory f) : super(f) { _sprt = new Sprite(f.sheet["ship.png"]); _sprt.scale = 0.2; radius = 12.0; maxDamage = 2.0; addChild(_sprt); constraints = [new ConstraintRotationToMovement(0.0, 0.5)]; } void setupActions() { List offsets = [ new Offset(-160.0, 160.0), new Offset(-80.0, -160.0), new Offset(0.0, 160.0), new Offset(80.0, -160.0), new Offset(160.0, 160.0)]; List points = []; for (Offset offset in offsets) { points.add(position + offset); } ActionSpline spline = new ActionSpline((a) => position = a, points, 4.0); actions.run(new ActionRepeatForever(spline)); } Sprite _sprt; } enum GameObjectType { asteroidBig, asteroidSmall, movingEnemy, } class GameObjectFactory { GameObjectFactory(this.sheet, this.sounds, this.level); SpriteSheet sheet; Map sounds; Level level; void addGameObject(GameObjectType type, Point pos) { GameObject obj; if (type == GameObjectType.asteroidBig) obj = new AsteroidBig(this); else if (type == GameObjectType.asteroidSmall) obj = new AsteroidSmall(this); else if (type == GameObjectType.movingEnemy) obj = new MovingEnemy(this); obj.position = pos; obj.setupActions(); level.addChild(obj); } } // class MovingObstacle extends Obstacle { // MovingObstacle(SpriteSheet sheet, Map effects, ObstacleType type) : super (sheet, effects, type); // // void setupAction() { // actions.stopAll(); // // List offsets = [ // new Offset(-160.0, 160.0), // new Offset(-80.0, -160.0), // new Offset(0.0, 160.0), // new Offset(80.0, -160.0), // new Offset(160.0, 160.0)]; // // List points = []; // for (Offset offset in offsets) { // points.add(position + offset); // } // // ActionSpline spline = new ActionSpline((a) => position = a, points, 4.0); // actions.run(new ActionRepeatForever(spline)); // } // } class StarField extends NodeWithSize { sky.Image _image; SpriteSheet _spriteSheet; int _numStars; bool _autoScroll; List _starPositions; List _starScales; List _rects; List _colors; final double _padding = 50.0; Size _paddedSize = Size.zero; Paint _paint = new Paint() ..setFilterQuality(sky.FilterQuality.low) ..isAntiAlias = false ..setTransferMode(sky.TransferMode.plus); StarField(this._spriteSheet, this._numStars, [this._autoScroll = false]) : super(Size.zero) { _image = _spriteSheet.image; } void addStars() { _starPositions = []; _starScales = []; _colors = []; _rects = []; size = spriteBox.visibleArea.size; _paddedSize = new Size(size.width + _padding * 2.0, size.height + _padding * 2.0); for (int i = 0; i < _numStars; i++) { _starPositions.add(new Point(randomDouble() * _paddedSize.width, randomDouble() * _paddedSize.height)); _starScales.add(randomDouble() * 0.4); _colors.add(new Color.fromARGB((255.0 * (randomDouble() * 0.5 + 0.5)).toInt(), 255, 255, 255)); _rects.add(_spriteSheet["star_${randomInt(2)}.png"].frame); } } void spriteBoxPerformedLayout() { addStars(); } void paint(PaintingCanvas canvas) { // Create a transform for each star List transforms = []; for (int i = 0; i < _numStars; i++) { sky.RSTransform transform = new sky.RSTransform( _starScales[i], 0.0, _starPositions[i].x - _padding, _starPositions[i].y - _padding); transforms.add(transform); } // Draw the stars canvas.drawAtlas(_image, transforms, _rects, _colors, sky.TransferMode.modulate, null, _paint); } void move(double dx, double dy) { for (int i = 0; i < _numStars; i++) { double xPos = _starPositions[i].x; double yPos = _starPositions[i].y; double scale = _starScales[i]; xPos += dx * scale; yPos += dy * scale; if (xPos >= _paddedSize.width) xPos -= _paddedSize.width; if (xPos < 0) xPos += _paddedSize.width; if (yPos >= _paddedSize.height) yPos -= _paddedSize.height; if (yPos < 0) yPos += _paddedSize.height; _starPositions[i] = new Point(xPos, yPos); } } void update(double dt) { if (_autoScroll) { move(0.0, dt * 100.0); } } } class RepeatedImage extends Node { Sprite _sprt0; Sprite _sprt1; RepeatedImage(sky.Image image, [sky.TransferMode mode = null]) { _sprt0 = new Sprite.fromImage(image); _sprt0.size = new Size(1024.0, 1024.0); _sprt0.pivot = Point.origin; _sprt1 = new Sprite.fromImage(image); _sprt1.size = new Size(1024.0, 1024.0); _sprt1.pivot = Point.origin; _sprt1.position = new Point(0.0, -1024.0); if (mode != null) { _sprt0.transferMode = mode; _sprt1.transferMode = mode; } addChild(_sprt0); addChild(_sprt1); } void move(double dy) { double yPos = (position.y + dy) % 1024.0; position = new Point(0.0, yPos); } } class Explosion extends Node { Explosion(SpriteSheet sheet) { // Add particles ParticleSystem particlesDebris = new ParticleSystem( sheet["explosion_particle.png"], rotateToMovement: true, startRotation:90.0, startRotationVar: 0.0, endRotation: 90.0, startSize: 0.3, startSizeVar: 0.1, endSize: 0.3, endSizeVar: 0.1, numParticlesToEmit: 25, emissionRate:1000.0, greenVar: 127, redVar: 127 ); particlesDebris.zPosition = 1010.0; addChild(particlesDebris); ParticleSystem particlesFire = new ParticleSystem( sheet["fire_particle.png"], colorSequence: new ColorSequence([new Color(0xffffff33), new Color(0xffff3333), new Color(0x00ff3333)], [0.0, 0.5, 1.0]), numParticlesToEmit: 25, emissionRate: 1000.0, startSize: 0.5, startSizeVar: 0.1, endSize: 0.5, endSizeVar: 0.1, posVar: new Point(10.0, 10.0), speed: 10.0, speedVar: 5.0 ); particlesFire.zPosition = 1011.0; addChild(particlesFire); // Add ring Sprite sprtRing = new Sprite(sheet["explosion_ring.png"]); sprtRing.transferMode = sky.TransferMode.plus; addChild(sprtRing); Action scale = new ActionTween( (a) => sprtRing.scale = a, 0.2, 1.0, 1.5); Action scaleAndRemove = new ActionSequence([scale, new ActionRemoveNode(sprtRing)]); Action fade = new ActionTween( (a) => sprtRing.opacity = a, 1.0, 0.0, 1.5); actions.run(scaleAndRemove); actions.run(fade); // Add streaks for (int i = 0; i < 5; i++) { Sprite sprtFlare = new Sprite(sheet["explosion_flare.png"]); sprtFlare.pivot = new Point(0.3, 1.0); sprtFlare.scaleX = 0.3; sprtFlare.transferMode = sky.TransferMode.plus; sprtFlare.rotation = randomDouble() * 360.0; addChild(sprtFlare); double multiplier = randomDouble() * 0.3 + 1.0; Action scale = new ActionTween( (a) => sprtFlare.scaleY = a, 0.3 * multiplier, 0.8, 1.5 * multiplier); Action scaleAndRemove = new ActionSequence([scale, new ActionRemoveNode(sprtFlare)]); Action fadeIn = new ActionTween( (a) => sprtFlare.opacity = a, 0.0, 1.0, 0.5 * multiplier); Action fadeOut = new ActionTween( (a) => sprtFlare.opacity = a, 1.0, 0.0, 1.0 * multiplier); Action fadeInOut = new ActionSequence([fadeIn, fadeOut]); actions.run(scaleAndRemove); actions.run(fadeInOut); } } } class Hud extends Node { SpriteSheet sheet; Sprite sprtBgScore; bool _dirtyScore = true; int _score = 0; int get score => _score; set score(int score) { _score = score; _dirtyScore = true; } Hud(this.sheet) { position = new Point(310.0, 10.0); scale = 0.6; sprtBgScore = new Sprite(sheet["scoreboard.png"]); sprtBgScore.pivot = new Point(1.0, 0.0); sprtBgScore.scale = 0.6; addChild(sprtBgScore); } void update(double dt) { // Update score if (_dirtyScore) { sprtBgScore.removeAllChildren(); String scoreStr = _score.toString(); double xPos = -50.0; for (int i = scoreStr.length - 1; i >= 0; i--) { String numStr = scoreStr.substring(i, i + 1); Sprite numSprt = new Sprite(sheet["number_$numStr.png"]); numSprt.position = new Point(xPos, 49.0); sprtBgScore.addChild(numSprt); xPos -= 37.0; } _dirtyScore = false; } } } class Flash extends NodeWithSize { Flash(Size size, this.duration) : super(size) { ActionTween fade = new ActionTween((a) => _opacity = a, 1.0, 0.0, duration); ActionSequence seq = new ActionSequence([fade, new ActionRemoveNode(this)]); actions.run(seq); } double duration; double _opacity = 1.0; Paint _cachedPaint = new Paint(); void paint(PaintingCanvas canvas) { // Update the color _cachedPaint.color = new Color.fromARGB((255.0 * _opacity).toInt(), 255, 255, 255); // Fill the area applyTransformForPivot(canvas); canvas.drawRect(new Rect.fromLTRB(0.0, 0.0, size.width, size.height), _cachedPaint); } }