Chapter     10

Odds and Ends

Okay, look, I realize that Astro Rescue isn’t likely to challenge Angry Birds for a spot on the top-selling apps list in any app store out there. It’s a neat enough game for the learning exercise it was meant to be, but it’s not likely to take the gaming world by storm.

So, let’s ask ourselves: what sorts of things might you want to add to it if the goal was actually to sell it and make some cash money from it? Certainly there are all sorts of things you might think of: better graphics and sound would likely be near the top of the list; more levels for sure; a wider variety of obstacles to overcome. The list is only limited by your imagination.

But since this is still a book focused on learning Corona, what sorts of things might be discussed that, at the same time, will further your knowledge of what the SDK has to offer? That’s what this chapter, in a somewhat isolated fashion from Astro Rescue itself, is all about!

In it, you won’t actually be modifying the Astro Rescue code—you’re effectively done with that phase of things. However, you’ll look at, in no particular order, a few topics, a few odds and ends (hey, that’d make a great chapter title, wouldn’t it?!) that would likely make the game better, and ultimately more saleable, if you wanted to go down that path. This will also nicely round out our look at the Corona SDK and, while it can’t be said that this chapter will examine every last nook and cranny, it will conclude a fairly robust and rather comprehensive look at what Corona provides you as an application developer.

Files Are So Passé: SQLite Database

In Astro Rescue, the save game state is saved in a plain old text file that happens to contain a JSON-ified version of a Lua table. This generally works very well, especially in a case like this where the data to be saved are small and so can easily be read and written all at once.

The story might be considerably different, however, if you were writing a contacts application, for example, where you want to save phone numbers, addresses, and the like for people you know. In that case, while you could imagine doing something like Listing 10-1—and in Lua that makes a fair bit of sense—the solution falls apart quickly when you have to write the contacts object to disk in its entirety every time, not to mention read it all just to look up one specific contact!

Listing 10-1.  Writing Some Contacts out to Disk the “Old-Fashioned Way”

json = require("json");

local contact1 = {
  firstName = "Bill",
  lastName = "Cosby",
  phoneNumber = "555-123-4567"
};

local contact2 = {
  firstName = "Robert",
  lastName = "Kennedy",
  phoneNumber = "555-888-9999"
};

local contacts = { contact1, contact2 };

local path = system.pathForFile("contacts.json", system.DocumentsDirectory);
if path ∼= nil then
  local fh = io.open(path, "w+");
  fh:write(json.encode(contacts));
  io.close(fh);
end

Then, to find contact information for John’s younger brother, you’d need to read in the entire contacts object, decode the JSON back into a Lua object, then scan through it to find the correct object. Once this gets larger than just a handful of contacts, it’s obviously not going to be a particularly good way to do it.

What’s This SQLite You Speak of?

A far better answer is a true and proper database of some sort. Thankfully, Corona provides just such a capability via its sqlite3.* package. This package contains functions that deal with SQLite databases.

If you’re not familiar with that, let me explain. SQLite is an open source library that implements a SQL-based database engine. This library is small, cross-platform, self-contained (it doesn’t require a server to run), and, perhaps most important for Corona, embeddable. SQLite is used by many products out there, including iOS itself, where it is supplied by default; the popular Firefox browser, where it’s used to store user configuration information, among other things; the desktop Skype client; and the well-known Dropbox, which uses it to store data in its client application.

In terms of how you use SQLite, it looks a whole lot like any relational database you’ve ever used. There are tables, which you query with SQL (Structured Query Language). It has support for things like transactions, indexes, constraints, triggers, and most of what the SQL92 dialect offers.

In Corona land, SQLite is available on all supported platforms and in the simulator. On iOS, as mentioned, SQLite ships with the operating system. On Android, however, a compiled version needs to be added to your generated binary (something you’ll see how to do in the very next chapter!), and it adds about 300 KB to the size of that binary.

Let’s Create a Database Already!

A database can be in-memory only, which means it will only exist for the duration of the application’s current execution, or it can be persisted to disk. The latter case is probably the more common, so let’s see how that’s done, in Listing 10-2.

Listing 10-2.  Fisher Price’s My First SQLite Database™

require "sqlite3"

print("SQLite Version: " .. sqlite3.version());

db = sqlite3.open(system.pathForFile("app.db", system.DocumentsDirectory));

db:exec([[
  CREATE TABLE IF NOT EXISTS contacts (
    id INTEGER PRIMARY KEY autoincrement,
    firstName TEXT,
    lastName TEXT,
    phoneNumber TEXT
  );
]]);

db:exec([[
  INSERT INTO contacts (firstName, lastName, phoneNumber) VALUES (
    "Bill",
    "Cosby",
    "555-123-4567"
  );
]]);

db:exec([[
  INSERT INTO contacts (firstName, lastName, phoneNumber) VALUES (
    "Robert",
    "Kennedy",
    "555-888-9999"
  );
]]);

for r in db:nrows("SELECT * FROM contacts") do
  print(
    r.id .. " = " .. r.firstName .. " " .. r.lastName .. " : " .. r.phoneNumber
  );
end

Of course, it all starts with importing the sqlite3 module. After that, if you want to, you can get the version of the SQLite engine in use. If you run this in the simulator you’ll see a version number of 3.7.14.1 (at least as of this writing).

After that, it’s time to open a database. You can have as many as you want, and you can store them where you want. However, as is best practice in nearly all cases where you have data related only to your application that you want to save to disk, the system.DocumentsDirectory value should be used to limit visibility to your application and ensure it has full read/write access on all platforms. The database name, contacts.db here, is entirely up to you.

Once the database is open it’s time to create a table. The basic syntax for nearly everything you’ll do with a SQLite database in Corona takes the general form:

