KatanaPG Bug Fix / Update

June 12th, 2008

I noticed that the max upload allowed was really 100KB and not 100MB, so I fixed the calculations and also added an address location in the browser. Hopefully the next update will have some basic photo editing features, like cropping, brightness, contrast. Maybe even add a feature that will email the link to the group. I will have to get with Nick, but it would also be cool to upload directly to a group and do some more managing in the air app, like moving pictures from group to group, etc.

Here is the new version: katanapg-v101-beta.air.zip

Unity3d SQLite Basic 3D Visualization Tutorial

June 4th, 2008

At work we are trying to develop some ways to visualize data. Since game engines are great at making complicated math look cool, we are trying to use them to visualize data-sets. We wanted to test how many “dimensions” we could represent (x, y, z..size, direction, color, etc). So I made a little proof of concept with Unity3d, SQLite, and AIR. (The AIR part of will not be covered in this tutorial; but I basically used it as a simple form to input data-points, nothing crazy.)

Unity3d uses Mono which has an SQLite Library which I then modified to javascript (for some reason it worked better in javascript than C#).

In this tutorial, I will assume you already have a basic understanding of databases, scripting, and even Unity.

So the first thing we’ll do is create a new project in Unity. Then create an Empty Game Object, place it at (0,0,0), and name it 3DGraph. To create an axis, simply create a Cube, make it a child of 3DGraph by dragging it onto 3DGraph in the Hierarchy view. Lets name this X-Axis and place it at (0,0,0) and scale it (100,0.5,0.5). This should give us a nice long bar on the x axis. Repeat these steps for Y-Axis and Z-Axis, scaling the respective axis by 100 and the other by 0.5.

Once that is complete, we should have a nice X,Y,Z graph. Feel free to get fancy and add materials and colors, and add a point light or some other light source, but I will not cover how to do that. With that done, lets take a moment to go over our database.

I used SQLite Database Browser to create and manipulate the SQLite DB. Create a new table called points and save the SQLite DB file in the same directory as the Unity project, name the DB file “SqliteTest.db” or something. Here is the SQLite CREATE statement for tabel points.

CREATE TABLE points (
	id INTEGER PRIMARY KEY,
	name VARCHAR,
	x NUMERIC,
	y NUMERIC,
	z NUMERIC,
	scale NUMERIC,
	r NUMERIC,
	g NUMERIC,
	b NUMERIC,
	timeMod date
)

The name is whatever name the point has, x,y,z are the coordinates, scale is how much to scale the point (size), r,g,b are the color values, and timeMod is the date it was last modified. These are the 7 dimensions that we will visualize in this tutorial. Since it is SQLite, a TRIGGER is needed to keep the timeMod up to date when data is updated or inserted. Here is the TRIGGER statement.

CREATE TRIGGER insert_point_timeMod AFTER INSERT ON points
BEGIN
	UPDATE points SET timeMod = DATETIME('NOW') WHERE id = new.id;
END;

CREATE TRIGGER update_point_timeMod AFTER UPDATE ON points
BEGIN
	UPDATE points SET timeMod = DATETIME('NOW') WHERE id = new.id;
END;

Now that the DB is all setup perfectly, quit the SQLite Database Browser (or else you will get an error when Unity tries to access the DB). Now we need to create the script in Unity to access the DB (Project view…Create JavaSctipt). You will have to rename it to something useful, like “MySqlite” or “SQLiteUpdater”. Here is the code.

	import System;
	import Mono.Data.Sqlite;

	var pointObject : Transform;

	private var childArray : Array = new Array();
	private var graphClone : Object;

	function Start()
	{
		var connStrn : String = "URI=file:SqliteTest.db";

		var dbcon : SqliteConnection = new SqliteConnection(connStrn);
		dbcon.Open();

		GetTable(dbcon);
		//GetLatest(dbcon);

		dbcon.Close();
		dbcon = null;
	}

	function Update ()
	{
		var connStrn : String = "URI=file:SqliteTest.db";

		var dbcon : SqliteConnection = new SqliteConnection(connStrn);
		dbcon.Open();

		var rowCount : int = GetRowCount(dbcon);
			//DestroyPoints();
		if((rowCount != transform.childCount - 3) || NeedUpdate(dbcon))
		{
			DestroyPoints();
			//graphClone = Instantiate(transform,Vector3.zero,Quaternion.identity);
			GetTable(dbcon);
		}
		//Debug.Log("children: " + transform.childCount);

		dbcon.Close();
		dbcon = null;
	}

	function GetRowCount(dbcon) : int
	{
		var dbcmd : Object = dbcon.CreateCommand();
		dbcmd.CommandText = "SELECT count(*) as count FROM points";

		var reader : Object;
		try
		{
			reader = dbcmd.ExecuteReader();
		}catch(err)
		{
			Debug.LogError("cmd failed:" + err.Message);
		}
		reader.Read();
		var rowCount : int = reader.GetInt32(0);

		reader.Close();
		reader = null;
		//dbcmd.Close();
		//dbcmd = null;

		return rowCount;
	}
	private var lastUpdated : DateTime;

	//If no update needed, don't redraw everything
	//Format 2008-01-30 15:38:41
	//       0123,56,89,1112,1415,1718
	//DateTime(Int32, Int32, Int32, Int32, Int32, Int32) constructor
	function NeedUpdate(dbcon) : Boolean
	{
		var latest = false;
		var dbcmd : Object = dbcon.CreateCommand();
		dbcmd.CommandText = "SELECT timeMod FROM points ORDER BY timeMod DESC LIMIT 1;";

		var reader : Object = dbcmd.ExecuteReader();
		reader.Read();

		var timeMod = reader.GetString(0);
		//Debug.Log("timeMod: " + timeMod);

		var tempYear = parseInt(timeMod[0].ToString()+timeMod[1].ToString()+timeMod[2].ToString()+timeMod[3].ToString());
		var tempMonth = parseInt(timeMod[5].ToString()+timeMod[6].ToString());
		var tempDay = parseInt(timeMod[8].ToString()+timeMod[9].ToString());
		var tempHour = parseInt(timeMod[11].ToString()+timeMod[12].ToString());
		var tempMinute = parseInt(timeMod[14].ToString()+timeMod[15].ToString());
		var tempSecond = parseInt(timeMod[17].ToString()+timeMod[18].ToString());

		var sqlUpdate  = new DateTime(tempYear,tempMonth,tempDay,tempHour,tempMinute,tempSecond);

		if(DateTime.Compare(sqlUpdate,lastUpdated) > 0) //sqlUpdate > lastUpdated
		{

			lastUpdated = sqlUpdate; //new DateTime(tempYear,tempMonth,tempDay,tempHour,tempMinute,tempSecond);
			latest = true;
		}

		//var timeInt : int = parseInt(timeMod);
		//var timeDivided : Array = timeMod.Substring(" ");
		//var timeDate : Array = timeDivided[0].split("-");
		//var timeTime : Array = timeDivided[1].split(":");
		//Debug.Log(timeDivided.ToString());// + timeDate.ToString() + timeTime.ToString());
		//pareInt

		reader.Close();
		reader = null;
		//dbcmd.Close();
		//dbcmd = null;

		return latest;
	}

	function GetTable(dbcon) : void
	{
		var dbcmd : Object = dbcon.CreateCommand();
		dbcmd.CommandText = "SELECT * FROM points;";

		var reader : Object = dbcmd.ExecuteReader();

		while(reader.Read())
		{

			var pos : Vector3 = new Vector3(reader.GetFloat(2),reader.GetFloat(3),reader.GetFloat(4));
			var scale : Vector3 = new Vector3(reader.GetFloat(5),reader.GetFloat(5),reader.GetFloat(5));
			var colour : Color = new Color(reader.GetFloat(6),reader.GetFloat(7),reader.GetFloat(8),1);

			var newChild : Transform = Instantiate(pointObject,Vector3.zero,Quaternion.identity);
			newChild.parent = transform;
			newChild.localPosition = pos;
			newChild.localScale = scale;
			newChild.renderer.material.color = colour;

			childArray.Push(newChild);
			//result += reader.GetInt32(0) + " " + reader.GetString(1) + " " + reader.GetString(2) + ", ";
			//http://msdn2.microsoft.com/en-us/library/system.data.idatareader_methods.aspx
		}

		reader.Close();
		reader = null;
		//dbcmd.Close();
		//dbcmd = null;
	}

	//Erase them all so we can draw all the new ones and we won't get duplicates
	function DestroyPoints() : void
	{

		//Destroy(graphClone);
		//Can't remove Transform because MeshFilter, SphereCollider, MeshRenderer depend on it!
		//var points = GetComponentsInChildren(typeof(pointObject));
		while(childArray.length > 0)
		//for(var child : Object in points)
		{
			var child = childArray.Pop();

			var meshFilters = child.GetComponentsInChildren (MeshFilter);
			for (var meshF : MeshFilter in meshFilters) {
				DestroyImmediate(meshF);
			}

			var sphereColliders = child.GetComponentsInChildren (SphereCollider);
			for (var sphere : SphereCollider in sphereColliders) {
				DestroyImmediate(sphere);
			}

			var meshRenderers = child.GetComponentsInChildren (MeshRenderer);
			for (var meshR : MeshRenderer in meshRenderers) {
				DestroyImmediate(meshR);
			}

			DestroyImmediate(child);
			//Debug.Log("Children after destroy: " + transform.childCount);
		}
	}

Change the URI to point to your SQLite DB.

	var connStrn : String = "URI=file:SqliteTest.db";

Drag this script from the Project view onto the 3DGraph game object. All that is left is to create a point object. Lets just use a sphere, so create a new sphere object, call it GraphPoint, click on 3DGraph and drag GraphPoint onto the Point Object under the script settings. You should disable the source point object so its not confused with the ones created by the script. That should be all you need. The graph should plot the points in 3D space! Yay!

To make it more appealing, we added some controls, like zooming and flying the camera around with an XBOX360 controller. Maybe I will do that in Part 2, after I get back from Hawaii!!

Willy Makeit

Non Standard (Custom) Window in AIR

May 27th, 2008

Monstagon was wanting to create a custom window for an AIR application at work and could not figure out how to do it. I had found a tutorial that used an apple shaped window, but I seem to have lost the bookmark. So I figured I’ll just post my own tutorial. Since that is what this dang blog is for!

First, create a new AIR application in Flex Builder. I called mine NonStandardGUI, but you can call it “whatever.” Then, open up the app-XML file (“NonStandardGUI-app.xml”) and we will edit the properties here first. This file is loaded automatically and contains all of the basic application properties. Set these properties to


systemChrome = none
transparent = true
visible = false
width = 800
height = 600
x = 100
y = 100

in the “initialWindow” section. The width, height, x and y are optional; you just need to make sure the size is big enough for the background image and the location on the screen allows you to see the whole window.

Now we will edit the NonStandardGUI.mxml (the main application file). To the WindowedApplication, add these attributes

<mx:WindowedApplication xmlns:mx="http://www.adobe.com/2006/mxml" layout="absolute"
        showTitleBar="false" showStatusBar="false"
        backgroundAlpha="0.0" borderThickness="0" >

This gets rid of the window. If you were to save and run the app now it would display nothing.

To add your image as the background, simply an Image tag with the source pointing to the actual image file. For better performance, we want to embed these static images. This could be a PNG, JPG, SWF… So the background can be anything you can create.

<mx:Image id="background" source="@Embed(source='nonStandard_GUI_test.png')" />

With the image in place a a background, you can add all the content you like to it. Due to the nonstandard shape of the new window, laying content out might be a little more time consuming since the Flex Builder Design view doesn’t load the image for you. I have attached the source code for my NonStandard GUI for you to dissect.

Source code: nonstandardgui.zip

New Car Gas Savings

May 20th, 2008

I made a little form to estimate how much I would save if I were to buy a new car vs keeping my current truck. Since I don’t have a car payment, the truck is the winner, but if I did then it will be a closer bet. So here is the little form to calculate if you should get that new Fit. (This isn’t going into how ecofriendly your new car is vs old, just monetary savings.)

The savings is how much I would save if I bought the car. You need to drive a lot and get great gas mileage to compensate for a monthly car payment. Buy I am sure you new that already…

P.S. Here is the formula: (Miles / MPG) * $Gas + Payment = Monthly Cost

KatanPG Uploader Beta 1

May 8th, 2008

I made a lot of updates to the KatanaPG picture up-loader. I added a little browser to view your Katana Inbox (where the picture are uploaded to). I could not get the authentication to stay outside of the AIR program; meaning, the AIR browser keeps you logged in or you can use your normal browser and have to login again. The browser will automatically pop up once your upload finishes or after you login and you click on the “username Inbox” button.

I also added a directory tree for those directories that are filled with subdirectories of pictures. Basically the list will display the thumbs of the pictures and the tree to the left are the sub directories that may contain more pictures (or no pictures). Double clicking on a directory in the tree will update the picture list. The max file upload is 100MB and it won’t let you upload more than that.

Download: katanapgair-v1beta.zip

Flex Image Resampling

May 6th, 2008

While working on KatanaPG (picture uploader) I ran into a memory issue. Pictures, especially those taken with 6+MP cameras, are HUGE. When I was creating the thumbnails for them in the TileList was was simply resizing them. This kept all that 6MP of data in memory just loooked smaller. I needed to resample it at a lower resolution and then dispose of the original. I tried it multiple ways, first using an Image and setting the source and then the width and height.  Then using a bitmap and scaleX and scaleY (as well as ImageSnapshop, but never go that to even work).  After posting a question to flexcoders a very kind replier said: “Use bitmap.draw.”  That method worked great.  Below is a chart of the Flex Profile peak memory for each method. Pictures imported: 23 Photo Booth pictures 640 x 480 and about 150KB on disk each.

Method Peak Memory (KB)
image.width/height 33,351
bitmap.scaleX/scaleY 61,837
bitmap.draw 7,107

I don’t know exactly why the first two methods take up almost as much memory as Firefox, but I am glad I managed to reduce the memory significantly. Now for he part you all have been waiting for, CODE. I used this code for a TileList.itemRenderer.

	<?xml version="1.0" encoding="utf-8"?>
	<mx:Canvas xmlns:mx="http://www.adobe.com/2006/mxml" width="100" height="100" initialize="init();" creationComplete="created();" dataChange="init()">
	    <mx:Script>
	        <![CDATA[
	            import mx.graphics.ImageSnapshot;
	            import mx.core.UIComponent;

	       		private var _created:Boolean = false;

		        private function created():void
		        {
		            _created = true;
		            init();
		        }
		        private var contentLoader:Loader;
		        private function init():void
		        {
		            if(!_created)
		                return;
		            removeAllChildren();
		            contentLoader = new Loader();
		            contentLoader.contentLoaderInfo.addEventListener(Event.COMPLETE, LoadComplete);
		            contentLoader.contentLoaderInfo.addEventListener(IOErrorEvent.IO_ERROR, IOErrorHandler);

		            var request:URLRequest = new URLRequest(data.url);
		            contentLoader.load(request);
		        }

		        //When image loaded, create scaled bitmap
		        private function LoadComplete(evt:Event):void
		        {
		            evt.stopImmediatePropagation();
		            var loader:Loader = Loader(evt.target.loader);
		            var image:Bitmap = Bitmap(loader.content);

		            var sxy:Number = (image.width > image.height)?(95/image.width):(95/image.height);
		            var scaleMatrix:Matrix = new Matrix();
		            scaleMatrix.scale(sxy,sxy);

		            var container:UIComponent = new UIComponent();
		            addChild(container);

		            var scaledImage:Bitmap = new Bitmap(new BitmapData(95,95,true,0));
		            scaledImage.bitmapData.draw(image,scaleMatrix);

		            container.addChild(scaledImage);
		            contentLoader = null;
		            image.bitmapData.dispose();
		        }
		        private function IOErrorHandler(evt:IOErrorEvent):void
		        {
		            trace("Error loading",data.url);
		        }
		    ]]>
		</mx:Script>
	</mx:Canvas>

KatanaPG AIR Pic Uploader

April 30th, 2008

I made a simple AIR app to upload pictures to my buddy’s online photo gallery, Katana Photo Groups.  The AIR app is very basic (first workign version released); but it does in fact work.  So this is what you do:

  1. Select directory to upload pictures from.
    OR
  2. Drag and Drop files onto app.
  3. Slecect the pictures you would like to upload.
  4. Login to KatanaPG.
  5. Click upload to upload pictures.

The above order clearly isn’t set in stone, but it gives you a basic idea of how the application works.

After that your pictures will be uploaded into your Inbox. You will need to already have a KatanaPG account for this to work.

It uses FZip (Actionscript zipping library) to compress the images into a zip and then uploads the zip to Katana.  I hope to had some basic images editing, drag and drop support, and it also needs some optimization.

Here is the package: katanapgair.zip

You will need Adobe AIR to run this.

UPDATE:

Added drag and drop from the PC and also fixed some memory issues.

Magic Deck Builder Updates

April 25th, 2008

I added some updates to the Magic Deck Builder.

  • Double click on a card to see a detailed view of the card.
  • Keyword search, so if you want to search for “slivers” type it in and only slivers will show.
  • Added mana images and the ability to sort cards by mana amount in the Library Grid.
  • Can save decks (only for the current session) and double click on decks to simulate the different ones.

Still need to develop the database and get all those card images in there…

Avetec Game Academy

April 17th, 2008

Yesterday we interviewed some kids for our summer pilot “Game Academy.” We are basically trying to teach kids how to make games, and specifically make games on XNA (for XBOX 360).

The first girl was not interested at all. So Jeff was all bumbed. The second one, Del, comes in and immediates says, “Hey, why do you have an XBOX controller?” He’s on board from the get go! Next were 3 more girls, uh oh.

Courtney comes in, nervous and with Grandpa needing to go to work. We quickly gauge she enjoys using computers and let her grandpa get back to work. Melissa is next. She is a cheerleader and skateboarder, who likes Mattalica and Grand Theft Auto (just like Candace!). Brittany is the last one of the day, and she is very nervous. Once we told her what we wanted to do, she couldn’t stop smiling!

In conclusion, 4 out of 5 students super excited to learn how to make games. Score! Now I need to learn how to do it!

P.S. I just finished combining a chase camera, and split screen. Meaning: 2 players can fly separate planes around the screen with the camera following their own plane! I will post it a little tutorial when I clean it up, but here is the ChaseCamera and SplitScreen examples I combined.