
Greetings Immediately I apologize for the title - I wanted to tell so much in it, but it turned out too long.
The story will be about my game (
iOS ,
Android ), made with the help of the Corona SDK, about the crown itself and developing with it, about the “write the game in 48 hours” competition, about the recent DevConf and about the Go language.
Introduction
I have long been developing a game in the Tower Defense genre with a crown, but the game takes a huge amount of time to become truly high-quality, and quality games are item # 1 on the road to success (item # 2 is a lot of marketing money if someone is not course). Therefore, for a long time I wanted to do something, let it be small, but its own and in a short time.
')
"Caution mnogabukav!"
GIGJam 48
And here in early May, a competition is organized from a small studio Glitch Games together with Corona Labs to develop the game for 48 hours. Like Ludum Dare, only for the Corona SDK. I wanted to do a puzzle, develop an idea, two almost sleepless nights and I made the game Laser Flow - Experiments with Lasers. Then she was called simply Laser Experiments. It is a little similar games and mine differs in adequate management, mixing of flowers and I hope it is pleasant by sight and hearing. I myself really like it. But according to the results of the competition, unfortunately, my views do not coincide with the views of the jury. There were 23 games in total from about 35 participants.
I made a nice main screen for the game to impress the jury at least a little. Most of the time was spent on the code responsible for the reflections of the lasers from the mirrors and the simultaneous passage of the lasers in one cell so that they do not overlap, but go side by side. This could not be completely avoided and now at some levels it is possible to arrange mirrors in such a way that different lasers overlap. I hope this does not spoil the game as a whole.
After sending the games began languid waiting for the results. Days went by, no results. Then it was announced that the jury would take several weeks to evaluate, and riots broke out in the #corona IRC room ...
It was a hard time. The results had to wait in the end more than a month.
By the way, here are screenshots of the game, showing how it has changed since it was sent to the contest.