file:exec([[ SOME SQL HERE ]]);

You can, and for readability probably should, use line breaks in the SQL, as I’ve done in the example code. Here I am assuming you have some knowledge of SQL generally, but even if you don’t I suspect the SQL used to create a table is pretty self-explanatory. The table will only be created if it doesn’t exist already, to keep you from overwriting any existing data.

The id field is the primary key of the table and is a number, an INTEGER to be precise. It also uses the AUTOINCREMENT qualifier, which means that every time a row is inserted into the table, this field will be a number generated by incrementing the value of the last inserted row. In this way, the value of the field is always unique and hence works as a primary key.

Note  Case is not generally important in SQL. However, my own convention is to always put SQL keywords in caps, and I also try to make sure the field names, however I choose to capitalize them, match everywhere, meaning in the SQL as well as in any objects I might create from a SQL query.

The other three fields are defined as TEXT type fields and aren’t keys or anything, so their definition is straightforward. However, the interesting thing about SQLite is that the column type is really more of a suggestion than anything else! You can, in fact, store a string in the id field, or you can store a number in either of the name fields. The data types you specify are really “affinities,” which gets into how the values are stored on disk versus what you get when you retrieve them—most of which you can ignore 9 times out of 10, frankly.

The long and short of it is that you can either

  • specify data types (from among TEXT, NUMERIC, INTEGER, or REAL) when creating tables and then ensure you never try and store the wrong type in a field (because SQLite will do it regardless!), or
  • not specify a data type at all and go about your business storing anything you like anywhere!

SQLite will happily work either way and you most likely won’t ever know the difference. My suggestion, however, is to exercise a little bit of self-discipline: specify the data types and stick to them. To me, that’s a lot less likely to come back and bite you somehow later!

Once you have a table created, you can go about inserting data into it with a regular old SQL insert statement. Since the id field auto-increments, you don’t need to pass a value for it.

Gettin’ at Your Data

You can read back your data in two basic ways. As shown in the sample code, if you want to retrieve multiple rows and iterate over them, then the nrows() method of the opened database object is your best bet. It returns an iterator over a result set, which you can then easily use in a loop. Within the loop, the variable r is a pointer to the next object in the result set, and you can then access the fields in a straightforward way.

Even if your query is more specific, say . . .

SELECT * FROM contacts WHERE id=4

. . . you can still use this approach, but then of course you know you’ll always get only one result (unless you get none, which is perfectly valid). In this case however, there’s another way you can do it:

rs = db:exec(
  "SELECT * FROM contacts WHERE id=4",
  function(udata, numCols, cVals, cNames)
    print(udata, numCols, json.encode(cNames), json.encode(cVals));
  end
);

Here, the function inline in the exec() call passed as the second argument will be executed for each result in the result set of the query. So, while you can in fact handle multiple items this way as well, it is, in my mind at least, a more natural fit for handling one, if for no other reason than it physically looks like it’s meant for that. Whether you agree with that or not, it’s nice to know you can use this sort of callback approach. By the way, the arguments that are passed to the function are the following:

  • udata is user data that you can use to persist information between invocations of the callback
  • numCols is the number of columns in a given row of the result set
  • cVals is the column values
  • cNames is the column names

Last, note that in this approach, if the callback returns a nonzero value then the iteration stops and all subsequent SQL statements are skipped. Returning nothing at all is considered a zero return, so the loop will continue as shown in the previous code snippet.

Tip  The underlying API that Corona uses is provided by the open source luasqlite3 project. Their documentation is actually more expansive than the Corona documentation, so if you’re ever unsure about how to do something with SQLite in Corona, take a look here for more extensive information: http://luasqlite.luaforge.net/lsqlite3.html.

Data Amnesia: In-Memory Databases

The database in the example here is stored on disk and persists between application runs. What if you have some transient data that you only need while the application is running? An in-memory database might be a better answer in that case. To create one, you have to make only one slight change. This:

db = sqlite3.open(system.pathForFile("contacts.db", system.DocumentsDirectory));

becomes this:

local db = sqlite3.open_memory()

From that point on you use the database exactly the same as any other. When the application shuts down, however, that database is destroyed and you start fresh the next time the application runs (so, you probably wouldn’t want to do this for a contacts database as in this example, but you get the idea!).

Talkin’ to the Outside World: Network Access

A mobile device is a personal thing. Generally speaking, it belongs to a single user, and most of the time a single user is using it at a time. Therefore, when you write a game for a mobile device, they are, more times than not, geared toward that single-player experience.

It’s also usually the case that the assets the game needs are downloaded when you install the game as part of the game package itself (read: embedded within the executable file you install). While that is still the most common model, more and more games are being released that, in large part due to the size of their graphics, audio, and other assets, require you to download from an app store what is really a pretty small executable, which then downloads all the additional resources when you first run it. This allows you to get the application on the user’s device as quickly as possible while also working around the size limitations all app stores currently have (although those size restrictions have been relaxed considerably in all stores over the past few years).

It also allows you greater flexibility. Perhaps you only want to download assets as they are actually needed. Imagine a game when you are walking around a virtual world. Why should the user need to spend 20 minutes upon first launch downloading the entire world, when you can instead only force her to download a small portion in, say, 30 seconds? The rest of the world, the tavern on the far side of a level for example, can be downloaded when she actually arrives. Or, perhaps you want the world to grow over time. Users can download the new areas as they become available without having to explicitly install a new version of your game.

All of this requires network access, and Corona is there to help you in this area! The SDK actually provides two pertinent packages: the network.* package and the socket.* package.

Some Basic Network Functionality

The network.* package provides some very basic, high-level networking support. First, it allows you to check network status, shown in Listing 10-3.

Listing 10-3.  Hello? Hello?! Anybody out there?!

