πŸ“œ ⬆️ ⬇️

Writing Penguin Daycare Simulator on Go (Google App Engine) and Lua (Corona SDK)


1. Introduction


This project is a simple example of using Google App Engine in a mobile application.

The server part provides a list of penguins in JSON format. The mobile client requests this list via HTTP or HTTPS.
The server part also records certain events in the database, namely the number of visits to a particular penguin and the number of button presses: feed the fish and scratch the tummy.
Each penguin has Name , Bio description fields and counter fields.

2. Subtleties of translation


I thought how to translate Penguin Daycare Simulator into Russian, but β€œkindergarten” as β€œdaycare” is not suitable, β€œday care” too. Therefore, it remained without translation.

3. Preparation


If you do not have Google App Engine Go SDK installed, then go to the Google App Engine link, click "Try it now" and follow all the points. Give your project a name, select Go, download and install the SDK. Make sure that the environment variables ( PATH , GOROOT , GOPATH , APPENGINE_DEV_APPSERVER ) are APPENGINE_DEV_APPSERVER , for this you should see the goapp command in the goapp . Looking ahead, I’ll say that to upload a simple project to the GAE server and start it, you need to run the goapp deploy command in the project directory. She will ask you for the email of the google account on which the project should be located. It is important that the project name matches on app.yaml and on the site. But in this project modules are used and the loading process is somewhat different.
')
As an IDE for Go, I recommend LiteIDE , and for Lua and Corona SDK - ZeroBrane Studio . Download Corona SDK on their website .

4. Clint server


The picture below shows a very complex pattern of communication between the client (left) and the server (right).


Apparently the client requests only a list of penguins and sends only three events. Communication takes place over HTTP, but you can use HTTPS for free. This can be attributed to one of the advantages of using GAE - there is no need to pay for an SSL certificate and customize working with it.

Since everything works over HTTP, it is possible to execute requests directly in the browser without using a special client.

penguin-daycare-simulator.appspot.com
A simple greeting, not used by the mobile client, but lets say whether the service is working. You can replace http with https and make sure that it works too.

penguin-daycare-simulator.appspot.com/penguins
This is the most important request. With it, the mobile client receives a list of all the penguins that are currently supervised.
For more convenient viewing of this data, I recommend the JSONview extension for Chrome.

penguin-daycare-simulator.appspot.com/stat/visit
penguin-daycare-simulator.appspot.com/stat/fish
penguin-daycare-simulator.appspot.com/stat/bellyrub
These three queries increase the corresponding counters for any penguin. Id penguin Id passed as a POST parameter. The server does not return anything in response, but you can, if you want, add the β€œOK” line to the response or another signal of successful operation.

5. More screenshots, more screenshots!




Already before the publication of the article, I remembered about this penguin
Watch the positive

6. Server part - Google App Engine


Now we can go directly to the code. Consider the project file structure on Go.
 PenguinDaycareSimulatorServer/ β”œβ”€β”€ default/ β”‚  β”œβ”€β”€ app.go β”‚  β”œβ”€β”€ default.yaml β”‚  └── penguins.json β”œβ”€β”€ static/ β”‚  β”œβ”€β”€ favicon.ico β”‚  └── static.yaml └── dispatch.yaml 

default and static are modules. The project for GAE can be divided into modules, and can work without them. In this case, only three files are app.yaml : app.yaml , app/app.go and penguins.json . Initially, it was in my project (you can see the first commit on GitHub), but I wanted to add the max_concurrent_requests setting, which is responsible for how many simultaneous requests can be processed by one instance of your application. The default value is only 10. Go is clearly capable of more. The maximum value is 500. If the load grows and this value is exceeded, additional copies of your application are launched and the load is distributed between them. If you want to fit only in free quotas for GAE, then the use of this setting is highly desirable. If the application does not cope with such a load, then lower this value and switch to paid billing.

So this setting is available only for modules. And your application should have at least 2 modules so that GAE considers it modular.

static is a very simple module that one could do without (if it were not for the GAE constraint above), its task is only to give a favicon.ico file statically.

default is the main module that does all the work.

The *.yaml files are settings and descriptions. One for each module and one dispatch.yaml file, which describes which URLs which module handles which URLs.
dispatch.yaml
 application: penguin-daycare-simulator dispatch: - url: "*/favicon.ico" module: static - url: "*/" module: default 

