How do I average table values in simple way and transition between two rotation values

I am using the 2.1.1 Windows Lua SDK using Vscode. I am very new to programming in general and still trying to understand tables. I am making a top-down car racer and trying to make drifting. To do this I am trying to average the angle of angle of my playerSprite.

I need to average the angle values to have a delay effect as if you were actually experiencing under and oversteering (kinda).

I am doing this by creating a table (averageAngle) and then setting a variable (valueToReplace) to the angle. I then find the spot I want to replace (curentSpot). I then replace the selected spot with the value. This has been working great.

My table averaging code is this:

----------- this is called at the top of the file.
currentSpot = 1
averageAngle = {}
numSpots = 20 -- sets the number of spots
for i = 1, numSpots do -- replaces ever spot with angle
    averageAngle[i] = angle
end


----------- this is called in the update function

    sumAngle = 0 --VERY IMPORTANT

    local valueToReplace = angle -- sets what is replacing things

    if currentSpot >= 2 then -- calculates curent spot 
        currentSpot -= 1
    else
        currentSpot = numSpots
    end

    averageAngle[currentSpot] = valueToReplace -- replaces curent spot

    for i = 1, numSpots do
        sumAngle += averageAngle[i]
    end
    -- funky way of averaging the table -- needs to be same length as the table
    
    avAngle = (sumAngle) / numSpots

This works great for averaging the actual angles but transitions in and out of drifting and quickly changing from actual rotation to averaged rotation can be quite choppy and can result in the car from "blinking".

Crucnchy
Crunchy

The actual problem.

To solve this I have implemented a complex system of fading and extra tables. Is this the most efficient way of doing this? I hate how I transition out of drifting with the angleAverage2 and notDrifting functions. Is there a better way to do this?

My full Control.lua code:

import "CoreLibs/object"
import "CoreLibs/graphics"
import "CoreLibs/sprites"
import "CoreLibs/timer"

local pd <const> = playdate
local gfx <const> = pd.graphics

dx = 0 -- CAN NOT BE LOCAL

angle = -90 -- CAN NOT BE LOCAL

local driftingLeniency = 30 

-- one time setup for the drifting average table
local currentSpot = 1
averageAngle = {}
local numSpots = 20 -- sets the number of spots
for i = 1, numSpots do -- replaces ever spot with angle
    averageAngle[i] = angle
end


-- one time setup for the not drifting average table  -- YES BOTH ARE NESSARY
averageAngle2 = {}
local numSpots2 = (driftingLeniency) / 5 -- sets the number of spots -- if driftingLeniency is higher then so is the numSpots
local spotNumber = 1
for i = 1, numSpots2 do -- replaces ever spot with angle
    averageAngle2[i] = angle
end

local avAngle = angle -- declare variables to start

local sumAngle = angle

local sumAngle2 = angle

local visableAngle = angle

local oneTime = true

local oneTime2 = true

local drifting = false

------------------------------------------------------------------------------------------------ controlls the rotation of playerSprite

function rotation() 

    if controllsWanted == 1 then  -- checks the wanted controlls
        if pd.buttonIsPressed(pd.kButtonLeft) then -- actual change of angle use LEFT and RIGHT
            angle -= 5
        end 

        if pd.buttonIsPressed(pd.kButtonRight) then
            angle += 5
        end 
        
    else -- if controllsWanted == 2

        local change = playdate.getCrankChange() -- asctual change using the CRANK using the change

        angle += (change) -- add it to the angle
    end

    if angleEasing == true and controllsWanted == 1 then
        playerSprite:setRotation( visableAngle ) -- set rotation using method
    else
        playerSprite:setRotation( angle ) -- set rotation using method
    end
end

------------------------------------------------------------------------------------------------ average the angle

function angleAverage()

    sumAngle = 0 --VERY IMPORTANT

    local valueToReplace = angle -- sets what is replacing things

    if currentSpot >= 2 then -- calculates curent spot 
        currentSpot -= 1
    else
        currentSpot = numSpots
    end

    averageAngle[currentSpot] = valueToReplace -- replaces curent spot

    for i = 1, numSpots do
        sumAngle += averageAngle[i]
    end
    -- funky way of averaging the table -- needs to be same length as the table
    
    avAngle = (sumAngle) / numSpots

end

function angleAverage2()

    sumAngle2 = 0

    for i = 1, spotNumber do -- replaces ever spot with angle
        averageAngle2[i] = angle
    end

    for i = 1, numSpots2 do
        sumAngle2 += averageAngle2[i]
    end
    -- funky way of averaging the table -- needs to be same length as the table
    
    visableAngle = (sumAngle2) / numSpots2

end

function weDrifting()

    local turnDifference = math.abs(angle - avAngle)

    angleEasing = false

    if oneTime == true then -- when you fist start drifting replace all angles
        for i = 1, numSpots do
            averageAngle[i] = angle
        end
        oneTime = false -- reset variables
        oneTime2 = true
        drifting = true
        spotNumber = 0

        pd.timer.performAfterDelay(1000, function ()  -- waight for one second to then be able to end a drift
            driftingCanStop = true 
        end )  
    end

    if turnDifference <= driftingLeniency and driftingCanStop == true then  -- drifting leniency checks if tires could regain traction
        drifting = false
    end

    angleAverage() -- calculate curent average

end

