Lesson: graphics3.nim

← Back to all lessons

Run Code Download Code

Source Code

# ****************************************************************************************
#
#   raylib [graphics] lesson 3 - A Scene of Objects
#
#   This lesson combines concepts from the `vectors` and `graphics` series.
#
#   We will build on the previous lesson by:
#   - Creating multiple polygon objects, each with its own state (position, rotation, scale).
#   - Iterating through a list of these objects in our draw loop.
#   - Applying unique transformations for each object. We use the rlgl matrix stack
#     for translation and scaling, but pass rotation directly to the drawing functions
#     for convenience.
#   - This demonstrates how to manage a "scene" of independent objects.
#
# ****************************************************************************************

import raylib
import raymath
import rlgl # raylib's OpenGL abstraction layer, for matrix transformations
import math
import strformat

const
  screenWidth = 800
  screenHeight = 600

# LESSON 1: EXPANDING THE OBJECT DEFINITION
# We enhance our Polygon object to include its own transformation properties.
# This is similar to the concepts in the `vectors` series where each object
# had its own state.
type
  Polygon = object
    name: string
    sides: int32 
    radius: float32 
    vertices: seq[Vector2]
    position: Vector2
    rotation: float32
    scale: float32
    rotationSpeed: float32
    color: Color

# This procedure is unchanged from graphics2.nim
proc generateRegularPolygon(sides: int, radius: float32): seq[Vector2] =
  var vertices: seq[Vector2] = @[]
  let angleStep = TAU / sides.float
  let angleOffset = -PI / 2.0

  for i in 0 ..< sides:
    let angle = i.float * angleStep + angleOffset
    vertices.add(Vector2(x: cos(angle) * radius, y: sin(angle) * radius))
  
  return vertices