static.yaml
 application: penguin-daycare-simulator module: static version: 1 runtime: python27 api_version: 1 threadsafe: true handlers: - url: /favicon.ico static_files: favicon.ico upload: favicon.ico 

default.yaml
 application: penguin-daycare-simulator module: default version: 1 runtime: go api_version: go1 automatic_scaling: max_concurrent_requests: 500 handlers: - url: /.* script: _go_app 

Note that in the static.yaml runtime Python is specified, not Go. This is done because GAE swears if you try to upload a module to Go without the Go files themselves. However, he does not swear in Python and PHP in this situation.
off topic
The attentive reader here may argue that β€œthe worse PHP is for Python to upload static files” and try to unleash holywar, but Python is closer to me personally, that's why I chose it. Anyone else can use PHP for this purpose. Of course, this is all meaningless, since neither Python nor PHP are involved in this process.

handlers in default.yaml indicate which executable files are handled by specific URLs. In our case, app.go handles all incoming requests (taking into account dispatch.yaml ). Description URL is very flexible, uses regular expressions. However, if for Python and PHP you can use different files to process different URLs within a single module, then for Go it should be one single file, which is designated as "_go_app". Further, already inside the program on Go, you can select handlers for different URLs and split the entire application into several files, if necessary.

More about configuration and yaml files can be read here .

penguins.json is a JSON file containing the names and descriptions of all the penguins used.
penguins.json
 [ {"id": "1", "name": "Tux", "bio": "Beloved Linux mascot" }, {"id": "2", "name": "Skipper", "bio": "Small combat squad leader" }, {"id": "3", "name": "Lolo", "bio": "Russian adventurer" }, {"id": "4", "name": "Gunter", "bio": "The darkest character in Adventure Time" }, {"id": "5", "name": "The Penguin", "bio": "Na, na, na, na, na, na, na, na, na, na... The Penguin! " } ] 

Adding, editing penguins occurs through this file.

Now we come to app.go - the heart of the entire application. It is convenient to look at the full listing immediately on GitHub - app.go.

The simplified structure of this file is:
 package app    . import (...)   : Id, , , . type penguin struct {...}  ()  . var penguins []penguin     . type penguinEntity struct {...} . func init() {...}  penguins.json   penguins. func loadPenguinsJson() {...}  / -   . func rootHandler(w http.ResponseWriter, r *http.Request) {...}  /penguins -        JSON. func penguinsHandler(w http.ResponseWriter, r *http.Request) {...}   /stat/visit -  . func visitHandler(w http.ResponseWriter, r *http.Request) {...}   /stat/fish -   . func fishHandler(w http.ResponseWriter, r *http.Request) {...}   /stat/bellyrub -    . func bellyrubHandler(w http.ResponseWriter, r *http.Request) {...} 

When you start the application, the init () function is started first, which reads from the penguins.json file and sets which function is responsible for different requests from the client. You could already use them on the links at the beginning of the article.

penguinsHandler() serializes the penguins slice into JSON format json.Marshal() function and gives it to clients via fmt.Fprint() .

visitHandler() , fishHandler() , bellyrubHandler() act according to the same logic - we take the penguin from the database, increment the corresponding parameter by one and write it back to the database. The database - Datastore - is not SQL compatible, that is, it is a NoSQL solution. The description of her work is worthy of a separate article.

Since many operations on GAE are charged separately, including access to Datastore, you should avoid unnecessary use of resources. For example, when requesting statistics for all penguins, it is not necessary to provide relevant data. You can cache this query with a cache lifetime of say 10 minutes. To do this, I entered the additional variable lastUpdateTime - the timestamp of the last update of the penguins slice. And with each query /penguins I call the updatePenguinsStatistics() function, which checks whether the cache has expired and updates the counter readings for each penguin in the penguins slice.

To force an update manually, I added an additional request / update and the corresponding updateHandler() handler.

Each request is processed in its own goroutine, so you need to protect the penguins slice from possible simultaneous recording or reading while writing. RWMutex is used for this - read or write mutex. Its use is more efficient than simple Mutex .

To avoid paid consumption of resources, you can also enter a deferred entry into the database, accumulating the values ​​of all incoming events.

To upload a project to the GAE server, you need to execute three commands in the terminal in the project directory:
 goapp deploy default/default.yaml goapp deploy static/static.yaml appcfg.py update_dispatch . 

