Lesson: vectors6.nim

← Back to all lessons

Run Code Download Code

Source Code

# ****************************************************************************************
#
#   raylib [vectors] lesson 6 - Interactivity and State
#
#   This lesson demonstrates:
#   - Handling user input to trigger actions.
#   - Managing the state of dynamic objects (e.g., a bullet).
#   - Calculating directional vectors for movement.
#   - Basic collision detection between a point and a polygon.
#
# ****************************************************************************************

import raylib
import raymath
import math

const
  screenWidth = 800
  screenHeight = 450

proc main =
  initWindow(screenWidth, screenHeight, "raylib [vectors] lesson 6 - Pew Pew!")
  setTargetFPS(60)

  # Define the screen boundaries as a rectangle for collision checks.
  let screenBounds = Rectangle(x: 0, y: 0, width: screenWidth, height: screenHeight)

  # --- Model Space Definitions ---
  let triangleVertices = [
    Vector2(x:  0.0, y: -30.0), # Tip of the "ship"
    Vector2(x: -25.0, y:  30.0),
    Vector2(x:  25.0, y:  30.0)
  ]

  let quadVertices = [
    Vector2(x: -25.0, y: -25.0),
    Vector2(x:  25.0, y: -25.0),
    Vector2(x:  25.0, y:  25.0),
    Vector2(x: -25.0, y:  25.0)
  ]

  # --- World Space Positions ---
  let triangleBasePos = Vector2(x: screenWidth * 0.25, y: screenHeight / 2.0)
  let quadWorldPos = Vector2(x: screenWidth * 0.75, y: screenHeight / 2.0)

  # --- Game State Variables ---
  var time: float32 = 0.0
  # These variables manage the "hit" feedback for the quad.
  var quadColor = DarkBlue
  # When the quad is hit, this cooldown is set. It counts down each frame.
  var quadHitCooldown: float32 = 0.0 
  const hitDisplayTime: float32 = 0.5 # How long the quad stays red (in seconds)

  # LESSON 1: BULLET STATE
  # We need variables to track the bullet's state: its activity, position, and velocity.
  var bulletActive = false
  var bulletPos: Vector2
  var bulletVelocity: Vector2
  # The 'f32 suffix is a shorthand to create a float32 literal.
  # Nim's type inference automatically makes `bulletSpeed` a float32,
  # so we don't need to write `const bulletSpeed: float32 = ...`.
  const bulletSpeed = 400.0'f32

  # Main game loop
  # --------------------------------------------------------------------------------------
  while not windowShouldClose():
    # Update
    # ----------------------------------------------------------------------------------
    let dt = getFrameTime() # Get the time elapsed since the last frame.
    time += dt
    # Manage the quad's hit-flash effect.
    if quadHitCooldown > 0:
      quadHitCooldown -= dt
    # When the cooldown finishes, reset the color from Red back to DarkBlue.
    elif quadColor == Red:
      quadColor = DarkBlue

    # --- Triangle Transformation ---
    let triRotation = time * 50.0
    let movementFactor = (sin(time) + 1.0) / 2.0
    let startPos = triangleBasePos
    let endPos = Vector2(x: 100.0, y: screenHeight - 100.0)
    # "lerp" stands for Linear Interpolation. It finds a point on the line
    # between startPos and endPos. The movementFactor (0.0 to 1.0) determines
    # how far along that line the point is, creating smooth back-and-forth movement.
    let triangleWorldPos = lerp(startPos, endPos, movementFactor)
    let rotationMatrix: Matrix = rotateZ(triRotation.degToRad)

    # --- Quad Transformation ---
    let scaleFactor = 1.0 + sin(time * 2.0) * 0.4
    let scalingMatrix: Matrix = scale(scaleFactor, scaleFactor, 1.0)
    let quadTranslationMatrix: Matrix = translate(quadWorldPos.x, quadWorldPos.y, 0.0)
    # remember: to scale then translate, we multiply in reverse order
    let quadModelMatrix: Matrix = multiply(quadTranslationMatrix, scalingMatrix)
    # right-to-left <-
    
    # --- Vertex Transformations (using loops for clarity) ---
    # For the triangle, we use a "hybrid" method: rotate with a matrix, 
    # then translate with vector addition.
    var transformedTriVertices: array[3, Vector2]
    # In Python, we would would say 'for i, v in enumerate(triangleVertices):'
    # In Nim, the 'enumerate' is implicit because we are using 2 variables to iterate
    for i, v in triangleVertices:
      transformedTriVertices[i] = transform(v, rotationMatrix) + triangleWorldPos

    # For the quad, we use the full model matrix to transform each vertex.
    var transformedQuadVertices: array[4, Vector2]
    for i, v in quadVertices:
      transformedQuadVertices[i] = transform(v, quadModelMatrix)

    # LESSON 2: INPUT HANDLING AND FIRING
    # Check if the space key is pressed and if there isn't already an active bullet.
    if isKeyPressed(Space) and not bulletActive:
      bulletActive = true
      # The bullet starts at the tip of the triangle. The tip is the first vertex,
      # so we use its transformed position.
      bulletPos = transformedTriVertices[0]

      # To get the direction, we start with a "forward" vector in model space.
      # Since the triangle's tip points up (negative Y), our forward is (0, -1).
      let forwardVector = Vector2(x: 0.0, y: -1.0)
      # We then rotate this vector by the triangle's current rotation to get the
      # final direction in world space. This gives us a normalized direction vector.
      let direction = rotate(forwardVector, triRotation.degToRad)

      # The velocity is the direction multiplied by the speed.
      bulletVelocity = direction * bulletSpeed

    # LESSON 3: BULLET UPDATE AND COLLISION
    if bulletActive:
      # Update bullet position based on its velocity and the frame time.
      bulletPos += bulletVelocity * dt

      # Deactivate bullet if it goes off-screen. A convenient shorthand for this
      # is to use raylib's built-in function to check if the point is no longer
      # inside the screen's rectangle.
      if not checkCollisionPointRec(bulletPos, screenBounds):
        bulletActive = false

      # Check for collision between the bullet's position and the quad's vertices.
      # `checkCollisionPointPoly` is a C-style function that expects a pointer to the
      # start of the vertex array. Nim's `[]` slice operator on an array provides
      # this pointer automatically.
      if checkCollisionPointPoly(bulletPos, transformedQuadVertices):
        bulletActive = false
        quadColor = Red          # Change color on hit!
        quadHitCooldown = hitDisplayTime # Start the cooldown timer.

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

    # --- Draw UI ---
    drawText("Press [Space] to fire!", 10, 10, 20, LightGray)

    # --- Draw Shapes ---
    drawTriangleLines(
      transformedTriVertices[0], transformedTriVertices[1], transformedTriVertices[2], Maroon)

    for i in 0 ..< transformedQuadVertices.len:
      drawLine(
        transformedQuadVertices[i], transformedQuadVertices[(i + 1) mod 4], 2.0, quadColor)

    # LESSON 4: DRAWING THE BULLET
    # Only draw the bullet if it's active.
    if bulletActive:
      drawCircle(bulletPos, 5.0, Black)

    endDrawing()
    # ------------------------------------------------------------------------------------
  # De-Initialization
  # --------------------------------------------------------------------------------------
  closeWindow()
  # --------------------------------------------------------------------------------------

main()

#[ Think of it like a "connect-the-dots" puzzle.

Your original shape (quadVertices) is the set of numbered dots on the page 
in their starting positions.
The transformation matrix (quadModelMatrix) is a set of instructions 
that tells you where to move each individual dot.
The drawing functions (drawLine in your case) are you, with a pencil, 
drawing straight lines between the new positions of the dots.
The matrix itself has no concept of a "line"; 
it only knows how to take an input point (x, y) and output a new point (x', y'). ]#