DevConf 2013
Toward the end of May, the DevConf organizer contacted me and offered to participate in their competition of reports. I suggested two topics: about Corona SDK and about Go. They took both reports, and I was very pleased. Thanks to the organizers for this, for free travel, room and board.
I thought it would be great to finish and release the game for the conference, so that I could give a talk on a live example right away so that people could download and play. Also in the plans was to arrange a small contest - decide the secret level faster than the others and get a T-shirt with the logo of the game (icon). But firstly, because of the bug with purchases, my application was rejected and it only came out on the second day of the conference, and secondly, the t-shirt could not be printed with the bright colors I needed (special CMYK profile). By the way, 1024x1024 pixels is enough for printing on a T-shirt drawing 20x20cm.
It turned out that it took me a little more than a month to finish the game.
I started making slides for reports at home, continued on the plane and finished at the hotel. Used on the trip iPad and Asus Eee Pc 701 4G with lubunt. The same one with a touch screen, a blue-tooth and an additional SD slot. The modem had to be removed. Slides did in LibreOffice.
Thanks to neighbor Pavel for providing MacBook for recompiling the game and sending it to the App Store after an unexpected redjack. Oddly enough, this was enough for the game to be approved this time.
The conference was undoubtedly interesting, there were a lot of people and some reports gathered so many listeners that there were not enough chairs. The Mobi section really did not enjoy the same popularity.
My reports were the latest in the grid and collected, frankly, a few listeners. I prepared a report on the crown with a view to show the subtleties, some time-tested best practices and tips, so that the report was not a dry retelling of the site’s main page and documentation, I wanted to make the report really useful. However, almost none of the listeners was familiar with either the crown or Lua. I had to rebuild the report closer to the introductory, show what the crown is capable of and make some kind of introduction to Lua.
Suddenly for me, while I was talking about the crown, the first day of the conference was over. And the report on Go, I still have not started. A quick opinion poll showed that the audience was waiting for this report and we decided to continue on the first floor of the hotel. There were a few of us, they gave me a laptop with a large screen (thanks!) Instead of a projector, unfortunately not everyone was comfortable and not everyone could see it normally, but okay. I posted the slides on the site, all who need to download. After the report, we talked a little more about Go, about his perspectives, about how we all like him and a book on this language was presented to one of the listeners (I also want to!)
On the second day there were very interesting reports about high load.
From the conference I still had a baseball cap with the DevConf logo, notepads, pens, badges and a mini toothbrush from the hotel. On this about DevConf basically everything. It was cool.
Slides on my reports can be downloaded from my site. Link at the bottom of the topic.
Corona SDK
The crown is good. It is the easiest to use game development framework, has recently acquired a free version and is highly recommended for review. Knowing Python and PHP, it only took me a couple of days to learn Lua, and another week went away to become aware of most of the concepts of the language, OOP, and the main part of the Crown API.
In Lua, there are some cool things, for example, logical operations return the last element that was triggered, you can make similar constructions: referring to an object as long as it exists and specifying a default value otherwise.
local txt = a and a.text and a.text:lower() or 'default'
Often make a mistake when using callbacks.
local function myFunc() print('Nya') end timer.performWithDelay(1000, myFunc(), 1)
In this case, the timer is passed not the function itself, but the result of its execution (nil). You just need to remove the parentheses after the function name.
Once in IRC they asked how to tell the timer to perform the function not only after the specified time, but also right now. The answer is to call it simply in the code in the same place where you create the timer.
Lua also comes with sugar:
object.property -> object['property'] local function nya() -> local nya = function() object:method(params) -> object.method(object, params) someFunction{name = 'Someone'} -> someFunction({name = 'Someone'}) .
OOP in Lua
OOP can be organized on the basis of pseudo-classes using metamethods (analogous to the magic methods in PHP). Or maybe quite well built on the basis of simple tables. Moreover, both private and public, and even protected can be organized without problems.
Show OOP codeThe simplest option with an empty object:
local function newObject() local object = {} return object end local myObject = newObject()
Add public variables:
local function newObject() local object = {} object.somePublicVar = 100 return object end local myObject = newObject() print(myObject.somePublicVar)
Now add some method:
local function newObject() local object = {} object.somePublicVar = 100 function object:setName(name) self.name = name end return object end local myObject = newObject() myObject:setName('Nyashka') print(myObject.name)
Note the use of a colon. In this case, a special variable self appears inside the function method, which is a reference to the object itself. Calling such methods is also necessary through a colon, or with a single point, but specify the object as the first argument.
Add a private variable, for example, money:
local function newObject() local object = {} object.somePublicVar = 100 local money = 0 function object:setName(name) self.name = name end function object:addMoney(amount) money = money + amount end return object end local myObject = newObject() myObject:setName('Nyashka') myObject:addMoney(50) print(myObject.money)
If we try to access money through a dot, we get nil, since this variable is not in the table object itself, but in the scope of the function that created this object. Therefore, this variable is visible in the addMoney () method.
Now inheritance:
local function newObject() local object = {} object.somePublicVar = 100 local money = 0 function object:setName(name) self.name = name end function object:addMoney(amount) money = money + amount end return object end local function newChild() local object = newObject() return object end local myChild = newChild()
In this case, the object is not created by an empty table {}, but by another base object. So in the function newChild, you can add properties and methods of the object or even override them.
Protected is implemented a little more interesting. We create a private table in which we add all protected variables, and return it along with the created object via return. Suppose we want to transfer money by inheritance.
local function newObject() local object = {} object.somePublicVar = 100 local protected = {money = 0} function object:setName(name) self.name = name end function object:addMoney(amount) protected.money = protected.money + amount end function object:getMoney() return protected.money end return object, protected end local function newChild() local object, protected = newObject() function object:spendMoney(amount) protected.money = protected.money - amount end return object end local myChild = newChild() myChild:addMoney(100) myChild:spendMoney(60) print(myChild:getMoney())
If you need to override any method, then it is declared the same way. New overwrites the old. If you need to make a wrapper, then the old method is first stored in some variable, and then it is called from the new method:
local function newObject() local object = {} object.somePublicVar = 100 local protected = {money = 0} function object:setName(name) self.name = name end function object:addMoney(amount) protected.money = protected.money + amount end function object:getMoney() return protected.money end return object, protected end local function newChild() local object, protected = newObject() function object:spendMoney(amount) protected.money = protected.money - amount end local parent_setName = object.setName function object:setName(name) name = 'Mr. ' .. name parent_setName(self, name) end return object end local myChild = newChild() myChild:setName('Nyashka') print(myChild.name)
In the case of objects displayed on the screen, it is enough to replace the empty table {} when creating an object with a call to a function from the Corona SDK API that returns a graphic object. This can be display.newImage (), display.newRect () or most often I use the display.newGroup () group. If you use a group, then such objects are very easy to expand with other graphic elements. But keep in mind that an excessive number of groups reduces performance, so if your code starts to slow down, then try to use them as little as possible.
A module is also a table and also an object. You can adhere to the same principles as with the PLO. Previously, modules were written using the keyword / module () function. The name of the created module was transferred to it and it replaced the script environment with its own local one, in order not to write a bunch of “local”, all variables and functions were placed in this local space. And everything would be fine, but at the same time inside the module there was no access to global functions like print and others. Then it was thought that when adding a special function as the second argument to the module () function, all global things will be searched for where it should be - in _G. This special function package.seeall was responsible for this. But in practice, not only did all global functions and variables appear inside all modules, so modules were created and placed by the module function in the global namespace. Porridge. Moon porridge. Global moon porridge.
I will insert my example module with pastebin:
Create a table-module and put the contents of the pens inside what we want Everything is convenient, everything is wonderful.
At the end of the file we write return _M. Otherwise, the module will not be loaded through require.
The rest of the Corona SDK
Did you know that three dots ... mean a variable list of arguments to the current function? It is easier to write wrappers or variable functions.
local function my_warning_print(...) print('Warning:', unpack(arg)) end
Lua's speed is about 20 times lower than C. LuaJIT is only a couple of times slower, but real-time compilation is prohibited in iOS. Pichalka. On android you can compile, but in the crown it is not implemented. The good news is that Lua is very rarely a bottleneck. Only with intensive calculations.
The speed of development is very important. And the crown can be forgiven much only for this.
Everything that is not local is global. Store variables in tables for convenience.
A lot of time is spent searching for a variable in service visibility tables. First, the visibility of the current block or function, then the chain up to the very _G. This happens in the case of global variables. Accordingly, they are slow. Cache all variables and functions with local to increase performance at a specific location. But do not overdo it. For most code, such a cache will not give any noticeable increase in speed. optimize only those places that are called very often. This may be a function in an infinite timer or an event handler for the enterFrame event called each frame. Until Lua finishes processing the entire code for the current frame, Corona will not launch the renderer. So if you change the parameter x let's say the images on the screen many times inside the function, then only the last value will be reflected on the screen.
Cache even things like math.random, table.insert, ipars, pairs, and other built-in functions.
Do not litter in the field of global visibility, you can break something. Or just be a memory leak.
It is possible to override everything in _G, but this is of course not recommended. To see what's in there, just print on it. Remember the derived variables and never use them. Some say that by no means put anything in _G. But this is too much. It’s quite normal in global visibility to have several elements for yourself, like a table for your modules (like a namespace, for example, “app”), configurations or just built-in modules, so that in each scene you don’t write a bunch of require.
When thinking about performance, add a module for monitoring FPS, texture memory and memory of Lua. For the first time after adding such a module, everyone has a panic "where does this memory leak come from". In fact, the smooth growth of memory consumption is the norm, up to about 10kB per second. The next time the garbage collector is called, the memory will return to its minimum value. The garbage collector can be customized if it bothers you in your algorithms. For example, run it yourself after the end of the calculations, so that it does not slow down them.
Lua memory in practice does not exceed a couple of megabytes. The main volume is texture. They are desirable to optimize. Inside the video chip, all textures (meaning any images) are aligned in powers of two. Therefore, for best results, stick to a power of two for your files.
In the crown, the frame rate per second is set to either 30 or 60, try to always use 60, as the interface of your application becomes much smoother and more pleasant to the eye.
Animate everything. It is very easy to achieve this if you follow the rules of good tone when designing your application. Use objects for everything. At least even the wrapper functions over the standard functions newImage, etc. In this case, you can add animation once in the generator of your object and it will appear immediately in the entire application. Just do not overdo it with them. Congestion or lethargy in the sense that you keep the user waiting has a very bad effect on the overall impression of the application.
Here is my wrapper for the standard newImage. You can set the position on the screen in one line and add to the group.
Wrapper over newImage and useful stuff local _M = {} _W = display.contentWidth _H = display.contentHeight _T = display.screenOriginY
A little more about the screen size
pastebin.com/GWG3sJLZ Also includes a very useful setRP function, eliminating the need to write long values ​​of control points. And also a way to set the color of objects by name.
To support the button back to Android, just add the following code to your main.lua
Runtime:addEventListener('key', function (event) if event.keyName == 'back' and event.phase == 'down' then local scene = storyboard.getScene(storyboard.getCurrentSceneName()) if scene and type(scene.backPressed) == 'function' then return scene:backPressed() end end end);
And determine in each scene where the reaction to this button is required, the function:
function scene:backPressed() storyboard.gotoScene('scenes.menu', 'slideRight', 200) return true end
Do not forget to add the dialog box "Are you sure you want to exit" and call native.requestExit () (only for Android, for iOS - os.exit ()).
config.lua panacea for device fragmentation
Dynamic scaling is great. Nearly.
The content is adjusted to the current screen size, while keeping the virtual part of your application visible (usually 320x480) and adding space along any two edges (letterbox). Arrange your elements by anchors - the corners of the screen, the centers of the sides or the center of the entire screen. Try not to use elements that need to be scaled. If on different screens the controls look a little less or more - not scary. The main thing is to make them big enough. Not lower than 32 pixels for the button. Better 64 or at least 48. There are corners of your content and there are corners of the screen directly, this is the difference between the virtual screen area and the real one.
Few people know that you can write code in config.lua. You can dynamically assign a content area. For what? To achieve pixel perfect graphics on most devices. Otherwise, the picture is slightly blurred and the perfect look is lost.
Put your files in subfolders - lib for libraries, scenes for storyboard scenes, images for pictures, sounds and maybe music for which you quite guessed it. Always use lower case for your files to avoid problems with loading files on file systems with a register.
Use bitbucket.com for backup and version control of your application. It is free for private projects with teams of just a few people.
You can search for music for your game on soundcloud, indiegamemusic, google. Don't forget about the Garabe band, FrootyLoops, Audacity and bfxr.
In the Garage Band with the 8-bit synthesizer, you can achieve a good modern old school sound. With the use of effects and equalizer.
You can call the button widget onRelease via button._view._onReleas ().
Carefully browse the library for standard features. Few people know that the crown supports the twitter client under ios (native.showPopu ('twitter')). Or that you can display the market page directly from the application.
Keep in mind that when a multitouch on your button, you can have time to press twice with two fingers. If something happens that can not be run many times in a row, put a flag inside the button that it has been activated and can no longer be pressed.
Use Flurry to understand what’s wrong with your game, how to improve it, so that more users recommend it.
As an IDE for Corona SDK, I chose Zero Brane Studio - an open source, Russian-speaking developer, written in Lua + wxWidgets, cross-platform. There are other IDEs, but this one is generally more like it.
Go
Go cool. All use.
I wrote on it the server part of another game (order) and I really liked it. The speed and ease of development is higher than that of C / C ++, the speed of execution is either slightly slower or the same. Now there is a vast community in the language and quite a lot of different libraries have appeared.
Laser flow
The game was released in the App Store on June 15. That day there were 3,000 downloads and 10 purchases. Perhaps the game was too complicated, and maybe just for fans of the game.
I want to tell you about the level generator and about the level editor. I killed a few days to write a solution search algorithm. I used different variations of brute force, with different exit conditions, with all the code optimizations and using LuaJIT, the result was deplorable - the levels are solved very slowly. It is necessary to develop an algorithm based on the shortest path search algorithms, but this is not easy. He left only the field generator from the generator and called it Abyss of Random.
The editor is more interesting. I chose to do it either inside the game itself, but here the inconvenience of control is that you need to think up where to place a bunch of buttons, if you make a hidden panel, it is very inconvenient. Another option is to make a separate desktop application. We have the power of the keyboard, but the development is almost from scratch.
In the end, I decided to combine. Naked keylogger for Mac OS on python and a script for determining the current active window. Thus, if the crown simulator is currently active, send keystrokes to it via plain text via UDP. In the crown, a UDP server rises and listens to commands when we are in the scene with the playing field itself.
As a result, by simply pressing the keyboard, I add any elements to the field, I can regenerate it, I can save, I can change the color of the lasers and arrange the target cells. Everything turned out very convenient. 140 levels I did in a day.
Three more bonus levels are hidden in the game itself, it is easy to find them.
Keylogger code from AppKit import NSWorkspace from Cocoa import * from Foundation import * from PyObjCTools import AppHelper import socket sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) class AppDelegate(NSObject): def applicationDidFinishLaunching_(self, aNotification): NSEvent.addGlobalMonitorForEventsMatchingMask_handler_(NSKeyDownMask, handler) def handler(event): if NSWorkspace.sharedWorkspace().activeApplication()['NSApplicationName'] == 'Corona Simulator': s = unicode(event).split(' ') if s[8]: s = s[8][-2:-1] if (s >= 'a' and s <= 'z') or s == '`' or s == '[' or s == ']': sock.sendto(s, ('127.0.0.1', 5000)) def main(): app = NSApplication.sharedApplication() delegate = AppDelegate.alloc().init() NSApp().setDelegate_(delegate) AppHelper.runEventLoop() if __name__ == '__main__': main()
Lua server code local udp = socket.udp() udp:setsockname('127.0.0.1', 5000) udp:settimeout(0.001) local char, ip, port timer.performWithDelay(200, function () char, ip, port = udp:receivefrom() if char then local scene = storyboard.getScene(storyboard.getCurrentSceneName()) if type(scene.keyboardPressed) == 'function' then scene:keyboardPressed(char) end end end, 0)
By the same principle, you can make a screen capture button directly from the game for downloading to various app stores. You need to use the display.save () function.
Before publishing to Habré, he added support for the Russian language, made the levels lighter available initially, added analytics, made a discount on the purchase of all levels and put it on Google Play.
Communicate on IRC for both the Corona SDK and Go. On freenode.net: #corona and # go-nuts.
Guo players are greatly offended if they go to the channel (#go) and start shipping questions on programming.
Links
Laser flow
iOS
itunes.apple.com/us/app/laser-flow/id647540345?ls=1&mt=8Android
play.google.com/store/apps/details?id=com.spiralcodestudio.laserflow:
Corona SDK ,
Go Language .
Corona SDK
coronalabs.comGo Language
golang.org! , .
.