function networkStatusListener(inEvent)
  print("address", inEvent.address);
  print("isReachable", inEvent.isReachable);
  print("isConnectionRequired", inEvent.isConnectionRequired);
  print("isConnectionOnDemand", inEvent.isConnectionOnDemand);
  print("IsInteractionRequired", inEvent.isInteractionRequired);
  print("IsReachableViaCellular", inEvent.isReachableViaCellular);
  print("IsReachableViaWiFi", inEvent.isReachableViaWiFi);
end

if network.canDetectNetworkStatusChanges then
  network.setStatusListener("www.google.com", networkStatusListener);
else
  print("Network reachability checking not supported");
end

First, the network.canDetectNetworkStatusChanges field tells you whether the platform the app is currently running on is capable of reporting network status. At present, only Mac and iOS are capable of doing that. So, if your app is on an Android device, or the simulator on Windows, the value here will always be false and there’s no reliable way to tell if network connectivity is available to your application in this situation, other than to simply try a request and handle it if it fails.

If it is available, however, then you can create a listener function and pass it to network.setStatusListener(). That function will then be called any time network status changes. However, note that this works on a per-target host basis. In other words, in the example it’s checking to see if the host www.google.com is reachable. The consequence of this is that it’s possible for www.google.com to be down and not responding, which will register in the code as network connectivity not being available when it actually is. This actually makes sense if you think about it: generally, it’s only a single host you’ll want to talk to anyway. You can also pass a comma-separated list of hosts if you truly are interested in more than one, or if you want to have a more generic check of network connectivity (it’s less likely that five different hosts would all be down at the same time, so if you can reach even one you know network connectivity is available).

Within the listener function, networkStatusListener() in the example, you can determine what host was reached (inEvent.isReachable and inEvent.address)—that is, if it was actually reachable, because remember that this function will be called if it’s not reachable, too; it’s called any time network status changes, not necessarily when connectivity is working. You can also determine over what type of connection it’s reachable (inEvent.isReachableViaWiFi and iEvent.isReachableViaCellular, which are good if you need to download a lot of data and only want to do so over a Wi-Fi connection), if the user will need to interact with the application to reconnect to the host (inEvent.isInteractionRequired, e.g., if a password is required), and if the connection will be brought up automatically or not (inEvent.isConnectionOnDemand).

Once you know whether you can reach an appropriate host or not, you can proceed to make requests to that host for resources. For example, let’s say you want to retrieve a file available via HTTP on a web server. Listing 10-4 shows that this is a simple task indeed.

Listing 10-4.  US Defense Spending at Work!

function networkListener(inEvent)
  if (inEvent.isError) then
    print("Network error!");
  else
    print(inEvent.response);
  end
end

network.request(
  "http://tycho.usno.navy.mil/cgi-bin/timer.pl", "GET", networkListener
);

If you run this in the simulator you’ll see that what is output to the console window is an HTML document retrieved from the US Naval Observatory’s web server showing the current time in various time zones. It’s just a dump of the “naked” HTML document of course, since there’s no web browser to parse and render the document, but that’s good enough for our purposes here. All you do is pass the URL, HTTP method, and callback function reference to network.request(), and you get back whatever resources are at that location on that server, or an error if something goes wrong.

If you need to send data, you can do that too, as a POST request. All you do is add as a fourth argument to the network.request() call that is an object with an attribute named body that contains the POST data (properly URL-encoded of course, if necessary). Don’t forget to change the third argument to "POST" as well and you’re good to go passing whatever data to the server you’d like.

You can even mess around with the request headers by adding a headers attribute to the object passed as the fourth parameter in name = value pair form. A complete example of both POST and header manipulation is shown in Listing 10-5. Although the site being called doesn’t care about these headers or the POST body, the request works just fine, and if you throw a network sniffer on your PC you’ll see the headers and POST body sent across the wire regardless.

Listing 10-5.  POST and Header Manipulation in One Go

function networkListener(inEvent)
  if (inEvent.isError) then
    print("Network error!");
  else
    print(inEvent.response);
  end
end

hdrs = { };
hdrs["Accept-Language"] = "en-US";
local rData = {
  headers = hdrs,
  body = "Miscellaneous%20Value"
};

network.request(
  "http://tycho.usno.navy.mil/cgi-bin/timer.pl", "POST", networkListener, rData
);

Caution  The one other attribute you can set on your data object passed as the fourth argument to network.request() is timeout, which specifies how long the request should wait before failing. The default is 30 seconds; however, this attribute is currently only supported on iOS and Mac, so use it carefully. With great power comes great responsibility, as Uncle Ben so wisely reminds us every chance he gets. Seriously, the guy is annoying—even from beyond the grave!

What if you want to download a file instead? Well, you certainly could just grab the response in the callback function via inEvent.response as in the previous example, but then you’re responsible for what gets done with it, which means extra work on your part. Perhaps you want to stash it in a SQLite database as you played with in the previous section. That’s perfectly fine and maybe even the best choice in some cases, but there’s a good chance you’ll want to save it to a file, and the network.* package has you covered if you do.

Take a look at Listing 10-6. It’s a simple example of grabbing an image off a web site and displaying it on the screen.

Listing 10-6.  Who Says Buying a House Is Hard??

function callback(inEvent)

  if (inEvent.isError) then
    print("Download no happen, sorry!");
  else
    display.newImage("house.png", system.TemporaryDirectory, 48, 48);
  end

end

network.download("http://etherient.com/img/home.png",
  "GET",
  callback,
  "house.png",
  system.TemporaryDirectory
);

The network.download() method takes care of getting the file at the specified URL, using the specified HTTP method (which would likely always be GET, but you could use others if the server requires it), the callback listener function to execute when it’s retrieved (or when an error occurs), the name of the file under which to save the content retrieved from the server and the location where to save it. The callback() function then just displays the image from the file system.