In the future, when changing app.go, you only need to run goapp deploy default/default.yaml .

In conclusion about the server part, I’ll say that in order to increase free limits I recommend connecting paid billing, but at the same time set the maximum cost per day equal to $ 1. However, some free quotas are increasing, and you still do not spend anything.

7. Client part - Corona SDK


Corona SDK is a cross-platform framework for developing mobile applications for Android, iOS, Windows Phone (soon) and HTML5 (in development). I have been using this product for quite some time now, writing games both for clients as a freelancer and for myself. I note a decent speed and speed of creating applications.

Let's start also with the project file structure. There are more files here, mainly due to the icons and pictures, so I'm cleaning up under the spoiler.
file structure
 PenguinDaycareSimulator/ β”œβ”€β”€ images/ β”‚  β”œβ”€β”€ penguins/ β”‚  β”‚  β”œβ”€β”€ 1.png β”‚  β”‚  β”œβ”€β”€ 1@2x.png β”‚  β”‚  β”œβ”€β”€ 2.png β”‚  β”‚  β”œβ”€β”€ 2@2x.png β”‚  β”‚  β”œβ”€β”€ 3.png β”‚  β”‚  β”œβ”€β”€ 3@2x.png β”‚  β”‚  β”œβ”€β”€ 4.png β”‚  β”‚  β”œβ”€β”€ 4@2x.png β”‚  β”‚  β”œβ”€β”€ 5.png β”‚  β”‚  └── 5@2x.png β”‚  β”œβ”€β”€ background.jpg β”‚  β”œβ”€β”€ background@2x.jpg β”‚  β”œβ”€β”€ button-over.png β”‚  β”œβ”€β”€ button-over@2x.png β”‚  β”œβ”€β”€ button.png β”‚  β”œβ”€β”€ button@2x.png β”‚  β”œβ”€β”€ dot-off.png β”‚  β”œβ”€β”€ dot-off@2x.png β”‚  β”œβ”€β”€ dot.png β”‚  β”œβ”€β”€ dot@2x.png β”‚  β”œβ”€β”€ fish.png β”‚  β”œβ”€β”€ fish@2x.png β”‚  β”œβ”€β”€ hand.png β”‚  β”œβ”€β”€ hand@2x.png β”‚  β”œβ”€β”€ popup.png β”‚  └── popup@2x.png β”œβ”€β”€ lib/ β”‚  β”œβ”€β”€ api.lua β”‚  β”œβ”€β”€ app.lua β”‚  └── utils.lua β”œβ”€β”€ scenes/ β”‚  β”œβ”€β”€ choose.lua β”‚  β”œβ”€β”€ menu.lua β”‚  └── penguin.lua β”œβ”€β”€ Default-568h@2x.png β”œβ”€β”€ Icon-60.png β”œβ”€β”€ Icon-60@2x.png β”œβ”€β”€ Icon-72.png β”œβ”€β”€ Icon-72@2x.png β”œβ”€β”€ Icon-76.png β”œβ”€β”€ Icon-76@2x.png β”œβ”€β”€ Icon-Small-40.png β”œβ”€β”€ Icon-Small-40@2x.png β”œβ”€β”€ Icon-Small-50.png β”œβ”€β”€ Icon-Small-50@2x.png β”œβ”€β”€ Icon-Small.png β”œβ”€β”€ Icon-Small@2x.png β”œβ”€β”€ Icon-hdpi.png β”œβ”€β”€ Icon-ldpi.png β”œβ”€β”€ Icon-mdpi.png β”œβ”€β”€ Icon-ouya.png β”œβ”€β”€ Icon-xhdpi.png β”œβ”€β”€ Icon-xxhdpi.png β”œβ”€β”€ Icon.png β”œβ”€β”€ Icon@2x.png β”œβ”€β”€ build.settings β”œβ”€β”€ config.lua └── main.lua 

You can still pay attention only to Lua files.

config.lua , build.settings - project configuration files for Corona SDK. Specify a portrait or landscape view has the application, the reference screen resolution, zoom method and other different settings. If the Corona SDK is new to you, you can ignore these files for now.