proc main =
  initWindow(screenWidth, screenHeight, "raylib [graphics] lesson 3 - A Scene of Objects")
  setTargetFPS(60)

  # LESSON 2: CREATING A SCENE OF OBJECTS
  # We create a sequence of polygons, each with unique properties.
  # This is our "scene".
  var scene: seq[Polygon] = @[
    # A rotating triangle
    Polygon(
      name: "Triangle",
      sides: 3,
      radius: 50,
      vertices: generateRegularPolygon(3, 50),
      position: Vector2(x: 150, y: 200),
      rotation: 0,
      scale: 1.0,
      rotationSpeed: 60.0, # degrees per second
      color: Maroon
    ),
    # A large, oscillating hexagon
    Polygon(
      name: "Hexagon",
      sides: 6,
      radius: 80,
      vertices: generateRegularPolygon(6, 80),
      position: Vector2(x: 400, y: 400), # Y-position is updated dynamically
      rotation: 15,
      scale: 1.0,
      rotationSpeed: 0,
      color: DarkGray
    ),
    # A "pulsing" pentagon
    Polygon(
      name: "Pentagon",
      sides: 5,
      radius: 60,
      vertices: generateRegularPolygon(5, 60),
      position: Vector2(x: 650, y: 250),
      rotation: 0,
      scale: 1.0,
      rotationSpeed: -30.0,
      color: DarkBlue
    ),
    # A new square drawn with drawPoly (filled)
    Polygon(
      name: "Square",
      sides: 4,
      radius: 40,
      vertices: generateRegularPolygon(4, 40), # Vertices not used for drawing, but good practice
      position: Vector2(x: 150, y: 450),
      rotation: 45.0,
      scale: 1.0,
      rotationSpeed: 20.0,
      color: colorAlpha(Orange, 0.8)
    )
  ]

  var time: float32 = 0.0
  let font = getFontDefault()

  # Main game loop
  # --------------------------------------------------------------------------------------
  while not windowShouldClose():
    # Update
    # ----------------------------------------------------------------------------------
    let dt = getFrameTime()
    time += dt

    # Update each object in the scene
    for i in 0 ..< scene.len:
      scene[i].rotation += scene[i].rotationSpeed * dt
    
    # Make the pentagon "pulse" using a sine wave for its scale
    scene[2].scale = 1.0 + sin(time * 2.5) * 0.4

    # Make the hexagon oscillate vertically.
    # The center of its movement is y=300, with an amplitude of 100.
    # This makes it move between y=200 (the triangle's height) and y=400 (its start).
    scene[1].position.y = 300.0 - cos(time) * 100.0

    # Draw
    # ------------------------------------------------------------------------------------
    beginDrawing()
    clearBackground(RayWhite)

    # LESSON 3: RENDERING THE SCENE
    # We loop through each object and apply its unique transformation.
    for i, poly in scene:
      # Save the current state of the world (which is the default, untransformed state)
      pushMatrix()

      # Apply this object's transformations in SRT order (Scale, Rotate, Translate)
      # but the function calls are in the reverse: Translate, then Scale.
      translatef(poly.position.x, poly.position.y, 0)
      # We use scalef() for the pulsing effect. For rotation, instead of using
      # rlgl.rotatef(), we'll pass the angle directly to the drawing functions below.
      # This is a convenient alternative for simple cases.
      scalef(poly.scale, poly.scale, 1.0) 

      # Now that the world is transformed for this object, draw it at (0,0).
      if i == 3: # The new square
        # Use drawPoly for a FILLED shape. It generates vertices internally.
        # We pass its own rotation value directly.
        drawPoly(Vector2(x:0, y:0), poly.sides, 40.0, poly.rotation, poly.color)
      else:
        # Use drawPolyLines for an OUTLINE shape. It also generates vertices internally.
        var thickness: float32 = 2.0
        if poly.name == "Hexagon":
          thickness = 5.0 # Make the hexagon's lines noticeably thicker
        
        drawPolyLines(
          Vector2(x:0, y:0),
          poly.sides, poly.radius, poly.rotation, thickness, poly.color)

      # The original loop method still works perfectly and is more versatile.
      # for j in 0 ..< poly.vertices.len:
      #   ...
      #   drawLine(v1, v2, 2.0, poly.color)


      # Restore the world to its previous state for the next object.
      popMatrix()

    # --- Draw UI and Object Labels ---
    drawText(font, "A Scene of Objects", 
        Vector2(x: 20, y: 20), 30.0, 1.0, DarkGray)
    drawText(font, "Each object has its own position, rotation, and scale.", 
        Vector2(x: 20, y: 70), 20.0, 1.0, Gray)
    drawText(font, "We loop through the scene, applying a unique transformation for each one.", 
        Vector2(x: 20, y: 100), 20.0, 1.0, Gray)    
    drawText(font, "Using drawPoly (filled) and drawPolyLines (outline).", 
        Vector2(x: 20, y: 130), 20.0, 1.0, Gray)
    
    # LESSON 4: DRAWING LABELS
    # We loop through the scene again (outside of any matrix transforms) to draw the UI.
    const labelFontSize = 15.0
    const labelSpacing = 1.5
    for poly in scene:
      let nameText = poly.name
      let posText = fmt"P:({poly.position.x:.0f}, {poly.position.y:.0f})"
      let displayRotation = math.mod(poly.rotation, 360.0)
      let rotText = fmt"R:{displayRotation:.0f}°"
      let scaleText = fmt"S:{poly.scale:.2f}"

      # Position the labels slightly below the object, accounting for its radius and current scale.
      let labelY = poly.position.y + poly.radius * poly.scale + 10

      # Center each line of text under the object's position.
      drawText(font, nameText, Vector2(
        x: poly.position.x - measureText(font, nameText, labelFontSize, labelSpacing).x / 2, 
        y: labelY), labelFontSize, labelSpacing, Black)
      drawText(font, posText, Vector2(
        x: poly.position.x - measureText(font, posText, labelFontSize, labelSpacing).x / 2, 
        y: labelY + 16), labelFontSize, labelSpacing, Gray)
      drawText(font, rotText, Vector2(
        x: poly.position.x - measureText(font, rotText, labelFontSize, labelSpacing).x / 2, 
        y: labelY + 32), labelFontSize, labelSpacing, Gray)
      drawText(font, scaleText, Vector2(
        x: poly.position.x - measureText(font, scaleText, labelFontSize, labelSpacing).x / 2, 
        y: labelY + 48), labelFontSize, labelSpacing, Gray)

    endDrawing()
    # ------------------------------------------------------------------------------------

  closeWindow()

main()