Nothing says you have to retrieve images, of course; you could retrieve any arbitrary data file this way,say map data or lists of valid servers to use in a multiplayer situation or whatever else your imagination can conjure up.

Note  It’s important to understand that both the network.request() and network.download() methods are asynchronous (as evidenced by the use of a callback function). This of course means that the code following their invocation will execute even before the response comes back, so you need to write your code with that in mind.

There’s More to Networking than Getting Content

The other option that Corona offers is the socket.* package. This allows you to get a little more low-level than just HTTP requests, although it can do that, too. It also provides support for other protocols including FTP, SMTP, and DNS.

The socket.* package is built on the open source LuaSocket library and provides its functionality to your Corona applications. For example, say you want to send an e-mail. That’s easy to accomplish, as shown in Listing 10-7.

Listing 10-7.  An E-mail . . . a . . . Err . . . Causes the US Post Office to Close Forever?!

smtp = require("socket.smtp");

msg = smtp.message({
  headers = {
    to = "[email protected]",
    subject = "Corona rocks!"
  },
  body = "Yeah, so, that was fun."
});

r, e = smtp.send({
   from = "[email protected]
   rcpt = "[email protected]
   source = msg,
   server = "zammetti.com",
   port = "587",
   user = "XXX",
   password = "YYY"
});

if (e) then
  print("Error: ", e)
end

Whew, that’s almost too easy, isn’t it? Once you import the socket.smtp module, it’s a simple matter of creating an smtp.message object, setting the appropriate headers and message body on it, and then calling smtp.send() to send it, specifying the sender, recipient (rcpt), the message object you created, SMTP server and port, and (optionally) a username and password under which to log in.

Note  For either the network.* or socket.* packages you’ll need to add the android.permission.INTERNET permission to your build.settings file to allow network access on the Android platform.

As mentioned, the LuaSocket library is used by Corona to provide this functionality. For much more detailed documentation than what you’ll find in the Corona SDK documentation, refer to http://w3.impa.br/∼diego/software/luasocket/reference.html.

I’m Always Losing My Gym Sockets: Socket Networking

The e-mail example is just that: an example. It’s meant to be just enough to give you a starting point. If you need to use DNS services (such as getting a host name from an IP address or vice versa), or you want to send and receive files via FTP, or want to URL-encode or escape data, then you’ll need to refer to the location mentioned and use the e-mail example as a rough starting point.

However, one other topic I want to demonstrate is building a socket-based client/server. This becomes very useful if you want to do direct player-to-player gameplay. Take a look at the example in Listing 10-8.

Listing 10-8.  A Simple TCP Client Example

socket = require("socket");

server, err = socket.tcp();
if server == nil then
  print(err);
  os.exit();
end

server:setoption("reuseaddr", true);

res, err = server:bind("*", 0);
if res == nil then
  print(err);
  os.exit();
end

res, err = server:listen(5);
if res == nil then
  print(err);
  os.exit();
end

Runtime:addEventListener("enterFrame",
  function()
    server:settimeout(0);
    local client, _ = server:accept();
    if client ∼= nil then
      local receivedContent, _ = client:receive("*l");
      if (receivedContent ∼= nil) then
        print(receivedContent);
        if receivedContent == "ping" then
          client:send("pong");
        end
      end
      client:close();
    end
  end
);

local _, port = server:getsockname();
print("localhost listening on port " .. port);

This example shows how to construct a server that listens on a port and echoes what it receives to the console, and if it receives the string "ping" responds to the caller with "pong".

Once you import the socket module, the next step is to create a socket with the socket.tcp() call (and yes, you can do UDP if you like as well). After a quick check to ensure that was successful, set the reuseaddr option on it. This allows for IP addresses to be reused, which is especially important for this to work in the simulator, since it and the host machine will share the same IP address.

Next, bind the socket to a port using the server:bind() method. Passing "*" and 0 as arguments as done here means that any available port will be used.

Next, tell the server to start listening for incoming requests and check if it’s running as expected. The number passed to the server:listen() method is the backlog number, which means how many requests can be queued waiting for service from this server.

Next, you need to ensure that our Corona app doesn’t end after this code is executed, so set up an enterFrame listener. Skip that for just a moment and instead jump down to the last two lines. The first, the one with the call to server:getsockname(), returns the IP address and port the server is listening on. The port number is then echoed for reference.

Tip  The use of the underscore in a couple of lines here is something you haven’t seen before. This is a convention when a function returns multiple values that indicates you don’t care about that particular value. Lua will discard it and not bother you about it. No need to create a variable that will just be ignored, after all!

Now, going back to the enterFrame handler, the first line, the call to server:settimeout(0), is very important because it ensures our usage of the server isn’t blocking. If you didn’t do this, you’d find that the app freezes inside the enterFrame handler, which isn’t good to say the least! Setting the timeout like this avoids that.

Next, you get a client object that connects to the socket the server is using. With this client you can then accept connections and check for any received content. Passing *l to client:receive() indicates that you want to read a line of text from the received content, if any. Assuming you did get some content, print() it to the console and reply if the proper trigger phrase is received. Last, the client is closed and the handler function ends, to be triggered again with the next enterFrame iteration.

You can test this by running the example code in the simulator and noting the port the server is listening on. If it’s 64613 for example, fire up your favorite telnet client (such as PuTTY or Tunnelier) and connect to localhost:64613. Once it connects, enter "ping", and see that you get "pong" back, and that what you type is echoed to the simulator console.

LUAFileSystem

