Advertisement
Snusmumriken

Love2d animation spritebatch/ffi

Jan 13th, 2017
462
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
Lua 15.96 KB | None | 0 0
  1. -- Use unicode-monotype fonts, be nice
  2.  
  3. local s = [[
  4. ░ ▒ ▓ █ ▄ ▌ ▐ ▀
  5. │ ─ ├ ┼ ┤ ┌ ┐ ┘ └ ┴ ┬
  6. ║ ═ ╠ ╬ ╣ ╔ ╗ ╝ ╚ ╩ ╦
  7. ╞ ╪ ╡ ╒ ╕ ╛ ╘ ╧ ╤
  8. ╟ ╫ ╢ ╓ ╖ ╜ ╙ ╨ ╥
  9. ]]
  10. --os.execute('chcp 65001')
  11.  
  12. --[[
  13. Reference:
  14.  
  15.  Here - spritesheet by 2, 3 resolution (sx, sy).
  16.  
  17.  \sx   1       2       3
  18.  s ╔═══════╦═══════╦═══════╗─
  19.  y ║       │       │       ║my
  20.    ║ ┌───┐ │ ┌───┐ │ ┌───┐ ║─
  21.    ║ │ ▲ │ │ │ ▲ │ │ │ ▲ │ ║
  22.  1 ║ │◢█◣│ │ │◢▓◣│ │ │◢▒◣│ ║
  23.    ║ │▀ ▀│ │ │▀v▀│ │ │▀V▀│ ║
  24.    ║ └───┘ │ └───┘ │ └───┘ ║
  25.    ║       │       │       ║
  26.    ╠───────╬───────╬───────╣
  27.    ║       │       │       ║
  28.    ║ ┌───┐ │ ┌───┐ │ ┌───┐ ║
  29.    ║ │ ▲ │ │ │ ▲ │ │ │ ▲ │ ║
  30.  2 ║ │◢▓◣│ │ │◢█◣│ │ │◢▒◣│ ║
  31.    ║ │▀V▀│ │ │▀Y▀│ │ │▀W▀│ ║
  32.    ║ └───┘ │ └───┘ │ └───┘ ║
  33.    ║       │       │       ║
  34.    ╚═══════╩═══════╩═══════╝
  35.     │m│
  36.      x
  37.  
  38.  w and h - width and height of one sprite.
  39.  mx and my - margin by x, margin by y.
  40.  The final dimensions are calculated automatically
  41.  
  42. New grid for this example make as:
  43.  grid = require'grid'
  44.  ship_img = love.graphics.newImage('ship.png')
  45.  ship_grid = grid(img, 3, 2, mx, my)
  46. where 3 and 2 - image-resolution, mx, my = margin by pixels
  47. TIP: you need to double-margin between sprites, it useful for
  48.  
  49. Now, we want to add two animations of ship blast-off and ship flying, like this:
  50. 1. Launch
  51.  ╔═══════╦═══════╦═══════╗
  52.  ║       │       │       ║
  53.  ║ ┌───┐ │ ┌───┐ │ ┌───┐ ║
  54.  ║ │ ▲ │ │ │ ▲ │ │ │ ▲ │ ║
  55.  ║ │◢█◣│ │ │◢█◣│ │ │◢█◣│ ║
  56.  ║ │▀v▀│ │ │▀Y▀│ │ │▀W▀│ ║
  57.  ║ └───┘ │ └───┘ │ └───┘ ║
  58.  ║       │       │       ║
  59.  ╚═══════╩═══════╩═══════╝
  60. 2. Flying
  61.  ╔═══════╦═══════╦═══════╗
  62.  ║       │       │       ║
  63.  ║ ┌───┐ │ ┌───┐ │ ┌───┐ ║
  64.  ║ │ ▲ │ │ │ ▲ │ │ │ ▲ │ ║
  65.  ║ │◢▓◣│ │ │◢█◣│ │ │◢▒◣│ ║
  66.  ║ │▀V▀│ │ │▀Y▀│ │ │▀W▀│ ║
  67.  ║ └───┘ │ └───┘ │ └───┘ ║
  68.  ║       │       │       ║
  69.  ╚═══════╩═══════╩═══════╝
  70.  
  71.  
  72. Syntax: [grid]:newAnimation('name', {'rowA:col1-col2 delay'[, 'row:col1-col2 delay'...]})
  73.  
  74.  ship_grid:newAnimation('launch', {'1:1-3 0.5'})    -- high delay, 1.5s summary launching animation
  75.  ship_grid:newAnimation('flying', {'2:1-3 0.3'})  -- high delay, ~1s summary lenth of animation
  76.  
  77. Create ship, with sprite-instance, update and draw.
  78.  
  79.     function love.load()
  80.         ship = {x = 10, y = 20, speed = 0, angle = 0}
  81.         ship.sprite = [grid]:new(ship.x, ship.y, ship.angle)
  82.     end
  83.  
  84.     function love.update(dt)
  85.         if love.keyboard.isDown('left') then ship.angle = ship.angle - 0.1*dt end
  86.         if love.keyboard.isDown('right') then ship.angle = ship.angle - 0.1*dt end
  87.         if love.keyboard.isDown('up') then ship.speed = ship.speed + 5*dt end
  88.         ship.speed = ship.speed * 0.9*dt        -- slowdown
  89.         ship.x, ship.y = ship.x + math.cos(ship.angle)*ship.speed, ship.y + math.sin(ship.angle)*ship.speed
  90.         ship.sprite:draw(ship.x, ship.y, ship.angle)
  91.         -- We can flip sprite:
  92.         local x, y = love.mouse.getPosition()
  93.         ship.sprite:flip(x > ship.x) -- true if mouse position > ship.position
  94.         -- ship.sprite:flip(y > ship.y, 'y') -- flip by y
  95.     end
  96.  
  97.     function love.draw()
  98.         ship_grid:draw()
  99.     end
  100.  
  101.  
  102.  
  103. ]]
  104.  
  105. local min, max, floor, ceil, sin, cos, abs = math.min, math.max, math.floor, math.ceil, math.sin, math.cos, math.abs
  106. local type, graphics = type, love.graphics
  107.  
  108. local conf = {autoflush = true, checks = true}
  109.  
  110. local ch = {n = 'number', s = 'string', t = 'table', f = 'function', c = 'cdata'}
  111. local errorstr = 'Grid: arg#%d error: "%s" expected, got "%s" '
  112. local function checker(...)
  113.     local list, ch, a, b = {...}, ch
  114.     for i = 1, math.floor(#list/2) do
  115.         a, b = list[i*2-1], list[i*2]
  116.         a = a and type(a) == ch[b:sub(1, 1)]
  117.                     or b:sub(-1) == '?' and type(a) == 'nil'
  118.                     or error(errorstr:format(i, ch[b:sub(1, 1)], type(a)), 3)
  119.     end
  120. end
  121.  
  122. local function err(v, lvl, ...)
  123.     error(v:format(...), lvl)
  124. end
  125.  
  126. local function mt__gc(t, mt)        --collect garbage method trick
  127.     local mt = mt or getmetatable(t)
  128.     local prox = newproxy(true)
  129.     getmetatable(prox).__gc = function() mt.__gc(t) end
  130.     t[prox] = true
  131.     return setmetatable(t, mt)
  132. end
  133.  
  134. local function _gcd(x, y, min)  -- greatest common divisor
  135.     min = min or 0.00000001             -- max precision, cause 0.1 + 0.2 = 0.300000000004
  136.     if x > y then x, y = y, x end
  137.     while y >= min do
  138.         x, y = y, x%y
  139.     end
  140.     return x
  141. end
  142.  
  143. local function gcd(...)
  144.     local t = {...}
  145.     table.sort(t)
  146.     local res = t[1]
  147.     for i = 2, #t do
  148.         res = _gcd(res, t[i])
  149.     end
  150.     return res
  151. end
  152.  
  153. local function checkTableType(t, tp, n)
  154.     for i, v in ipairs(t) do
  155.         if type(v) ~= tp then err('Grid: arg#%s error: %s expected, got %s', 4, i, tp, type(v)) end
  156.     end
  157.     return t
  158. end
  159.  
  160. local function tableid(t)
  161.     return tonumber(tostring(t):match('.-(0x%x+).*'))           -- get link of table for storage
  162. end
  163.  
  164. local gridlist = {}             -- storage for links to grids/animations and DirtyHack
  165.  
  166. local entitie = {}
  167. entitie.__index = function(self, key)
  168.     return entitie[key] or
  169.         key == 'grid' and gridlist[self.gridid].grid or
  170.         key == 'anim' and gridlist[self.gridid].anim[self.animid]
  171. end
  172.  
  173. function entitie:__gc()
  174.     print('free '..self.id)
  175.     self.grid:freeSprite(self.id)
  176. end
  177.  
  178. local ffi = require("ffi")
  179. ffi.cdef[[
  180. typedef struct {
  181.     long double x, y, r, sx, sy, ox, oy, kx, ky;
  182.     bool fx, fy, paused;
  183.     char loop;
  184.     float time, speed;
  185.     int id, quad;   // sprite id, quad number
  186.     int gridid;     // table id of grid
  187.     int animid;     // table id of grid-animaton
  188. } grid_entitie;
  189. ]]
  190.  
  191. local g_entitie = ffi.metatype("grid_entitie", entitie) -- OOP
  192.  
  193.  
  194. setmetatable(entitie, {
  195.     --x, y, r, sx, sy, ox, oy, kx, ky
  196.     __call = function(self, grid, ...)
  197.         local t = checkTableType({...}, 'number', 1)
  198.         local o = g_entitie()
  199.         o.x, o.y, o.r, o.sx, o.sy, o.ox, o.oy, o.kx, o.ky =
  200.             t[1] or 0, t[2] or 0, t[3] or 0, t[4] or 1, t[5] or 1, t[6] or 0, t[7] or 0, t[8] or 0, t[9] or 0
  201.         o.time, o.speed = 0, 1.0
  202.         o.fx, o.fy = false, false
  203.         o.loop, o.paused = false, false
  204.         o.gridid, o.animid = tableid(grid), 0
  205.         o.id, o.quad = 1, 1
  206.         return o
  207.     end
  208. })
  209.  
  210. function entitie:getGrid()
  211.     return gridlist[self.gridid].grid
  212. end
  213.  
  214. local flushlist = {}
  215.  
  216. function entitie:draw(...)
  217.     local t = checkTableType({...}, 'number')
  218.     self.x, self.y, self.r, self.sx, self.sy, self.ox, self.oy, self.kx, self.ky =
  219.     t[1] or 0, t[2] or 0, t[3] or 0, t[4] or 1, t[5] or 1, t[6] or 0, t[7] or 0, t[8] or 0, t[9] or 0
  220.     self:update()
  221. end
  222.  
  223. function entitie:getAnim()
  224.     return gridlist[self.gridid].anim[self.animid]
  225. end
  226.  
  227. function entitie:_flush()
  228.     flushlist[self] = true
  229. end
  230.  
  231. function entitie:getDrawData()
  232.     return self.fx and self.x + self.w*self.sx or self.x,
  233.                  self.fy and self.y + self.h*self.sy or self.y,
  234.                  self.r,
  235.                  self.fx and -self.sx or self.sx,
  236.                  self.fy and -self.sy or self.sy,
  237.                  self.ox, self.oy, self.kx, self.ky
  238. end
  239.  
  240. function entitie:setPosition(x, y)
  241.     self.x, self.y = x, y
  242.     if conf.autoflush then self:_flush() end
  243. end
  244.  
  245. function entitie:getPosition(x, y)
  246.     return self.x, self.y
  247. end
  248.  
  249. function entitie:setOffset(x, y)
  250.     self.ox, self.oy = x or self.ox, y or self.oy
  251.     if conf.autoflush then self:_flush() end
  252. end
  253.  
  254. function entitie:setScale(x, y)
  255.     self.sx, self.sy = x or self.sx, y or self.sy
  256.     if conf.autoflush then self:_flush() end
  257. end
  258.  
  259. function entitie:setAngle(v)
  260.     self.r = v or self.r
  261.     if conf.autoflush then self:_flush() end
  262. end
  263.  
  264. function entitie:setSprite(x, y)
  265.     local a = self.anim
  266.     if self.animid > 0 and not y then
  267.         self.time = a.sprite[    ceil( self.time / a.delay % a.count)]
  268.     else
  269.         self.quad = self.grid:checkSprite(x, y)
  270.     end
  271.     if conf.autoflush then self:_flush() end
  272. end
  273.  
  274. function entitie:setPause(v)
  275.         self.paused = not not v
  276. end
  277.  
  278. local mode = {}
  279. function mode.loop(t, delay, count) -- repeat-loop
  280.     return ceil( t / delay % count)
  281. end
  282.  
  283. function mode.wave(t, delay, count) -- bounce-loop
  284.     return ceil(abs(t/delay % (count*2)-count))
  285. end
  286.  
  287. local function round(v) return floor(v) - v > .5 and ceil(v) or floor(v) end
  288. function mode.sin(t, delay, count)  -- sinus
  289.     return round((sin( (3.14 * t) / (delay * count) ) *.5 + .5)*(count))
  290. end
  291.  
  292. function mode.stop(t, delay, count) -- single
  293.     return min(ceil(t / delay), count)
  294. end
  295.  
  296. -- aliaces
  297. mode[1] = mode.loop
  298. mode[2] = mode.wave
  299. mode[3] = mode.sin
  300. mode[4] = mode.stop
  301.  
  302. setmetatable(mode, {__index = function() return mode.loop end})
  303.  
  304. function entitie:update(dt)
  305.     local grid = self.grid
  306.     self.time = not self.paused and self.time + (dt or grid.dt)*self.speed or self.time
  307.     grid.batch:setColor(graphics.getColor())
  308.     if self.animid > 0 then
  309.         local a = self.anim
  310.         self.quad = a.sprite[mode[self.loop](self.time, a.delay, a.count)]
  311.                              or 1
  312.     end
  313.     grid:set(self.id, self.quad, self:getDrawData())
  314. end
  315.  
  316. function entitie:setSpeed(v)
  317.     self.speed = v or 1
  318. end
  319.  
  320. function entitie:setAnimation(name, speed, mode)
  321.     local anims = self.grid.anim
  322.     self.time = 0
  323.     self.animid = anims[name] and anims[name].id or 0
  324.     self.speed = speed or 1
  325.     self.loop = mode or 1
  326.     self.quad = 1
  327. end
  328.  
  329. function entitie:flip(v, ord)
  330.     if ord == 'y' then
  331.         self.fy = not not v
  332.     else
  333.         self.fx = not not v
  334.     end
  335. end
  336.  
  337. local grid = {}                                                             -- Spritegrid class
  338.  
  339. setmetatable(grid, {
  340.     __call = function(self, img, sx, sy, mx, my, count)             -- sx & sy - sprite count in grid, mx, my - margin
  341.         local o = {img = img}
  342.         o.batch = graphics.newSpriteBatch(o.img, count or 10000)
  343.         o.free = {} -- Free id for reusing
  344.         o.anim = {} -- Animations list
  345.         o.sx = sx or 1  -- Resolution by sprites, one by default (1:1 grid)
  346.         o.sy = sy or 1
  347.         o.dt = love.timer.getDelta()
  348.         local imgx, imgy = o.img:getDimensions()
  349.         local mx, my = mx or 0, my or 0                     -- margin of every sprite
  350.         o.sw, o.sh = floor(imgx / o.sx), floor(imgy / o.sy) -- One sprite dimensions
  351.         o.quad = {}                                                             -- Quad list
  352.         for j = 0, o.sy-1 do
  353.             for i = 0, o.sx-1 do
  354.                 local x, y, w, h = i*o.sw + mx, j*o.sh + my, o.sw - mx*2, o.sh - my*2
  355.                 table.insert(o.quad, graphics.newQuad(x, y, w, h, imgx, imgy)) -- quad initialisation
  356.             end
  357.         end
  358.         o.id = tableid(o)
  359.         self.__index = self
  360.         gridlist[o.id] = {grid = o, anim = setmetatable({}, {__mode = 'kv'})}
  361.         return mt__gc(o, {__index = self, __gc = function(self) gridlist[self.id] = false; print('grid freed') end})
  362.     end
  363. })
  364.  
  365. local function checkSet(t, x, y)            -- check the existence of sprites in set
  366.     local count = x * y
  367.     for i = 1, #t do
  368.         t[i] = floor(t[i])
  369.         if t[i] < 1 or t[i] > count then
  370.             err('Grid:newAnimation error: %d sprite not found, grid dimensions: [%d, %d], min/max: 1, %d', 4, i, x, y, count)
  371.         end
  372.     end
  373.     return t
  374. end
  375.  
  376. local function linearize(t, y)                                  -- transform [x, y] - spritegrid to absolute [x] position
  377.     for i, v in ipairs(t) do
  378.         print(v[1], v[2], v[1] + (v[2]-1) * y)
  379.     end
  380.    
  381.     local buf = {}
  382.     if type(t[1]) == 'table' then
  383.             for i, v in ipairs(t) do
  384.                 table.insert(buf, v[1] + (v[2]-1) * y)
  385.             end
  386.     end
  387.     return #buf > 0 and buf or t
  388. end
  389.  
  390. function grid:newAnimation(name, t, loop)
  391.     checker(name, 's', t, 't', loop, 'b?')
  392.     local tn = tonumber
  393.     local set = {}                      -- raw quad-set data
  394.     local delays = {}                   -- table of all delays for gcd
  395.     if type(t) ~= 'table' or #t == 0 then error('Grid: newAnimation: arg#2 table expected, got '..type(t)) end
  396.     for i, v in ipairs(t) do    -- parse strings
  397.         if type(v) ~= 'string' then error('Grid: newAnimation: arg#2 table of strings like "2:1-6 0.2" expected, got "'..type(t[1])..'" arg on '..i..' position') end
  398.         local pattern = '(%d+):(%d+)%-(%d+)%s(%d+%.?%d*)' -- "a:b-c d.?e?"
  399.         local y, x1, x2, d = v:match(pattern)                           -- column-start-end-delay
  400.         y, x1, x2, d = tn(y), tn(x1), tn(x2), tn(d)
  401.         print(name, y, x1, x2, d)
  402.         if not (y and x1 and x2 and d) then error('Grid: newAnimation: arg#2-'..i..' error: pattern like "row:column1-column2 delay" in digits expected, got: '..v) end
  403.         table.insert(set, {y = y, x1 = x1, x2 = x2, d = d})
  404.         table.insert(delays, d)
  405.     end
  406.     local del = #delays > 1 and gcd(unpack(delays)) or delays[1]    -- gcd
  407.     t = {}
  408.     for _, v in ipairs(set) do
  409.         for i = v.x1, v.x2, (v.x1 > v.x2 and -1 or 1) do                        -- forward and reverse order like 1-10 and 10-1
  410.             for n = 1, v.d/del do
  411.                 table.insert(t, {i, v.y})
  412.             end
  413.         end
  414.     end
  415.     print('YS', self.sx)
  416.     local o = {
  417.         sprite = checkSet(linearize(t, self.sx), self.sx, self.sy),
  418.         count = #t,
  419.         delay = del,
  420.         loop = loop or 'loop',
  421.         name = name,
  422.     }
  423.     o.id = tableid(o)
  424.     self.anim[name] = o
  425.     gridlist[tableid(self)].anim[tableid(self.anim[name])] = self.anim[name]
  426. end
  427.  
  428. function grid:getQuad(x, y)
  429.     if conf.checks then checker(x, 'n', y, 'n?') end
  430.     return y and self.quad[(y-1)*self.sy+x]     --вычисление позиции квада в списке
  431.                         or self.quad[x]
  432. end
  433.  
  434. function grid:checkSprite(x, y)
  435.     if y then
  436.         return self.quad[(y-1)*self.sy+x] and (y-1)*self.sy+x or error('Error')
  437.     else
  438.         return self.quad[x] and x or error('error2')
  439.     end
  440.    
  441. end
  442.  
  443. function grid:addSprite(n, ...)
  444.     if #self.free > 0 then
  445.         local p = self.free[#self.free]; table.remove(self.free, #self.free)
  446.         print('reuse sprite '..p)
  447.         return p, self.batch:set(p, n and self.quad[n] or self.quad[1], ...)
  448.     end
  449.     return self.batch:add(n and self.quad[n] or self.quad[1], ...)
  450. end
  451.  
  452. function grid:freeSprite(n)
  453.     table.insert(self.free, n)
  454.     self.batch:set(n, 0, 0, 0, 0, 0)
  455. end
  456.  
  457. function grid:new(...)
  458.     if conf.checks then checkTableType({...}, 'number') end
  459.     local entitie = entitie(self, ...)
  460.     entitie.gridid = self.id
  461.     entitie.id = self:addSprite(1, ...)
  462.     return entitie
  463. end
  464.  
  465. function grid:clear(id, n, ...)
  466.     self.batch:clear()
  467.     collectgarbage()
  468. end
  469.  
  470. function grid:set(id, n, ...)
  471.     self.batch:set(id, self:getQuad(n), ...)
  472. end
  473.  
  474. function grid:getSpriteDimensions()
  475.     return self.sw, self.sh
  476. end
  477.  
  478. function grid:getDimensions()
  479.     return self.sx, self.sy
  480. end
  481.  
  482. function grid:draw(...)
  483.     if conf.checks then checkTableType({...}, 'number') end
  484.     if conf.autoflush then
  485.         for k, _ in pairs(flushlist) do
  486.             k:update()
  487.         end
  488.         flushlist = {}
  489.     end
  490.     self.dt = love.timer.getDelta()
  491.     collectgarbage()
  492.     graphics.draw(self.batch, ...)
  493. end
  494.  
  495. function grid:autoupdate(v)
  496.     conf.autoflush = not not v
  497. end
  498.  
  499. function grid:debug(v)
  500.     conf.checks = not not v
  501. end
  502.  
  503. local function unittest()
  504.     img = love.graphics.newImage'Sphere_4.png'
  505.     img:setFilter('nearest')
  506.     gr = grid(img, 16)
  507.     gr:newAnimation('move', {'1:1-16 0.05'})
  508.     gr:newAnimation('remove', {'1:16-1 0.02'})
  509.     sprites = {}
  510.     local r = math.random
  511.     for i = 1, 1000000 do
  512.         local e = gr:new(r(100), r(100), r(314)/100, r(-10, 10)/10, r(-10, 10)/10, -22, -22)
  513.         e:setAnimation(r() == 1 and 'move'or 'remove')
  514.         table.insert(sprites, e)
  515.     end
  516. end
  517.  
  518. --unittest()
  519.  
  520. return grid
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement