Basic Crank rotate sprite

I have a sprite that I can move around with the d-pad but what I want is to rotate the sprite with the crank, struggling to understand the basics of the crank in that regard I can grab values and such but I am not sure how I attach that to the sprite ?

I had a look at the crank.lua file and the docs but was not sure where to start...

function playdate.update()
    playdate.timer.updateTimers()
    gfx.sprite.update()  

    if playdate.buttonIsPressed( playdate.kButtonUp ) then
        playerShip:moveBy( 0, -2 )
    end
    if playdate.buttonIsPressed( playdate.kButtonRight ) then
        playerShip:moveBy( 2, 0 )
    end
    if playdate.buttonIsPressed( playdate.kButtonDown ) then
        playerShip:moveBy( 0, 2 )
    end
    if playdate.buttonIsPressed( playdate.kButtonLeft ) then
        playerShip:moveBy( -2, 0 )
    end


end

This appears to setRotation (when I print it out) but the sprite isn't rotating on screen

import "CoreLibs/object"
import "CoreLibs/graphics"
import "CoreLibs/sprites"
import "CoreLibs/timer"
import "CoreLibs/easing"
import 'CoreLibs/crank'

local gfx <const> = playdate.graphics
local playerShip = nil
local r = 50
local centerX = 200
local centerY = 120
local angle = 0
local crankRads = math.rad(angle)
local x = math.sin(crankRads)
local y = -1 * math.cos(crankRads)


function myGameSetUp()
    playerShip = gfx.sprite.new()
    playerShip:setSize(32, 32)
    playerShip:moveTo(200,120) 
    playerShip:add() 
       
    function playerShip:draw()
        gfx.drawPolygon(16,0, 24,16, 32,16, 16,32, 0,16, 8,16)
	end 

    function normalizeAngle(a)
        if a >= 360 then a = a - 360 end
        if a < 0 then a = a + 360 end
        return a
    end

    function degreesToCoords(angle)
        x = (r * x) + centerX
        y = (r * y) + centerY
        return x,y
    end

    local x,y = degreesToCoords(angle)
    
end

myGameSetUp()

function playdate.update()
    playdate.timer.updateTimers()
    gfx.sprite.update()  
    local change = playdate.getCrankChange()

    if change ~= 0 then
        angle += change
        angle = normalizeAngle(angle)
        -- print(change, angle)
    
        x,y = degreesToCoords(angle)
    
      end

      playerShip:setRotation(angle)

end

function playdate.cranked(change, acceleratedChange)
 	print(playerShip:getRotation())
end

sprite:setRotation will rotate the sprite’s image if it has one. Yours doesn’t - it’s drawing a polygon directly to the screen instead:

function playerShip:draw()
        gfx.drawPolygon(16,0, 24,16, 32,16, 16,32, 0,16, 8,16)
	end 

If you want to use sprites with images and use sprite:setRotation, you could do something like:

function myGameSetUp()
    local ship_image = gfx.image.new(32, 32)
    gfx.pushContext(ship_image)
    gfx.drawPolygon(16,0, 24,16, 32,16, 16,32, 0,16, 8,16)
    gfx.popContext()
    
    playerShip = gfx.sprite.new()
    playerShip:setImage(ship_image)
    playerShip:moveTo(200,120) 
    playerShip:add() 
    
    function playerShip:update()
        playerShip:setRotation(playerShip:getRotation() + playdate.getCrankChange())
        -- to keep things tidy, shift the rest of your input handling from playdate.update to here
    end
end