During the course of exploring the Astro Rescue code, you learned how to read and write files. As you’ll recall, most of that functionality is housed in the io.* package. That’s not the only package that Corona offers for dealing with the file system, though. The other, lfs.*, provides access to the features of the open source LuaFileSystem (http://keplerproject.github.com/luafilesystem/index.html).

This library provides a number of features, including the ability to manipulate directories (add and delete them), the ability to scan the contents of a directory (to list the files and directories it contains) and manipulating attributes of files and directories (among other things). You can see some of this in action by looking at Listing 10-9.

Listing 10-9.  Using the LuaFileSystem to Mess with, Well, the File System!

local lfs = require("lfs");

-- List files in the documents directory.
for f in lfs.dir(system.pathForFile("", system.DocumentsDirectory)) do
  print(f);
end

print("------------------------------------------------------------");

-- Add a directory to the temporary directory.
tempDir = system.pathForFile("", system.TemporaryDirectory);
if lfs.chdir(tempDir) then
  lfs.mkdir("A_New_Directory");
  for f in lfs.dir(tempDir) do
    print(f);
  end
end

print("------------------------------------------------------------");

-- Update timestamp of access on the new directory.
dirPath = system.pathForFile("A_New_Directory", system.TemporaryDirectory);
lfs.touch(dirPath);
attrs = lfs.attributes(dirPath);
print(attrs.modification);

print("------------------------------------------------------------");

-- Now delete that new directory.
lfs.rmdir("A_New_Directory");
for f in lfs.dir(tempDir) do
  print(f);
end

The first block of code lists all the files and directories within the documents directory. To do this, the lfs.dir() method is used, which returns an iterator that you can then iterate over, where each object returned by the iteration is a file or directory that is a child of the target directory. The system.pathForFile() method, which you’ve seen before, is used to reference the directory you’re interested in scanning.

The second block of code adds a directory named A_New_Directory to the temporary directory. Before doing that, though, you need to change the current working directory so that the directory is created where you intend it to be, and the lfs.chdir() method is what allows you to do this. Once the working directory is changed the lfs.mkdir() method lets you create the new directory using the specified name. The code then redisplays the contents of the temporary directory so you can see that the new directory was created.

The third block of code uses the lfs.touch() method to effectively update the timestamp on the directory. Once that is done, the lfs.attributes() method gets a collection of various attributes about the directory, one of which is the modification attribute, or when the directory was last touched. This value (in milliseconds) is displayed.

The last block removes the new directory using lfs.rmdir() and redisplays the contents once more to see that the directory was, in fact, removed.

You can see the results of executing this program in Figure 10-1.

9781430250685_Fig10-01.jpg

Figure 10-1 .  Proof, admissible in court (although IANAL), that the LuaFileSystem functionality in Corona works!

If you’re familiar with Unix command lines, you’ll quickly realize that the LuaFileSystem methods match closely with the typical commands you would use on the command line. Similarly, the attributes available for a file or directory are those that you can typically see in a command line environment.

The lfs.* package augments the io.* package, and even some of the functionality present in the system.* package, to give you greater access to the underlying file system of the device your application is running on. Use it wisely, my young Padawan!

Note  This example does not do any sort of error checking. That’s just to make it as simple as possible for you to digest. If you’re going to use this package for real, though, you’ll want to refer to the Corona and LuaFileSystem documentation to ensure you react to errors properly.

Ads

The monetization model of mobile apps is an interesting discussion to have. There are a number of approaches, from direct charges for a given application to in-app purchases to just making the application entirely free.

Another common approach is the ad-supported model. Here, you generate revenue based on ads displayed within your application. You are paid by a given ad supplier that you work with. While I’m sure you can imagine creating this code yourself, and it wouldn’t be all that tricky, Corona again is here to make it even easier for you!

I won’t be giving you a working example here as I have in other sections because in fact, I can’t; to do so would require you have a key provided by one of the two supported ad services, InMobi or Inneractive. That’s something you would have to get on your own. However, I can talk in generalities here and give you the basic picture of how it works, which is actually quite simple (something I know I’ve said time and again about Corona!).

There are actually only three basic steps.

Step 1: import the ads module.

local ads = require("ads");

Step 2: initialize the ads subsystem.

ads.init("inmobi", "xxx");

This assumes you’re using the InMobi ad network; otherwise pass inneractive instead as the first argument (or another value corresponding to another ad provider, as Corona supports them). The second argument is the unique key the ad provider assigns to you when you sign up to work with them.

Step 3: show an ad (who saw that one coming, right?!)

Ads.show("banner320x48", { x = 0, y = 10, interval = 30, testMode = false });

The first argument is the type of ad to show, which more or less means how big the ad banner is to be. Note though that the dimensions you see in the values are in points, not pixels, so on a Retina display device like the iPhone 4 the values are doubled in pixels. Also note that for the Inneractive provider, you actually have a choice of type of ad rather than simply size as for InMobi.

The value you pass here depends on what the ad provider supports. The possible values, as present, are as follows:

  • For InMobi: banner320x48 or banner300x250 on all devices, or banner728x90, banner468x60, or banner120x600 on the iPad only
  • For Inneractive: banner, text, or fullscreen

The second argument is an object that can contain a number of attributes. The x and y attributes are simply where on the screen the ad banner is to appear. The interval attribute tells Corona how frequently, in seconds, to change to another ad. The testMode attribute, when true, tells the ad provider you’re using a testing account and they shouldn’t process ad views like they normally would in terms of statistics or payments to you.

Last, you can hide the ad currently showing by calling the aptly named ads.hide() method. No ads will show up again until ads.show() is called once more.

That is essentially all there is to putting ads in your games! It’s a simple API for sure, and once you sign up with one of the ad providers you can get going in no time. Pretty sweet, huh?

In-App Purchases

An ad-supported model of application monetization is all well and good, but it’s certainly not the only option. Another common approach to making money with your games is the in-app purchase route. Here, the user downloads a game, sometimes for free, sometimes for a small cost. As they play and advance in the game they are presented with the opportunity to purchase additional content to continue. This content can take many forms including buying access to additional levels, power-ups that give their in-game character new abilities, options that allows them to customize things in the game, and anything else that you as the developer dream up that you think is a good candidate for monetization. When the user can do this from within the game itself, this is an in-app purchase.

Corona’s store.* package contains functionality for dealing with this. It currently supports the Apple iTunes store and the Google Play market for Android. Other storefronts, such as Amazon or Barnes & Noble, may be supported in the future, but currently are not.

As with the discussion of ads, I can’t show you complete, working example code that you can play with because this all depends on your having accounts set up on one or both of the app stores. You can, however, get a good feel for how this works, as with ads.

The basic flow of events to perform an in-app purchase looks something like what you see in Listing 10-10.

Listing 10-10.  An Example of In-App Purchases

local store = require("store");

store.init(
  function (inEvent)

    if inEvent.transaction.state == "purchased" then
      print("Complete");
      print("productIdentifier", inEvent.transaction.productIdentifier);
      print("receipt", inEvent.transaction.receipt);
      print("transactionIdentifier", inEvent.transaction.identifier);
      print("date", inEvent.transaction.date);

    elseif inEvent.transaction.state == "restored" then
      print("Restored (already purchased)");
      print("productIdentifier", inEvent.transaction.productIdentifier);
      print("receipt", inEvent.transaction.receipt);
      print("transactionIdentifier", inEvent.transaction.identifier);
      print("date", inEvent.transaction.date);
      print("originalReceipt", inEvent.transaction.originalReceipt);
      print("originalinEvent.transactionIdentifier",
        inEvent.transaction.originalIdentifier
      );
      print("originalDate", inEvent.transaction.originalDate);

    elseif inEvent.transaction.state == "cancelled" then
      print("Cancelled by user")

    elseif inEvent.transaction.state == "failed" then
      print("Purchase failed: ", transaction.errorType, transaction.errorString);

    else
        print("D'oh! Something went wrong!");

    end

    store.finishTransaction(inEvent.transaction);

  end
);

if store.canMakePurchases then

  store.loadProducts(
    {
      "com.etherient.myGame.purchaseableItem1",
      "com.etherient.myGame.purchaseableItem2"
    },
    function(inEvent)
      for i = 1, #inEvent.products do
        print(inEvent.products[i].title);
        print(inEvent.products[i].description);
        print(inEvent.products[i].price);
        print(inEvent.products[i].localizedPrice);
        print(inEvent.products[i].productIdentifier);
      end
    end
  );

  store.restore();

  store.purchase( { "com.etherient.purchaseableItem1"} );

else

  print("You are not allowed to make in-app purchases on this device");

end

The first step is a call to store.init(). Well, the first step is actually to import the store module, but you know that! What you pass to store.init() is a callback function that will respond to the various events that can occur when you attempt to make a purchase. Let’s come back to that in just a moment.

Once you initialize the store, what happens next depends on what you’re trying to do. One thing you might do is check if in-app purchases are allowed on this device by checking store.canMakePurchases. In iOS at least, purchases can be disabled, usually for parental control purposes, so it’s a good idea to check this first lest your code blow up later (which the example code would do, by the way, since the check is performed and then promptly ignored—this is just an example, not production code!)

Something else you’ll probably want to do is call store.restore(), which will restore any purchases this user has already made. This will result in your callback function being called and the restored branch executed for each item restored. What you do at this point is up to you: maybe you need to download the new content, or maybe you check to ensure it’s already done and do nothing in that case.

You also might want to list all the items available for purchase. To do this you can use the store.loadProducts() method. You pass to this a list of identifier strings for items that can be purchased for your game. The store responds with information about it including description and price, which are probably the most important attributes. In the example code, the information is simply printed to the console; in a real game you’d want to show them to the user and let them select, of course.

Note  The string identifiers are things you create in the storefront you are working with. All the attributes are stored there as well. I haven’t gone into those details because they vary from store to store, but you’ll get at least a sense of what’s involved in the very next chapter when I discuss distributing your game.

Ultimately, making a purchase is what you’re here to do, and the store.purchase() method, not surprisingly, lets you do that. You pass to it a list of string identifiers for what the user wants to purchase; once again, the callback function you registered with store.init() will be called, once for each item; and the outcome of the transaction will determine what branch in that function is taken. You, as the person wanting to make money, are hoping for the purchased or restored branch (they may try to purchase something that’s already purchased, which would get you to the restored branch, although if you think about it that’s a flaw in your user interface!). You might hit the cancelled branch if the user changes his mind about the purchase, or failed if something goes wrong, such as his credit card being declined. Any unknown errors are handled in the else branch.

Tip  The store API has no method to set the quantity for a purchase at present. For this, you need to cheat: just specify an item multiple times in the call to store.purchase() and Corona will do the equivalent of setting a quantity for you.

Last, when you’re done with the transaction, call store.finishTransaction(), passing it the object passed into the callback by way of inEvent.transaction, and everything gets buttoned up nicely. If you fail to do this, the store you’re communicating with will assume the transaction was simply interrupted and will attempt to resume it the next time the application is launched. Since this isn’t a great user experience, you’ll want to ensure you call this function for sure.

Social Gaming

One of the somewhat newer aspects to gaming, whether mobile or not, is the introduction of so-called social aspects. This can mean many things to different people, but one thing that it frequently means is online tracking of scores, leaderboards and such, that compare you to other players. There are a number of popular mechanisms for doing this; just as with ads, as discussed earlier, there are providers that will track this information for you. These third-party libraries include the iOS Game Center, which just so happens to be the only one that Corona supports out of the box!

Warning

Previously, Corona also supported OpenFeint, which was a very popular cross-platform game network but which no longer exists as such, having been acquired by another company. Corona subsequently dropped support for it. This unfortunately means that there is no longer a cross-platform game system that Corona supports out of the box. At this point you would have to explore other possible third-party options that may exist, roll your own service (which is far outside the scope of this book), or simply live with only supporting these sorts of features on iOS devices for the time being. Corona has recently made their Corona Cloud service available too, which provides some of these same services to Corona SDK subscribers.

Something else that people very frequently mean when talking about social gaming is integration with Facebook. Simple things like being able to post their high scores or telling their friends what they’re playing at the moment are popular these days.

Let’s start with how you can hook into Game Center, and potentially other game networks that Corona may one day support.

Game Network Integration

As with ads and in-app purchases, I’ll have to talk in generalities here because without signing up with one of these providers, I can’t properly show you working code. Like those topics, however, I can provide you a good overview to help you started if you want to use these mechanisms.

As with ads and in-app purchases, the API Corona provides for game network integration, packaged in the gameNetwork.* namespace, is very simple and boils down to three steps (or two, depending on how you look at it):

  • Call gameNetwork.init() after importing the module by including require("gameNetwork") in your code and passing to it as its first argument the name of the game network to work with (currently only gamecenter is a valid value).
  • Call gameNetwork.show() to show information from the provider.
  • Call gameNetwork.request() to send or request specific information.

A second argument can optionally be passed to the init() method. This argument is a callback function that handles certain lifecycle events, determined by a number of different factors, such as:

  • When the value in the type attribute of the event object passed in is showSignIn, that means the Game Center sign-in screen is about to be shown, and your game now has the chance to pause itself or perform any other tasks that are specific to your game and that need to occur before the sign in view is shown.
  • When the data attribute of the event object passed in is true, that means sign-in was successful and you can continue with your game code accordingly. When the value is false, sign-in failed and you can branch as appropriate there, too.
  • If any errors occur, such as network errors, values will be populated in the errorCode and errorMessage attributes of the event object.
  • When the value init is present in the type attribute, that means the sign-in view was dismissed and you can continue your game code to handle the results of the login.

Note  In iOS, if your app is “backgrounded,” your app will automatically be logged out of Game Center. In that case, it will automatically attempt to log in again when the app is resumed. Your callback handler function will be invoked again in this case, so you must code it to handle this case appropriately as well.

Once everything is initialized, you can go about showing or getting/setting information. For example, to show a leaderboard of high scores for the past week, you can do:

gameNetwork.show(
  "leaderboards",
  { leaderboard = { timeScope = "week" }
});

The first argument is what you want to display, the second is a data object that further defines the request. This object is optional in many cases but you will probably use it more times than not. In this case, only the timeScope attribute is present, which takes a value of week, today, or alltime to define what range of data you want.

As another example, let’s say you want to send a friend request on Game Center. For that, you might do:

gameNetwork.show(
  "friendRequest",
  {
    message = "I'm desperate, please like me!",
    playerIDs = { "G:111111111", "G:222222222" },
    emailAddresses = { "[email protected]" },
    listener = myCallback
  }
);

Here, the message attribute is obvious, I’d say. The playerIDs is the list of identifiers of Game Center users you want to send the request to. The emailAddresses attribute is a list of e-mail addresses for players you want to send a request to. Last, the listener attribute references a function that will receive callback events. This attribute is actually applicable for nearly all gameNetwork.show() calls, although is optional.

Now, what happens when you want to record a high score? That’s easy enough! The code for that looks something like this:

gameNetwork.request(
  "setHighScore",
  {
    localPlayerScore = { category="com.etherient.Engineer", value=25 }
  }
);

Want to get a list of the achievements your game supports? Here you go:

gameNetwork.request(
  "loadAchievementDescriptions",
  { listener = myCallback }
);

Assuming myCallback() is a function that appropriately does something with the achievements, like display them most prominently, you’re good to go.

Last, what about unlocking an achievement the player just got? Simple enough:

gameNetwork.request(
  "unlockAchievement",
  {
    achievement = {
      identifier = "com.etherient.five_levels_finished",
      percentComplete = 100,
      showsCompletionBanner = true,
    },
    listener = myListenerFunction
  }
);

As you can see, the API isn’t particularly difficult, but this sort of social interaction is exactly the sort of thing players have come to expect from modern games, and thankfully Corona gets you there with a minimum of effort.

Facebook Integration

Now let’s talk about working with Facebook. The first step is to register your application with Facebook. This will get you a unique app ID that you’ll use with the Corona facebook.* package.

Once you have it, you’ll need to enable Single Sign-On (SSO) for iOS apps (for Android you can skip this step). This amounts to modifying your build.settings file like so:

settings = {  iphone = {    plist = {      CFBundleURLTypes =      {        {          CFBundleURLSchemes =          {            "fbXXXXX",          }        }      }    }  }}

Caution  The nesting of this configuration must be exactly as shown here or all manner of hellspawn will be brought forth upon the Earth . . . or your app just won’t work. Why risk it, I say?

Replace fbXXXXX with the Facebook ID provided when you registered your application.

Once that’s done, the next thing your app will have to do is log the user in to Facebook. This is accomplished by a call to facebook.login(). To this method you pass:

  • Your app ID as the first argument.
  • A reference to a callback function as the second argument.  This function is passed a fbconnect event object that contains information you’ll need, including: phase (with possible values login, loginFailed, loginCancelled, or logout, which allow you to determine how your code should react), type (with possible values session, request, or dialog, which tell you what type of call your code made to trigger the callback) and token (the access token generated by a login request).

Once the user is successfully logged in, you can make facebook.request() calls to accomplish various tasks. For example, if you want to get a list of the user’s friends, you can do:

facebook.request( "me/friends" );

This returns a JSON object to your listener function (event type "request") that you can parse and work with.

To post a message to the user’s activity feed:

facebook.request(
  "me/feed",
  "POST",
  { message = "I'm playing the most awesome game ever!"}
);

Last, to upload a photo, perhaps a screenshot of the player completing the game, you could do:

facebook.request(
  "me/photos",
  "POST",
  {
    message = "I won! I won! I won!",
    source = {
      baseDir = system.DocumentsDirectory,
      filename = "screenshot.png",
      type = "image"
    }
  }
);

Now, all of this requires you to build the UI for posting and displaying information yourself. There’s another way though: you can allow Facebook to do it! This is accomplished with the facebook.showDialog() method. You pass this a string that tells it what dialog you want to show, and optionally a second argument that defines the data to use when showing the dialog. This data is specific to the call you want to make. For example, to show a dialog allowing the user to post to their timeline, you might do:

facebook.showDialog("feed", {
  link = "www.etherient.com",
  caption = "Check out this site!",
  description = "It's a cool site, seriously!"
});

See this page for details on what calls are possible and what data to pass for each: http://developers.facebook.com/docs/reference/dialogs.

Last, when you’re all done with Facebook, you can log the user out like so:

facebook.logout();

Yep, that’s really it! With just a few lines of code you can allow your players to obnoxiously declare to all their friends and family how great they are at your game. It’s what Facebook is for, after all! (Oops, I seem to have forgotten the sarcasm tags there . . . they’re implicit, though!)

Analytics

The final topic for this chapter is something users of your application will probably never see, but that can be of great importance to you as an application developer: analytics.

Analytics allow you to collect information about your application and how people use it. Want to know how many people install your game? Analytics. Want to know how often people play it? Analytics. The number of in-app purchases of a given type? Analytics. Information about crashes? I’ll give you one guess!

The Corona analytics.* package makes it easy to do this and more. The API present here is the model of simplicity itself. First, call:

analytics.init(xxxx);

The value you pass to analytics.init() is a key given to you by the analytics service you sign up with. At the time of this writing, Flurry (www.flurry.com) is the only service supported. Flurry is a free service that tracks a great deal of data for you, including custom data, and then presents to you a web-based interface for viewing it all. You can slice and dice the data in various ways to visualize that data however you need to in order to get the information you’re after.

If all you did was the analytics.init() call, Flurry would automatically track a host of information for you, such as number of unique users who launch your app, average session length, and more. However, that’s only part of the equation! You can also do this:

analytics.logEvent(xxxx, yyyy);

In this case, xxxx is a string defining a custom event. In this way, you can track situations specific to your application. For example, if you are writing a Pac-Man clone, maybe you want to track how many ghosts players eat. For this, you might pass a string ghostsEaten, and Flurry will then track this for you. The second argument, which is optional, is a table with key-value pairs that provide more information about the event. This way you might track how many Blinkys, Pinkys, Inkys, and Clydes are eaten.

Corona Launchpad

One other option to be aware of with regard to analytics is Corona Launchpad. Once you become a Corona SDK subscriber you’ll automatically have access to this service. With it, you will by default get a number of basic analytic data points, among other services. The only “special” thing you need to do to turn this on is to build your app with a distribution provisioning profile for iOS or a private key for Android—and don’t worry, the next chapter is where I’ll talk about those things if they aren’t familiar to you!

Note  During testing, you can also record data for iOS apps by using an ad hoc provisioning profile. You’ll still need to use a private key for Android builds. This lets you collect information from your beta testers, for example, before your app is fully baked.

In Figure 10-2 you can see an example of what the data display on the Corona web site looks like. As you can see, it’s not a lot of information, but it’s certainly helpful.

9781430250685_Fig10-02.jpg

Figure 10-2 .  Okay, so my game isn’t doing too well, but still, it illustrates the idea just fine anyway!

The data collected, it should be noted, is not 100% real-time. There is a delay of up to 24 hours in fact, and there is some buffering logic in place for when devices don’t have a persistent network connection, so you won’t miss anything in those cases.

Finally, while this is all more or less automatic, you do of course have the option of not participating. You can simply remove the application from your dashboard on the Corona web site, where you find this data. However, that will only effectively hide your app from view—it will still be transmitting data to the Corona cloud. To avoid this, too, you need to add launchPad=false to your config.lua file.

Note  Usage of analytics.logEvents() is not recorded by the Corona Launchpad analytics feature. At least, I am unaware of any way to see it if it is. The statistics captured by this service, while certainly useful, really are pretty basic in nature. If you need something more robust, take a look at Flurry for sure.

One final note: By law, when you are using analytics you are now required to include a privacy policy in your application that a user can easily view at will. Unless you fancy yourself a trial lawyer, or have a bunch of spare cash lying around to hire such a lawyer, I suggest keeping this in mind!

Summary

In this chapter, you covered a wide range of topics, from data storage in a SQLite database to in-game ads and purchases, from location awareness services to more advanced networking capabilities. You also looked at things like analytics and some more advanced file system capabilities.

In the next and final chapter, you’ll look at the last logical step in creating a Corona app: packaging it up and getting it into the various app stores for a variety of mobile platforms. This is the final piece of the puzzle that you’ll need to bring Astro Rescue to a real conclusion, as well as your knowledge of Corona. After reading it, you should have all the basic information you’ll need to start creating your apps, be they games or not, and to get them out into the world at large.

What are you waiting for? Turn the page, let’s get to it!

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
13.59.160.92