Mini Golf Demo – Conceptual Overview

February 21, 2026

 

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