Also at the root you will find a bunch of icons for iOS and Android, plus Default-568h@2x.png for iPhone 5 to work correctly. Inside the images / directory there are regular files and their doubled HD versions @2x . Now, in principle, it is already possible not to support devices with screens like the iPhone 3GS, their percentage is very small, but nonetheless non-zero. To fully support the iPad Retina, you will already need @4x files and a line in config.lua , but most games work fine.

Corona SDK launches the application starting from the main.lua file, the necessary libraries are connected to it, some variables are declared and a transition to the stage with the Enter the Daycare button takes place. All scenes (screens) of the application are stored in different files and collected in the scenes/ directory, and I placed all user libraries in lib/ . The developer is free to place these files as he wants, I prefer so.

lib/ contains app.lua and utils.lua - together this is my collection of useful functions for working with the Corona SDK. In app.lua , convenient wrappers are implemented over the standard Corona SDK functions for displaying images, text, buttons, etc.

The transition from main.lua to scenes/menu.lua done through the line
 storyboard.gotoScene('scenes.menu') 

Where, in turn, the penguin request is already running on the server. Here is the main piece of code from menu.lua .
 function scene:createScene (event) local group = self.view app.newText{g = group, text = 'Penguin Daycare', size = 32, x = _CX, y = _CY - 150} app.newText{g = group, text = 'Simulator', size = 32, x = _CX, y = _CY - 110} local pleaseWait = app.newText{g = group, text = 'Please Wait', size = 16, x = _CX, y = _CY} local button = app.newButton{g = group, x = _CX, y = _CY, text = 'Enter the Daycare', onRelease = function() storyboard.gotoScene('scenes.choose', {effect = 'slideLeft', time = app.duration}) end} button.isVisible = false app.api:getPenguins(function() pleaseWait.isVisible = false button.isVisible = true end) end 

Three lines of text and one button are created. The button is hidden until we get a response from the server. The request itself is executed by the app.api:getPenguins() function, with a callback function as its argument.

After clicking on the button, we get to the scene of the penguin selection, also give only the main part of the code from the file choose.lua .
 function scene:createScene(event) local group = self.view self.backButton = app.newButton{g = group, x = _L + 10, y = _T + 10, w = 48, h = 32, rp = 'TopLeft', text = 'Back', fontSize = 14, onRelease = function() storyboard.gotoScene('scenes.menu', {effect = 'slideRight', time = app.duration}) end} local function gotoPenguin(ind) storyboard.gotoScene('scenes.penguin', {effect = 'slideLeft', time = app.duration, params = ind}) end local slideView = newSlideView{g = group, x = 0, y = _CY, dots_y = 180, onRelease = gotoPenguin} for i = 1, #app.api.penguins do local p = app.api.penguins[i] local slide = display.newGroup() app.newImage('images/popup.png', {g = slide, w = 300, h = 335}) app.newImage('images/penguins/' .. p.id .. '.png', {g = slide, w = 200, h = 256}) app.newText{g = slide, x = 0, y = -140, text = p.name, size = 18, color = 'white'} app.newText{g = slide, x = 0, y = 140, text = p.bio, size = 14, color = 'white', w = 220, align = 'center'} slideView:addSlide(slide) end slideView:makeDots() slideView:gotoSlide(1) end 

Here, newSlideView() is a function that creates a simple widget with which you can scroll through slides with penguins. The code for this widget is located right at choose.lua at the beginning of the file.

A slide is created for each penguin. Images of penguins are stored inside the application and correspond to the penguin Id. This case can be fixed by storing images on the GAE server or any other. To upload images from the network to the Corona SDK, there is a display.loadRemoteImage() function or a lower-level network.download() .

By clicking on the slide, the gotoPenguin() function is gotoPenguin() , which receives the number (not Id ) of the penguin in the array (table) of all the penguins obtained. This function makes the transition to the final scene of penguin.lua , passing the same argument to this scene.
penguin.lua
 function scene:createScene(event) local group = self.view local background = app.newImage('images/background.jpg', {g = group, w = 384, h = 640, x = _CX, y = _CY}) self.backButton = app.newButton{g = group, x = _L + 10, y = _T + 10, w = 48, h = 32, rp = 'TopLeft', text = 'Back', fontSize = 14, onRelease = function() storyboard.gotoScene('scenes.choose', {effect = 'slideRight', time = app.duration}) end} local ind = event.params local p = app.api.penguins[ind] local visitsLabel = app.newText{g = group, x = _CX, y = _T + 20, text = 'Visits: ' .. p.visit_count, size = 18, color = 'white'} local fishLabel = app.newText{g = group, x = _CX, y = _T + 40, text = 'Fish: ' .. p.fish_count, size = 18, color = 'white'} local bellyrubsLabel = app.newText{g = group, x = _CX, y = _T + 60, text = 'Belly rubs: ' .. p.bellyrub_count, size = 18, color = 'white'} local penguin = app.newImage('images/penguins/' .. p.id .. '.png', {g = group, w = 200, h = 256, x = _CX, y = _CY - 25}) app.newButton{g = group, x = _CX - 80, y = _B - 50, w = 128, h = 48, text = 'Fish', fontSize = 14, onRelease = function() local fish = app.newImage('images/fish.png', {g = group, x = penguin.x, y = penguin.y + 200, w = 512, h = 188}) fish.alpha = 0.8 transition.to(fish, {time = 400, alpha = 1, y = penguin.y, xScale = 0.1, yScale = 0.1, transition = easing.outExpo, onComplete = function(obj) transition.to(fish, {time = 400, alpha = 0, onComplete = function(obj) display.remove(obj) end}) end}) app.api:sendFish(p.id) p.fish_count = p.fish_count + 1 fishLabel:setText('Fish: ' .. p.fish_count) end} app.newButton{g = group, x = _CX + 80, y = _B - 50, w = 128, h = 48, text = 'Belly rub', fontSize = 14, onRelease = function() local hand = app.newImage('images/hand.png', {g = group, x = penguin.x - 40, y = penguin.y + 30, w = 80, h = 80, rp = 'TopLeft'}) transition.to(hand, {time = 1200, x = penguin.x + 40, transition = easing.swing3(easing.outQuad), onComplete = function(obj) display.remove(obj) end}) app.api:sendBellyrub(p.id) p.bellyrub_count = p.bellyrub_count + 1 bellyrubsLabel:setText('Belly rubs: ' .. p.bellyrub_count) end} app.api:sendVisit(p.id) p.visit_count = p.visit_count + 1 visitsLabel:setText('Visits: ' .. p.visit_count) end 

In penguin.lua , the background image, the image of the selected penguin is loaded, several text labels and two action buttons are displayed. When you click on them, the action is animated and the request is sent to the server through the app.api:sendFish() and app.api:sendBellyrub() . And app.api:sendVisit() is called immediately after creating the scene. After calling each of these functions, the corresponding text labels are updated, even if there is no Internet. This can be corrected by entering a check for receiving a response from the server for each request and providing callback functions.

Finally, all work with the server is carried out in the file lib/api.lua .
api.lua
 local _M = {} local app = require('lib.app') _M.hostname = 'http://penguin-daycare-simulator.appspot.com' function _M:getPenguins(callback) local url = '/penguins#' .. math.random(1000, 9999) network.request(self.hostname .. url , 'GET', function (event) if not event.isError then local response = json.decode(event.response) if response then self.penguins = response callback() end end end) end function _M:sendVisit(id) local url = '/stat/visit' local request = {body = 'id=' .. id} network.request(self.hostname .. url , 'POST', function (event) if event.isError then app.alert('Network error') end end, request) end function _M:sendFish(id) local url = '/stat/fish' local request = {body = 'id=' .. id} network.request(self.hostname .. url , 'POST', function (event) if event.isError then app.alert('Network error') end end, request) end function _M:sendBellyrub(id) local url = '/stat/bellyrub' local request = {body = 'id=' .. id} network.request(self.hostname .. url , 'POST', function (event) if event.isError then app.alert('Network error') end end, request) end return _M 

As you might guess, working with the server is done by simple POST requests. In the case of getPenguins() , the response from the server is converted from JSON format to an array (table) by the json.decode() function and placed in the field (property) of the module.

As you can see, it is very easy to send POST requests and respond to their responses in the Corona SDK. Accordingly, the very simple integration with Google App Engine came out. I do not describe what each line does, I hope the syntax is intuitive.

8. References


The sources are on my GitHub:

You can install the client on Android 2.3.3+, here is the APK ( mirror ).
Either download the Corona SDK, download the source from GitHub and launch it in the Corona Simulator.

M0sTH8 .

@SergeyLerg

That's all. Thanks for attention!

Source: https://habr.com/ru/post/220031/


All Articles