After a whole day of thinking I figured out how to add a moving NPC driver, which was quite an achievement for me, as I'm a programming noob. And I haven't found a tutorial for NPC cars in pseudo-3D racing so I had to improvise.
Unfortunately, the result is relatively choppy, especially when I move the player car near NPC car.

Could anyone look at my code and give me a hint of how I could make the trip smoother for these poor cats?
@daprice maybe you got an idea?
import "CoreLibs/sprites"
import "CoreLibs/graphics"
local pd <const> = playdate
local gfx <const> = pd.graphics
--[[The road is made out of "corners" (for our purposes we will call straight bits "corners" as well).
Corners need to curve in the direction the road turns, so we will simulate this by building them out of smaller straight “segments”.
We can define the track as an array of these corners. Each corner has a segment count "ct" and a value indicating how much the direction turns between each segment “tu”.
So tu=0 creates a straight piece, tu=1 will turn to the right, -1 left etc. For simplicity we'll ignore hills and valleys for now.--]]
gfx.sprite.setAlwaysRedraw(true) -- ensures the entire background gets drawn in one go every frame, important if the background drawing callback does anything complex
gfx.setBackgroundColor(gfx.kColorWhite)
gfx.sprite.setBackgroundDrawingCallback(
function( x, y, width, height )
drawScreen()
end
)
local bg_tree = {
img = "images/sakura_tree",
pos={3,0}, -- position relative to side of road
siz={1.5,2}, -- size
spc=6, -- spacing
bgscl=0.06, -- param to scale sprite
flpr=true
}
local bg_turn_sign = {
img = "images/turn_sign",
pos={1,0.3},
siz={1.5,1.5},
spc=2,
bgscl=0.03,
flpr=true -- flip when on right hand side
}
local npcSprite = {
img = "images/car",
siz={1.0,1.0},
bgscl=0.011,
roadPosition=1,
maxRoadSegments=0, -- moment when road position of the NPC should be reset to 1. A value is added in calculateRoad() function
speed=0.01,
}
local road={
{ct=20,tu=0,bgl=bg_tree,bgr=bg_tree},
{ct=20,tu=-.25,bgl=bg_tree,bgr=bg_turn_sign},
{ct=20,tu=0,bgl=bg_tree,bgr=bg_tree},
}
local drawnSprites = {}
local playerCarSpeed = 0
function npcDrive()
if (npcSprite.roadPosition > npcSprite.maxRoadSegments) then npcSprite.roadPosition = 1
else npcSprite.roadPosition +=0.45
end
end
function calculateRoad()
-- calculate the # of segments
-- in the road before each corner.
-- this is useful for spacing things
-- out evenly along the road
local sumct=0
local i = 1
while i <= #road do
local corner = road[i]
corner.sumct = sumct
sumct = sumct + corner.ct
i = i + 1
end
npcSprite.maxRoadSegments=sumct
end
function getsumct(cnr,seg)
return road[cnr].sumct+seg-1
end
-- camera
camcnr,camseg=1,1
camx,camy,camz=0,0,0
-- function to turning 2D into psuedo 3D
function project(x,y,z)
local scale=200/z
return x*scale+200,y*scale+120,scale
end
-- advance along road, goes trhough segments of each corners and jump to another corner after last segment
function advance(cnr,seg)
seg += 1
if (seg > road[cnr].ct) then
seg = 1
cnr += 1
if (cnr > #road) then
cnr = 1
end
end
return cnr, seg
end
function skew(x,y,z,xd,yd)
return x+z*xd,y+z*yd,z
end
function drawScreen()
-- cleaning screen from sprites added in previous animation frame
gfx.sprite.removeSprites(drawnSprites)
-- direction
local camang=camz*road[camcnr].tu
local xd,yd,zd=-camang,0,1
-- skew camera to account for direction
local cx,cy,cz=skew(camx,camy,camz,xd,yd)
-- cursor, relative to skewed camera
local x,y,z=-cx,-cy+2,-cz+2
-- road position
local cnr,seg=camcnr,camseg
-- previous projected position
local ppx,ppy,pscale=project(x,y,z)
-- array of sprites to draw
local spritesToDraw={}
-- draw forward
for i=1, 30 do
-- project (turns x, y, z into 3D using a scale)
local px,py,scale=project(x,y,z)
-- draw road
local sumct=getsumct(cnr,seg)
drawroad(px,py,scale,ppx,ppy,pscale,sumct)
-- add background sprites
addbgsprite(spritesToDraw,sumct,road[cnr].bgl,-1,px,py,scale)
addbgsprite(spritesToDraw,sumct,road[cnr].bgr, 1,px,py,scale)
addNPCsprite(spritesToDraw,sumct,npcSprite,0,px,py,scale)
--[[ add background sprites to draw
if (sumct%6)==0 then
-- left tree
local tx,ty=px-4.5*scale,py
local tw,th=1.5*scale,3*scale
local tscale=0.06*scale
table.insert(spritesToDraw,{x=tx,y=ty,w=tw,h=th,s=tscale})
-- right tree
tx=px+6.5*scale
table.insert(spritesToDraw,{x=tx,y=ty,w=tw,h=th,s=tscale})
end]]--
-- move forward
x+=xd
y+=yd
z+=zd
-- turn, cnr means corners which are elements from road table
xd+=road[cnr].tu
-- advance along road, goes trhough segments of each corners and jump to another corner after last segment
cnr, seg = advance(cnr,seg)
-- track previous projected position
ppx,ppy,pscale = px, py, scale
end
-- draw background sprites in reverse order
for i=#spritesToDraw,1,-1 do
drawbgsprite(spritesToDraw[i])
end
end
function addbgsprite(spritesToDraw,sumct,bg,side,px,py,scale)
if(not bg) then return end
if((sumct%bg.spc)~=0) then return end -- checks if sprite should be drawn or not
-- find position
px+=3*scale*side
if bg.pos then
px+=bg.pos[1]*scale*side
py+=bg.pos[2]*scale
end
-- pico8 calculate size for proper placement
local w,h=bg.siz[1]*scale,bg.siz[2]*scale
-- flip horizontally?
local flp=side>0 and bg.flpr
-- set proper scaling
local tscale=bg.bgscl*scale
-- add to sprite array
table.insert(spritesToDraw,{
x=px,y=py,w=w,h=h,
img=bg.img,
s=tscale,
flp=flp
})
end
function addNPCsprite(spritesToDraw,sumct,bg,side,px,py,scale)
if(not npcSprite) then return end
if ((bg.roadPosition-sumct)<=0) or ((bg.roadPosition-sumct)>1) then return end
-- find position
--px+=3*scale*side
px-=0.1*scale
if bg.pos then
px+=bg.pos[1]*scale*side
py+=bg.pos[2]*scale
end
-- pico8 calculate size for proper placement
local w,h=bg.siz[1]*scale,bg.siz[2]*scale
-- flip horizontally?
local flp=side>0 and bg.flpr
-- set proper scaling
local tscale=bg.bgscl*scale
-- add to sprite array
table.insert(spritesToDraw,{
x=px,y=py,w=w,h=h,
img=bg.img,
s=tscale,
flp=flp
})
end
function drawbgsprite(s)
local image = gfx.image.new(s.img)
local spriteToBeDrawn = gfx.sprite.new(image)
if s.flp then spriteToBeDrawn:setImageFlip(gfx.kImageFlippedX) end
spriteToBeDrawn:setScale(s.s)
spriteToBeDrawn:moveTo(s.x-s.w/2,s.y-s.h)
spriteToBeDrawn:add()
table.insert(drawnSprites, spriteToBeDrawn)
end
function drawroad(x1,y1,scale1,x2,y2,scale2,sumct)
if(math.floor(y2)<math.ceil(y1))then return end
-- draw ground
gfx.setColor(gfx.kColorBlack)
gfx.setPattern({ 0x0f, 0xf0, 0x0f, 0xf0, 0x0f, 0xf0, 0x0f, 0xf0 })
if((sumct%6)>=3) then
gfx.setColor(gfx.kColorWhite)
gfx.setPattern({ 0xee, 0xbb, 0xee, 0xbb, 0xee, 0xbb, 0xee, 0xbb })
end
gfx.fillRect(0,math.ceil(y1),400, (y1-y2))
-- main road
local w1,w2=3*scale1,3*scale2 -- start and end widths
gfx.setColor(gfx.kColorBlack)
gfx.setPattern({ 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55 })
--drawTrapezium(x1,y1,w1,x2,y2,w2)
gfx.fillPolygon(x1-w1, y1, x1+w1, y1, x2+w2, y2, x2-w2, y2)
-- center line markings
gfx.setColor(gfx.kColorBlack)
if (sumct%4)==0 then
local mw1, mw2 = .1*scale1, .1*scale2
gfx.fillPolygon(x1-mw1, y1, x1+mw1, y1, x2+mw2, y2, x2-mw2, y2)
end
-- shoulders
gfx.setColor(gfx.kColorWhite)
if((sumct%2)==0) then gfx.setColor(gfx.kColorBlack) end
local sw1,sw2= 0.2*scale1, 0.2*scale2
drawTrapezium(x1-w1,y1,sw1,x2-w2,y2,sw2)
--gfx.fillPolygon(x1-w1, y1, x1+sw1, y1, x2+sw2, y2, x2-sw2, y2)
drawTrapezium(x1+w1,y1,sw1,x2+w2,y2,sw2)
--gfx.fillPolygon(x1-sw1, y1, x1+sw1, y1, x2+sw2, y2, x2-sw2, y2)]]--
end
function drawTrapezium(x1,y1,w1,x2,y2,w2)
-- draw a trapezium by stacking horizontal lines
local h=y2-y1 -- height
local xd,wd=(x2-x1)/h,(w2-w1)/h -- width and x deltas
local x,y,w=x1,y1,w1 -- current position
local yadj=math.ceil(y)-y
x+=yadj*xd
y+=yadj
w+=yadj*wd
while y<y2 do
-- draw horizontal line
gfx.drawLine(x-w,y,x+w,y)
x+=xd
y+=1
w+=wd
end
end
function roadUpdate()
--camz+=0.3 -- speed of the car
camz+=playerCarSpeed
if camz>1 then
camz-=1
camcnr,camseg=advance(camcnr,camseg)
end
end
-- adds and removes speed value which is then used in roadUpdate()
function speedingPlayersCar()
if pd.buttonIsPressed(pd.kButtonA) and playerCarSpeed<=0.5 then
playerCarSpeed+=0.03
elseif playerCarSpeed>0 then playerCarSpeed-=0.01
elseif playerCarSpeed<=0 then playerCarSpeed=0
end
end
-- car and cat drivers
local image = gfx.image.new('images/car')
local sprite = gfx.sprite.new(image)
sprite:setZIndex(32767)
sprite:setScale(1)
sprite:moveTo(250, 210)
sprite:add()
local i=0
function pd.update()
gfx.clear()
speedingPlayersCar()
calculateRoad()
roadUpdate()
npcDrive()
gfx.sprite.update()
pd.drawFPS(0,0)
i +=1
if i==nil then pd.stop()end
end