Or if you want to retain the polygon drawing (it'll look cleaner and run faster):

function myGameSetUp()
    
    playerShip = gfx.sprite.new()
    playerShip.polygon = geom.polygon.new(16,0, 24,16, 32,16, 16,32, 0,16, 8,16, 16,0)
    playerShip.polygon:translate(184, 104)
    playerShip.rotate_transform = geom.affineTransform.new()
    playerShip:moveTo(200,120) 
    playerShip:add() 
    
    function playerShip:update()
        -- rotate the transform
        playerShip.rotate_transform:rotate(playdate.getCrankChange(), playerShip.x, playerShip.y)
        -- apply the transform to the ship polygon
        playerShip.rotate_transform:transformPolygon(playerShip.polygon)
        -- reset the transform so that crank change doesn’t accumulate over time
        playerShip.rotate_transform:reset()
    end

    function playerShip:draw()
        gfx.drawPolygon(playerShip.polygon)
    end 
end

But this is half in and half out of the sprite system (you need playerShip:draw() in playdate.update()).

So it might be better to combine the two - give the sprite an image, then draw the ship polygon to that image instead of directly to the screen:

function myGameSetUp()
    playerShip = gfx.sprite.new()
    playerShip.polygon = geom.polygon.new(16,0, 24,16, 32,16, 16,32, 0,16, 8,16, 16,0)
    playerShip.rotate_transform = geom.affineTransform.new()
    playerShip:moveTo(200,120) 
    playerShip:add() 
    
    function playerShip:update()
        -- rotate the transform
        playerShip.rotate_transform:rotate(playdate.getCrankChange(), 16, 16)

        -- apply the transform to the ship polygon
        playerShip.rotate_transform:transformPolygon(playerShip.polygon)

        -- reset the transform so that crank change doesn’t accumulate over time
        playerShip.rotate_transform:reset()

        -- draw the ship polygon to the sprite's image
        local img = gfx.image.new(32, 32)
        gfx.pushContext(img)
        gfx.drawPolygon(playerShip.polygon)
        gfx.popContext()
        playerShip:setImage(img)
    end
end
2 Likes

Awesome.... I like the final idea and the moment I am getting

Attempt to index a nil value (upvalue 'geom')

1 Like

Oops, sorry!

geom is an alias for playdate.geometry, like gfx is for playdate.graphics, so put it at the start of main.lua:


local gfx <const> = playdate.graphics
local geom <const> = playdate.geometry

2 Likes

thank you. slowly getting to grips with SDK etc :slight_smile:

Thanks for this detailed response. It helped me understand what's going on, a lot.

When I tried to run this code though, the sprite in its rotated form would get redrawn on top of the previous one, so it ended up being a giant spot in the end. I took the sprite creation part out

playerShip.polygon = geom.polygon.new(16,0, 24,16, 32,16, 16,32, 0,16, 8,16, 16,0)
    playerShip.polygon:translate(184, 104)
    playerShip.rotate_transform = geom.affineTransform.new()
    playerShip:moveTo(200,120) 
    playerShip:add() 

...and moved it into a separate function that is only called once. That seemed to fix things.

1 Like

Hi guys, what if I would like add some inertia to this rotation? I mean, the ship rotate until I use the crank. When I stop use the crank the ship continue to rotate slower and after a short period of time it stops its rotation.

Any suggestion?
Thanks in advance,
Antonio

To achieve the effect you’re after you need to keep a separate variable around to represent angular velocity. Then you’d add some amount to that each frame based on crank rotation instead of directly updating your transform.

Then, each frame you’d update the transform using your current angular velocity, and then reduce it some small amount each frame. There's a "right" way and an easy way to do that. The easy way is to just multiply your angular velocity by a value just less than 1 each frame, so it gradually slows down. Something like:

-- increase angular velocity based on crank input
angularVelocity += playdate.getCrankChange()

-- update the transform
playerShip.rotate_transform:rotate(angularVelocity, 16, 16)

-- gradually slow rotation
angularVelocity *= 0.95

-- use the transform...

You can play around with the amount you multiply by to slow down rotation, or multiply the amount you add to angularVelocity in response to the crank change, to tweak the feel of things.

You can also optimize this slightly by checking to see if angularVelocity is above some near-zero epsilon value first, so you aren't applying the transform when it won't have any effect. Finally, you could look into a time-based (dt) approach, instead of this frame-based shortcut which just reduces the angular velocity by a small percentage each frame. (A time-based approach ensures the speed of the rotation effects at which rotation slows remains consistent regardless of the frame rate.)

1 Like

Thanks Eben, your suggestion has been useful to go deeper the issue :slight_smile:

1 Like