bzr branch
http://9ix.org/bzr/ld26
1
by Josh C
zoetrope 1.4 |
1 |
-- Class: App |
2 |
-- An app is where all the magic happens. :) It contains a |
|
3 |
-- view, the group where all major action happens, as well as the |
|
4 |
-- meta view, which persists across views. Only one app may run at |
|
5 |
-- a time. |
|
6 |
-- |
|
7 |
-- An app's job is to get things up and running -- most of its logic |
|
8 |
-- lives in its onRun handler, but for a simple app, you can also |
|
9 |
-- use the onUpdate handler instead of writing a custom <View>. |
|
10 |
-- |
|
11 |
-- Once an app has begun running, it may be accessed globally via |
|
12 |
-- <the>.app. |
|
13 |
-- |
|
14 |
-- Extends: |
|
15 |
-- <Class> |
|
16 |
-- |
|
17 |
-- Event: onRun |
|
18 |
-- Called once, when the app begins running. |
|
19 |
-- |
|
20 |
-- Event: onEnterFullscreen |
|
21 |
-- Called after entering fullscreen successfully. |
|
22 |
-- |
|
23 |
-- Event: onExitFullscreen |
|
24 |
-- Called after exiting fullscreen successfully. |
|
25 |
||
26 |
App = Class:extend |
|
27 |
{ |
|
28 |
-- Property: name |
|
29 |
-- This is shown in the window title bar. |
|
30 |
name = 'Zoetrope', |
|
31 |
||
32 |
-- Property: icon |
|
33 |
-- A path to an image to use as the window icon (a 32x32 PNG is recommended). |
|
34 |
-- This doesn't affect the actual executable's icon in the taskbar or dock. |
|
35 |
||
36 |
-- Property: fps |
|
37 |
-- Maximum frames per second requested. In practice, your |
|
38 |
-- FPS may vary from frame to frame. Every event handler (e.g. onUpdate) |
|
39 |
-- is passed the exact elapsed time in seconds. |
|
40 |
fps = 60, |
|
41 |
||
42 |
-- Property: timeScale |
|
43 |
-- Multiplier for elapsed time; 1.0 is normal, 0 is completely frozen. |
|
44 |
timeScale = 1, |
|
45 |
||
46 |
-- Property: active |
|
47 |
-- If false, nothing receives update-related events, including the meta view. |
|
48 |
-- These events specifically are onStartFrame, onUpdate, and onEndFrame. |
|
49 |
active = true, |
|
50 |
||
51 |
-- Property: deactivateOnBlur |
|
52 |
-- Should the app automatically set its active property to false when its |
|
53 |
-- window loses focus? |
|
54 |
deactivateOnBlur = true, |
|
55 |
||
56 |
-- Property: view |
|
57 |
-- The current <View>. When the app is running, this is also accessible |
|
58 |
-- globally via <the>.view. In order to switch views, you must set this |
|
59 |
-- property, *not* <the>.view. |
|
60 |
||
61 |
-- Property: meta |
|
62 |
-- A <Group> that persists across all views during the app's lifespan. |
|
63 |
||
64 |
-- Property: keys |
|
65 |
-- A <Keys> object that listens to the keyboard. When the app is running, this |
|
66 |
-- is also accessible globally via <the>.keys. |
|
67 |
||
68 |
-- Property: width |
|
69 |
-- The width of the app's canvas in pixels. Changing this value has no effect. To |
|
70 |
-- set this for real, edit conf.lua. This may *not* correspond to the overall |
|
71 |
-- resolution of the window when in fullscreen mode. |
|
72 |
||
73 |
-- Property: height |
|
74 |
-- The height of the window in pixels. Changing this value has no effect. To |
|
75 |
-- set this for real, edit conf.lua. This may *not* correspond to the overall |
|
76 |
-- resolution of the window when in fullscreen mode. |
|
77 |
||
78 |
-- Property: fullscreen |
|
79 |
-- Whether the app is currently running in fullscreen mode. Changing this value |
|
80 |
-- has no effect. To change this, use the enterFullscreen(), exitFullscreen(), or |
|
81 |
-- toggleFullscreen() methods. |
|
82 |
||
83 |
-- Property: inset |
|
84 |
-- The screen coordinates where the app's (0, 0) origin should lie. This is only |
|
85 |
-- used by Zoetrope to either letterbox or pillar fullscreen apps, but there may |
|
86 |
-- be other uses for it. |
|
87 |
inset = { x = 0, y = 0}, |
|
88 |
||
89 |
-- internal property: _sleepTime |
|
90 |
-- The amount of time the app intentionally slept for, and should not |
|
91 |
-- be counted against elapsed time. |
|
92 |
||
93 |
-- internal property: _nextFrameTime |
|
94 |
-- Set at the start of a frame to be the next time a frame should be rendered, |
|
95 |
-- in timestamp format. This is used to properly maintain FPS -- see the example |
|
96 |
-- in https://love2d.org/wiki/love.timer.sleep for how this works. |
|
97 |
||
98 |
new = function (self, obj) |
|
99 |
obj = self:extend(obj) |
|
100 |
||
101 |
-- set icon if possible |
|
102 |
||
103 |
if self.icon then |
|
104 |
love.graphics.setIcon(Cached:image(self.icon)) |
|
105 |
end |
|
106 |
||
107 |
-- view containers |
|
108 |
||
109 |
obj.meta = obj.meta or Group:new() |
|
110 |
obj.view = obj.view or View:new() |
|
111 |
||
112 |
-- input |
|
113 |
||
114 |
if love.keyboard then |
|
115 |
obj.keys = obj.keys or Keys:new() |
|
116 |
love.keyboard.setKeyRepeat(0.4, 0.04) |
|
117 |
obj.meta:add(obj.keys) |
|
118 |
end |
|
119 |
||
120 |
if love.mouse then |
|
121 |
obj.mouse = obj.mouse or Mouse:new() |
|
122 |
obj.meta:add(obj.mouse) |
|
123 |
end |
|
124 |
||
125 |
if love.joystick then |
|
126 |
obj.gamepads = {} |
|
127 |
the.gamepads = obj.gamepads |
|
128 |
end |
|
129 |
||
130 |
if obj.numGamepads and obj.numGamepads > 0 then |
|
131 |
for i = 1, obj.numGamepads do |
|
132 |
obj.gamepads[i] = Gamepad:new{ number = i } |
|
133 |
obj.meta:add(obj.gamepads[i]) |
|
134 |
end |
|
135 |
end |
|
136 |
||
137 |
-- screen dimensions and state |
|
138 |
||
139 |
obj.width, obj.height, obj.fullscreen = love.graphics.getMode() |
|
140 |
||
141 |
-- housekeeping |
|
142 |
||
143 |
the.app = obj |
|
144 |
if obj.onNew then obj:onNew() end |
|
145 |
return obj |
|
146 |
end, |
|
147 |
||
148 |
-- Method: run |
|
149 |
-- Starts the app running. Nothing will occur until this call. |
|
150 |
-- |
|
151 |
-- Arguments: |
|
152 |
-- none |
|
153 |
-- |
|
154 |
-- Returns: |
|
155 |
-- nothing |
|
156 |
||
157 |
run = function (self) |
|
158 |
math.randomseed(os.time()) |
|
159 |
||
160 |
-- sync the.view |
|
161 |
||
162 |
the.view = self.view |
|
163 |
||
164 |
-- attach debug console |
|
165 |
||
166 |
if DEBUG then |
|
167 |
self.console = DebugConsole:new() |
|
168 |
self.meta:add(self.console) |
|
169 |
end |
|
170 |
||
171 |
-- set up callbacks |
|
172 |
||
173 |
love.graphics.setCaption(self.name) |
|
174 |
love.update = function (elapsed) self:update(elapsed) end |
|
175 |
love.draw = function() self:draw() end |
|
176 |
love.focus = function (value) self:onFocus(value) end |
|
177 |
||
178 |
if self.onRun then self:onRun() end |
|
179 |
self._nextFrameTime = love.timer.getMicroTime() |
|
180 |
end, |
|
181 |
||
182 |
-- Method: quit |
|
183 |
-- Quits the application immediately. |
|
184 |
-- |
|
185 |
-- Arguments: |
|
186 |
-- none |
|
187 |
-- |
|
188 |
-- Returns: |
|
189 |
-- nothing |
|
190 |
||
191 |
quit = function (self) |
|
192 |
love.event.quit() |
|
193 |
end, |
|
194 |
||
195 |
-- Method: useSysCursor |
|
196 |
-- Shows or hides the system mouse cursor. |
|
197 |
-- |
|
198 |
-- Arguments: |
|
199 |
-- value - show the cursor? |
|
200 |
-- |
|
201 |
-- Returns: |
|
202 |
-- nothing |
|
203 |
||
204 |
useSysCursor = function (self, value) |
|
205 |
if STRICT then |
|
206 |
assert(value == true or value == false, |
|
207 |
'tried to set system cursor visibility to ' .. type(value)) |
|
208 |
end |
|
209 |
||
210 |
love.mouse.setVisible(value) |
|
211 |
end, |
|
212 |
||
213 |
-- Method: enterFullscreen |
|
214 |
-- Enters fullscreen mode. If the app is already in fullscreen, this has no effect. |
|
215 |
-- This tries to use the highest resolution that will not result in distortion, and |
|
216 |
-- adjust the app's offset property to accomodate this. |
|
217 |
-- |
|
218 |
-- Arguments: |
|
219 |
-- hint - whether to try to letterbox (vertical black bars) or pillar the app |
|
220 |
-- (horizontal black bars). You don't need to specify this; the method |
|
221 |
-- will try to infer based on the aspect ratio of your app. |
|
222 |
-- |
|
223 |
-- Returns: |
|
224 |
-- boolean whether this succeeded |
|
225 |
||
226 |
enterFullscreen = function (self, hint) |
|
227 |
if STRICT then |
|
228 |
assert(not self.fullscreen, 'asked to enter fullscreen when already in fullscreen') |
|
229 |
end |
|
230 |
||
231 |
local modes = love.graphics.getModes() |
|
232 |
||
233 |
if not hint then |
|
234 |
if self.width * 9 == self.height * 16 then |
|
235 |
hint = 'letterbox' |
|
236 |
elseif self.width * 3 == self.height * 4 then |
|
237 |
hint = 'pillar' |
|
238 |
end |
|
239 |
end |
|
240 |
||
241 |
-- find the mode with the highest screen area that |
|
242 |
-- matches either width or height, according to our hint |
|
243 |
||
244 |
local bestMode = { area = 0 } |
|
245 |
||
246 |
for _, mode in pairs(modes) do |
|
247 |
mode.area = mode.width * mode.height |
|
248 |
||
249 |
if (mode.area > bestMode.area) and |
|
250 |
((hint == 'letterbox' and mode.width == self.width) or |
|
251 |
(hint == 'pillar' and mode.height == self.height)) then |
|
252 |
bestMode = mode |
|
253 |
end |
|
254 |
end |
|
255 |
||
256 |
-- if we found a match, switch to it |
|
257 |
||
258 |
if bestMode.width then |
|
259 |
love.graphics.setMode(bestMode.width, bestMode.height, true) |
|
260 |
self.fullscreen = true |
|
261 |
||
262 |
-- and adjust inset and scissor |
|
263 |
||
264 |
self.inset.x = math.floor((bestMode.width - self.width) / 2) |
|
265 |
self.inset.y = math.floor((bestMode.height - self.height) / 2) |
|
266 |
love.graphics.setScissor(self.inset.x, self.inset.y, self.width, self.height) |
|
267 |
||
268 |
if self.onEnterFullscreen then self:onEnterFullscreen() end |
|
269 |
end |
|
270 |
||
271 |
return self.fullscreen |
|
272 |
end, |
|
273 |
||
274 |
-- Method: exitFullscreen |
|
275 |
-- Exits fullscreen mode. If the app is already windowed, this has no effect. |
|
276 |
-- |
|
277 |
-- Arguments: |
|
278 |
-- none |
|
279 |
-- |
|
280 |
-- Returns: |
|
281 |
-- nothing |
|
282 |
||
283 |
exitFullscreen = function (self) |
|
284 |
if STRICT then |
|
285 |
assert(self.fullscreen, 'asked to exit fullscreen when already out of fullscreen') |
|
286 |
end |
|
287 |
||
288 |
love.graphics.setMode(self.width, self.height, false) |
|
289 |
love.graphics.setScissor(0, 0, self.width, self.height) |
|
290 |
self.fullscreen = false |
|
291 |
self.inset.x = 0 |
|
292 |
self.inset.y = 0 |
|
293 |
if self.onExitFullscreen then self:onExitFullscreen() end |
|
294 |
end, |
|
295 |
||
296 |
-- Method: toggleFullscreen |
|
297 |
-- Toggles between windowed and fullscreen mode. |
|
298 |
-- |
|
299 |
-- Arguments: |
|
300 |
-- none |
|
301 |
-- |
|
302 |
-- Returns: |
|
303 |
-- nothing |
|
304 |
||
305 |
toggleFullscreen = function (self) |
|
306 |
if self.fullscreen then |
|
307 |
self:exitFullscreen() |
|
308 |
else |
|
309 |
self:enterFullscreen() |
|
310 |
end |
|
311 |
end, |
|
312 |
||
313 |
-- Method: saveScreenshot |
|
314 |
-- Saves a snapshot of the current frame to disk. |
|
315 |
-- |
|
316 |
-- Arguments: |
|
317 |
-- filename - filename to save as, image format is implied by suffix. |
|
318 |
-- This is forced inside the app's data directory, |
|
319 |
-- see https://love2d.org/wiki/love.filesystem for details. |
|
320 |
-- |
|
321 |
-- Returns: |
|
322 |
-- nothing |
|
323 |
||
324 |
saveScreenshot = function (self, filename) |
|
325 |
if not filename then |
|
326 |
error('asked to save screenshot to a nil filename') |
|
327 |
end |
|
328 |
||
329 |
local screenshot = love.graphics.newScreenshot() |
|
330 |
screenshot:encode(filename) |
|
331 |
end, |
|
332 |
||
333 |
-- Method: add |
|
334 |
-- A shortcut for adding a sprite to the app's view. |
|
335 |
-- |
|
336 |
-- Arguments: |
|
337 |
-- sprite - sprite to add |
|
338 |
-- |
|
339 |
-- Returns: |
|
340 |
-- nothing |
|
341 |
||
342 |
add = function (self, sprite) |
|
343 |
self.view:add(sprite) |
|
344 |
end, |
|
345 |
||
346 |
-- Method: remove |
|
347 |
-- A shortcut for removing a sprite from the app's view. |
|
348 |
-- |
|
349 |
-- Arguments: |
|
350 |
-- sprite - sprite to remove |
|
351 |
-- |
|
352 |
-- Returns: nothing |
|
353 |
||
354 |
remove = function (self, sprite) |
|
355 |
self.view:remove(sprite) |
|
356 |
end, |
|
357 |
||
358 |
update = function (self, elapsed) |
|
359 |
-- set the next frame time right at the start |
|
360 |
self._nextFrameTime = self._nextFrameTime + 1 / self.fps |
|
361 |
||
362 |
elapsed = elapsed - (self._sleepTime or 0) |
|
363 |
||
364 |
local realElapsed = elapsed |
|
365 |
elapsed = elapsed * self.timeScale |
|
366 |
||
367 |
-- sync the.view with our current view |
|
368 |
||
369 |
local view = self.view |
|
370 |
if the.view ~= view then the.view = view end |
|
371 |
||
372 |
-- if we are not active at all, sleep for a half-second |
|
373 |
||
374 |
if not self.active then |
|
375 |
love.timer.sleep(0.5) |
|
376 |
self._sleepTime = 0.5 |
|
377 |
return |
|
378 |
end |
|
379 |
||
380 |
self._sleepTime = 0 |
|
381 |
||
382 |
-- update everyone |
|
383 |
-- all update events bubble up from child to parent |
|
384 |
-- (we consider the meta view a little |
|
385 |
||
386 |
view:startFrame(elapsed) |
|
387 |
self.meta:startFrame(elapsed) |
|
388 |
if self.onStartFrame then self:onStartFrame(elapsed) end |
|
389 |
||
390 |
view:update(elapsed) |
|
391 |
self.meta:update(elapsed) |
|
392 |
if self.onUpdate then self:onUpdate(elapsed) end |
|
393 |
||
394 |
view:endFrame(elapsed) |
|
395 |
self.meta:endFrame(elapsed) |
|
396 |
if self.onEndFrame then self:onEndFrame(elapsed) end |
|
397 |
end, |
|
398 |
||
399 |
draw = function (self) |
|
400 |
local inset = self.inset.x ~= 0 or self.inset.y ~= 0 |
|
401 |
||
402 |
if inset then love.graphics.translate(self.inset.x, self.inset.y) end |
|
403 |
self.view:draw() |
|
404 |
self.meta:draw() |
|
405 |
if inset then love.graphics.translate(0, 0) end |
|
406 |
||
407 |
-- sleep off any unneeded time to keep up at our FPS |
|
408 |
||
409 |
local now = love.timer.getMicroTime() |
|
410 |
||
411 |
if self._nextFrameTime < now then |
|
412 |
self._nextFrameTime = now |
|
413 |
else |
|
414 |
love.timer.sleep(self._nextFrameTime - now) |
|
415 |
end |
|
416 |
end, |
|
417 |
||
418 |
onFocus = function (self, value) |
|
419 |
if self.deactivateOnBlur then |
|
420 |
self.active = value |
|
421 |
||
422 |
if value then |
|
423 |
love.audio.resume() |
|
424 |
else |
|
425 |
love.audio.pause() |
|
426 |
end |
|
427 |
end |
|
428 |
end, |
|
429 |
||
430 |
__tostring = function (self) |
|
431 |
local result = 'App (' |
|
432 |
||
433 |
if self.active then |
|
434 |
result = result .. 'active' |
|
435 |
else |
|
436 |
result = result .. 'inactive' |
|
437 |
end |
|
438 |
||
439 |
return result .. ', ' .. self.fps .. ' fps, ' .. self.view:count(true) .. ' sprites)' |
|
440 |
end |
|
441 |
} |