I got a lot of great response and questions about my post with the UIStoryBoard. This means people are interested in using it, and I'm not good with explaining things.
I did combo this drill down with a sorta complex core data model and assumed this was setup before handing off objects to each subsequent class. I'm going to nix the core data in favor of a simple input, but we are going to keep music on the table!
Lets start here : GitHub-UIStoryboardExamples
This Xcode project will start you out with some plist data that is a nested list of music genre, artist, album, song. At the same time not to complicate the ViewControllers on how it handles these different tiers the data is only nested as a "NSString name" , "NSArray items" pair. This way we concentrate straight on the loopback of the table segue.
First take a look at the "Auto" side of this app, this table view controller uses the cells prototype push outlet to auto loopback to the controller again. Because this does not utilize the didSelectRowAtIndexPath: (or to be clear) this does call but, performs the segue regardless. This practice will make use of the sender value in the prepareForSegue: to identify which cell was clicked on. Realize we are not making a decision where to go next, thats already happening via the segue, we are only deciding what to take with us. We send some values to the next view controller that is another instance of this current one, but is not this exact one. And the view controller is then stacked in the navcontroller and is now only aware of the sub-array we handed down to it.
This auto drilling can be a little limited because of the way we always drill into the same view controller class type. But! if you want to explore here, I suggest you make another cell prototype with a different identifier and a different segue outlet, and use it at your desired tier.
The "Manual" side takes on calling segues from code and needs the fake UIBarButtonItem to create the loopback segue. This little trick will let you define segue identifiers and then let you decide what direction to take when you call performSegueWithIdentifier: The other important thing is to know what object you want to send along here as the sender. The cell is no longer the sender that is triggering the segue, didSelectRowAtIndexPath is and we could likewise send the UITableViewCell or filter it down to the object we are after. This is the place I test for the data nesting and chose to end the drill down. Calling the different segue will push over to the details view, but the stack of drills downs is still managed by the navcontroller.
Good luck and happy storyboarding.
Showing posts with label idevblogaday. Show all posts
Showing posts with label idevblogaday. Show all posts
Thursday, May 10, 2012
Sunday, February 26, 2012
Game On, TurnBased Game Center
I was excited to see Apple add the TurnBased system to their GameCenter last year. Since it's beta release I have been testing with it on and off for some time now to get the best experience from it that I believe I can offer. The difference for me is I approached this TurnBased GameCenter as a partial replacement for the PHP async system I was mildly developing before Apple's announcement.
That's not enough, we also need to show the user "who's" turn it is, the GKTurnBaseMatch only gave use the player_ids. So first thing is to get the ids into an array.
- loadPlayersForIdentifiers: Once you have this array of GKPlayers back with alias for the player_ids you sent in you can start to match up the ids from the GKTurnBasedParticipants and display a name for the user who's turn it is.
Then finally before you can rebuild you game board from the 4kb of data, you have to request it.
- loadMatchDataWithCompletionHandler:
Having all these separated calls can seem like a bit A->B->C->D but to tell you the truth it's not that different from the calls I was designing in PHP. Data that can plug together but can really be handled in any order or concurrently if you store it right.
Synchronizing GKObjects:
Well the idea isn't much different, When an opportunity to get data now from the local storage beats the need to retrieve data, I have something to show the user. If after a request is fulfilled and data has changed then I have a new opportunity to update things behind the scene while the user is not looking at something, or slide in the additional data.
For example: The Users phone knows of a game thats in progress, he is waiting for a friend to take a turn. Because this games data is being sync'd the players stored locally are Mike and John, As soon as the app is shown that info about John is already known and I don't have to retrieve the alias again before displaying it. Another invite comes in for a 3 player game including Mike, John, and now ID:32789462 well I can display this new cell in a table and use a temp alias of 'New Player' while the request goes out to get the players alias. In the mean time Mike opens the game board and looks at the last few turns. With the correlation of the loadPlayersForIdentifiers feeding directly back to the CoreData storage of the Player ManagedObject then UITableViews, GameBoradViewControllers can be notified of the changes to the Alias value independent of the Match Object.
In addition here are some other methods that I've built to help puzzle together some of the GKTurnBased separations.
Is it this local user's turn?
+ (bool)IsLocalTurn:(GKTurnBasedMatch*)aMatch {
How many of these players are still in the game?
The next player should be a active user, or the game will get stuck, or error.
Where I started in PHP;
I started with a SQLLite model that was going to mirror the online storage of the games. This maybe an over ambitious approach to a game largely played while online and connected, but to develop good UX I felt that the game should not just be a brick when no connectivity is around. (Even NewToy's games had offline modes before the take over)
GameCenter for 4.1 was in full swing and I made the decision to leap frog off the GameCenter generated Apple player_ids for organizing user accounts, while avoiding making people create some new account in my system. What this meant was if a user signed into the game with a GameCenter account, I would store there player_id, alias, and push-token. I even devised a pattern for flagging the account for open invites, direct invites, or dnd.
The challenge here was when the app had time to refresh was to synchronize the records of turns that were behind. This could be one turn record back, two if a 3 player game, maybe the phone was changed or deleted, then all records would need to be resync'd. In this design I was storing each turn as a record of 5 or more fields and doing these local vs server index checks to avoid transferring too much repeat json data.
So what did this mirror solve? The ability to look at the game board, plan your next move, even make your next move and cache the result for later upload, if and when no connectivity was available. Additional side effects, less lock down loading screens. If any persistent data about the handful of games a player is playing is saved and synced locally then I could stay away from locking down the interface with a loading overlay while online request get fulfilled.
Use Apple's servers;
Apple announces Api changes to handle games non-realtime, and right out of the gate they solve one trick that I was trying to work around. Push Notifying new players about a game they've yet to ever download. Even though GameCenter could always Push-Notify a friend of a game invite. But that process must have gone a little like: Mike invites John to play a game, John is busy watching a Movie, Mike is left hang'in. Or John hasn't heard of this game, he visits the AppStore and reads about the game, Mike is tired of sitting at the invite screen waiting to connect, Mike closes the game and the invite is lost.
So now with the TurnBasesMatch objects the user involved are tracked, the whole game state needs to be compressed and fit into 4kb or less, and push notifications are handled by Game Center and I don't even need to request a token. So whats left for me to do?
If you are leaving all the data on Apple's end and only updating from request then there are a few online calls that seem to get chained up rather quickly.
- authenticateWithCompletionHandler: This one gets you in the door but is the first request you have to wait for before you can get updated info.
- loadMatchesWithCompletionHandler: This gets you some rudimentary data, nothing you can show many details about. Telling how many matches the player is involved in, who's turn it is (sorta) and when the last turn was taken (again sorta) I say this because the individual GKTurnBasedParticipant objects in the GKTurnBasedMatch object have the timestamps, if you'd like to show when the matches last turn was taken I figure a sort is the easiest way:
NSSortDescriptor* sort = [NSSortDescriptor sortDescriptorWithKey:@"lastTurnDate" ascending:YES];
GKTurnBasedParticipant* pat = [[self.gkMatch.participants sortedArrayUsingDescriptors:[NSArray arrayWithObject:sort]] lastObject];
self.lastTurnDate = pat.lastTurnDate;
That's not enough, we also need to show the user "who's" turn it is, the GKTurnBaseMatch only gave use the player_ids. So first thing is to get the ids into an array.
NSMutableArray* playerids = [NSMutableArray arrayWithCapacity:[m_gkMatch.participants count]];
[m_gkMatch.participants enumerateObjectsUsingBlock:^(GKTurnBasedParticipant* obj, NSUInteger idx, BOOL *stop) {
[playerids addObject:obj.playerID];
}];
- loadPlayersForIdentifiers: Once you have this array of GKPlayers back with alias for the player_ids you sent in you can start to match up the ids from the GKTurnBasedParticipants and display a name for the user who's turn it is.
Then finally before you can rebuild you game board from the 4kb of data, you have to request it.
- loadMatchDataWithCompletionHandler:
Having all these separated calls can seem like a bit A->B->C->D but to tell you the truth it's not that different from the calls I was designing in PHP. Data that can plug together but can really be handled in any order or concurrently if you store it right.
Synchronizing GKObjects:
Well the idea isn't much different, When an opportunity to get data now from the local storage beats the need to retrieve data, I have something to show the user. If after a request is fulfilled and data has changed then I have a new opportunity to update things behind the scene while the user is not looking at something, or slide in the additional data.
For example: The Users phone knows of a game thats in progress, he is waiting for a friend to take a turn. Because this games data is being sync'd the players stored locally are Mike and John, As soon as the app is shown that info about John is already known and I don't have to retrieve the alias again before displaying it. Another invite comes in for a 3 player game including Mike, John, and now ID:32789462 well I can display this new cell in a table and use a temp alias of 'New Player' while the request goes out to get the players alias. In the mean time Mike opens the game board and looks at the last few turns. With the correlation of the loadPlayersForIdentifiers feeding directly back to the CoreData storage of the Player ManagedObject then UITableViews, GameBoradViewControllers can be notified of the changes to the Alias value independent of the Match Object.
In addition here are some other methods that I've built to help puzzle together some of the GKTurnBased separations.
Is it this local user's turn?
+ (bool)IsLocalTurn:(GKTurnBasedMatch*)aMatch {
if ([[GKLocalPlayer localPlayer].playerID isEqualToString:aMatch.currentParticipant.playerID]) return true;
else return false;
}
How many of these players are still in the game?
+ (NSArray*)ActivePlayers:(GKTurnBasedMatch*)aMatch {
if (!aMatch) return NULL;
NSMutableArray* actives = [NSMutableArray arrayWithCapacity:[aMatch.participants count]];
[aMatch.participants enumerateObjectsUsingBlock:^(GKTurnBasedParticipant* player, NSUInteger idx, BOOL *stop) {
if (player.status != GKTurnBasedParticipantStatusDeclined && player.status != GKTurnBasedParticipantStatusDone) {
[actives addObject:player];
}
}];
return [actives copy];
}
The next player should be a active user, or the game will get stuck, or error.
+ (GKTurnBasedParticipant*)NextPlayerAfter:(GKTurnBasedParticipant*)player InMatch:(GKTurnBasedMatch*)aMatch {
if (!aMatch) return NULL;
NSArray* actives = [SOTurnManager ActivePlayers:aMatch];
int acount = [actives count];
if (acount > 1) {
int index = [actives indexOfObject:player] + 1;
return [actives objectAtIndex:index%acount];
}
return NULL;
}
Tuesday, January 24, 2012
UIStoryboard Power Drill

I've read some good reasons why Storyboards are not ready for prime time. Some of the articles like Jonathan at toxic software.com simply help me find out that I wasn't doing something wrong, it just wasn't meant for that. But there are some benefits from using a storyboard that I wanted to keep, so I got adamant about finding workarounds. Here's one:
Drill down UITableViews - Standard

1. Provided you've started with a subclassed UITableViewController, or a TableView in a UIViewController that is already the root relationship from a UINavigationController.
2. You link the push outlet from the cell prototype back to the table view controller.
This will automatically provide the control to push-in another of the same ViewController when a cell is clicked.

3. Give the loopback segue an identifier string in the attributes inspector.
4. Instead of implementing tableView:didSelectRowAtIndexPath: in the subclassed viewcontroller you will use
prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender

This method will be passed the segue object that is the "tableLoopBack" the sender object for this segue type will be the UITableViewCell.
5. Prepare what data to send to the next instance the the table.
- (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender {
NSArray* myObjectArray;
UITableView* myTable;
if ([segue.identifier isEqualToString:@"tableLoopBack"]) {
SubClassedTable* nextSct = segue.destinationViewController;
UITableViewCell* myCell = (UITableViewCell*)sender;
NSIndexPath* idx = [myTable indexPathForCell:myCell];
nextSct.myObject = [self.myObjectArray objectAtIndex:idx.row];
}
}
6. In the TableDataSourceDelegate have it decide how to propagate your tables data array based now on the defined and set value of "myObject". This is very different for everyones data types.
An example, using Core Data and have say AMusicLib > Artists > Albums > Songs > Info
The myObject would ideally be of type NSManagedObject* so that you could be any of the above. When the dataDelegates are called like tableview:numberOfRowsInSection: you would want to implement something like the following to handle the many layers.
- (int)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
if ([self.myObject isKindOfClass:[Music class]]) {
Music* myMusicLib = self.myObject;
return [myMusicLib.artists count];
} else if ([self.myObject isKindOfClass:[Artist class]]) {
Artist* myArtist = self.myObject;
return [myArtist.albums count];
} else if ([self.myObject isKindOfClass:[Album class]]) {
Album* myAlbum = self.myObject;
return [myAlbum.songs count];
}
}
Now in the implementation of the tableview:cellForRowAtIndexPath: use a similar kindOf conditional so that the cells properties can feed from the difference of the objects at each layer.
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
static NSString *cellID = @"musicCell";
UITableViewCell* newCell = [tableView dequeueReusableCellWithIdentifier:cellID];
if ([self.myObject isKindOfClass:[Music class]]) {
Music* myMusicLib = self.myObject;
Artist* aArtist = [[myMusicLib.artists allObjects] objectAtIndex:indexPath.row];
newCell.textLabel.text = aArtist.name;
} else if ([self.myObject isKindOfClass:[Artist class]]) {
Artist* myArtist = self.myObject;
Album* aAlbum = [[myArtist.albums allObjects] objectAtIndex:indexPath.row];
newCell.textLabel.text = aAlbum.title;
newCell.imageView.image = [UIImage imageNamed:aAlbum.art];
} else if ([self.myObject isKindOfClass:[Album class]]) {
Album* myAlbum = self.myObject;
Song* aSong = [[myAlbum.songs allObjects] objectAtIndex:indexPath.row];
newCell.textLabel.text = aSong.name;
NSDate* aTime = [NSDate dateWithTimeIntervalSince1970:[aSong.length intValue]];
NSDateFormatter *dateFormatter = [[[NSDateFormatter alloc] init] autorelease];
[dateFormatter setDateFormat:@"m:ss"];
newCell.detailTextLabel.text = [dateFormatter stringFromDate:aTime];
}
return newCell;
}
And finally because earlier we used a property self.myObjectArray, this property could have been a consistent type simple array, but because I walked you through handing down different managedObjects this myObjectArray getter would also need to return an NSArray from the different managedObject's intended NSSet. Same as just above, just return [myArtist.albums allObjects] or [myAlbum.songs allSongs] and so on for the prepareForSegue can hand off the right object to the next controller.Drill Down TableViews - Options
The above is simple but can be limiting. The segue always pushes down to another of the same table view. Now you want options, lets go this why, or that.
1. Don't use the push outlet on the cell prototype. This is whats limiting the control of where we go.
2. Do Implement the tableView:didSelectRowAtIndexPath: this is where we will decide what segue we take, Or none.
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
[tableView deselectRowAtIndexPath:indexPath animated:YES];
if (!sec) {
Section* aSect = [[self.sectionFetchedController fetchedObjects] objectAtIndex:indexPath.row];
[self performSegueWithIdentifier:@"tableLoopBack" sender:aSect];
} else {
Clip* aClip = [self.clipFetchedController objectAtIndexPath:indexPath];
[[VideoPlayerController Player] setMoviePlayerForClip:aClip];
}
}
What we are effectively doing here is getting the option to loop into another table, or stay and do something else like load a video in a modal MPMoviePlayerController. Or even call another segue that goes to detail view.
3. Continue to make use of the prepareForSegue method
- (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender {
if ([segue.identifier isEqualToString:@"tableLoopBack"]) {
VideoListController* vlc = segue.destinationViewController;
Section* mySec = (Section*)sender;
vlc.title = mySec.title;
vlc.sec = mySec;
}
}
This is so data is still passed to the new instance of this controller for drill down purposes, even if not all cells drill.
4. And now the hack, if in step 1 we do not link the cell to push to the this viewController, we still need to define a segue and name it "tableLoopBack".
Add a UIBarButtonItem to the viewController and set the push outlet on it to loopback, identify that new segue connection as "tableLoopBack" and you have access to it when calling performSegue.
5. Add another UIBarButtonItem, and link it to another ViewController like one that layout details level. Identify that segue as another name like "detailSegue" and performSegue from didSelectRowAtIndexPath: where you can pass down the object full of details right into the sender, which will also be handed through the prepareForSegue, so make sure you hand it down again to the desired property of the new destinationViewController.
Helpful? Vote me up: Stackoverflow.com
Sunday, December 4, 2011
Cut the fat, NSData diet
Fat XML
I'm a long time user of the XML format for transferring data between client and host. It's human readable, can be edited by hand easily in most cases, and it conveys objects and nesting well. So in the past I was often designing a object scheme and custom parsing loops that solved the issues that faced constantly updating applications and site I've been in charge of through out my flash career. The problem I was always concerned with when designing the scheme is how much data was just being taken up by open tags, close tags, and parameter defines. This became exceptionally a problem when working for the company responsible for "MMO crack". I came on board to find that they where converting C objects out to XML. Now I wasn't programming in C yet myself and had lacking experience there, all my objects where far more dynamic in action script. But when I was tasked to parse open some game data I was shocked to see my work be brought to it's knees when getting the users character roster and sub inventory arrays that were compiled into XML Data and downloaded to the Flash client. This may seem to make sense you need to know this information in order to play that game. But for a new character that had no team members, and starter inventory, the XML data surely didn't need to be 2-3mb to tell me I'm a noob. It turned out that when the object structures are being defined, the play character with some properties, has a max inventory lets say 30 items, and a roster of teammates with a max of say 30, each with their own properties and inventory of 30. So in memory this struct was created with these arrays of array defined. And even though the inventory had maybe two items, and the team roster empty, the space for this storage had been allocated. This left the company's choice XML writer with the task of creating a node tree for every allocation, even if the value was null. Not only was the XML nodes verbose, the property attributes fully spelled out, but every download came in like a template worksheet to be filled in and a later time.
Average JSON
I had always made it a conscious effort to slim down the data that was being sent back and forth in applications I designed, even though this was often not my choice when working at large companies. JSON came along in the midst of all this work and I found it a great way to slim down a lot of the parseable data. A few iOS projects that I started on in 08' I was doing a lot of tracking multiplayer turns moves on a PHP/SQL backend. I was comfortable using JSON and NSDictionaries to convert the data because I could understand how it was being handled on both places. But I did alway prefer to have my classes with their instance variables. I felt like KVO in NSDictionaries was just a long way to get at values in code and made for more lines. I also want to avoid waiting and loading screens, so I separated many requests into small object calls, and take it even further I got on a kick where I would make property names only one or two characters long. If the property name was 'int character_id = 500;' I would hold the value in a temporary NSDictionary to encode the property to {c:500}. Every little bit helps I figure.
Thin Binary Data
This year I have been getting more ambitious about using some Multiplayer GameCenter features, wether it be live games, or turn based games. Now the out of the box features of encoding NSDictionaries to a compatible NSData binary I think leave you somewhere in-between the first to parts of this post. Where you can define small objects maps, but this its coded into a XML like string before its coded to binary. Game data needs to be condense to make a quick round trip in peer to peer or to fit in a maximum size allowed for turn based storage. I've come full circle now to better understand C structs and have come to some pros and cons about using them to transmit game data. What really helped me prepare for the use of C structs as NSData was the GKTank sample. Now if your not entirely sure whats going on in this sample like I once was, then hopefully I can help you understand it.
First you must be comfortable using C structs to define your properties. If you are very used to the way NSObjects and Class in Obj-C act for you when you retain and release, you have to be aware of when you assign a struct you are making copies and may be dealing less in pointers. Lets talk about GKTank tankInfo:
This struct has four properties, but two are also structs. CGPoint is a C struct that just contains two sub float values. And understanding how this all really boils down in memory helps understand how much data your saving or sending. A float is 4 bytes, this example of struct ultimately consists of 6 floats. 4*6 = 24 bytes to send this struct. Just to compare that point here, the resources out there tell you normally would want to run your NSDictionary through a NSKeyedArchiver which can write to a NSData. An example of this:
Same six float values, for a total of 436 bytes to hold the values that map the key values. So how do we get the struct into a NSData object so the rest of the Objective-C framework can handle it. NSData has a handy way of making a copy of the binary data of the structure provided we can tell NSData how many bytes the object is.
This method Data with Bytes lets us send in the pointer argument of our struct which is giving it the first byte to start from. The length lets the NSData know how far in memory to copy, so we need to know how many bytes our struct is. We did the math earlier and could enter 24 for the length. But C offers a function sizeof() which looks up the definition table of the struct type and returns it for us. Whats great about a struct is when allocated it's properties are lined up in memory so that each of the 6 floats are stacked back to back. How it is aligned is not saved in the memory block with the 6 values but the structs definition itself acts as a parser/de-parser for the memory. Even if the floats are back to back, the definition is the road map to know that the first and second floats are part of the first property, the third and forth floats are the second property, etc. When you get this NSData on the other side it needs to be converted back into a structure so it's easy to access again.
When we create the instance of tankInfo the memory for 24 bytes has been made available but like is random data or set to null. Like before this now copies the bytes into the pointer location which has already been allocated the line before and the data that just came in can now be accessed and changes by accessing the struct properties and all we sent over the wire was 24 bytes (give or take any gamecenter overhead).
With my mind well wrapped around this C struct concept I can wisely get a lot of game data send over the wire quickly, or stored on Apple servers when making a turn based game. Apple has set a turn based miscellaneous data limit to 4KB. Now for some these seem unreasonable, but those are often the ones I see trying to KeyArchive encode a big Dictionary and it makes sense that they are hitting the limit quick. I've headed into planning my game data storage and for the most part have not come close to needing to worry about how much binary data I need to get my games conveyed. up to 4 players, up to 70 turns, in under 880bytes.
The Con
I know I'm picking a threads here, but now I've come back to the issue I once though obsessive when working for the MMO. I'd love to hear input there from any others that deal in structure binary. I've designed my game data struct to handle these 4 players and the 70 turns possible to complete a game. To define this maximum possible amount of turns I an array of turn structs. This again cause the data to be allocated, and empty early on. An average game will be complete in 60 turns, and as the turns progress the bytes in the turns array are being written to something other Null, but in order to know the size of a matchData struct the turns need to be defined with 70 elements. Only fortunately is this set of struct small and the whole combo comes to 880bytes. But thats 880 on turn 1, 2 .. 54 etc. If later I take on any larger game storages I may come back to talk about how to sub parse the first few bytes in the NSData to help predefine the amount of elements that should be expected, making the struct more variable.
I'm a long time user of the XML format for transferring data between client and host. It's human readable, can be edited by hand easily in most cases, and it conveys objects and nesting well. So in the past I was often designing a object scheme and custom parsing loops that solved the issues that faced constantly updating applications and site I've been in charge of through out my flash career. The problem I was always concerned with when designing the scheme is how much data was just being taken up by open tags, close tags, and parameter defines. This became exceptionally a problem when working for the company responsible for "MMO crack". I came on board to find that they where converting C objects out to XML. Now I wasn't programming in C yet myself and had lacking experience there, all my objects where far more dynamic in action script. But when I was tasked to parse open some game data I was shocked to see my work be brought to it's knees when getting the users character roster and sub inventory arrays that were compiled into XML Data and downloaded to the Flash client. This may seem to make sense you need to know this information in order to play that game. But for a new character that had no team members, and starter inventory, the XML data surely didn't need to be 2-3mb to tell me I'm a noob. It turned out that when the object structures are being defined, the play character with some properties, has a max inventory lets say 30 items, and a roster of teammates with a max of say 30, each with their own properties and inventory of 30. So in memory this struct was created with these arrays of array defined. And even though the inventory had maybe two items, and the team roster empty, the space for this storage had been allocated. This left the company's choice XML writer with the task of creating a node tree for every allocation, even if the value was null. Not only was the XML nodes verbose, the property attributes fully spelled out, but every download came in like a template worksheet to be filled in and a later time.
Average JSON
I had always made it a conscious effort to slim down the data that was being sent back and forth in applications I designed, even though this was often not my choice when working at large companies. JSON came along in the midst of all this work and I found it a great way to slim down a lot of the parseable data. A few iOS projects that I started on in 08' I was doing a lot of tracking multiplayer turns moves on a PHP/SQL backend. I was comfortable using JSON and NSDictionaries to convert the data because I could understand how it was being handled on both places. But I did alway prefer to have my classes with their instance variables. I felt like KVO in NSDictionaries was just a long way to get at values in code and made for more lines. I also want to avoid waiting and loading screens, so I separated many requests into small object calls, and take it even further I got on a kick where I would make property names only one or two characters long. If the property name was 'int character_id = 500;' I would hold the value in a temporary NSDictionary to encode the property to {c:500}. Every little bit helps I figure.
Thin Binary Data
This year I have been getting more ambitious about using some Multiplayer GameCenter features, wether it be live games, or turn based games. Now the out of the box features of encoding NSDictionaries to a compatible NSData binary I think leave you somewhere in-between the first to parts of this post. Where you can define small objects maps, but this its coded into a XML like string before its coded to binary. Game data needs to be condense to make a quick round trip in peer to peer or to fit in a maximum size allowed for turn based storage. I've come full circle now to better understand C structs and have come to some pros and cons about using them to transmit game data. What really helped me prepare for the use of C structs as NSData was the GKTank sample. Now if your not entirely sure whats going on in this sample like I once was, then hopefully I can help you understand it.
First you must be comfortable using C structs to define your properties. If you are very used to the way NSObjects and Class in Obj-C act for you when you retain and release, you have to be aware of when you assign a struct you are making copies and may be dealing less in pointers. Lets talk about GKTank tankInfo:
typedef struct {
CGPoint tankPreviousPosition;
CGPoint tankPosition;
float tankRotation;
float tankDirection;
} tankInfo;
This struct has four properties, but two are also structs. CGPoint is a C struct that just contains two sub float values. And understanding how this all really boils down in memory helps understand how much data your saving or sending. A float is 4 bytes, this example of struct ultimately consists of 6 floats. 4*6 = 24 bytes to send this struct. Just to compare that point here, the resources out there tell you normally would want to run your NSDictionary through a NSKeyedArchiver which can write to a NSData. An example of this:
NSDictionary* tank = [NSDictionary dictionaryWithObjectsAndKeys:
[NSNumber numberWithFloat:32.f], @"oldx",
[NSNumber numberWithFloat:32.f], @"oldy",
[NSNumber numberWithFloat:32.f], @"newx",
[NSNumber numberWithFloat:32.f], @"newy",
[NSNumber numberWithFloat:32.f], @"rot",
[NSNumber numberWithFloat:32.f], @"dir", nil];
NSMutableData *data = [[NSMutableData alloc]init];
NSKeyedArchiver *archiver = [[NSKeyedArchiver alloc]initForWritingWithMutableData:data];
[archiver encodeObject:tank forKey:@"theTank"];
[archiver finishEncoding];
Same six float values, for a total of 436 bytes to hold the values that map the key values. So how do we get the struct into a NSData object so the rest of the Objective-C framework can handle it. NSData has a handy way of making a copy of the binary data of the structure provided we can tell NSData how many bytes the object is.
tankInfo mytank = tankInfo();
mytank.tankPreviousPosition = CGPointMake(32, 32);
mytank.tankPosition = CGPointMake(32, 32);
mytank.tankRotation = 32;
mytank.tankDirection = 32;
NSData *data = [NSData dataWithBytes:&mytank length:sizeof(tankInfo)];
[myGKSession sendData:data toPeers:peers];
This method Data with Bytes lets us send in the pointer argument of our struct which is giving it the first byte to start from. The length lets the NSData know how far in memory to copy, so we need to know how many bytes our struct is. We did the math earlier and could enter 24 for the length. But C offers a function sizeof() which looks up the definition table of the struct type and returns it for us. Whats great about a struct is when allocated it's properties are lined up in memory so that each of the 6 floats are stacked back to back. How it is aligned is not saved in the memory block with the 6 values but the structs definition itself acts as a parser/de-parser for the memory. Even if the floats are back to back, the definition is the road map to know that the first and second floats are part of the first property, the third and forth floats are the second property, etc. When you get this NSData on the other side it needs to be converted back into a structure so it's easy to access again.
- (void)receiveData:(NSData *)data fromPeer:(NSString *)peer ... {
tankInfo recievedTank;
if ([data length]) [data getBytes:&recievedTank length:sizeof(tankInfo)];
}
When we create the instance of tankInfo the memory for 24 bytes has been made available but like is random data or set to null. Like before this now copies the bytes into the pointer location which has already been allocated the line before and the data that just came in can now be accessed and changes by accessing the struct properties and all we sent over the wire was 24 bytes (give or take any gamecenter overhead).
With my mind well wrapped around this C struct concept I can wisely get a lot of game data send over the wire quickly, or stored on Apple servers when making a turn based game. Apple has set a turn based miscellaneous data limit to 4KB. Now for some these seem unreasonable, but those are often the ones I see trying to KeyArchive encode a big Dictionary and it makes sense that they are hitting the limit quick. I've headed into planning my game data storage and for the most part have not come close to needing to worry about how much binary data I need to get my games conveyed. up to 4 players, up to 70 turns, in under 880bytes.
The Con
I know I'm picking a threads here, but now I've come back to the issue I once though obsessive when working for the MMO. I'd love to hear input there from any others that deal in structure binary. I've designed my game data struct to handle these 4 players and the 70 turns possible to complete a game. To define this maximum possible amount of turns I an array of turn structs. This again cause the data to be allocated, and empty early on. An average game will be complete in 60 turns, and as the turns progress the bytes in the turns array are being written to something other Null, but in order to know the size of a matchData struct the turns need to be defined with 70 elements. Only fortunately is this set of struct small and the whole combo comes to 880bytes. But thats 880 on turn 1, 2 .. 54 etc. If later I take on any larger game storages I may come back to talk about how to sub parse the first few bytes in the NSData to help predefine the amount of elements that should be expected, making the struct more variable.
struct matchData {
unsigned int seed;
unsigned char playerCount;
unsigned char turnIndex;
playerData players[4];
turnData turns[70];
matchData() : turnIndex(0), playerCount(2) {}
};
Sunday, November 20, 2011
Level building nostalgia
Something about the idea of creating levels and actually creating levels is hard to follow through on. I look forward to trying out some puzzle making when Valve releases the level editor for Portal. Most recently I last spent a bit of my time in the Little Big Planet level creator and had some fun with it's physics system. For me this all steams back to a time ago that I was playing my share of Doom over the local BBS and using ID Software's tools to make more arenas to share with my friends. Then I think even before that, one of the first computers I was lucky to have as a grade school kid, I had a copy of a game called Jetpack. This was a solid game with some very challenging levels and a lot of dying. I spent so much time just mastering and over thinking the creation of one level I couldn't imagine having to create dozens, or a hundred.
I started this time with some good old white board drawings of levels that will help the player progress and get use to the control I want to propose in this game. In addition to my existing placement tools I have in Flash, I'm was going to employ the use of Tiled to handle the nature of tile block maps. This was a great fast start for getting the prototype made and the feel for the game. I must have played that first level design 100 times getting the input right. Then it came time to build a couple more, and quickly I find myself running out of ideas. I need to have my friends and family help, even if they just messed around with the block to create some forms I wouldn't have thought up. I then remember I'll have to get these few people Flash, and Tiled, and teach them how to use the two apps, and no thats not going to work.
Back when I was making Petunk I came to a point where I was going to make a levels in XML code, in game builder, or just some type of layout sheet. Saving myself time I came up with the layout classes in Flash, but at the cost of not making that part of the game. It's got me to wonder how much of the game is the desire to create levels. I wish I had someones numbers here, it would help me decide if thats a sales perk. The developer from jetpack claims it's one of the most popular features, Little Big Planet I'm sure is thriving on user created content. Even Gary from Bouncy Ball Games may have a lot of users that prefer to make levels over playing them. So this time around I'm leaning toward making a easy to use level editor and see what user created content can do for a game. Partially because I want to see if a larger fan following would get onboard earlier to submit levels for the games release like JoyrideLabs.de is doing. Or if serving up downloadable levels helps retain users long after the packaged levels get completed. At this point I'm a long way off but I will try to make a series of this experiment as I hit the milestones of my jetpack version of an iOS game.
I started this time with some good old white board drawings of levels that will help the player progress and get use to the control I want to propose in this game. In addition to my existing placement tools I have in Flash, I'm was going to employ the use of Tiled to handle the nature of tile block maps. This was a great fast start for getting the prototype made and the feel for the game. I must have played that first level design 100 times getting the input right. Then it came time to build a couple more, and quickly I find myself running out of ideas. I need to have my friends and family help, even if they just messed around with the block to create some forms I wouldn't have thought up. I then remember I'll have to get these few people Flash, and Tiled, and teach them how to use the two apps, and no thats not going to work.Back when I was making Petunk I came to a point where I was going to make a levels in XML code, in game builder, or just some type of layout sheet. Saving myself time I came up with the layout classes in Flash, but at the cost of not making that part of the game. It's got me to wonder how much of the game is the desire to create levels. I wish I had someones numbers here, it would help me decide if thats a sales perk. The developer from jetpack claims it's one of the most popular features, Little Big Planet I'm sure is thriving on user created content. Even Gary from Bouncy Ball Games may have a lot of users that prefer to make levels over playing them. So this time around I'm leaning toward making a easy to use level editor and see what user created content can do for a game. Partially because I want to see if a larger fan following would get onboard earlier to submit levels for the games release like JoyrideLabs.de is doing. Or if serving up downloadable levels helps retain users long after the packaged levels get completed. At this point I'm a long way off but I will try to make a series of this experiment as I hit the milestones of my jetpack version of an iOS game.
Sunday, November 6, 2011
Let's Be Friends. Core Data Relationships
With any good database tutorial, you will learn how to make employees, departments, and managers. Then how to reference the employees to a department and be overseen by which manager. Instead I'm going to talk about a data structure that I made to help me with my next project to help me handle game scenes, characters, and their animations.
Now with Actors setup with many animations, and many frames, I want to add the actors to many scenes. The reason is so I can have a character that is standing out by the boat by the docks, and/or in the kitchen making a sandvich. The things I want to avoid are the position of actor being overwritten by the different locations he will be at in those scenes, and from duplicating the relationship instances I just built for the one actor and his animation tree. I also need the actor to be added to the Scenes' assets set so the Actor can have a index(z-layer) in that scene, which will be a different index than another scene he may be in. If you watched Dr.Larson yet or know about Core Data you'll hear about the Abstract/Parent Entity setting. Using a new entity definition as a parent entity, SceneActor acts like a subclass of SceneAsset and then I can add a extra property to do a non-inversed relationship to a Actor. At this point I also added a non-inverse player relationship to the Scene so that I know which Actor object is defined as the player and to forward the user inputs to when the scene in created. Defining the parent entity and the model object classes that go along with the design this adds one last convenience when looping through the assets set to generate the scene. I can test the class type from the set and let it generate a actor sprite or base class sprite.
The first time I heard of Core Data, I thought I wouldn't need to learn how to use it, I already know how to access SQL and can make use of the SQLite C library. And for a few of my early iOS projects thats what I used. I made lots of classes that handled my common queries and they grew fast as my need to cross reference tables got more complex. This all changed once I watched Dr. Brad Larson's Madison Area Tech course on Core Data. Now that I use Core Data, I find myself being able to do more with table relationships that I would have thought up had I handled the queries myself.
What I want to accomplish is to have multiple Scenes which will be home to Sprites and character Sprites with defined animations. In the past I usually approach this type of design by having a top tier object just contain a list of the child items, the child items would have their settings for position in the scene. I designed this first table relationship to contain multiple scenes, with multiple assets(sprites). For simple still sprites this will still be my basic foundation. I'm only containing the sprite file name reference and the position. The index field is to retain the z-layer of the sprite in the scene. More recently Core Data now offers to track many-to-one collections in a NSOrderedSet, but I've discovered are a few quarks to this still, so I still track the z-depth manually to control the sprite layers. When accessing the 'assets' set I get the ordered array again using:
NSArray* sorters = [NSArray arrayWithObject:[NSSortDescriptor sortDescriptorWithKey:@"index" ascending:YES]];
NSArray* assets = [m_scene.assets sortedArrayUsingDescriptors:sorters];
Each asset would not be shared between different scenes. If I did reference a asset in two different scenes with different position they would overwrite the position field. My actor charters though are going to have their own inner animation relationships. I'm starting with an actor that will have a name and a set of animations. They will likely include things like idle, run, jump, climb, fix, talk. Each animation then has a series of frame references to the file names used to comprise the animation, and the rate in which to play the frames. Then for sake of having a starting point I have another single animation relationship to decide which animation will be default when the actor is loaded. In most cases I set the animation field to equal the 'idle' named animation. But it is just a reference to the Animation object so the animation name is not important, and more importantly not defined as a string field. Again I use a index field in the Frame object to control the order of the frames. Because Animation.frames is stored as a NSSet they may come back out of desired order and need to be sorted like above.
A quick note on the power and feature of Core Data that I like the most, is inverse relationship mapping. Thats not to say it's a unique feature, it's a cross reference. But having the Core Data frame work set the relationship field for you in both records by just simply setting the property of one object, just makes storing the relationships all the more easier. Above I have the Animation object that has a relationship set of frames. In the Frame I have a relationship inverse to animation. So during the loop in my program to set these frames, all I need to do is allocate a new Frame, set the name, index, and simply set the animation to equal the Animation instance I created before entering the loop. Instead of requiring me to also access the Animation and add the frame to the NSSet, I'm getting that for free by the Core Data framework and the inverse connection I setup in the model.
Animation* ani = [animationRay objectAtIndex:selectedIndex];
for (NSString* file in fileNames) {
NSEntityDescription* frmEntity = [NSEntityDescription entityForName:@"Frame" inManagedObjectContext:context];
Frame* frameIn = (Frame*)[[NSManagedObject alloc] initWithEntity:frmEntity insertIntoManagedObjectContext:context];
frameIn.name = file;
frameIn.index = [NSNumber numberWithInt:[ani.frames count]];
frameIn.animation = ani;
}
[assets enumerateObjectsUsingBlock:^(SceneAsset* obj, NSUInteger idx, BOOL *stop) {
NSString* sheetName = [[obj.sheet.file pathComponents] lastObject];
[[CCSpriteFrameCache sharedSpriteFrameCache] addSpriteFramesWithFile:sheetName];
CCSprite* aSprite;
if ([obj class] == [SceneActor class]) {
SceneActor* sa = (SceneActor*)obj;
TTActor* actor = [[TTActor alloc] init:sa];
aSprite = actor;
} else {
aSprite = [CCSprite spriteWithSpriteFrameName:obj.file];
}
Sunday, October 23, 2011
Making Tools for Making Games
I've been creating apps and games for sometime, and each time I seem to make a second app/tool to help in the creation. Defining layouts, levels, settings, data, designs. I could define all this pre-existing data in code, and then muddle through it when I need to change or upgrade it. But often I find myself working with others that haven't devoted themselves to understanding cryptic programing languages and scripts. This prompts them to send me change request that seam so minor I prioritize them low and let them stack up until major code changes have been finished. I try to encourage the co-workers to decipher my xml,json,or sqlite files to change the simple but critical design features themselves, but this just brews difficulties in a project's lifecycle time and again. Even on an off day I'd rather not plot out x and y points in my head only to find that I needed to subtract other offsets and half widths when defining the property. Maybe it's all those years I spend developing in Flash and the authoring stage space that allowed me to place and go.
When I first started using Xcode and InterfaceBuilder, I understood it fairly well because it would help me place and then code. But unlike Flash, it did not strike me as the type of layout app that I should be defining each and every lesson, level, or scene. I combined my experiences with each of these IDEs and leveraged laying out in Flash, while bringing the raw values to the iOS. My co-workers also know their way around the Flash stage so less learning curve there too.
I've used this approach when developing a e-learning engine, and I'm refining it on my next point-and-click adventure game, but today I will cover how it helped making levels in Petunk.
ActionScript has grown since it limited commands from '98. Today you can access just about every property, which is nice for even breaking down per frame properties. For now I'm only going to go about moving screen elements to get some initial definitions. What I start with is making an ActionScript class that will write out the movieclip properties I'm interested in.
public class Obstacle extends MovieClip {
public function Obstacle() {}
public function getDef():String {
return '{'+
'type = "'+getQualifiedClassName(this)+'";'+
'cord = "'+int(this.x) +','+int(480-this.y)+','+int(this.rotation)+'";'+
'size = "'+this.scaleX+','+this.scaleY+'";'+
'},';
}
}
The design behind this class gets the stage properties I'm interested in so they can be bundled in an iOS app. Each instance of this Obstacle class will define the settings into a simple format that un-translate later in Objective-c. The getQualifiedClassName() is kinda a trick with the library, it will return the subclass name. In this case I define the subclass by the item or image I place in the movieclip at the library level.
This class returns: {type = "goal";cord = "160,417,0";size = "1,1";}
The main timeline needs to aggregate all these instances that laying around the stage, so the movieclip of the FLA file itself needs to be subclassed as well. The main purpose here to let the frame have time to draw the clips that I've laid out, and then read the values back and write in a plist friendly format so I don't have to change the values by hand and make the mistake of missing a bracket or semicolon. The LevelPlist.as file looks like this:
public class LevelPlist extends MovieClip {
public function LevelPlist() {
//Clips aren't on stage yet
this.addEventListener(Event.EXIT_FRAME, onFrame);
}
public function onFrame(evt:Event) {
//Remove event so this does not call repeatedly
this.removeEventListener(Event.EXIT_FRAME, onFrame);
var count:int = this.numChildren;
trace("{");
trace("levelId = "+this.currentFrame+";");
trace("obstacles = (");
for(var i:int = 0; i < count; ++i) {
var obj:Obstacle = this.getChildAt(i) as Obstacle;
obj.getDef());
}
trace(");");
trace("}");
}
}
}
With just a little bit of script I can now drag new clips out of the library, place them on stage, move them around on stage, use Flash interface to scale, rotate, and align. Then just simply run a "Test Movie" to cause the script to run and trace string data to the output window. Cut, Paste, Save Plist file and the changes are ready to be bundled in the next build of the Xcode project.
Because the setup of a level is being disconnected from the compiled code, there needs to be a simple way to map the obstacle names back to types that can be interpreted into physics objects. I have a Factory class help me with this, so as I load a level plist file I then loop through the array of obstacles and have a 'else if' stack translate the names to a enum type that helps in switch statements later in the the game loop.
+ (displayType)StringToType:(NSString*)aStr {
if ([aStr isEqualToString:@"ball"]) return display_ball;
else if ([aStr isEqualToString:@"wall00"]) return obst_wall00;
else if ([aStr isEqualToString:@"wall01"]) return obst_wall01;
else if ([aStr isEqualToString:@"angle"]) return obst_angle;
else if ([aStr isEqualToString:@"bumper"]) return obst_bumper;
else if ([aStr isEqualToString:@"goal"]) return sens_goal;
return display_null;
}
Then because earlier I saved the X, Y and Rotation into a comma delimited string I use the nsstring separator to make a quick array of the 3 float values I need to use to define position. This next method gives me back my PhysicBase objects which have references to both Box2d objects and the Cocos2d objects.
+ (PhysicBase*)CreateObject:(NSDictionary*)objData inMapSpace:(CGSize)mapsize {
displayType dType = [ObjectFactory StringToType:[objData objectForKey:@"type"]];
physicType pType = [ObjectFactory DisplayToPhysicType:dType];
PhysicBase* base = [[PhysicBase alloc] initWithDisplay:dType];
base->m_basetype = pType;
NSArray* values = [[objData objectForKey:@"cord"] componentsSeparatedByString:@","];
base.position = ccp([[values objectAtIndex:0] floatValue], [[values objectAtIndex:1] floatValue]);
base.rotation = [[values objectAtIndex:2] floatValue];
[base autorelease];
return base;
}
So what does this really solve?
Well I mainly found myself editing plist files and changing the x or y value, only to find that I miss calculated or want to nudge it a bit more, and then a bit more. Changing a value like x = 124 was not a instance change and gave me know way of "Eye balling it" for spacing until I compiled and ran the project. Seeing the placement drawn as you move and rotate something is clearer to see than just numbers.
Why not make a level builder on the iPhone?
As nice of a framework UIKit and Cocos2d are they do not have out of the box, move, scale, and rotate tools, I could spend time making them. Then there is the confined workspace of the 320x480. Software like Photoshop and Flash are great for comping out designs. (And if I could export propertied from Photoshop, I would.) So why not levels, Flash already has move, scale, and rotate. Has a stage area and extra workspace area this zoom controls already hooked up.
When I first started using Xcode and InterfaceBuilder, I understood it fairly well because it would help me place and then code. But unlike Flash, it did not strike me as the type of layout app that I should be defining each and every lesson, level, or scene. I combined my experiences with each of these IDEs and leveraged laying out in Flash, while bringing the raw values to the iOS. My co-workers also know their way around the Flash stage so less learning curve there too.
I've used this approach when developing a e-learning engine, and I'm refining it on my next point-and-click adventure game, but today I will cover how it helped making levels in Petunk.
ActionScript has grown since it limited commands from '98. Today you can access just about every property, which is nice for even breaking down per frame properties. For now I'm only going to go about moving screen elements to get some initial definitions. What I start with is making an ActionScript class that will write out the movieclip properties I'm interested in.
public class Obstacle extends MovieClip {
public function Obstacle() {}
public function getDef():String {
return '{'+
'type = "'+getQualifiedClassName(this)+'";'+
'cord = "'+int(this.x) +','+int(480-this.y)+','+int(this.rotation)+'";'+
'size = "'+this.scaleX+','+this.scaleY+'";'+
'},';
}
}
The design behind this class gets the stage properties I'm interested in so they can be bundled in an iOS app. Each instance of this Obstacle class will define the settings into a simple format that un-translate later in Objective-c. The getQualifiedClassName() is kinda a trick with the library, it will return the subclass name. In this case I define the subclass by the item or image I place in the movieclip at the library level.
This class returns: {type = "goal";cord = "160,417,0";size = "1,1";}
The main timeline needs to aggregate all these instances that laying around the stage, so the movieclip of the FLA file itself needs to be subclassed as well. The main purpose here to let the frame have time to draw the clips that I've laid out, and then read the values back and write in a plist friendly format so I don't have to change the values by hand and make the mistake of missing a bracket or semicolon. The LevelPlist.as file looks like this:
public class LevelPlist extends MovieClip {
public function LevelPlist() {
//Clips aren't on stage yet
this.addEventListener(Event.EXIT_FRAME, onFrame);
}
public function onFrame(evt:Event) {
//Remove event so this does not call repeatedly
this.removeEventListener(Event.EXIT_FRAME, onFrame);
var count:int = this.numChildren;
trace("{");
trace("levelId = "+this.currentFrame+";");
trace("obstacles = (");
for(var i:int = 0; i < count; ++i) {
var obj:Obstacle = this.getChildAt(i) as Obstacle;
obj.getDef());
}
trace(");");
trace("}");
}
}
}
Because the setup of a level is being disconnected from the compiled code, there needs to be a simple way to map the obstacle names back to types that can be interpreted into physics objects. I have a Factory class help me with this, so as I load a level plist file I then loop through the array of obstacles and have a 'else if' stack translate the names to a enum type that helps in switch statements later in the the game loop.
+ (displayType)StringToType:(NSString*)aStr {
if ([aStr isEqualToString:@"ball"]) return display_ball;
else if ([aStr isEqualToString:@"wall00"]) return obst_wall00;
else if ([aStr isEqualToString:@"wall01"]) return obst_wall01;
else if ([aStr isEqualToString:@"angle"]) return obst_angle;
else if ([aStr isEqualToString:@"bumper"]) return obst_bumper;
else if ([aStr isEqualToString:@"goal"]) return sens_goal;
return display_null;
}
Then because earlier I saved the X, Y and Rotation into a comma delimited string I use the nsstring separator to make a quick array of the 3 float values I need to use to define position. This next method gives me back my PhysicBase objects which have references to both Box2d objects and the Cocos2d objects.
+ (PhysicBase*)CreateObject:(NSDictionary*)objData inMapSpace:(CGSize)mapsize {
displayType dType = [ObjectFactory StringToType:[objData objectForKey:@"type"]];
physicType pType = [ObjectFactory DisplayToPhysicType:dType];
PhysicBase* base = [[PhysicBase alloc] initWithDisplay:dType];
base->m_basetype = pType;
NSArray* values = [[objData objectForKey:@"cord"] componentsSeparatedByString:@","];
base.position = ccp([[values objectAtIndex:0] floatValue], [[values objectAtIndex:1] floatValue]);
base.rotation = [[values objectAtIndex:2] floatValue];
[base autorelease];
return base;
}
So what does this really solve?
Well I mainly found myself editing plist files and changing the x or y value, only to find that I miss calculated or want to nudge it a bit more, and then a bit more. Changing a value like x = 124 was not a instance change and gave me know way of "Eye balling it" for spacing until I compiled and ran the project. Seeing the placement drawn as you move and rotate something is clearer to see than just numbers.
Why not make a level builder on the iPhone?
As nice of a framework UIKit and Cocos2d are they do not have out of the box, move, scale, and rotate tools, I could spend time making them. Then there is the confined workspace of the 320x480. Software like Photoshop and Flash are great for comping out designs. (And if I could export propertied from Photoshop, I would.) So why not levels, Flash already has move, scale, and rotate. Has a stage area and extra workspace area this zoom controls already hooked up.
Subscribe to:
Posts (Atom)













