The game that I'm currently working on (based on parts of Box2D, Artemis, libGDX and a dash of RUBE for level design) employs reflection. I've got custom properties defined for various bodies, fixtures, and joints in RUBE. Some of these custom properties are strings that are interpreted as classes for instantiation at run-time via Java reflection. For example, I have eggs in the game that have a custom property "PreSolverScript". This field is defined with EggImpact. EggImpact is a Java class that defines what happens when the related fixture is detected for the Box2D presolver. This class checks the velocity of the egg object just before impact. If the velocity is great enough, the egg receives damage and potentially breaks.
The script references are resolved at run-time through the use of Java reflection. Doing reflection with simple Java constructors is fairly simple. It gets somewhat tricky when you have to worry about parameters. Here's an example of my code that does reflection with parameterized script strings. If parameters for a script exist, they appear as comma-separated values following the script name.
In the example below, preSolverProperty is a string containing the custom property for the string.
String [] parms = preSolverProperty.split(",");
IPreSolverScript preSolverScript;
if (parms.length > 1)
{
preSolverScript = (IPreSolverScript)Class.forName(ScriptConstants.SCRIPT_PACKAGE_NAME + parms[0]).getConstructor(String[].class).newInstance((Object)parms);
}
else
{
preSolverScript = (IPreSolverScript)Class.forName(ScriptConstants.SCRIPT_PACKAGE_NAME + preSolverProperty).newInstance();
}
The sticking part for me was casting parms to (Object). Without this I got a invalid number of parameters error at run-time.
A blog detailing my experiences with Android as an independent (indie) game & app developer.
Monday, April 1, 2013
Sunday, December 2, 2012
libgdx and iOS deployment quest - Part IV
I discovered that my installation of TortoiseHg was not 100% complete. I had not installed kdiff3 or added any of the necessary configurations to get that tool to work. The net result was that I was not able to do any sort of merges or "diff to local" type operations. After a bit of hunting, there are additional configurations that you need to implement after installing kdiff3.
To wit:
1) Install kdiff3 from kdiff3.sourceforge.net.
2) Update your .hgrc similar to below:
To wit:
1) Install kdiff3 from kdiff3.sourceforge.net.
2) Update your .hgrc similar to below:
[extensions]
hgext.bookmarks =
hggit =
mq =
hgext.extdiff =
[extdiff]
cmd.kdiff3 =
[merge-tools]
kdiff3.args = $base $local $other -o $output
kdiff3.priority=-3
kdiff3.args=--auto --L1 base --L2 parent1 --L3 parent2 $base $local $other -o $output
;kdiff3.fixeol=True
kdiff3.gui=True
kdiff3.diffargs=--L1 '$plabel1' --L2 '$clabel' $parent $child
kdiff3.diff3args=--L1 '$plabel1' --L2 '$clabel' --L3 '$plabel2' $parent1 $child $parent2
;kdiff3.diff3args=--L1 '$alabel' --L2 '$plabel1' --L3 '$plabel2' $ancestor $parent1 $parent2 -o $child
kdiff3.dirdiff=True
Sunday, November 25, 2012
libgdx and iOS deployment quest - Part III
I discovered that my version of libgdx is a bit outdated and I needed to sync things up with the latest and greatest that includes ikvm support. Since I'm trying to do everything on the MacBook Pro, that means I need hg-git support. This allows you to deal with a git repo (which is how libgdx is version controlled) from the Mercurial tool.
On Windows, this is fairly easy. Instructions are here.
On a Mac, it's also pretty easy. If you followed the instructions for installing TortoiseHg from my earlier blogpost, you can simply open up a terminal and run this: "sudo easy_install hg-git". From there you need to edit the .hgrc file:
Verify things work by entering "hg help hggit.
Hat tip to this site.
In other news, I was able to get both Outbreak: Zombie Slots and Toyland Slots running on the MacBook Pro. It was scary easy. libgdx is awesome!!!
Obligatory screenshot below:
On Windows, this is fairly easy. Instructions are here.
On a Mac, it's also pretty easy. If you followed the instructions for installing TortoiseHg from my earlier blogpost, you can simply open up a terminal and run this: "sudo easy_install hg-git". From there you need to edit the .hgrc file:
[extensions]
hgext.bookmarks =
hggit =
Verify things work by entering "hg help hggit.
Hat tip to this site.
In other news, I was able to get both Outbreak: Zombie Slots and Toyland Slots running on the MacBook Pro. It was scary easy. libgdx is awesome!!!
Obligatory screenshot below:
libgdx and iOS deployment quest - Part II
I got SourceTree
installed and cloning my repos, but didn't care for how it operated since I'm used to the
TortoiseHg work flow. Additionally, I noticed that it didn't set up 'hg' to be run from the command line by default. I could do a "hg clone" from the terminal which is sometimes useful versus using a GUI to do all the heavy lifting.
I did some more searching and came across this page: https://bitbucket.org/tortoisehg/thg/wiki/developers/MacOSX. I came across this before as I was looking at my Hg options but didn't pursue it because the steps look way too involved and I wanted a pre-packaged approach to getting things running. It turns out there are actually several different approaches at getting TortoiseHg running that are listed on this page... they just aren't presented very well to make you realize that.
At any rate, if you scroll down, you'll see instructions for installing things using HomeBrew. They were very easy and very straightforward. I only had a couple of hiccups where I tried to run the command and the instruction failed because it expected that I was running as root instead of a standard user. Boiling things down, here's what I did:
- Install Xcode from App Store
- ruby -e "$(curl -fsSkL raw.github.com/mxcl/homebrew/go)"
- brew doctor
- Open Xcode → Preferences → Downloads → Install command line tools
- Set up .bash_profile as specified on the Wiki page (BREW setup, etc.)
- brew install pyqt
- brew install qscintilla2
- sudo easy_install pip
- sudo pip install Pygments iniparse Mercurial
- hg clone http://bitbucket.org/tortoisehg/thg ~/Documents/TortoiseHg
- cd ~/Documents/TortoiseHg
- ./thg
Success! Although
it doesn't appear to have Finder integration, like the Windows
version and its Windows Explorer integration, it does work and
present me with the TortoiseHg Workbench I'm extremely familiar with.
Success!
Next up, to see if I can get my Outbreak: Zombie Slots running with Eclipse on the Mac platform.
libgdx and iOS deployment quest - Part I
After seeing the successful deployment
of Noblemaster's libgdx-based games to the iTunes store, I figured it
would be fun to port the libgdx games that I've created to iOS and
release them to the iTunes store as well. So, today I went out to the local Best Buy and picked up a Macbook Pro 15” MD103LL/A. It was on sale for about $120 less than the retail price that was listed. I could have went for the more expensive model that included more system memory, but after doing some reading, saw that adding memory is pretty simple and cost effective so I'll probably go that route.
I brought the machine home and the first order of business was to get it running on my network. Fail.
First and foremost – for whatever
reason, the MacBook Pro would knock my Netgear WND3300 Wireless
router out of commission. The net result (heh!) was that not only could the
Apple machine NOT get on the Internet, but it also fouled up the rest
of my network. Other machines that were working just fine would no
longer be able to access the Internet. This is not the first time
I've seen this behavior.
My brother-in-law has had a MacBook Pro
for a while now. Every time he has brought his machine over, he has
caused the same issues... after setting up the wireless
configuration, boom – my network goes down. The only way to
correct this in the past has been to reset the router.
This time I was determined to figure
out a workaround if not an out and out fix. Using Google was worthless. None of the tips I saw worked out. After poking around with “ifconfig” and messing around with general settings, I noticed
that I could get things to work once again if I manually defined the
DNS values. My standard test to verify is simply “ping www.google.com” from a terminal window.
Now, normally, the DHCP server
capabilities of the router kick in and assign the router as the DNS
server for the DHCP client. This has worked flawlessly with Windows
machine. For some odd reason, this causes issues using an Apple
machine. I logged into the WND3300 and took a look at its DNS
capabilities. It's cable modem side acts a DHCP client itself, which
gets its configuration from the cable configurations that are assigned by the cable company. I looked at
the assigned DNS server values. They looked correct. Based on a
hunch, I modified the “Domain Name Server (DNS) Address” setting
on the Basic Settings page from “Get Automatically From ISP” to
“Use These DNS Servers” and specified what the cable modem always
gives me. After doing that, the MacBook Pro no longer has any issues
accessing the Internet and the other machines on the network
similarly work fine. Whew! That blew about an hour or so of time.
Next on the list was to download Xcode.
Xcode is the IDE used for developing iOS applications. I've used it
a little bit in the past. I had to log into the Apple App Store and
download it. Annoyingly, I also had to update my credit card info on
file with Apple. As great as everyone claims Apple is, I find this
to grate on my nerves... Especially given that Xcode is available as
a free download. I guess I should also mention, I already had an
Apple ID that I created back when I had an iPod Touch. So, getting that stuff up and running was annoying but not world-ending.
With Xcode downloaded, I needed to
install Eclipse. Wow. Installing Eclipse is easy. The problem
comes in that there are about 10 different flavors of Eclipse.
Historically, I've always gone for Eclipse Classic. However, this
only provides a bare minimum. After doing some looking around, I
discovered that there is a version of Eclipse for Mobile Developers.
Sweet! It includes Java and
C development tools. The libgdx library backend includes certain
things written in C for performance reasons (the Box2D Physics
Engine, for example.) I've had to make my own tweaks to that in the
past so its been cool to have C tools available. At any rate, I
installed that.
Then,
I tried to run Eclipse. Blah. You need the Java JDK before it will
work. On a Windows machine, you get that from Sun. Not so on an
Apple machine. At a terminal prompt, I entered “javac -version”
and it prompted me for an install. I wanted to specify an exact
version of the JDK but I didn't see an option. I've heard of
problems with JDK 7 and wanted to stick with JDK 6. I bit the bullet
and just accepted whatever version the default installer selected.
Turns out that I lucked out and am now running 1.6.0_37. Woot!
Next up I wanted
to get my Mercurial repos running. On Windows, I use TortoiseHg
which has really nice integration with Windows Explorer. On the Mac,
there were a few options and I decided to give Atlassian's SourceTree
a shot. I've got my repos hosted on Bitbucket so it seemed to make
sense since BB is operated by Atlassian. (Note: There has been an update on this -- I'm now using TortoiseHg on my Macbook Pro.)
In order to access
my BB repos, I apparently need to get the ssh keys configured. I
tried to follow Atlassian's directions where they imply that you'll
be prompted to enter your ssh key info when you try to clone a
project. This will supposedly add your key to your system's
keychain. That's not the case. Trying to clone a repo resulted in
an invalid URL problem and I couldn't get any further than that.
So, I copied my
private BB ssh key over to the MacBook. In order to access my repos,
I need to have my ssh key in the Mac's key ring. This is done via
“ssh-add -K /path/to/key/here”. So I did this, got prompted for
the passphrase and then.. it failed. Mwah? I generated the key
using Puttygen and use it on nearly a daily basis, so I'm 100% the
passphrase was being entered in correctly. Turns out that you need
to use PuttyGen to export the key in an openSSH format. After doing
that, I could add the key successfully.
Next up was to get
my graphical development environment going. This meant getting
Inkscape and GIMP up and running. Since I'm running OS X 10.8
(Mountain Lion), this is not as straightforward as simply installing
the related dmgs. No. Trying to run Inkscape gives you a prompt
saying “Where is X11?” Turns out that you need to first install
Xquartz, which is the Open Source X11 that Apple no longer includes on its machines.
Yippee.
After downloading
that and rebooting, and trying to Inksape again, I was still prompted
for “Where is X11”. Mwah? You need to go to your Utilities
folder and start Xquartz and THEN start Inkscape. Geez. So much for
the “ease” of using Apple products. On the other hand, the GIMP
version was already setup with Mountain Lion compatibility so I
didn't need to do much there other than copying the GIMP application
to my Applications folder. Sweet.
Saturday, September 1, 2012
Stupid AdMob tricks
I use AdMob exclusively for my applications. There's no particular reason other than it is a lot easier to set things up than having to futz with three or four different ad company SDKs, and having a single location to track my voluminous income (heh) is just super-convenient.
Today, I set things up so that all my apps will serve house ads for the duration of the (US) holiday weekend.
What's an AdMob House Ad? An AdMob house ad allows you to serve up *free* ads in your applications. You can do things like specify how many impressions you want to serve, which applications will display which ads, etc. The ads are restricted to applications you define, but otherwise free you from having to pay AdMob for them to appear.
At a high level, I've got two ad configurations defined for every app. One version serves the Google Play link and the other version serves the Amazon App Store link. The ad that gets served is based on the device that ad is appearing on.
Specifically, here's how I set things up:
1) I created a house ad campaign called "all house ads". At this level in AdMob, you are able to define the start and end times. I've got things setup to run from today until midnight Monday (end of the Labor Day holiday). That's a total of three days of house ad rotation.
2) I created several ad groups under the "all house ads" campaign. At this level in AdMob, you are able to define which apps the ad group will appear in and your impression goal.
I defined separate ad groups for: DB42 Lite, DB42 Full Version, Doodle Bug, Drawccupations, MatchCard, Pyromancer's Laboratory, and Sky Ninja.
Now, I could have defined a single group and placed all of these ads in that group. I did not go this route because I wanted fine tuned control over which apps my ads appear in. For example, I don't want the Sky Ninja ad to appear in the Sky Ninja app... The user is already playing Sky Ninja -- therefore I don't need to drive him or her to download that exact same app.
For each ad group in this section, I selected the impression goal of "Fully Allocate". This means, I want to completely turn my ad mediation over to my house ads. In other words -- don't serve up any paid ads! I'm trying to drum up support in my apps for my other apps, so that's why I'm bailing on the paid ads for this duration.
I wasn't sure if the "Fully Allocate" configuration would limit the ad group to a single group or not. Through experimentation, I see that the ads are rotating through all the ad groups I have defined for the campaign. This is EXACTLY what I want.
It is also at this level that you can define target demographics and devices. I initially set things up to serve all demographics and devices EXCEPT the Amazon Kindle Fire. Ads served to the Kindle Fire are served somewhat differently because I want to reference the Amazon App Store link for those. Keep reading...
3) For each ad in the group, I've got the URL defined for the app and the image to display with that URL. I initially created URLs that pointed to the Google Play Market URL. For example, the DB42 Lite URL points to:
4) After I set all of that up for the Google Play links, I copied every single Ad group, and added "Amazon" to the name (e.g. "Sky Ninja Amazon"). Then, I went into each Amazon ad group and edited the URL to point to the Amazon App Store's URL. For example, the DB42Lite URL changed from what I list above to:
The 'asin' value is listed on the Amazon App Store's page for the app.
When you click on this app from the Kindle Fire, you open up the App Store view of your app instead of a the Amazon Retail Web Site. This makes it easier for the user to install your app on a Kindle Fire for obvious reasons.
I'll post the results of doing all this in a few days. Hope this helps someone else out there!
Today, I set things up so that all my apps will serve house ads for the duration of the (US) holiday weekend.
What's an AdMob House Ad? An AdMob house ad allows you to serve up *free* ads in your applications. You can do things like specify how many impressions you want to serve, which applications will display which ads, etc. The ads are restricted to applications you define, but otherwise free you from having to pay AdMob for them to appear.
At a high level, I've got two ad configurations defined for every app. One version serves the Google Play link and the other version serves the Amazon App Store link. The ad that gets served is based on the device that ad is appearing on.
Specifically, here's how I set things up:
1) I created a house ad campaign called "all house ads". At this level in AdMob, you are able to define the start and end times. I've got things setup to run from today until midnight Monday (end of the Labor Day holiday). That's a total of three days of house ad rotation.
2) I created several ad groups under the "all house ads" campaign. At this level in AdMob, you are able to define which apps the ad group will appear in and your impression goal.
I defined separate ad groups for: DB42 Lite, DB42 Full Version, Doodle Bug, Drawccupations, MatchCard, Pyromancer's Laboratory, and Sky Ninja.
Now, I could have defined a single group and placed all of these ads in that group. I did not go this route because I wanted fine tuned control over which apps my ads appear in. For example, I don't want the Sky Ninja ad to appear in the Sky Ninja app... The user is already playing Sky Ninja -- therefore I don't need to drive him or her to download that exact same app.
For each ad group in this section, I selected the impression goal of "Fully Allocate". This means, I want to completely turn my ad mediation over to my house ads. In other words -- don't serve up any paid ads! I'm trying to drum up support in my apps for my other apps, so that's why I'm bailing on the paid ads for this duration.
I wasn't sure if the "Fully Allocate" configuration would limit the ad group to a single group or not. Through experimentation, I see that the ads are rotating through all the ad groups I have defined for the campaign. This is EXACTLY what I want.
It is also at this level that you can define target demographics and devices. I initially set things up to serve all demographics and devices EXCEPT the Amazon Kindle Fire. Ads served to the Kindle Fire are served somewhat differently because I want to reference the Amazon App Store link for those. Keep reading...
3) For each ad in the group, I've got the URL defined for the app and the image to display with that URL. I initially created URLs that pointed to the Google Play Market URL. For example, the DB42 Lite URL points to:
market://details?id=com.gushikustudios.db42lite
4) After I set all of that up for the Google Play links, I copied every single Ad group, and added "Amazon" to the name (e.g. "Sky Ninja Amazon"). Then, I went into each Amazon ad group and edited the URL to point to the Amazon App Store's URL. For example, the DB42Lite URL changed from what I list above to:
amzn://apps/android?asin=B006KU55UO
The 'asin' value is listed on the Amazon App Store's page for the app.
When you click on this app from the Kindle Fire, you open up the App Store view of your app instead of a the Amazon Retail Web Site. This makes it easier for the user to install your app on a Kindle Fire for obvious reasons.
I'll post the results of doing all this in a few days. Hope this helps someone else out there!
Sunday, August 26, 2012
Ant, Javadoc, Libgdx, CreateProcess error=87, and YOU!
Gratuitously copied from the Western Skies blog since I *just* got bit by this -- and apparently got bit by it before 'cuz Google showed that I had visited this link sometime in the past:
I got bit by the most recent version libgdx when trying to run "ant" in order to build the "dist", which includes the API documentation that I was really aiming for.
I've been developing an Ant build file for a large-ish base of existing code. I've been using my Mac as the primary devleopment platform, but the ultimate target (for the other developers) is Windows. My task to build the javadocs runs fine on OS X, but the first time I rant it on Windows, I got the following:
c:\demo\build.xml:180: Javadoc failed: java.io.IOException: Cannot run program "c:\Program Files\Java\jdk1.6.0\bin\javadoc.exe": CreateProcess error=87, The parameter is incorrect
After about 10 minutes of Googling, I found a/the simple solution: add the useexternalfile attribute to the javadoc task in Ant. Voila , back in business, on the PC at least, and it didn't seem to break the build on the Mac.
I got bit by the most recent version libgdx when trying to run "ant" in order to build the "dist", which includes the API documentation that I was really aiming for.
Monday, August 20, 2012
3D Math Primer for Graphics and Game Development, 2nd Edition
I've picked up a copy of 3D Math Primer on Graphics and Game Development recently and have been slowly going through it. It's been around 15 years since I took Linear Algebra, or had to do any real matrix or vector based math, so the brain cells are slowly being re-initialized. Heh. Hopefully, this turns into a series of posts to share as I relearn things cruising through the book.
Here's what I've covered so far.
Chapter 1 deals with coordinate systems and concepts. I was always fairly decent at trig so this went by really quick. Chapter 2 gets into vectors and discusses the importance of the dot and cross products. I remember learning and using these in school, but their application in game development terms wasn't obvious to me. There are a couple of really good exercises that hit home the importance of these two operations.
One of the exercises describes an NPC in a game at a point p, that has a forward vector v. Given this, and a point x, how can you use a dot product for determining whether or not the point x is in front or behind the NPC?
There are a couple of things to know about the dot product. First, it gives you a scalar. Big whoop. How can this be used? Well, the sign of the scalar gives you an indication of the relative direction / angle between one vector and another. If the dot product is positive, then the angle between them is between 0 and 90 degrees. If the dot product is 0, then the vectors are orthogonal (ie. at right angles). If the dot product is < 0, then the angle between the vectors is greater than 90 degrees (and less than 180).
So, given a NPC location of p, we can calculate a vector x-p that gives us a vector describing the relative position of point x from point p. If we know the facing of the character (vector v at point p), then we can take the dot product between v and x-p and know the relative positioning.
Recall that the dot product yields a scalar, defined as:
a dot b =
(ax*bx + ay*by + az*bz).
Given this, we can determine if point x is in front-of or behind the NPC:
If vx*(xx-px)+vy*(xy-py)+vz*(xz-pz) > 0, it's in front. If it's < 0, is behind. If it's == 0, then it's to the side of the NPC.
Cool!
Using the magnitudes of the related vectors and the relation: cos theta = a dot b / (||a|| * ||b||), we can further determine the angle between the two vectors.
Another cool exercise from the book details the practical application of the cross-product. If the NPC above navigates from point p to point q and then to point r in the x-z plane (using left hand orientation), how can we use a cross-product to determine if the NPC should turn clockwise or counter-clockwise at point q in order to reach r?
Recall that the cross-product yields a new vector, defined as:
a x b =
[ax ay az] x [bx by bz] =
[ay*bz-az*by -(ax*bz-az*bx) ax*by-bx*by]
Here, it helps to know that the cross-product for a couple of vectors yields another vector that is orthogonal to the other two vectors. This vector gives a magnitude that is proportional to the area of the parallelogram area formed by the vector sides. We can look at the direction of the vector to clue us in to whether the orientation from one vector to another is clockwise or counter-clockwise. If the angle from a to b is less than 180 degrees, the resulting vector will point up (left-hand rule orientation) on the orientation is considered clockwise. If the angle from a to b is greater than 180 degrees (ie. from b to a is less then 180 degrees), the orientation is considered counter-clockwise and the resulting vector will point down.
In the NPC case above, it's pretty straightforward to calculate the cross-product. Doing this results in a single vector that either points in the +y direction, or the -y direction. Using this info, we can easily determine which way to rotate the NPC at the point q. The NPC could actually rotate either way and still reach the same position. The cross-product ends up giving you the shortest direction to rotate through.
Cool x2!
Here's what I've covered so far.
Chapter 1 deals with coordinate systems and concepts. I was always fairly decent at trig so this went by really quick. Chapter 2 gets into vectors and discusses the importance of the dot and cross products. I remember learning and using these in school, but their application in game development terms wasn't obvious to me. There are a couple of really good exercises that hit home the importance of these two operations.
One of the exercises describes an NPC in a game at a point p, that has a forward vector v. Given this, and a point x, how can you use a dot product for determining whether or not the point x is in front or behind the NPC?
There are a couple of things to know about the dot product. First, it gives you a scalar. Big whoop. How can this be used? Well, the sign of the scalar gives you an indication of the relative direction / angle between one vector and another. If the dot product is positive, then the angle between them is between 0 and 90 degrees. If the dot product is 0, then the vectors are orthogonal (ie. at right angles). If the dot product is < 0, then the angle between the vectors is greater than 90 degrees (and less than 180).
So, given a NPC location of p, we can calculate a vector x-p that gives us a vector describing the relative position of point x from point p. If we know the facing of the character (vector v at point p), then we can take the dot product between v and x-p and know the relative positioning.
Recall that the dot product yields a scalar, defined as:
a dot b =
(ax*bx + ay*by + az*bz).
Given this, we can determine if point x is in front-of or behind the NPC:
If vx*(xx-px)+vy*(xy-py)+vz*(xz-pz) > 0, it's in front. If it's < 0, is behind. If it's == 0, then it's to the side of the NPC.
Cool!
Using the magnitudes of the related vectors and the relation: cos theta = a dot b / (||a|| * ||b||), we can further determine the angle between the two vectors.
Another cool exercise from the book details the practical application of the cross-product. If the NPC above navigates from point p to point q and then to point r in the x-z plane (using left hand orientation), how can we use a cross-product to determine if the NPC should turn clockwise or counter-clockwise at point q in order to reach r?
Recall that the cross-product yields a new vector, defined as:
a x b =
[ax ay az] x [bx by bz] =
[ay*bz-az*by -(ax*bz-az*bx) ax*by-bx*by]
Here, it helps to know that the cross-product for a couple of vectors yields another vector that is orthogonal to the other two vectors. This vector gives a magnitude that is proportional to the area of the parallelogram area formed by the vector sides. We can look at the direction of the vector to clue us in to whether the orientation from one vector to another is clockwise or counter-clockwise. If the angle from a to b is less than 180 degrees, the resulting vector will point up (left-hand rule orientation) on the orientation is considered clockwise. If the angle from a to b is greater than 180 degrees (ie. from b to a is less then 180 degrees), the orientation is considered counter-clockwise and the resulting vector will point down.
In the NPC case above, it's pretty straightforward to calculate the cross-product. Doing this results in a single vector that either points in the +y direction, or the -y direction. Using this info, we can easily determine which way to rotate the NPC at the point q. The NPC could actually rotate either way and still reach the same position. The cross-product ends up giving you the shortest direction to rotate through.
Cool x2!
PDF Xchange Viewer Fu
I do a lot of PDF technical document reading where I'm constantly flipping back and forth between various document sections. Although my favorite PDF viewer of choice, PDF XChange Viewer, rocks the house for annotating, searching, saving, etc., it sucks when it comes to intra-document navigation. It's got bookmarks and capabilities to run multiple instances, but I haven't liked the navigation these implement. I've also tried using multiple copies of my documents and gotten that to work the way I liked (ie. tabbed document browsing), but having multiple copies seems like a real waste to me.
Enter Chrome.
Using Chrome tabs, you can easily and quickly open up multiple instances of a document by using the address bar to navigate to the local file. From there, just right-click and select duplicate. You end up creating multiple instances of PDF XChange Viewer in this case, but the navigation is SOOOO much better than clicking on the Windows taskbar.
You may have to disable the built-in PDF Viewer in Chrome to get PDF XChange Viewer working. To do this, enter "chrome://plugins" and simply disable "Chrome PDF Viewer".
QED.
Enter Chrome.
Using Chrome tabs, you can easily and quickly open up multiple instances of a document by using the address bar to navigate to the local file. From there, just right-click and select duplicate. You end up creating multiple instances of PDF XChange Viewer in this case, but the navigation is SOOOO much better than clicking on the Windows taskbar.
You may have to disable the built-in PDF Viewer in Chrome to get PDF XChange Viewer working. To do this, enter "chrome://plugins" and simply disable "Chrome PDF Viewer".
QED.
Sunday, May 6, 2012
SVN -> Mercurial mirroring
I'm continuing to use libgdx for my game development. However, locally I'm using Mercurial for software configuration while libgdx is currently stored up on GoogleCode using SVN. Ugh. How to stay in sync or otherwise mirror the SVN repo? So far, I've been maintaining things manually. The approach I take for pulling in SVN updates is outlined on the Selenic site here:
http://mercurial.selenic.com/wiki/SubversionToMercurialHowto
This boils down to keeping a single directory that serves both as a SVN *and* Hg working directory. From this directory, I do a clone per project to set things up. For example, I have a clone of libgdx for my MatchCard game, another clone for Doodle Bug, another clone for Pyromancer's Laboratory, and yet another one for DB42 and DB42Lite.
The nasty part of this business is that means I have to go to my local SVN repo, perform an SVN update, and then do an add/remove/commit on the Hg repo. Since this is a manual operation, it doesn't happen all that often. I'm between games right now and wanted to more or less set things straight in terms of keeping in sync without having to rely on a manual process. Here's the solution I came up with:
1) I'm sticking with the shared SVN / Hg working directory. This is really straightforward and I have my brain completely wrapped around what's going on.
2) I've got cygwin installed on my Win7 x64 workstation to give me all the great tools that Unix developers know and love... grep, head, tail, bash scripting, etc.
3) I've got Jenkins installed on this same machine. This does periodic job runs that query the state of the SVN repo, pull in new updates, and then add/remove/commit those changes to the Hg side of the shared directory. The build trigger is simply "Build Periodically" that runs the following script:
The one thing I'm lacking at this point is an "hg push" up to a default repo out on the Intertubes. I have it in mind to share the repo on Assembla, but I currently don't have the libgdx's team blessing to do this (yet) and I also have a support request into the Assembla team to see if they have any server side tools that might accomplish the same thing I'm doing locally. This whole process is highly reliant on my build server not dying. If I could push those operations out to some other server, that'd be better in the long run. For now, though, it works
Hope this helps someone trying to accomplish something similar.
http://mercurial.selenic.com/wiki/SubversionToMercurialHowto
This boils down to keeping a single directory that serves both as a SVN *and* Hg working directory. From this directory, I do a clone per project to set things up. For example, I have a clone of libgdx for my MatchCard game, another clone for Doodle Bug, another clone for Pyromancer's Laboratory, and yet another one for DB42 and DB42Lite.
The nasty part of this business is that means I have to go to my local SVN repo, perform an SVN update, and then do an add/remove/commit on the Hg repo. Since this is a manual operation, it doesn't happen all that often. I'm between games right now and wanted to more or less set things straight in terms of keeping in sync without having to rely on a manual process. Here's the solution I came up with:
1) I'm sticking with the shared SVN / Hg working directory. This is really straightforward and I have my brain completely wrapped around what's going on.
2) I've got cygwin installed on my Win7 x64 workstation to give me all the great tools that Unix developers know and love... grep, head, tail, bash scripting, etc.
3) I've got Jenkins installed on this same machine. This does periodic job runs that query the state of the SVN repo, pull in new updates, and then add/remove/commit those changes to the Hg side of the shared directory. The build trigger is simply "Build Periodically" that runs the following script:
for ((COUNT=`svn log -r base:head | grep -E "^r[0-9].*" | wc -l`; COUNT > 1; COUNT-- ))What this script does is read the number of changes between the local base and the GoogleCode head. If it is > 1 (ie. there are new changes), it iteratively pulls each SVN changeset and then commits those to the Hg repo using SVN revision, author, and comment info pulled from SVN's log command. This took a while to get going -- my bash scripting skills are extremely rusty.
do
GETREV=`svn log -r base:head -l 2 | grep -E "^r[0-9].*" | tail -n1`
TOKENS=( $GETREV )
REV=${TOKENS[0]}
AUTHOR=${TOKENS[2]}
COMMENT=`svn log -r $REV | tail -n +4 | head -n-1`
CHECKINCOMMENT=$(echo -e "$COMMENT\nSVN $REV")
svn update -r $REV
hg addremove
hg ci -u "$AUTHOR" -m "$CHECKINCOMMENT"
done
The one thing I'm lacking at this point is an "hg push" up to a default repo out on the Intertubes. I have it in mind to share the repo on Assembla, but I currently don't have the libgdx's team blessing to do this (yet) and I also have a support request into the Assembla team to see if they have any server side tools that might accomplish the same thing I'm doing locally. This whole process is highly reliant on my build server not dying. If I could push those operations out to some other server, that'd be better in the long run. For now, though, it works
Hope this helps someone trying to accomplish something similar.
Friday, December 9, 2011
Andronavi post
I think this explains the high number of Japanese folks playing the game:
http://andronavi.com/2011/12/144583
Google Chrome butchers it in translation I'm sure. I can't tell if the reviewer liked it or not.
http://andronavi.com/2011/12/144583
Google Chrome butchers it in translation I'm sure. I can't tell if the reviewer liked it or not.
Tuesday, December 6, 2011
Current standings
Here are the current standings of DB42 in the various markets. China has got some huge growth going on. Cool stuff.
| Market Name | Upload Date | Publish Date | Downloads | Current Rating (# reviews) | Notes |
| Android Market | 11/27/2011 | 11/27/2011 | 813 (324 active) | 5/5 (2) | |
| SlideMe | 12/4/2011 | 12/4/2011 | 287 | none yet | |
| AppChina | 12/4/2011 | 12/5/2011 | 507 | none yet | |
| Mobango | 12/4/2011 | 12/6/2011 | 0 | 4/5 (1) | There's one review, but the download count is currently 0. Curious. |
| Amazon App Store | 12/4/2011 | Still Waiting |
Sunday, December 4, 2011
More alternatives
Uploaded DB42 to Mobango and Amazon. I said i wasn't going to upload it to Amazon... but since I'm doing the lite version I don't think it counts. ;)
I understand the wait to get something published on Amazon is on the order of a couple weeks. We'll see if it is up there by mid-December.
Mobango requires a review before an app is published so I'm waiting on that. On AppChina, my app has entered the 'audit' state, whatever that means. Hope it is accepted!
I understand the wait to get something published on Amazon is on the order of a couple weeks. We'll see if it is up there by mid-December.
Mobango requires a review before an app is published so I'm waiting on that. On AppChina, my app has entered the 'audit' state, whatever that means. Hope it is accepted!
Saturday, December 3, 2011
Alternative Android Markets
Taking a tip from Neil Rajah and his blog (Ziggy's Games), I've gone ahead and added DB42 to a couple of new markets: AppChina and SlideMe. I registered the game on AppChina about an hour ago and am still waiting for it to be approved. *HOPEFULLY* Google translate didn't totally slaughter the simple descriptions I put in. I figure the simpler they were, the less likely the translation would be something weird for a Chinese person to understand. On the flip side, I've fired off an e-mail to a former co-worker of mine that knows Chinese. Hopefully he can help me round things out.
I registered the app about 5 minutes ago on SlideMe. It also requires a pre-approval process, but I just got an indication as I was typing up this blog post that it has been published. We'll see how things go!!!
I registered the app about 5 minutes ago on SlideMe. It also requires a pre-approval process, but I just got an indication as I was typing up this blog post that it has been published. We'll see how things go!!!
For Great Justice
DB42 is listed on a Japanese site. English -> Japanese -> English makes for some weird reading.
Here's the Japanese -> English translation:
Wrestling? I don't remember that! Here's the original English copy. Funny stuff!
Here's the Japanese -> English translation:
DB42 is a platform puzzle game placed in the path of the robot is great! I droid - 42 Dust Bin specified, DB42 As .... Or just "Deeby", service mark spam bots humble friend and fellow electricity. あなたの大きなものの運命にあるし、最後に、あなたのやる気を証明するあなたの監督の認可が知っている ! The fate of the big things in your last, you know your approval of the director to prove your motivation? あなたの方法 Cynobotic 軍の高度なロボットの訓練の場を作る。 Create opportunities for advanced training of military robots Cynobotic your way. これらのあなたのウィットと知恵を最大限にテストされます ! The wit and wisdom are tested to maximize those of you! ジャンプ、実行、スイング、格闘し、テレポート、トップへの道 ! Jump, run, swing, wrestling, teleport way to the top! ゆっくりと、ゲームのコンポーネントを紹介、入門レベルを探るロープを学ぶ。 Slowly introduce the components of the game, learn the ropes explore the introductory level. そこからは、ブラスト オフに多くのレベルの楽しさと脳のからかいの狂気!道に沿ってあなたの機能を強化するアップグレードを選択します。 From there, the madness of brain teasing fun and many levels of blasting off the enhancements to select the upgrade you along the way. ようなものを見つける:-、迷惑バディ。 Find something like: - Buddy annoying. これは、オブジェクトへの引くため短い範囲トラクター ビームです。 This is a tractor beam to pull a short range to the object. 非常に便利なだけである事を得るため… つ..到達 ! To obtain a very useful ... it is only one arrival ..! -、REPUGNICATOR。 -, REPUGNICATOR. これは、リモート スイッチを押しのようなことをすることができます repulsor レイです.フープを撮影します。 This is the repulsor rays that can be like pressing the remote switch. To shoot hoops. はい。 Yes. 真剣に ! Seriously! -無限現実間質性表面 (アイリス)。 - Infinite Reality stromal surface (iris). テレポートあなたアイリス強化の程度を使用します。 Iris uses a degree of strengthening your report. スペースを折り畳みしないので、簡単にされています ! Space does not collapse, has been easier! 環境レベルとしては、水の危険性、重力のフィールド、およびオブジェクト フィールド サプレッサーのようなものが発生します。 As the environmental level, the risk of water, gravity field, like the object occurs, and the suppressor field. DB42 はカジュアルなプラットフォーム\/パズル、パスにロボットの偉大さに置くゲーム!消化しやすいバージョンには 20 のレベルが含まれています。 DB42 is a platform \ / casual puzzle game placed in the path of the robot is great! Is easily digestible version includes 20 levels. フルバージョン (近刊) 広告を削除し、100 のレベルにする方法の詳細が含まれています ! Full version (in press) to remove the ads, include details of how to level 100! アクセス許可の説明: インターネットとネットワークの状態: 広告 & バグ報告のサービスを提供します。 Description of access: Internet and Network Status: Provides the services of bug reports and advertising.
ハードウェアの振動: 触覚 feedbackBug レポート & フィードバック: ごメール gushikustudios@gmail.com には、または私達のフォーラム、web サイトを参照してください ! Vibration Hardware: feedbackBug report and tactile feedback: gushikustudios@gmail.com Please e-mail, or our forums, please see the web site! [タグ: ロボット、ドロイド、テレポート、ポータル、格闘、トラクター ビーム、レパルスベイ線] [Tags: robots, droids, teleport, portals, wrestling, tractor beam, Repulse Bay Line]
Wrestling? I don't remember that! Here's the original English copy. Funny stuff!
You are a lowly Mark I service droid - Dust Bin Designation 42, aka DB42.... or just "Deeby" to your electro-FRIENDS and fellow junk bots. You KNOW you are destined for greater things and have finally won the approval of your overseer to prove your mettle!
Make your way through the Cynobotic Army's Advanced Robotic Training grounds. These will test your wits and resourcefulness to the UTMOST! Jump, run, swing, GRAPPLE and TELEPORT your way to the top!! Learn the ropes as you explore the introductory levels that slowly introduce you to the components of the game. From there, you BLAST OFF into many more levels of fun and brain-teasing craziness!!!
Along the way you will pick up UPGRADES that enhance your capabilities. Find things like:
- The JUNK BUDDY. This is a short range tractor beam for pulling objects towards you. Very useful for obtaining things that are just... out.. of... REACH!
- The REPUGNICATOR. This is a repulsor ray that allows you to push remote switches and do things like... SHOOT HOOPS. Yes. Seriously!!
- The Infinite Reality Interstitial Surface (IRIS). Use your IRIS enhancements to TELEPORT around a level. Folding space has NEVER been so EASY!
As you level environments, you'll encounter things like water HAZARDS, ANTI-GRAVITY fields, and object field SUPPRESSORS.
DB42 is a CASUAL PLATFORM / PUZZLE GAME that puts you on the path to robotic GREATNESS!
The lite version contains 20 levels. The full version (forthcoming) removes ads and contains 100 levels with more on the way!
PERMISSIONS EXPLAINED:
Internet and Network State: for serving ads & bug reporting
Hardware vibrate: for haptic feedback
Bug reports & feedback: please send an email to gushikustudios@gmail.com, or visit our Forums on the web site!
[ tags: robot, droid, teleport, portal, grapple, tractor beam, repulse ray ]
DB42 Lite version released
I released the lite version of my game on 11/27/2011 (DB42 Lite on the Android Market). So, we're not quite a week out here.
Here are the current stats:
60% of the installations are located in Japan
12% of the installations are located in the US
All apps in my category have the following stats:
42% of installations are located in the US
5% of installations are located in Japan
Quite a contrast there! Hmmm. I guess "robot" based games are more popular in Japan? Stay tuned....
Here are the current stats:
60% of the installations are located in Japan
12% of the installations are located in the US
All apps in my category have the following stats:
42% of installations are located in the US
5% of installations are located in Japan
Quite a contrast there! Hmmm. I guess "robot" based games are more popular in Japan? Stay tuned....
Friday, November 25, 2011
Ouch! The pains of a new installation
If you read my previous post, you'll discover I'm not a fan of Dell. Given the poor customer support from Dell, I switched companies opted for an ASUS G53S laptop. This has been working nicely so far. The intent has been to get a portable machine that satiates my gaming needs as well as works for dev efforts... I bring my laptop to work and work on personal stuff over the lunch hour.
Well, starting yesterday I began working on getting the laptop loaded up with my project and the necessary tools to build my game, DB42. Oi! What pain! I've finally got things working and thought I'd post my experiences here for future reference and in the event it might help someone else out.
A bit of background first. I'm using Eclipse and the Android SDK. I've forked the libgdx project into my own distribution with a couple of tweaks here and there. I also don't have to worry completely about relying on a specific nightly build since I've got a snapshot of things. This means that I also have the support tools for *that* installed -- the Android NDK, MinGW for compiling, Cygwin & ant for building.
So, the first thing I did was install the latest and greatest Eclipse 3.7.1 (codenamed Indigo). This went off fine. Next up was TortoiseHG 2.2 so I could pull my repo from the hosting site I'm using. First problem encountered. After installing this, I noticed that the icon overlays were missing on the root directory folders. WTF. Even a reboot did not correct this. The sub-directories seemed to be ok, but there was no indication on the root as to "no change" or "modifications". So, I downgraded to 2.1.4. This cleared up that problem.
Next up was the installation of Android SDK r15. Downloading as a snap. Installing the ADT was pretty simple as well (done from within Eclipse).
After getting all of that going (which took a while -- installing the various Android components results in a pretty big download), I ran into a snag. I first cloned my repo to my hard drive. Then, I tried to import the various components. Instead of giving me a source folder and sub-packages, Eclipse showed everything as a package. For example, instead of the hierarchy of "src" with "com.gushikustudios.db42lite" as a package, I saw the package "src.com.gushikustudios.db42lite". I don't know what was up with that, but it hosed the build. To correct that, all you have to do is right-click on the project > New > Source Folder > src. For whatever reason, that corrected the layout.
Next up were errors with the project. I have no clue why, but my project settings were missing. I had to right-click on the project > properties > Java Build Path. Here I had to do several things: Add Library > JRE System Lib (which was missing for whatever reason.) Project references were missing. Recall that I've got my own build of libgdx, so I reference the project instead of things like gdx.jar. My lib settings were also missing, so I had to reference a lib that I have copied in my libs dir.
Ugh.
After correcting all of that, I tried to build my Android project and get an .apk file. Nothing. No apk file or anything produced in the bin/ sub-directory. It turns out that this behavior is the new default for the latest ADT. To 'fix' this, in Eclipse go to Window > Preferences > Android > Build > deselect the 'Skip packaging' check box. I also changed build output from 'silent' to normal. That fixed that problem and I was able to produce an .apk file.
Next up was the desktop build of gdx. I failed to realize that the resulting gdx.dll that I created referenced other dlls that I didn't have installed on my new laptop. Oh no, of course when you try to execute, the JVM doesn't give you any sort of error that is helpful in any way. I kept getting "UnsatisfiedLinkError" / "Can't find dependent libraries" errors. Great -- which dependent libraries can't you find? I mean, throw me a bone here. After poking around in the gdx.dll, it turned out that there were dynamic references to some build dlls: libstdc++-6.dll & libgcc_s_dw2-1.dll. Oops. Of course, I didn't discover this until after a couple of hours troubleshooting.
All I had to do was install MinGW and reboot and all was fine. Very frustrating, but things are finally starting to work.
Well, starting yesterday I began working on getting the laptop loaded up with my project and the necessary tools to build my game, DB42. Oi! What pain! I've finally got things working and thought I'd post my experiences here for future reference and in the event it might help someone else out.
A bit of background first. I'm using Eclipse and the Android SDK. I've forked the libgdx project into my own distribution with a couple of tweaks here and there. I also don't have to worry completely about relying on a specific nightly build since I've got a snapshot of things. This means that I also have the support tools for *that* installed -- the Android NDK, MinGW for compiling, Cygwin & ant for building.
So, the first thing I did was install the latest and greatest Eclipse 3.7.1 (codenamed Indigo). This went off fine. Next up was TortoiseHG 2.2 so I could pull my repo from the hosting site I'm using. First problem encountered. After installing this, I noticed that the icon overlays were missing on the root directory folders. WTF. Even a reboot did not correct this. The sub-directories seemed to be ok, but there was no indication on the root as to "no change" or "modifications". So, I downgraded to 2.1.4. This cleared up that problem.
Next up was the installation of Android SDK r15. Downloading as a snap. Installing the ADT was pretty simple as well (done from within Eclipse).
After getting all of that going (which took a while -- installing the various Android components results in a pretty big download), I ran into a snag. I first cloned my repo to my hard drive. Then, I tried to import the various components. Instead of giving me a source folder and sub-packages, Eclipse showed everything as a package. For example, instead of the hierarchy of "src" with "com.gushikustudios.db42lite" as a package, I saw the package "src.com.gushikustudios.db42lite". I don't know what was up with that, but it hosed the build. To correct that, all you have to do is right-click on the project > New > Source Folder > src. For whatever reason, that corrected the layout.
Next up were errors with the project. I have no clue why, but my project settings were missing. I had to right-click on the project > properties > Java Build Path. Here I had to do several things: Add Library > JRE System Lib (which was missing for whatever reason.) Project references were missing. Recall that I've got my own build of libgdx, so I reference the project instead of things like gdx.jar. My lib settings were also missing, so I had to reference a lib that I have copied in my libs dir.
Ugh.
After correcting all of that, I tried to build my Android project and get an .apk file. Nothing. No apk file or anything produced in the bin/ sub-directory. It turns out that this behavior is the new default for the latest ADT. To 'fix' this, in Eclipse go to Window > Preferences > Android > Build > deselect the 'Skip packaging' check box. I also changed build output from 'silent' to normal. That fixed that problem and I was able to produce an .apk file.
Next up was the desktop build of gdx. I failed to realize that the resulting gdx.dll that I created referenced other dlls that I didn't have installed on my new laptop. Oh no, of course when you try to execute, the JVM doesn't give you any sort of error that is helpful in any way. I kept getting "UnsatisfiedLinkError" / "Can't find dependent libraries" errors. Great -- which dependent libraries can't you find? I mean, throw me a bone here. After poking around in the gdx.dll, it turned out that there were dynamic references to some build dlls: libstdc++-6.dll & libgcc_s_dw2-1.dll. Oops. Of course, I didn't discover this until after a couple of hours troubleshooting.
All I had to do was install MinGW and reboot and all was fine. Very frustrating, but things are finally starting to work.
Monday, November 14, 2011
Will NEVER buy another Dell or recommend their products
I just wasted about an hour and a half talking to Dell tech support about my Dell XPS M1730. I easily paid over $3000 for this beast 3 years ago... Top of the line. etc. etc. etc. My problems with this thing first started up when I installed Win7 on it a couple of years ago. For whatever reason, the video drivers never quite seemed right with the hardware and although I have 2x8800M video cards installed, I had to disable one due to incompatibilities. Whatever.
Then, about 7 or 8 months ago, the battery quit holding a charge. Which, I understand, but instead of dying off quietly, I would getting annoying nags about it, so I just pulled the battery completely. Last week the Power Supply quit being recognized. Huh? So, what does the software do? Derate of course. I purchased a new power supply. That didn't fix it.
I'm stuck with a machine that is about as crippled as you can get. The derating brings my programming builds to a crawl. Oh, and I'm only 149 days out of the extended three year warranty I purchased. The support people were sympathetic and polite, which is great, but how does that help me? It doesn't.Their solution was to upgrade the BIOS. Oh, but wait, you can't run the reflash program unless you have a recognized power supply and battery. Oops. There goes that idea.
I figured my only option was to get a new motherboard. Tech support gave me the out-of-warranty repairs number, who then redirected me to somebody that claimed I shouldn't have been directed to. Argh! They said my best bet was to skip a motherboard replacement altogether and instead buy a new computer. Ummm. Excuse me? I have no desire to purchase another Dell. I recommend everyone avoid them at all costs. I was considering to 'upgrade' to an Alienware machine -- but they share the Dell taint so I guess I need to look elsewhere.
If you are considering buying a Dell, think again.
A disappointed, FORMER Dell customer.
Then, about 7 or 8 months ago, the battery quit holding a charge. Which, I understand, but instead of dying off quietly, I would getting annoying nags about it, so I just pulled the battery completely. Last week the Power Supply quit being recognized. Huh? So, what does the software do? Derate of course. I purchased a new power supply. That didn't fix it.
I'm stuck with a machine that is about as crippled as you can get. The derating brings my programming builds to a crawl. Oh, and I'm only 149 days out of the extended three year warranty I purchased. The support people were sympathetic and polite, which is great, but how does that help me? It doesn't.Their solution was to upgrade the BIOS. Oh, but wait, you can't run the reflash program unless you have a recognized power supply and battery. Oops. There goes that idea.
I figured my only option was to get a new motherboard. Tech support gave me the out-of-warranty repairs number, who then redirected me to somebody that claimed I shouldn't have been directed to. Argh! They said my best bet was to skip a motherboard replacement altogether and instead buy a new computer. Ummm. Excuse me? I have no desire to purchase another Dell. I recommend everyone avoid them at all costs. I was considering to 'upgrade' to an Alienware machine -- but they share the Dell taint so I guess I need to look elsewhere.
If you are considering buying a Dell, think again.
A disappointed, FORMER Dell customer.
Thursday, October 27, 2011
Repulser, pass #1
I modded the libgdx particle emitter class to allow the programmer to have old particles track a specific angle. I did this so that I could get the effect you see above. It's a repulse ray. I need to polish it up a bit -- the player's tracer line appears when it shouldn't (this is a screenshot of the first, proof-of-concept pass). But, it looks functional and I like how it emanates outward. I was going to try to animate this action with multiple images, but using particles gives a waaaaay better effect. Things should be fixed up tomorrow.
Wednesday, October 19, 2011
*DING*
Slide rule Android app - 10k user Achievement Unlocked.
Checked the developer console today and saw this:
"10012 total installs (users)"
Cool! Broke the 10k barrier.
Checked the developer console today and saw this:
"10012 total installs (users)"
Cool! Broke the 10k barrier.
Subscribe to:
Posts (Atom)