function notDrifting()

    drifting = false

    if oneTime2 == true then -- when you fist start drifting replace all angles
        for i = 1, numSpots2 do
            averageAngle2[i] = angle
        end
        
        angle = avAngle -- reset variables
        oneTime = true
        oneTime2 = false
        angleEasing = true
        driftingCanStop = false 
    end
    
    if spotNumber < numSpots2 and driftingLeniency > 5 then -- calculates curent spot 
        spotNumber += 1

        angleAverage2() -- calculate curent average
    else
        angleEasing = false -- when done make shure to switch away just for simplicity
    end
end
------------------------------------------------------------------------------------------------ movement of playerSprite

function movement()
    local brake = .1  -- basic variables for movement
    local acceleration = .4
    local deceleration = .4
    local maximumSpeed = 12
    local turnDifference = math.abs(angle - avAngle)
  
    if controllsWanted == 1 then

        if pd.buttonIsPressed(pd.kButtonB) then 
            dx = math.min(dx + acceleration - offTrack, maximumSpeed) -- when B is pressed then accelerate
        else
            dx = math.max(dx - deceleration - offTrack, 0) -- when B is not pressed decelerat
        end
  
        if pd.buttonIsPressed(pd.kButtonA) then
            dx = math.max(dx - brake - offTrack, 0) -- when A is pressed brake
        end
        
        if turnDifference >= 55 - dx or drifting == true then -- check if "drifting"
            weDrifting()

            local moveToX = playerSprite.x + math.cos(math.rad(avAngle)) * dx
            local moveToY = playerSprite.y + math.sin(math.rad(avAngle)) * dx

            playerSprite:moveTo(moveToX, moveToY)
        else
            notDrifting()
           
            local moveToX = playerSprite.x + math.cos(math.rad(angle)) * dx
            local moveToY = playerSprite.y + math.sin(math.rad(angle)) * dx

            playerSprite:moveTo(moveToX, moveToY)
        end

    else -- if controllsWanted == 2 then  -- crank controlls
        local change = pd.getCrankChange()
        local turnSpeed =  math.min(math.abs(change/6), (dx * .1))

        if pd.buttonIsPressed(pd.kButtonB) then
            dx = math.min(dx + acceleration - turnSpeed - offTrack, maximumSpeed)
        else
            dx = math.max(dx - deceleration - turnSpeed - offTrack, 0)
        end
  
        if pd.buttonIsPressed(pd.kButtonA) then
            dx = math.max(dx - brake - turnSpeed - offTrack, 0)
        end

        local moveToX = playerSprite.x + math.cos(math.rad(angle)) * dx
        local moveToY = playerSprite.y + math.sin(math.rad(angle)) * dx

        playerSprite:moveTo(moveToX, moveToY)
    end

end

Here are some things things I found. The smoothing coming out of a drift needs to be short enough that the player can't initiate another. The smooth effect does not affect the actual direction which is why checking if you have corrected the drift is important to make the player feel like they are actually in control.

Having a higher leniency can actually improve the smoothness is some cases.

Crucnchy
Crunchy

driftingLeniency = 5
Smooth 5

driftingLeniency = 30
Smooth 30

It obviously has its trade-offs. and is a lot more noticeable when you are controlling.

Final bits.

The game is super bare-bones, on a test track, and a hellscape of variables and if statements.

I have tried to document my code to the best of my ability but if you need any clarification please ask! I would love feedback on the code and on any changes you would make. I have tried a lot of different methods. This is probably the 5th version of drifting.

Yes. Use (angular) momentum.

Firstly, if you think about when you move a sprite in X and Y you should do it with DX and DY delta values. These will slowly decrease, which provides momentum in X and Y directions. When you change direction it won't change immediately. Like with Mario running, when you change direction he first slows down and then changes direction.

So too with Angles. Have your main angle A and make changes to it using DA to provide momentum. When you change direction, from say +1/right, first of all the DA value will be reduced, to zero and then past zero to the maximum of the new direction, -1/left. Hopefully that makes sense.

You can Google this, or it's the type of thing that ChatGPT is really good at answering. Though then the task becomes how to ask the question in the best way.

I'd encourage you to start a new project plotting only these values on screen. So you can concentrate only on this problem. Once you've done it, find a way to integrate it into your game.

You could do it as a single X, Y dot with an arrow showing current direction, and a second arrow showing the current direction plus the angle delta which will slowly converge on the first arrow after each input/change. This is exactly how I do it in Daily Driver!

1 Like

Also see this recent discussion and following thread on the Playdate Squad Discord https://discord.com/channels/675983554655551509/821661913393004565/1185786363728117760

1 Like

Thank you so much I just implemented it and I love it!

1 Like

Would love to see a new animation!

Of course!

I ended up adding both drifting methods to the game!
The original I cleaned up a bit and named it Realistic. Because it felt a little more so. The angular momentum system I named Arcade.

Realistic:
Realistic

Arcade:
Arcade
Both have ramped drifting for how fast the car is turning and having the angular momentum system allowed me to add drifting to my crank controls.

Extras

Here is more of a full showcase of what I have so far.
Showcase

Times are saved over sessions and most shown are placeholders that get switched out when you actually complete a lap.

I also added resets to the playdate menu system, full title card, and icon animations. I still need to make the game more beautiful. Graphics and animations etc. I also want to add modes and a few other tweaks.

Thank you so much for the help! I stumbled across Daily Driver after I started working on my game and although I haven't played it I find the animation and physics stunning.

1 Like

Yours is looking great!

I just worked bit by bit, adding one thing at a time. The early GIFs of Daily Driver are pretty "bad" :sweat_smile:

1 Like