Lunar Lander was an arcade game developed by Atari, Inc. in 1979, which used a unique vector display to display graphics.
I remember playing Lunar Lander after school (or perhaps during ), and later online Javascript clones.
Whilst working on an unrelated project, I hit a motivational plateau and decided a distraction what needed, so put this together in an afternoon.
Introducing PlanetFall
PlanetFall is a simple Windows Phone 8.1 "Lunar Lander" style game, written in C#/XAML using MonoGame framework to replace Microsoft's unsupported XNA framework.
The Game
PlanetFall is a simple, 16-level platform-style game where you control your lander using three rocket thrusters. The lander falls towards the ground due to gravity, and your mission is to guide it to the safe landing pad before running out of fuel.
Each game starts with lives, and the game ends when you complete all 16 levels or have run out of lives.
Additional bonuses are gained from collecting Fuel and other items scattered around the levels.
In later levels, asteroids appear to distract you!
Frameworks and Extensions
I've used a few different frameworks within this game, to help diagnose issues, as well as generate a little cross promotional marketing.
- Flurry Analytics - a simple to use hosted Analytics platform, Flurry Analytics is free and available for iOS, Android, Blackberry, Windows Phone, and mobile web.
- AdDuplex - AdDuplex is a cross-promotion network specifically targeted at Windows 8 and Windows Phone apps and games. It empowers developers to promote apps for free by helping each other.
- MonoGame - MonoGame is an Open Source implementation of the Microsoft XNA 4 Framework. Their goal is to allow XNA developers on Xbox 360, Windows & Windows Phone to port their games to the iOS, Android, Mac OS X, Linux and Windows 8 Metro.
What is Flurry?
Flurry Analytics is an extremely useful, free hosted analytics service that provides you a huge range of metrics around your application, users and behavior.
From their website: Events are Flurry’s way of measuring actions, and they are the foundation for understanding what your customers do in your app. Track every menu tap, level completion, and purchase to optimize the customer experience. Go one level deeper and capture even more detail, such as purchase type or quantity with Event Parameters. Dive deeper into your Events with User Paths, Funnels, and Segments.
Why use aduplex?
You might be asking, what's the benefit of AdDuplex over Microsoft's standard pubcenter advertising controls? AdDuplex is a effectively an advert sharing network - you show adverts for other Apps, and they show ones for yours. Simple!
AdDuplex works on an exchange ratio of 8:10. For each 10 ads displayed in your app 8 of your ads will be displayed in other apps. The remaining 2 ads will be sold to support the service.
As you can see from the screenshot above (for this app), the ratio works well and generates consistent cross-marketing of applications. Great to get the message out!
Creating the Application and installing Packages
The game can be created by installing MonoGame into Visual Studio, and then creating an empty game using the templates available in Add New Project.
Alternatively, you can create a blank Windows Phone 8.1 Silverlight application from the Template library, and then install additional packages. Flurry and Adduplex are entirely optional, but it's worth checking them out.
Install-Package MonoGame
Install-Package FlurrySDK
Install-Package AdDuplexWinPhone81
You can learn more about these packages:
Game Structure
XNA and MonoGame templates use the common Game Loop Sequence or Pattern, and there is a great explanation of this on the Game Programming Patterns web site, and also on RB Whitaker's game development blog.
The majority of game logic in PlanetFall is located within this loop, in the game1.cs file described below.
Core Game Files
game1.cs
This file contains the game logic for PlanetFall. Throughout this file you will also see calls to the Flurry API, which is used to store event analytics.
FlurryWP8SDK.Api.LogEvent("Something just happend");
public class Game1 : Game
{
// Function that returns a random number within the specified range
private static readonly Random getrandom = new Random();
private static readonly object syncLock = new object();
public static int GetRandomNumber(int min, int max)
{
lock (syncLock)
{
return getrandom.Next(min, max);
}
}
// Global game objects
GraphicsDeviceManager graphics;
SpriteBatch spriteBatch;
// Global random number
Random r;
// Game variables, used to control the game activity.
double lander_x;
double lander_y;
double lander_y_start = 30;
int lander_height;
int lander_width;
int lander_pixel_factor;
bool lander_destroyed;
double thrust_y;
double thrust_x;
double velocity_x;
double velocity_y;
double gravity = 0.98;
int level = 0;
int lives = 3;
int level_type = 0;
int level_count = 15;
int fuel;
int fuel_max = 500;
int score;
int pause_countdown =0; //used to delay key presses during paused games
//thruster animation
bool[] show_thrusters;
int[] show_thrusters_countdown;
int[] show_thrusters_animid;
Texture2D[] thruster;
// The textures used to simply the map (background and moon's ground)
Texture2D map_sprite;
Texture2D t; //for drawing lines
int vertical_offset = 120;
Texture2D[] map_thumbs;
Rectangle[] map_thumbs_location;
// General textured used for buttons and menu objects
Texture2D menu_close_button;
bool menu_open = false;
int menu_id = 0;
int menu_type = 0; //0=left, 1=center
string[] menu_items;
string[] menu_titles;
Rectangle[] menu_rectangles;
int menu_item_count;
Texture2D titleimage;
Texture2D menu_background;
Texture2D logo;
Texture2D logo_text;
Texture2D logo_final_screen;
Texture2D whiteRectangle;
// Overal flag used to determine the state of the game
bool game_paused = true;
// Texture for the scary asteroids
Texture2D[] asteroids_sprite;
vars._asteroids[] asteroids;
int asteroid_count = 0;
int asteroid_direction = 0;
// Texture for the actual lander, bounties and thruster flames
Texture2D lander_sprite;
Texture2D[] bounties_sprites;
Texture2D background;
int background_origin; //middle of background, for parallax scroll
vars._bounties[] bounties;
Texture2D[] explosions;
int explosion_frame;
int ScreenWidth, ScreenHeight;
// Fonts used within the game
SpriteFont retroFont;
SpriteFont titleFont;
SpriteFont titleFont2;
SpriteFont messageFont;
SpriteFont myFont;
// Textures for the game buttons
Texture2D button_down_texture;
Texture2D button_up_texture;
Texture2D button_left_texture;
Texture2D button_right_texture;
Texture2D button_cancel;
// Rectangles for the game buttons are used for touch-detection
Rectangle tap_button_up;
Rectangle tap_button_down;
Rectangle tap_button_left;
Rectangle tap_button_right;
public Game1()
{
graphics = new GraphicsDeviceManager(this);
Content.RootDirectory = "Content";
graphics.IsFullScreen = true;
graphics.SupportedOrientations = DisplayOrientation.LandscapeLeft;
}
///
/// Allows the game to perform any initialization it needs to before starting to run.
/// This is where it can query for any required services and load any non-graphic
/// related content. Calling base.Initialize will enumerate through any components
/// and initialize them as well.
///
protected override void Initialize()
{
ScreenWidth = GraphicsDevice.Viewport.Width; //770
ScreenHeight = GraphicsDevice.Viewport.Height; //480
menu_items = new string[16];
menu_rectangles = new Rectangle[16];
menu_titles = new String[16];
map_thumbs = new Texture2D[level_count + 1];
map_thumbs_location = new Rectangle[level_count + 1];
//asteroids
asteroids_sprite=new Texture2D[10];
asteroids = new vars._asteroids[10];
//bounties
bounties_sprites = new Texture2D[3];
r = new Random(Guid.NewGuid().GetHashCode());
thruster = new Texture2D[5]; //animated sprites
show_thrusters = new bool[3];
show_thrusters_countdown = new int[3];
show_thrusters_animid = new int [3];
explosions = new Texture2D[6];
gamedata.load_map_coords();
lander_x = Convert.ToInt32(ScreenWidth / 2);
lander_y = lander_y_start;
gravity = vars.gravity_values[0];
level_type = vars.level_types[level];
fuel = fuel_max;
level = 0;
score = 0;
thrust_x = 0;
thrust_y = 0;
velocity_y = 0;
menu_open = true;
game_paused = true;
vars.arewepaused = game_paused;
// We now create the actual location of the touch buttons.
// TODO - update this to permit screen rotation and also
// larger resolutions.
vars.button_down = new vars._myrect();
vars.button_up = new vars._myrect();
vars.button_left = new vars._myrect();
vars.button_right = new vars._myrect();
vars.button_down.x = Convert.ToInt32(690);
vars.button_down.y = Convert.ToInt32(399);
vars.button_down.w = Convert.ToInt32(64);
vars.button_down.h = Convert.ToInt32(64);
vars.button_up.x = Convert.ToInt32(600);
vars.button_up.y = Convert.ToInt32(399);
vars.button_up.w = Convert.ToInt32(64);
vars.button_up.h = Convert.ToInt32(64);
vars.button_left.x = Convert.ToInt32(20);
vars.button_left.y = Convert.ToInt32(399);
vars.button_left.w = Convert.ToInt32(64);
vars.button_left.h = Convert.ToInt32(64);
vars.button_right.x = Convert.ToInt32(120);
vars.button_right.y = Convert.ToInt32(399);
vars.button_right.w = Convert.ToInt32(64);
vars.button_right.h = Convert.ToInt32(64);
// Rectangles representing the player movement buttons
tap_button_down = new Rectangle(vars.button_down.x, vars.button_down.y, vars.button_down.w, vars.button_down.h);
tap_button_up = new Rectangle(vars.button_up.x, vars.button_up.y, vars.button_up.w, vars.button_up.h);
tap_button_left = new Rectangle(vars.button_left.x, vars.button_left.y, vars.button_left.w, vars.button_left.h);
tap_button_right = new Rectangle(vars.button_right.x, vars.button_right.y, vars.button_right.w, vars.button_right.h);
// Set the level to 0
level = 0;
// Open the menu
menu_open = true;
base.Initialize();
}
///
/// LoadContent will be called once per game and is the
/// place to load all of your content.
///
protected override void LoadContent()
{
// Create a new SpriteBatch, which can be used to draw textures.
spriteBatch = new SpriteBatch(GraphicsDevice);
// Use Content to load your game content here
background = Content.Load("backdrops\\twitter-backgrounds7");
background_origin = Convert.ToInt32(background.Width / 4);
logo = Content.Load("logo\\logo400x300b");
logo_text = Content.Load("logo\\logo293x30");
logo_final_screen = Content.Load("logo\\logo-final-screen");
retroFont = Content.Load("fonts\\Font");
titleFont = Content.Load("fonts\\titlefont");
titleFont2 = Content.Load("fonts\\titlefont2");
messageFont = Content.Load("fonts\\MessageFont");
myFont = Content.Load("fonts\\myfont");
//bounties
bounties_sprites[0] = Content.Load("bounties\\bounty1");
bounties_sprites[1] = Content.Load("bounties\\bounty2");
bounties_sprites[2] = Content.Load("bounties\\bounty3");
menu_close_button = Content.Load("buttons\\closewindow");
menu_background = Content.Load("backdrops\\1-star-background");
whiteRectangle = new Texture2D(GraphicsDevice, 1, 1);
whiteRectangle.SetData(new[] { Color.White });
//buttons
button_down_texture = Content.Load("buttons\\appbar.arrow.down64");
button_up_texture = Content.Load("buttons\\appbar.arrow.up64");
button_left_texture = Content.Load("buttons\\appbar.back.rest64");
button_right_texture = Content.Load("buttons\\appbar.next.rest64");
button_cancel = Content.Load("buttons\\appbar.door.leave64");
//lander
lander_sprite = Content.Load("lander\\lander24");
lander_height = lander_sprite.Height;
lander_width = lander_sprite.Width;
lander_pixel_factor = Convert.ToInt32(lander_width / 4) + 1;
//map
map_sprite = Content.Load("maps\\map" + level.ToString());
//map thumbs
for (int i = 0; i <= level_count; i++ )
{
map_thumbs[i] = Content.Load("maps\\thumb_map" + i.ToString());
}
//dot used for drawing lines
t = new Texture2D(GraphicsDevice, 1, 1);
t.SetData(new Color[] { Color.White });// fill the texture with white
//thruster
thruster[0] = Content.Load("flames\\flame_yellow-0-0");
thruster[1] = Content.Load("flames\\flame_yellow-0-1");
thruster[2] = Content.Load("flames\\flame_yellow-0-2");
thruster[3] = Content.Load("flames\\flame_yellow-0-3");
thruster[4] = Content.Load("flames\\flame_yellow-0-4");
explosions[0] = Content.Load("flames\\enemy_explosion-0-0");
explosions[1] = Content.Load("flames\\enemy_explosion-0-1");
explosions[2] = Content.Load("flames\\enemy_explosion-0-2");
explosions[3] = Content.Load("flames\\enemy_explosion-0-3");
explosions[4] = Content.Load("flames\\enemy_explosion-0-4");
menu_titles[0] = "Main Menu";
menu_titles[1] = "Level Select";
menu_titles[2] = "Instructions";
}
///
/// UnloadContent will be called once per game and is the place to unload
/// all content.
///
protected override void UnloadContent()
{
// TODO: Unload any non ContentManager content here
base.UnloadContent();
spriteBatch.Dispose();
}
// This function resets all game variables and loads the specified level,
// ready to play.
void load_level(int newlevel)
{
FlurryWP8SDK.Api.LogEvent("Level #" + newlevel + " loaded");
map_sprite = Content.Load("maps\\map" + newlevel.ToString());
level = newlevel;
game_paused = true;
vars.arewepaused = game_paused;
menu_open = false;
lander_y = lander_y_start;
lander_x = GetRandomNumber(50, ScreenWidth - 50);
fuel = 300;
lander_destroyed = false;
explosion_frame = 0;
asteroid_count = 0;
// if we are in Level 4 or above, create the asteroids
if (level>=4 )
{
asteroid_count = Convert.ToInt16(level / 5) + 1;
for (int i=0;i<=asteroid_count;i++)
{
new_asteroid(i);
}
}
}
// Create an asteroid, in a random position, with a random
// vertical and horizontal velocity.
void new_asteroid(int asteroid_id)
{
r = new Random(Guid.NewGuid().GetHashCode());
asteroids[asteroid_id] = new vars._asteroids();
asteroids[asteroid_id].active = true;
asteroids[asteroid_id].inplay = false;
asteroids[asteroid_id].timer = 0;
asteroid_direction++;
if (asteroid_direction > 2) { asteroid_direction = 0; }
if (asteroid_direction == 1)
{
// Create the asteroid to the left of the escreen
asteroids[asteroid_id].x = -50;
asteroids[asteroid_id].y = 20 + GetRandomNumber(1, ScreenHeight - 40);
asteroids[asteroid_id].velocity_x = (GetRandomNumber(0, 10)) / 8;
asteroids[asteroid_id].velocity_y = (5 - GetRandomNumber(0, 10)) / 5;
}
else if (asteroid_direction == 2)
{
// Create the asteroid to the right of the screen
asteroids[asteroid_id].x = ScreenWidth + 50;
asteroids[asteroid_id].y = 20 + GetRandomNumber(1, ScreenHeight - 40);
asteroids[asteroid_id].velocity_x = -(GetRandomNumber(0, 10)) / 7;
asteroids[asteroid_id].velocity_y = (5 - r.Next(0, 10)) / 6;
}
else
{
// Create the asteroid above the screen
asteroids[asteroid_id].x = GetRandomNumber(0, ScreenWidth);
asteroids[asteroid_id].y = -50;
asteroids[asteroid_id].velocity_x = GetRandomNumber(-10, 10);
asteroids[asteroid_id].velocity_y = (GetRandomNumber(0, 10)) / 5;
}
asteroids[asteroid_id].rotation = 0;
asteroids_sprite[asteroid_id] = Content.Load("asteroids\\" + GetRandomNumber(1, 9).ToString());
}
string lastGesture;
int waitabit = 0;
///
/// Allows the game to run logic such as updating the world,
/// checking for collisions, gathering input, and playing audio.
///
/// Provides a snapshot of timing values.
protected override void Update(GameTime gameTime)
{
// Before we do anything, check to see whether the game is finished.
if (lives < 1 && menu_open != true)
{
// game over man, game over!
menu_open = true;
pause_countdown = 50;
lander_destroyed = false;
menu_id = 12;
return;
}
// -----------------------------------------------------------------------------------------------
// PROCESS TOUCH EVENTS
// -----------------------------------------------------------------------------------------------
lastGesture = "";
TouchPanel.EnabledGestures = GestureType.Tap | GestureType.DragComplete | GestureType.Flick | GestureType.DoubleTap | GestureType.Hold;
var gesture = default(GestureSample);
while (TouchPanel.IsGestureAvailable)
{
gesture = TouchPanel.ReadGesture();
if (gesture.GestureType == GestureType.Tap || gesture.GestureType == GestureType.DoubleTap || gesture.GestureType == GestureType.Hold)
{
waitabit=0;
if (waitabit < 0) { waitabit = 0; }
if (waitabit == 0)
{
if (gesture.GestureType == GestureType.Hold) { lastGesture = "Hold"; }
if (gesture.GestureType == GestureType.DoubleTap) { lastGesture = "DoubleTap"; }
if (gesture.GestureType == GestureType.Tap) { lastGesture = "Tap"; }
// x and y are created to represent the touch position on
// on the screen.
float x = gesture.Position.X;
float y = gesture.Position.Y;
if (game_paused == true)
{
Rectangle r = new Rectangle(120, 120, ScreenWidth - 239, ScreenHeight - 239);
if (r.Contains((int)x, (int)y))
{
game_paused = false;
vars.arewepaused = game_paused;
}
}
if (menu_id == 12) //game over screen
{
Rectangle r = new Rectangle(1, 1, ScreenWidth - 1, ScreenHeight - 1); //anywhere!
if (r.Contains((int)x, (int)y))
{
//restart game
menu_open = true;
game_paused = true;
vars.arewepaused = false;
level = 0;
menu_id = 0;
return;
}
}
// The pause_countdown is decremented if necessary. This is used to
// briefly ignore touch events after the game is paused. On some devices
// touch events trigger multiple times, and thus can instantly unpause the
// the game accidentally. There's probably a better way to do this.
if (pause_countdown > 0) { pause_countdown = pause_countdown - 1; }
if (menu_open == true && (menu_id <= 10 || menu_id==13 ))
{
// -------------- MAIN MENU TOUCH OPTIONS ------------------- //
Rectangle r = new Rectangle(ScreenWidth - 74, ScreenHeight - 74, 64, 64);
if (r.Contains((int)x, (int)y))
{
menu_open = false;
}
if (menu_item_count >= 1)
{
for (int i = 0; i <= menu_item_count; i++)
{
if (menu_rectangles[i].Contains((int)x, (int)y))
{
if (menu_id == 0 && i == 4)
{
//quit
}
if (menu_id == 0 && i == 0)
{
//new game
score = 0;
lives = 3;
load_level(0);
waitabit = 10;
FlurryWP8SDK.Api.LogEvent("New Game");
}
if (menu_id == 0 && i == 1)
{
//resume game
game_paused = true;
vars.arewepaused = game_paused;
menu_open = false;
waitabit = 10;
}
if (menu_id == 0 && i == 2)
{
//level select
menu_id = 1;
waitabit = 10;
pause_countdown = 3;
FlurryWP8SDK.Api.LogEvent("Level Select");
}
if (menu_id == 0 && i == 3)
{
//instructions
menu_id = 2;
waitabit = 10;
FlurryWP8SDK.Api.LogEvent("Instructions");
}
if (menu_id == 0 && i == 4)
{
//about
pause_countdown = 50;
menu_id = 13;
waitabit = 10;
FlurryWP8SDK.Api.LogEvent("About");
}
if (menu_id == 1 && i == 4)
{
//level select, go back
menu_id = 0;
waitabit = 10;
}
//about menu
if (menu_id == 13 && i == 3)
{
//feedback
FlurryWP8SDK.Api.LogEvent("Feedback");
vars.openfeedback = true;
waitabit = 10;
}
if (menu_id == 13 && i == 5)
{
//level select, go back
menu_id = 0;
waitabit = 10;
}
}
}
}
if (menu_id == 1)
{
//level select
if (pause_countdown > 0) { pause_countdown = pause_countdown - 1; }
vars.arewepaused = false; //preempt banner change
for (int i = 0; i <= level_count; i++)
{
if (pause_countdown ==0 && map_thumbs_location[i].Contains((int)x, (int)y))
{
load_level(i);
}
}
}
if (menu_id == 2) //instructions
{
if (tap_button_down.Contains((int)x, (int)y))
{
// Cancel out of game
menu_id = 0;
menu_open = true;
}
}
}
else if (menu_open == true && menu_id == 10 && pause_countdown <= 0)
{
Rectangle r = new Rectangle(1, 1, ScreenWidth - 1, ScreenHeight - 1); //anywhere!
if (r.Contains((int)x, (int)y))
{
menu_open = false;
game_paused = true;
vars.arewepaused = false; //preempt banner change
}
}
else if (menu_open == true && menu_id == 11 && pause_countdown <= 0)
{
Rectangle r = new Rectangle(1, 1, ScreenWidth - 1, ScreenHeight - 1); //anywhere!
if (r.Contains((int)x, (int)y))
{
//restart game
menu_open = true;
game_paused = true;
vars.arewepaused = false; //preempt banner change
level = 0;
menu_id = 0;
}
}
else
{
// -------------- MAIN MENU TOUCH OPTIONS ------------------- //
// look for movement button taps
if (tap_button_down.Contains((int)x, (int)y))
{
// Cancel out of game
menu_id = 0;
menu_open = true;
}
if (tap_button_up.Contains((int)x, (int)y))
{
// the "UP" button was tapped, so change vertical
// velocity and turn on the thruster animation
velocity_y = velocity_y + 1.5;
show_thrusters[0] = true;
show_thrusters_countdown[0] = 15;
show_thrusters_animid[0] = 0;
}
if (tap_button_left.Contains((int)x, (int)y))
{
// the "LEFT" button was tapped, so change horizontal
// velocity and turn on the thruster animation
thrust_x = thrust_x - 0.5;
show_thrusters[1] = true;
show_thrusters_countdown[1] = 5;
show_thrusters_animid[1] = 0;
}
if (tap_button_right.Contains((int)x, (int)y))
{
// the "RIGHT" button was tapped, so change horizontal
// velocity and turn on the thruster animation
thrust_x = thrust_x + 0.5;
show_thrusters[2] = true;
show_thrusters_countdown[2] = 5;
show_thrusters_animid[2] = 0;
}
}
}
}
}
// -----------------------------------------------------------------------------------------------
// PROCESS TURN-BASED EVENTS
// -----------------------------------------------------------------------------------------------
if (menu_open == false)
{
if (game_paused != true)
{
if (lander_destroyed==true)
{
if (explosion_frame > 100)
{
pause_countdown = 50;
game_paused = true;
vars.arewepaused = game_paused;
lander_destroyed = false;
lives = lives - 1;
if (lives<1)
{
// game over man, game over!
menu_open = true;
pause_countdown = 50;
menu_id = 12;
}
lander_y = lander_y_start;
lander_x = Convert.ToInt16(ScreenWidth / 2);
fuel = 300;
load_level(level);
}
}
}
}
if (menu_open == false)
{
if (game_paused != true)
{
//move asteroids
if (asteroid_count > 0)
{
for (int ii = 0; ii <= asteroid_count; ii++)
{
if (asteroids[ii].active == true)
{
asteroids[ii].rotation = asteroids[ii].rotation + 1;
if (asteroids[ii].rotation >= 360) { asteroids[ii].rotation = 0; }
asteroids[ii].x = asteroids[ii].x + asteroids[ii].velocity_x;
asteroids[ii].y = asteroids[ii].y + asteroids[ii].velocity_y;
if (asteroids[ii].inplay == true)
{
if (asteroids[ii].x < 0 || asteroids[ii].x > ScreenWidth || asteroids[ii].y < 0 || asteroids[ii].y > ScreenHeight)
{
new_asteroid(ii);
}
}
if (asteroids[ii].timer >= 30 && asteroids[ii].inplay != true) { new_asteroid(ii);
}
}
}
}
//increase vertical thrust until it reaches the gravity value;
thrust_y = thrust_y + 0.05;
//work out the thrust/velocity
if (thrust_y >= gravity*1.5) { thrust_y = gravity*1.5; }
if (velocity_y >= 0) { velocity_y = velocity_y - 0.1; }
// see if lander has hit the ground, if so, stop thrust and velocity - do it 6x times (24pixels wide)
bool hit_the_ground = false;
int approx_x = Convert.ToInt32(lander_x / 4);
if (approx_x >= gamedata.map_cords[level].Length - 1) { approx_x = gamedata.map_cords[level].Length - 1; }
for (int i = 0; i <= lander_pixel_factor - 1; i++)
{
if (approx_x + i <= gamedata.map_cords[level].Length - 1 && approx_x>=0)
{
if (lander_y >= gamedata.map_cords[level][approx_x + i] - (vertical_offset + lander_height) && velocity_y <= 0) { hit_the_ground = true; }
if (lander_y+24 >= ScreenHeight) { lander_destroyed = true; }
}
}
if (hit_the_ground == true)
{
thrust_y = 0;
velocity_y = 0;
thrust_x = 0;
show_thrusters[0] = false;
show_thrusters[1] = false;
show_thrusters[2] = false;
// Check if the Lander is in the landing position
int landing_x = gamedata.map_landing[level];
if (lander_x>=landing_x-24 && lander_x<=landing_x+24)
{
List articleParams = new List{ new Parameter("Fuel", Convert.ToString(fuel)) };
FlurryWP8SDK.Api.LogEvent("Level Complete");
menu_open = true;
level = level + 1; //next level
menu_id = 10; //congrates menu page
if (level>=level_count)
{
menu_id = 11; //game over!
return;
}
load_level(level);
//map_sprite = Content.Load("maps\\map" + level.ToString());
pause_countdown = 50;
score = score + fuel;
}
}
// Decrease fuel if thrusters are burning. The game finishes
// if the lander runs out of fuel.
if (show_thrusters[1] == true || show_thrusters[2] == true) { fuel = fuel - 1; }
if ((thrust_y - velocity_y) < 0) { fuel = fuel - 1; }
if (fuel < 0)
{
lander_destroyed = true;
thrust_x = 0;
thrust_y = 0;
velocity_x = 0;
velocity_y = 0;
}
//now move the lander
lander_y = lander_y + (thrust_y - velocity_y);
lander_x = lander_x + thrust_x;
//make sure lander is within screen-coordinates
if (lander_x <= 1) { lander_x = 1; }
if (lander_x >= ScreenWidth - lander_width) { lander_x = ScreenWidth - lander_width; }
//if (thrust_y > 0) { thrust_y = thrust_y - 0.5; }
//if (thrust_x > 0) { thrust_x = thrust_x - 0.25; } //inertia doesn't change - no friction!
// draw bounties
int x, y;
Rectangle r;
for (int i = 0; i <= 2; i++)
{
x = gamedata.map_bounties[level][i];
if (x != 0 && gamedata.map_bounties_active[level][i])
{
y = gamedata.map_cords[level][Convert.ToInt16(x / 4) + 1];
r = new Rectangle(x-16, y - vertical_offset - 32, x + 32, y+16);
if (r.Contains((int)lander_x, (int)lander_y))
{
gamedata.map_bounties_active[level][i] = false;
score = score + 1000;
if (i == 1) { fuel = 300; }
}
}
}
} //not paused
}//menu not open
base.Update(gameTime);
}
///
/// Draw menu
///
///
protected void DrawMenu(GameTime gameTime)
{
spriteBatch.Begin(SpriteSortMode.Immediate, BlendState.AlphaBlend);
// draw background
spriteBatch.Draw(menu_background, new Vector2(0, 0));
// Start drawing menu. We use a small array of strings to represent the
// menu items, and then draw them dynamically later, creating their
// touch positions at draw time.
menu_item_count = 0;
if (menu_id == 0)
{
// main menu
menu_type = 0;
menu_items[0] = "New Game";
menu_items[1] = "Resume Game";
menu_items[2] = "Level Select";
menu_items[3] = "Instructions";
menu_items[4] = "About";
menu_items[5] = "";
menu_item_count = 4;
//draw logo
spriteBatch.Draw(logo, new Rectangle(300, 50, 400, 300), Color.White * 0.9f);
}
if (menu_id == 13)
{
//draw logo
spriteBatch.Draw(logo, new Rectangle(400, 50, 400, 300), Color.White * 0.9f);
menu_type = 0;
menu_items[0] = "About PlanetFall";
menu_items[1] = "";
menu_items[2] = "";
menu_items[3] = "Feedback";
menu_items[4] = "";
menu_items[5] = "Back";
menu_item_count = 5;
String[] lines;
lines = new string[16];
lines[0] = "Written by Matthew Proctor, Neotronic Studios 2014";
lines[1] = "Developed using Visual Studio 15, Monogame, XAML and C#";
lines[2] = "";
lines[3] = "";
for (int ii = 0; ii <= 3; ii++)
{
spriteBatch.DrawString(myFont, lines[ii], new Vector2(65, 120 + (ii * 35)), Color.Black);
spriteBatch.DrawString(myFont, lines[ii], new Vector2(65, 120 + (ii * 35)), Color.White);
}
}
if (menu_id == 1)
{
// Draw level selector - a row of 2x8 thumbnails of the
// levels
menu_type = 0;
menu_items[4] = "Back";
menu_items[0] = "";
menu_items[1] = "";
menu_items[2] = "";
menu_items[3] = "";
menu_items[5] = "";
menu_item_count = 4;
int xx = 50;
int yy = 110;
int count = 0;
for (int i=0;i<=level_count;i++)
{
map_thumbs_location[i] = new Rectangle(xx, yy, 64, 64);
spriteBatch.Draw(map_thumbs[i], map_thumbs_location[i], Color.White*0.5f);
xx = xx + 80;
count = count + 1;
if (count >= 8)
{
count = 0;
yy = yy + 80;
xx = 50;
}
}
}
if (menu_id == 2)
{
String[] lines;
lines = new string[16];
lines[0] = "Your mission is to land your craft successfully on all " + level_count.ToString() + " planets.";
lines[1] = "Your craft is antiquated but has three powerful thrusters. ";
lines[2] = "Two for lateral movement, and one large vertical thruster";
lines[3] = " to slow your descent. Use them wisely, fuel is scarce.";
for (int ii = 0; ii <= 3; ii++)
{
spriteBatch.DrawString(myFont, lines[ii], new Vector2(40, 100 + (ii * 35)), Color.Black);
spriteBatch.DrawString(myFont, lines[ii], new Vector2(40, 100 + (ii * 35)), Color.White);
}
spriteBatch.Draw(button_up_texture, new Vector2(50, 300), Color.White * 0.5f);
spriteBatch.DrawString(myFont, "Main Thruster", new Vector2(120, 300), Color.Black);
spriteBatch.DrawString(myFont, "Main Thruster", new Vector2(120, 300), Color.White);
spriteBatch.Draw(button_right_texture, new Vector2(300, 300), Color.White * 0.5f);
spriteBatch.DrawString(myFont, "Left Thruster", new Vector2(370, 300), Color.Black);
spriteBatch.DrawString(myFont, "Left Thruster", new Vector2(370, 300), Color.White);
spriteBatch.Draw(button_left_texture, new Vector2(550, 300), Color.White * 0.5f);
spriteBatch.DrawString(myFont, "Right Thruster", new Vector2(620, 300), Color.Black);
spriteBatch.DrawString(myFont, "Right Thruster", new Vector2(620, 300), Color.White);
}
if (menu_id == 10)
{
// Level Completed! Draw congratulations screen and text.
spriteBatch.Draw(whiteRectangle, new Rectangle(26, 26, ScreenWidth - 49, ScreenHeight - 49), Color.Black * 0.5f);
spriteBatch.Draw(whiteRectangle, new Rectangle(25, 25, ScreenWidth - 50, ScreenHeight - 50), Color.Silver * 0.5f);
menu_type = 1;
menu_items[0] = "Congratulations";
menu_items[1] = "Score:" + Convert.ToInt32(score);
menu_items[2] = "Tap to Continue";
menu_items[3] = "";
menu_items[4] = "";
menu_items[5] = "";
menu_item_count = 2;
lander_y = lander_y_start;
lander_x = Convert.ToInt32(ScreenWidth / 2);
fuel = fuel_max;
game_paused = true; //for when they exit the level
vars.arewepaused = game_paused;
pause_countdown--;
}
if (menu_id == 11)
{
// Game over (WON) - draw screen and message
menu_type = 1;
menu_items[0] = "Congratulations - Game Over!";
menu_items[1] = "Score:" + Convert.ToInt32(score);
menu_items[2] = "";
menu_items[3] = "";
menu_items[4] = "";
menu_items[5] = "";
menu_item_count = 2;
lander_y = lander_y_start;
lander_x = Convert.ToInt32(ScreenWidth / 2);
fuel = fuel_max;
game_paused = true; //for when they exit the level
vars.arewepaused = game_paused;
pause_countdown--;
spriteBatch.Draw(logo_final_screen, new Vector2(Convert.ToInt32(ScreenWidth / 2) - Convert.ToInt32(323 / 2), ScreenHeight - 195), Color.Silver * 0.9f);
}
if (menu_id == 12)
{
// Game Over (LOST)
menu_type = 1;
menu_items[0] = "Game Over";
menu_items[1] = "Score:" + Convert.ToInt32(score);
menu_items[2] = "";
menu_items[3] = "Tap for Main Menu";
menu_items[4] = "";
menu_items[5] = "";
menu_item_count = 3;
lander_y = lander_y_start;
lander_x = Convert.ToInt32(ScreenWidth / 2);
fuel = fuel_max;
game_paused = true; //for when they exit the level
vars.arewepaused = game_paused;
pause_countdown--;
spriteBatch.Draw(logo_final_screen, new Vector2(Convert.ToInt32(ScreenWidth / 2) - Convert.ToInt32(323 / 2), ScreenHeight - 195), Color.Silver * 0.9f);
}
if (menu_id != 0 && menu_id<=9)
{
spriteBatch.DrawString(titleFont2, menu_titles[menu_id], new Vector2(37, 41), Color.Black);
spriteBatch.DrawString(titleFont2, menu_titles[menu_id], new Vector2(36, 40), Color.White);
spriteBatch.Draw(menu_close_button, new Rectangle(ScreenWidth - 74, ScreenHeight - 74, 48, 48), Color.White);
}
// Draw text menu (if one is needed)
int y = 0;
int x = 64; // Convert.ToInt32(ScreenWidth / 2);
if (menu_item_count >= 1)
{
for (int i = 0; i <= menu_item_count; i++)
{
y = y + 56;
if (menu_items[i] != "")
{
if (menu_type == 1)
{
//center the text
x = Convert.ToInt32(ScreenWidth / 2) - Convert.ToInt32(titleFont2.MeasureString(menu_items[i]).Length() / 2);
}
spriteBatch.DrawString(titleFont2, menu_items[i], new Vector2(x, y), Color.Black);
spriteBatch.DrawString(titleFont2, menu_items[i], new Vector2(x - 1, y - 1), Color.White);
menu_rectangles[i] = new Rectangle(x - 20, y, Convert.ToInt32(titleFont2.MeasureString(menu_items[i]).Length()) + 40, 50);
}
}
}
// end drawing menu
spriteBatch.End();
base.Draw(gameTime);
}
///
/// This is called when the game should draw itself.
///
/// Provides a snapshot of timing values.
protected override void Draw(GameTime gameTime)
{
// TODO: Add your drawing code here
if (menu_open == true)
{
DrawMenu(gameTime);
return;
}
// start sprite processing
spriteBatch.Begin(SpriteSortMode.Immediate, BlendState.AlphaBlend);
// draw background
int background_position = background_origin+Convert.ToInt32(lander_x/2);
Vector2 origin = new Vector2(background_position, 0);
spriteBatch.Draw(background, new Vector2(0, 0), null, Color.White,0,origin, 1,SpriteEffects.None, 0f);
// draw map
spriteBatch.Draw(map_sprite, new Vector2(0, 0));
//draw logo
spriteBatch.Draw(logo_text, new Vector2(5, 5), Color.White);
// draw top message
string myString = "";
//draw fuel guage
spriteBatch.DrawString(retroFont, "Fuel:", new Vector2(551, 11), Color.Black);
spriteBatch.DrawString(retroFont, "Fuel:", new Vector2(550, 10), Color.Cyan);
int fuel_x = Convert.ToInt16(fuel / 2);
spriteBatch.Draw(whiteRectangle, new Rectangle(600,10,160,20), Color.White* 0.5f);
if (fuel >= 150)
{
spriteBatch.Draw(whiteRectangle, new Rectangle(605, 13, fuel_x, 14), Color.Green * 0.5f);
}
else
{
spriteBatch.Draw(whiteRectangle, new Rectangle(605, 13, fuel_x, 14), Color.Red * 0.5f);
}
// Draw Lander
if (lander_destroyed)
{
explosion_frame = explosion_frame + 1;
int frame = Convert.ToInt16(explosion_frame / 5);
if (explosion_frame > 1000) { explosion_frame = 1000; }
if (frame <= 4)
{
spriteBatch.Draw(explosions[frame], new Vector2(Convert.ToInt32(lander_x), Convert.ToInt32(lander_y)), Color.White);
}
else
{
//keep lander off the screen
lander_x = -50;
lander_y = -50;
}
}
else
{
spriteBatch.Draw(lander_sprite, new Vector2(Convert.ToInt32(lander_x), Convert.ToInt32(lander_y)), Color.White);
}
// Draw lives
spriteBatch.DrawString(retroFont, "Lives:", new Vector2(366, 11), Color.Black);
spriteBatch.DrawString(retroFont, "Lives:", new Vector2(365, 10), Color.Cyan);
for (int i = 1; i <= lives; i++)
{
spriteBatch.Draw(lander_sprite, new Vector2(400+i*30,5), Color.White);
}
// Draw Thrusters
for (int i = 0; i <= 2; i++)
{
if (show_thrusters[i] == true)
{
show_thrusters_animid[i]++;
if (show_thrusters_animid[i] > 4) { show_thrusters_animid[i] = 0; }
if (i == 0)
{
//down flame
spriteBatch.Draw(thruster[show_thrusters_animid[i]], new Vector2(Convert.ToInt32(lander_x), Convert.ToInt32(lander_y + 24)), Color.White);
}
else if (i == 1)
{
//move left, flame out the right
spriteBatch.Draw(thruster[show_thrusters_animid[i]], new Vector2(Convert.ToInt32(lander_x + 36), Convert.ToInt32(lander_y + 12)), null, Color.White, 4.7123f, new Vector2(12, 12), 1f, SpriteEffects.None, 0);
}
else if (i == 2)
{
//move right, flame out the left
spriteBatch.Draw(thruster[show_thrusters_animid[i]], new Vector2(Convert.ToInt32(lander_x - 10), Convert.ToInt32(lander_y + 12)), null, Color.White, 1.5707f, new Vector2(12, 12), 1f, SpriteEffects.None, 0);
}
show_thrusters_countdown[i]--;
if (show_thrusters_countdown[i] <= 0) { show_thrusters[i] = false; }
}
}
//draw asteroids
if (asteroid_count>0)
{
for (int ii = 0; ii <= asteroid_count; ii++)
{
if (asteroids[ii].active == true)
{
spriteBatch.Draw(asteroids_sprite[ii], new Vector2((int)asteroids[ii].x, (int)asteroids[ii].y), null, Color.White, MathHelper.ToRadians(asteroids[ii].rotation), new Vector2(12, 12), 1f, SpriteEffects.None, 0);
if (asteroids[ii].x >= 0 && asteroids[ii].x <= ScreenWidth && asteroids[ii].y >= 0 && asteroids[ii].y <= ScreenHeight) { asteroids[ii].inplay = true; } else { asteroids[ii].timer++; }
}
}
}
// Draw map
spriteBatch.Draw(map_sprite, new Vector2(0, 0), new Rectangle(0, vertical_offset, ScreenWidth, ScreenHeight), Color.White);
int y,x;
// draw bounties
for (int i = 0; i <= 2; i++)
{
x = gamedata.map_bounties[level][i];
if (x != 0 && gamedata.map_bounties_active[level][i])
{
y = gamedata.map_cords[level][Convert.ToInt16(x / 4) + 1];
spriteBatch.Draw(bounties_sprites[i], new Vector2(x, y - vertical_offset-16), Color.White);
}
}
// Draw landing pad
int landing_x = gamedata.map_landing[level];
y = gamedata.map_cords[level][Convert.ToInt32(landing_x/4)];
DrawLine(spriteBatch, new Vector2(landing_x - 16, y - vertical_offset), new Vector2(landing_x + 16, y - vertical_offset), Color.Yellow);
DrawLine(spriteBatch, new Vector2(landing_x - 16, y - vertical_offset + 1), new Vector2(landing_x + 16, y - vertical_offset + 1), Color.Yellow);
DrawLine(spriteBatch, new Vector2(landing_x - 16, y - vertical_offset - 1), new Vector2(landing_x + 16, y - vertical_offset - 1), Color.Red);
// Draw buttons and a slightly transparent rectangle behind them to make them stand out
if (game_paused!=true)
{
spriteBatch.Draw(button_cancel, new Vector2(vars.button_down.x, vars.button_down.y), Color.White * 0.9f);
spriteBatch.Draw(button_up_texture, new Vector2(vars.button_up.x, vars.button_up.y), Color.White * 0.9f);
spriteBatch.Draw(button_right_texture, new Vector2(vars.button_right.x, vars.button_right.y), Color.White * 0.9f);
spriteBatch.Draw(button_left_texture, new Vector2(vars.button_left.x, vars.button_left.y), Color.White * 0.9f);
}
if (game_paused)
{
//draw faded box, and big paused text
spriteBatch.Draw(whiteRectangle, new Rectangle(126, 126, ScreenWidth - 249, ScreenHeight - 249), Color.Black * 0.5f);
spriteBatch.Draw(whiteRectangle, new Rectangle(125, 125, ScreenWidth - 250, ScreenHeight - 250), Color.Silver * 0.5f);
if (level>1)
{
x = Convert.ToInt32(ScreenWidth / 2) - Convert.ToInt32(titleFont.MeasureString("Continue").Length() / 2);
spriteBatch.DrawString(titleFont, "Continue", new Vector2(x+2, ScreenHeight - 298), Color.Black);
spriteBatch.DrawString(titleFont, "Continue", new Vector2(x, ScreenHeight - 300), Color.White);
}
else
{
x = Convert.ToInt32(ScreenWidth / 2) - Convert.ToInt32(titleFont.MeasureString("Start Game").Length() / 2);
spriteBatch.DrawString(titleFont, "Start Game", new Vector2(x+2, ScreenHeight - 298), Color.Black);
spriteBatch.DrawString(titleFont, "Start Game", new Vector2(x, ScreenHeight - 300), Color.White);
}
}
// end drawing menu
spriteBatch.End();
base.Draw(gameTime);
}
void DrawLine(SpriteBatch sb, Vector2 start, Vector2 end, Color color)
{
Vector2 edge = end - start;
// calculate angle to rotate line
float angle =
(float)Math.Atan2(edge.Y, edge.X);
sb.Draw(t,
new Rectangle(// rectangle defines shape of line and position of start of line
(int)start.X,
(int)start.Y,
(int)edge.Length(), //sb will strech the texture to fill this rectangle
1), //width of line, change this to make thicker line
null,
color, //colour of line
angle, //angle of line (calulated above)
new Vector2(0, 0), // point in line about which to rotate
SpriteEffects.None,
0);
}
}
Supporting Game Files
The files below contain supporting XAML layout and C# routines. I've only listed methods and properties that are not default or built into the Windows Phone 8.1 Silverlight template, and I've abbreviated longer collections of data. Full versions of the files are available for download at the bottom of this article.
gamedata.cs
This file is dynamically created by a little map-maker tool I write that parses the PNG files of the actual background screens, to generate the floor collision points, and also determine the starting position for the various bounties.
In retrospect, this should have been achieved with collision detection and two layered backgrounds (one being the ground itself for detection, one being the background star scene).
Once I've achieved this this file can be consolidated with vars.cs
class gamedata
{
public static int[][] map_cords;
public static int[] map_landing;
public static int[] map_start_point;
public static int[][] map_bounties;
public static bool[][] map_bounties_active;
internal static void load_map_coords()
{
map_start_point = new int[16];
for (int i = 0; i <= 15; i++) { map_start_point[i] = 10; }
map_bounties = new int[16][];
// an array of coordinates used to determine the location of bounties in each level.
// Each level can have upto 3 bounties.
map_bounties_active = new bool[16][]; // 16 represents individual maps
for (int i = 0; i <= 15; i++) { map_bounties[i] = new int[] { 0, 0, 0 }; }
for (int i = 0; i <= 15; i++) { map_bounties_active[i] = new bool[] { true,true,true }; }
// array of coordinates for the bounties in each level. Not all levels have bounties
map_bounties[04] = new int[] { 27, 287, 0 };
map_bounties[05] = new int[] { 20, 719, 471 };
...
...
map_bounties[14] = new int[] { 0, 624, 0 };
map_bounties[15] = new int[] { 708, 242, 438 };
// an array of vertical coordinates that represent the location of the landing pad. These are averaged into a 16-wide block.
map_landing = new int[16]; // 16 represents individual maps
map_landing[0]=79;
map_landing[1] = 145;
...
...
map_landing[14] = 343;
map_landing[15] = 45;
// an array of vertical coordinates that represent the collision point of the ground. These are averaged into 4-pixel wide blocks, to save space.
map_cords = new int[16][]; // 16 represents individual maps
map_cords[0] = new int[] { 481, 483, 485, 488, 490, 491, ..., 430, 430, 430, 430, 430 };
...
...
map_cords[15] = new int[] { 456, 456, 456, 456, 457, 457, ..., 439, 436, 432, 430, 340 };
}
}
GamePage.xaml
XAML is used to describe the layout of the screen, including the two advertising controls (adduplex and Microsoft's pubcenter).
<!--LayoutRoot is the root grid where all page content is placed-->
<grid x:name="LayoutRoot" background="Transparent">
<!--Drawing surface for DirectX content - supports Landscape and Portrait-->
<drawingsurface x:name="XnaSurface" />
<!-- Media element for audio -->
<mediaelement></mediaelement>
<ui:adcontrol x:name="adc1"
applicationid="[appid]"
adunitid="[adunitid]"
horizontalalignment="Left"
height="50"
verticalalignment="Top"
width="320" margin="240,427,0,0" />
<adduplex:adcontrol x:name="adDuplexAd"
appid="[appid]"
visibility="Collapsed"
height="50"
width="320"
margin="160,400,160,0" />
</grid>
GamePage.xaml.cs
The code below is bound to the XAML layout, and is used to dynamically change advertisements based on whether the game is paused or in play.
public partial class GamePage : PhoneApplicationPage
{
private Game1 _game;
// Constructor
public GamePage()
{
InitializeComponent();
_game = XamlGame.Create("", this);
//kick off timer to check for advert changes
DispatcherTimerSetup();
}
DispatcherTimer dispatcherTimer;
void dispatcherTimer_Tick(object sender, object e)
{
// if the game is not paused, hide the AdDuplex advert and show the pubcentre advert at the bottom of the screen (see gamepage.xaml for layout)
if (vars.arewepaused == false)
{
adc1.Visibility = Visibility.Visible;
adDuplexAd.Visibility = Visibility.Collapsed;
} else
// if the game is paused, show the adDuplex advert at the bottom of the screen (see gamepage.xaml for layout)
{
adc1.Visibility = Visibility.Collapsed;
adDuplexAd.Visibility = Visibility.Visible;
}
if (vars.openfeedback == true)
{
// open the Windows Phone Store feedback dialog box.
Windows.System.Launcher.LaunchUriAsync(new Uri("ms-windows-store:reviewapp?appid=[put your appid here]"));
vars.openfeedback = false;
}
}
public void DispatcherTimerSetup()
{
dispatcherTimer = new DispatcherTimer();
dispatcherTimer.Tick += dispatcherTimer_Tick;
dispatcherTimer.Interval = new TimeSpan(0, 0, 1);
dispatcherTimer.Start();
}
}
vars.cs
Vars is a global class of shared variables and classes used throughout the game.
class vars
{
//global variables, used to determine the game play status
public static bool arewepaused = false;
public static bool openfeedback = false;
// General environmental Settings
public bool sound_enabled;
// Shared Game Variables
public int current_level;
public int level_max = 16;
// 1=moon, 2=mars, 3=asteroid, 4=crazy planet
public static int[] level_types = new int[] { 1, 1, 1, 2, 3, 4, 1, 2, 3, 4, 3, 4, 3, 4, 3, 1 };
public static double[] gravity_values = new double[] { 0.98, 0.6, 0.3, 1.4 };
// bounties are things like fuel and minerals
public class _bounties
{
public int x { get; set; }
public int y { get; set; }
public bool found { get; set; }
}
// asteroids begin in later levers. TODO - add collision detection in next version
public class _asteroids
{
public bool active { get; set; }
public bool inplay { get; set; }
public double x { get; set; }
public double y { get; set; }
public double velocity_x { get; set; }
public double velocity_y { get; set; }
public bool destroyed { get; set; }
public float rotation { get; set; }
public int timer { get; set; }
}
// shared class for drawing fast rectangles
public class _myrect
{
public int x { get; set; }
public int y { get; set; }
public int w { get; set; }
public int h { get; set; }
}
public static _myrect button_left;
public static _myrect button_right;
public static _myrect button_up;
public static _myrect button_down;
public static _myrect[] controlbuttons;
}
Interested in playing PlanetFall?
PlanetFall is available in the Windows Phone Store as a free download.
Download Code & Assets
Use the link below to download a copy of the game code (C#, XAML) and Assets (graphics, sounds, etc.) from Dropbox.
Download
C#, XAML and Content Assets
Future Enhancements
Let me know if you have any ideas for future enhancements. Those I currently have in mind include:
- Improved level designs
- Asteroid collision detection
- Ground-based rockets or lasers
- More than one plane of collision detection (i.e. not just the moon's surface)
- Conversion of ground collision detection from hard-coded to dynamic based on two-layer background.
- Replace hard-coded maximum levels (16) in variable and gamedata files with dynamic value to support more levels
- Add more types of bounties
Tags
Lunar Lander,
PlanetFall,
C#,
XAML,
Windows Phone,
8.1,
App Store