Hey @PizzaFuel @daprice I optimized my code to load graphic files only once. I also changed sprites to images. As I use only "playdate.graphics.image:drawScaled" I prepared already flipped sprites of tres and road signs for the opposite side of the road. Here is my current code:
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 leftSakuraTreeImage = gfx.image.new('images/left_sakura_tree')
local rightSakuraTreeImage = gfx.image.new('images/right_sakura_tree')
local leftTurnSignImage = gfx.image.new('images/left_turn_sign')
local rightTurnSignImage = gfx.image.new('images/right_turn_sign')
local playerCarImage = gfx.image.new('images/car')
local bg_left_sakura_tree = {
img = "left_sakura_tree",
pos={4.5,0}, -- position relative to side of road
siz={3,4.5}, -- size
spc=5, -- spacing
bgscl=0.06, -- param to scale sprite
flpr=true
}
local bg_right_sakura_tree = {
img = "right_sakura_tree",
pos={2,0}, -- position relative to side of road
siz={3,4.5}, -- size
spc=5, -- spacing
bgscl=0.06, -- param to scale sprite
flpr=true
}
local bg_left_turn_sign = {
img = "left_turn_sign",
pos={2,0},-- position relative to side of road
siz={1.5,1.5},-- size
spc=2,-- spacing
bgscl=0.03,-- param to scale sprite
flpr=true -- flip when on right hand side
}
local bg_right_turn_sign = {
img = "right_turn_sign",
pos={2,0},-- position relative to side of road
siz={1.5,1.5},-- size
spc=2,-- spacing
bgscl=0.03,-- param to scale sprite
flpr=true -- flip when on right hand side
}
local npcSprite = {
img = "car",
siz={1,1},
bgscl=0.02,
roadPosition=0,
maxRoadSegments=0, -- moment when road position of the NPC should be reset to 1. A value is added in calculateRoad() function
speed=0,
}
local road={
{ct=40,tu=0,bgl=bg_left_sakura_tree,bgr=bg_right_sakura_tree},
{ct=20,tu=.25,bgl=bg_left_turn_sign,bgr=bg_right_sakura_tree},
{ct=40,tu=0,bgl=bg_left_sakura_tree,bgr=bg_right_sakura_tree},
{ct=20,tu=-.25,bgl=bg_left_sakura_tree,bgr=bg_right_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
elseif npcSprite.speed > 1 then npcSprite.speed = 1 npcSprite.roadPosition += 1
else npcSprite.speed += 0.2
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
local camcnr,camseg=1,1
local 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
--py+=-10
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
if not (((bg.roadPosition-sumct)==0)) then return end
-- find position
--px+=3*scale*side
px-=1.5*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)
-- checks whhich image should be put into drawing function
if s.img == "left_sakura_tree" then leftSakuraTreeImage:drawScaled((s.x-s.w/2), (s.y-s.h), s.s) end
if s.img == "right_sakura_tree" then rightSakuraTreeImage:drawScaled((s.x-s.w/2), (s.y-s.h), s.s) end
if s.img == "left_turn_sign" then leftTurnSignImage:drawScaled((s.x-s.w/2), (s.y-s.h), s.s) end
if s.img == "right_turn_sign" then rightTurnSignImage:drawScaled((s.x-s.w/2), (s.y-s.h), s.s) end
if s.img == "car" then playerCarImage:drawScaled((s.x-s.w/2), (s.y-s.h), s.s) end
end
--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.6 then
playerCarSpeed+=0.03
elseif playerCarSpeed>0 then playerCarSpeed-=0.01
elseif playerCarSpeed<=0 then playerCarSpeed=0
end
end
-- car and cat drivers
local sprite = gfx.sprite.new(playerCarImage)
sprite:setZIndex(32767)
--sprite:setScale(1)
sprite:moveTo(250, 210)
sprite:add()
local i=0
function pd.update()
gfx.clear()
npcDrive()
speedingPlayersCar()
calculateRoad()
roadUpdate()
gfx.sprite.update()
pd.drawFPS(0,0)
i +=1
if i==nil then pd.stop()end
end
I will be very thankful if any of You will check how updated version runs on the device In the meantime, author of original pico8 tutorial answered my questions about method for drawing NPC cars on the road. Here is what he proposed:
So basically the issue is that the road drawing currently only projects at the end of each segment. Which works fine for the background objects (trees etc), as they are aligned with the segment boundaries, but not for the NPC car, which can be midway along a segment.
You need to do a couple of things:
– There's a check in the addNPCsprite as to whether the NPC car is on the segment being drawn. I think this needs to be moved into the main drawing loop.
– Then when you detect the NPC car is on the segment, you need to calculate its 3D screen position.
– So you get the fractional distance along the segment, which I think is the fractional part of npcsprite.roadPosition (say call that "f")
– Then you need to calculate where that position along the current segment is, something like:
local npcx,npcy,npcz=x+fxd,y+fyd,z+f*zd
– Then call project that position: e.g.:
local npcpx,npcpy,npcscale=project(npcx,npcy,npcz)
– The pass that projected position to your addNPCsprite routine, which should (hopefully) now draw the car in the right place and scale.
It has a lot of sense for me and I though that I understand, and made a function which I initiated in the drawScreen() function just before that "Draw forward" loop.
function calculateNPCposition(npcSprite,x,y,z,xd,yd,zd,cnr,seg)
local sumct=getsumct(cnr,seg)
if not (((npcSprite.roadPosition-sumct)==0)) then return end
local f = npcSprite.segmentFraction
local npcx,npcy,npcz=x+f*xd,y+f*yd,z+f*zd
npcSprite.npcpx,npcSprite.npcpy,npcSprite.npcscale=project(npcx,npcy,npcz)
end
Also I modified npcSprite to be like this:
local npcSprite = {
img = "images/car",
siz={1.0,1.0},
bgscl=0.011,
roadPosition=1,
segmentFraction=0,
maxRoadSegments=0, -- moment when road position of the NPC should be reset to 1. A value is added in calculateRoad() function
speed=1,
npcx=0,
npcy=0,
npcscale=0,
}
And moving NPC function to be like this:
function npcDrive()
if (npcSprite.roadPosition > npcSprite.maxRoadSegments) then npcSprite.roadPosition = 1
elseif npcSprite.segmentFraction >= 1 then npcSprite.segmentFraction -= 1 npcSprite.roadPosition += 1
else npcSprite.segmentFraction += npcSprite.speed
end
end
Unfortunately, after passed modified values to the drawing function I had weird results. So probably I messed something strongly somewhere. Maybe You have a clue how to fix it? I'm attaching current version of my game package in the post, but without this NPC car changes.
Koko & Kebi Racing (image wersion).zip (55.0 KB)