Mini Golf Demo – Conceptual Overview
This demo shows a simple but powerful way to build a top-down mini-golf style simulation in PlayBASIC.
Rather than focusing on complex physics, the code demonstrates how to combine a few clean ideas to create believable behaviour using fast, data-driven techniques.
1. Separating visuals from gameplay logic
The program uses two images of the same size, each with a different purpose:
• Visible Screen
This is what you actually see: grass, shapes, and moving balls.
• Zone Screen
This image is never shown to the player. Each pixel stores a zone value that tells the code what type of surface the ball is currently rolling over.
This is a very common game-dev trick:
images aren’t just for graphics – they can also be fast lookup maps for gameplay data.
2. Zone-based friction using lookup tables
Each terrain type (short grass, medium grass, long grass) is assigned:
• A zone ID
• A friction value
• A display colour
When a ball moves, the code reads the pixel value from the zone screen at the ball’s position and uses that value as an index into a friction array.
This avoids large blocks of conditionals and makes it easy to add or tweak terrain types later.
3. Direction-based movement
Each ball stores its:
• Position
• Angle
• Speed
Movement is calculated using basic trigonometry (sin and cos) to convert angle and speed into X and Y motion.
The friction value from the current zone scales the speed, giving the impression of different grass lengths slowing the ball down.
4. Ray-based collision and reflection
Instead of moving the ball first and checking overlaps later, the code uses ray intersection:
• A ray is cast from the current position to the next position
• If it hits geometry, the surface normal is used to calculate a reflection angle
• The ball bounces naturally off walls and obstacles
This method is robust, avoids tunnelling, and works well even at higher speeds.
5. Vector-based world geometry
The course is built from vector lines and convex shapes, not tiles.
• Borders are simple line segments
• Obstacles are procedurally generated convex polygons
• The world is partitioned to improve collision performance
This keeps the layout flexible and easy to expand or randomise.
6. Data-driven design
One of the key ideas in this demo is data-driven gameplay:
• Terrain behaviour is defined by data, not hard-coded logic
• Friction, colour, and movement all come from lookup tables
• New terrain types can be added with minimal code changes
This approach scales well and is widely used in real game engines.
Summary
At its core, this demo demonstrates how to:
• Separate visuals from logic
• Use images as gameplay data
• Apply simple physics with believable results
• Handle collision using raycasting
• Build systems that are easy to extend
It’s a compact example of practical game programming techniques, written in a way that new coders can understand and build upon.
Constant Zone_ShortGrass =ac(1)
Constant Zone_MedGrass =ac(1)
Constant Zone_LongGrass =ac(1)
Dim Palette(100)
Palette(Zone_ShortGrass) = rgb(55,200,80)
Palette(Zone_MedGrass) = rgbfade(Palette(Zone_ShortGrass),50)
Palette(Zone_LongGrass) = rgbfade(Palette(Zone_ShortGrass),25)
dim Friction#(100)
Friction#(Zone_ShortGrass) =1
Friction#(Zone_MedGrass) =1.5
Friction#(Zone_LongGrass) =2
; get the screen size
sw=GetScreenWidth()
sh=GetScreenHeight()
; create the visible screen
DisplayScreen =NewIMage(sw,sh,true)
; create the zone screen.
; the zone screen is the same size as the 'visible'
; but is just used to tell what zone the ball is currently over
ZoneScreen =GetFreeimage()
createfximageex ZoneScreen,sw,sh,32
// Draw the Visible scene..
rendertoimage Displayscreen
c=Palette(Zone_ShortGrass)
shadebox 0,0,sw,sh,c,rgbfade(c,80),c,rgbfade(c,80)
inkmode 1+32
ellipsec sw*0.7,sh*0.5,200,100,true,Palette(Zone_MedGrass)
ellipsec sw*0.7,sh*0.5,100,50,true,Palette(Zone_LongGrass)
inkmode 1
// draw the collision (zone) scene.
rendertoimage Zonescreen
Cls Zone_ShortGrass
ellipsec sw*0.7,sh*0.5,200,100,true,Zone_MedGrass
ellipsec sw*0.7,sh*0.5,100,50,true,Zone_LongGrass
// create collision world
CollisionWorld=CreateVectorWorld()
rendertoscreen
camera=newcamera()
cameracls camera,off
setfps 60
Type tBall
x#,y#,angle#,speed#
endType
Dim Ball as tball List
AddNewBallTime=0
Do
CT = Timer() and $7fffffff
if CT>AddNewBallTime
ball = new Tball
ball.x#=rnd(sw)
ball.y#=rnd(sh)
Ball.Angle#=rnd(360)
Ball.speed#=rndrange(2,10)
AddNewBallTime=CT+1000
endif
capturetoscene
clsscene
CaptureDepth 100
CameraGrabWorld Camera,CollisionWorld
DrawImage DisplayScreen,0,0,false
CaptureDepth 10
rendertoimage ZoneScreen
for each Ball()
; check what zone the ball is currently over.
ZoneType=Point(Ball.x#,Ball.y#) and 255
; get the amount of friction this zone has
Friction#=Friction#(ZoneType)
; move the ball
Speed#=Ball.Speed#*1/Friction#
newx#=ball.x#+(cos(Ball.angle#)*speed#*1)
newy#=ball.y#+(sin(Ball.angle#)*speed#*1)
if RayIntersectWOrld(CollisionWorld,Ball.x#,Ball.y#,NewX#,NewY#)=true
x2# =getintersectx#(0)
y2# =getintersecty#(0)
; Calc reflection Direction
WallAngle#=AtanFull(getnormaly#(0),getnormalx#(0))
RayAngle#=GetAngle2d(x2#,y2#,ball.x#,ball.y#)
Ball.Angle#=WrapAngle(WallAngle#,WallAngle#-RayAngle#)
ball.x#=x2#
ball.y#=y2#
else
ball.x#=newx#
ball.y#=newy#
endif
; draw the circle to represent the ball
circle ball.x#,ball.y#,10,true
next
drawcamera camera
Sync
loop
Function CreateVectorWorld()
WorldWidth=GetScreenWidth()
WorldHeight=GetScreenHeight()
; Create world
World=NewWorld()
CaptureToWorld World
; draw a series of boarder line for this world
line 0,0,worldwidth,0
line worldwidth,0,worldwidth,worldheight
line worldwidth,worldheight,0,worldheight
line 0,worldheight,0,0
for lp=0 to 7
x=100+lp*80
y=100+lp*80
Make_Convex(4,x,y,50,90+lp*20)
next
Make_Convex(6,700,100,90,70)
PartitionWorld World,32
DrawGFXImmediate
EndFunction World
; This function creates a convex polygon shape
Function Make_Convex(edges,xpos#,ypos#,Size,angle)
sa#=360.0/edges
c=rndrgb()
for lp=0 to edges-1
a#=angle+(lp*sa#)
x1#=xpos#+cosRadius(a#,size)
y1#=ypos#+SinRadius(a#,size)
if lp<(edges-1)
a#=angle+((lp+1)*sa#)
else
a#=angle
endif
x2#=xpos#+cosRadius(a#,size)
y2#=ypos#+SinRadius(a#,size)
line x2#,y2#,x1#,y1#
next lp
endfunction i


