Let’s Make Honey! Version 0.10 – Starshine the Graphics!


 

Let’s Make Honey!

 

I’ve always liked cute stars in cute games.

 

 

Today we’ll modify Honey Graphics to allow scale, rotation, color and transparency, and add an oscillation to Effects, and we’ll use all of that to make some dang stars.

 

Standard caveat: I’m opening up my learning process in the spirit of the CRAPL license. I’ll be making C++ mistakes and learning to correct them in public! For the first ten or twenty posts, don’t trust me as a C++ teacher!

 

First, let’s set the version to 0_10 in both Engine’s SConstruct and BearsLoveHoney’s SConstruct files.

 

Let’s make a little change to Effects. We’re going to add oscillations, basically tweens that go back and forth on a sinewave forever (or until they’re destroyed). Add this to effects.h:

 


/*

  Honey
  Copyright 2018 - Matthew Carlin

  Effects class provides convenience methods to create visual effects.
*/

#ifndef HONEY_EFFECTS_H_
#define HONEY_EFFECTS_H_

#include <chrono>
#include <math.h>
#include <string>
#include <stdlib.h>
#include <unordered_map>

#include "graphics.h"
#include "logic.h"

namespace Honey {
  class Effects {
   public:

      Effects();

      enum STYLES {
        LINEAR,
        SIGMOID,
        CUBIC,
        RAMPDOWN,
        RAMPUP,
        SINEWAVE
      };

      const float sigmoid_steepness = 5;

      std::unordered_map<std::string, float> tween_starts;
      std::unordered_map<std::string, float> tween_ends;
      std::unordered_map<std::string, float> widths;

      void makeTween(std::string label, float start_value, float end_value, float seconds);
      float tween(std::string label, int style);

      void makeShake(std::string label, int shake_width, float seconds);
      float shake(std::string label);

      void makeOscillation(std::string label, float oscillation_width, float period_in_seconds);
      float oscillation(std::string label);
  };

  extern Effects* effects;
}
#endif

 

We’re adding graphics to effects for the future, so we can do complex graphical effects. We won’t really use it today thought. We also change the shake_widths map to widths, because we’re going to re-use it for oscillations. Finally, we add methods to make and get oscillations.

 

We’ll need to make a few changes to account for widths instead of shake_widths, and we’ll need to fill in our new methods. Do this in effects.cpp:

 


/*

  Honey
  Copyright 2018 - Matthew Carlin

*/

#include "effects.h"

namespace Honey {
  Effects* effects = new Effects();

  Effects::Effects() {
    tween_starts = {};
    tween_ends = {};
    widths = {};
  }

  ...

  void Effects::makeShake(std::string label, int shake_width, float seconds) {
    logic->markDuration(label, seconds);
    logic->markTime(label);
    widths[label] = shake_width;
  }

  float Effects::shake(std::string label) {
    // If there's no label, return 0
    if (logic->time_markers.count(label) == 0) {
      return 0;
    }

    // If the label is expired, destroy it and return 0
    if (logic->timeSince(label) > logic->duration(label)) {
      logic->remove(label);
      widths.erase(label);
      return 0;
    }

    // Return a random number between -width/2 and width/2
    return rand() % (int) widths[label] - widths[label] / 2.0;
  }

  void Effects::makeOscillation(std::string label, float oscillation_width, float period_in_seconds) {
    logic->markDuration(label, period_in_seconds);
    logic->markTime(label);
    widths[label] = oscillation_width;
  }

  float Effects::oscillation(std::string label) {
    // If there's no label, return 0
    if (logic->time_markers.count(label) == 0) {
      return 0;
    }

    // We *don't* check if the oscillation has expired. Oscillations are infinite, until they're manually deleted.

    // We're misusing duration for period
    float time_fraction = logic->timeSince(label) / logic->duration(label);
    float space_fraction = sin(2 * M_PI * time_fraction);
    return (2 * space_fraction - 1) * widths[label];
  }
}

 

Only that last bit needs explanation. Instead of checking for the end of our effect, we let it go on forever. We use the sin function to run one complete oscillation per time period, and we use (2 * space_fraction - 1) * widths[label] to oscillate between -width and width. Forever!

 

Randomly, I need you to do this in graphics.cpp:

 


  void Graphics::clearScreen(std::string color) {
    // The clear screen method takes a hex-string color (eg #A4F4E3 or #FFFFFF or #003030)
    // and decomposes it into r, g, and b floats, which are each a fraction from 0 (black)
    // to 1 (fully saturated). (1,0,0) is full red, (0,1,0) is full green, (0,0,1) is full
    // blue, and (1,1,1) is full white.
    float r = std::stoi(color.substr(1,2), 0, 16) / 255.0f;
    float g = std::stoi(color.substr(3,2), 0, 16) / 255.0f;
    float b = std::stoi(color.substr(5,2), 0, 16) / 255.0f;

    // Tell OpenGL to clear the whole screen to our chosen color.
    glClearColor(r, g, b, 1.0f);
    glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

    // Also, for convenience, reset the model matrix
    model = glm::mat4(1.0);
    if (using_2d) {
      glUniformMatrix4fv(mvp_matrix_id, 1, GL_FALSE, glm::value_ptr(projection * model));
    }
  }

 

I forgot to do that in a previous commit. It resets the model every time we clear the screen, meaning if we translate something in one iteration, it doesn’t stick around in later iterations to throw off our calculations.

 

If you build the library (Go to Engine and run scons), it should work just fine.

 

Let’s test the oscillations with… a star! I made you a star. Just a plain old simple star. It’s nice though. You can find it here. Add it to BearsLoveHoney/Art.

 

Let’s add some things to BearsLoveHoney’s config.txt:


<section name="layout">
  <param type="int" name="screen_width">960</param>
  <param type="int" name="screen_height">600</param>
  <param type="bool" name="full_screen">false</param>
  <param type="int" name="bear_margin">340</param>
  <param type="string" name="screen_color">#F0F0F0</param>
  <param type="int" name="first_bear_x">60</param>
  <param type="int" name="first_bear_y">350</param>
  <param type="int" name="star_y">100</param>
</section>

<section name="animation">
  <param type="float" name="animation_duration">0.35</param>
  <param type="float" name="choose_duration">0.25</param>
  <param type="int" name="tween_type">1</param>
  <param type="float" name="dip_height">40</param>
  <param type="float" name="shake_width">10</param>
  <param type="float" name="starbounce_height">20</param>
  <param type="float" name="starbounce_period">2</param>
</section>

 

Just some stuff to move the bears a little lower, position a star, and bounce it.

 

Add this to BearsLoveHoney’s main.cpp:

 


  // Load images
  graphics->addImage("explorer_bear", "Art/explorer_bear.png");
  graphics->addImage("grim_bear", "Art/grim_bear.png");
  graphics->addImage("lawn_dart_bear", "Art/lawn_dart_bear.png");
  graphics->addImage("star", "Art/star.png");

  // Add music and some sound effects
  sound->addMusic("background_music", "Sound/Nothing_to_Fear.mp3");
  sound->addSound("move_left", "Sound/C_Square1.wav");
  sound->addSound("move_right", "Sound/C_Square2.wav");
  sound->addSound("choose_1", "Sound/Chant1.wav");
  sound->addSound("choose_2", "Sound/Chant2.wav");
  sound->addSound("choose_3", "Sound/Chant3.wav");
  sound->addSound("choose_4", "Sound/Chant4.wav");
  sound->addSound("choose_5", "Sound/Chant5.wav");

  // Start playing music
  sound->playMusic("background_music", -1);

  // Constants to determine the carousel
  std::string bears[] = {"explorer_bear", "grim_bear", "lawn_dart_bear"};

  // Variables to track which bear is where and which animation is which.
  int first_bear = 0; // Mod 3
  int animation_direction = 0;

  // Set action keys
  input->addActionKey("select left", hot_config->getString("input", "select_left_key"));
  input->addActionKey("select right", hot_config->getString("input", "select_right_key"));
  input->addActionKey("choose", hot_config->getString("input", "choose_key"));

  // Make a star oscillation effect
  int star_y = hot_config->getInt("layout", "star_y");
  float starbounce_height = hot_config->getFloat("animation", "starbounce_height");
  float starbounce_period = hot_config->getFloat("animation", "starbounce_period");
  effects->makeOscillation("starbounce", starbounce_height, starbounce_period);


 

We’re adding a star, and creating an oscillation effect using the height and period values from configuration. Note that this bit of configuration won’t hot update, because we only checked it outside the loop.

 

Add some more stuff to main.cpp:

 


    // Clear the screen to a soft white color
    graphics->clearScreen(screen_color);

    // Switch to 2D drawing mode
    graphics->draw2D();

    // Draw the bears
    for (int i = 0; i <= 2; i++) {
      // Set the basic bear position
      float x = first_bear_x + i * bear_margin;
      float y = first_bear_y;
      // Add movement tweens if we're moving
      if (logic->isTimeLocked("movement")) {
        x += effects->tween("slide_bear_" + std::to_string(i), tween_type);
        y += effects->tween("dip_bear_" + std::to_string(i), effects->SINEWAVE);
      }
      // Add shakey shakey to the middle bear.
      // This will only do anything if the effect is turned on.
      if (i == 1) {
        x += effects->shake("shakey shakey");
        y += effects->shake("shakey shakey");
      }
      // Draw the dang ol' bear.
      graphics->drawImage(bears[(first_bear + i) % 3], x, y);
    }

    // Draw the stars
    // Translate once to change the position of all the stars
    graphics->translate(0, effects->oscillation("starbounce"), 0);
    for (int i = 0; i <= 2; i++) {
      float x = first_bear_x + i * bear_margin;
      float y = star_y;
      graphics->drawImage("star", x, y);
    }

    // Put everything we've drawn on screen
    graphics->updateDisplay();
  }

  // Get rid of the bears. Throw those bears down the garbage chute!
  graphics->destroyImage("explorer_bear");
  graphics->destroyImage("grim_bear");
  graphics->destroyImage("lawn_dart_bear");
  graphics->destroyImage("star");


 

Now we draw the stars, three of ’em, above the heads of the bears. Note that we’re re-using the x values we already had for the bears. Also, we’re putting our tween inside a call to graphics->translate. That way we only have to do it once and it will apply to all three stars. Actually, it will apply to anything we draw below this point, and if we hadn’t reset the model on the next loop, it would apply then too. On the last line of this change, we destroy the star when we end the program.

 

Build the game (Go to BearsLoveHoney and run scons BearsLoveHoney), and then run ./BearsLoveHoney.

 

 

Wow, it’s kinda ugly. These are too large. They’re too white. They’re not well centered. They kinda just hang in place while they oscillate. We can change all that!

 

Back to graphics! It’s time to add some new powers to the Graphics class. We don’t need big ones. Small ones will do for today. Add this to graphics.h:

 


void pushModelMatrix();
void popModelMatrix();
void translate(float x, float y, float z);
void rotate(float angle, float x, float y, float z);
void scale(float x, float y, float z);

void clearScreen(std::string color);
void draw2D();
void cacheRectangle(float width, float height);
void drawRectangle(float x_position, float y_position, float width, float height);

void addImage(std::string label, std::string path);
void setImage(std::string label);
void drawImage(std::string label, int x_position, int y_position);
void drawImage(std::string label, int x_position, int y_position, bool centered, float rotation, float scale);
void destroyImage(std::string label);

 

We’ve added methods to rotate and scale, and we’ve added a second version of drawImage which looks like it’s going to let us use the rotation and scale, and decide whether to center the image.

 

Let’s fill in the rotate and scale methods. We’re doing these the same way we did translate: by using glm’s built in methods, multiplying them by the existing model matrix, and passing an updated copy of projection*model to the graphics card.

 


void Graphics::rotate(float angle, float x, float y, float z) {
  // GLM has a method for us to rotate objects. This changes the model matrix,
  // through multiplication, as though we applied a rotation, ie, rotated the object
  // by angle degrees around the vector x, y, z.
  model = model * glm::rotate(glm::radians(angle), glm::vec3(x, y, z));
  if (using_2d) {
    glUniformMatrix4fv(mvp_matrix_id, 1, GL_FALSE, glm::value_ptr(projection * model));
  }
}

void Graphics::scale(float x, float y, float z) {
  // GLM has a method for us to scale objects. This changes the model matrix,
  // through multiplication, as though we applied a scale, ie, scaled the object
  // by factors of x, y, and z in those respective directions.
  model = model * glm::scale(glm::vec3(x, y, z));
  if (using_2d) {
    glUniformMatrix4fv(mvp_matrix_id, 1, GL_FALSE, glm::value_ptr(projection * model));
  }
}

 

drawImage is a little more interesting. We’re now in a position to translate, rotate, and scale, and if there image is centered, we’ll have to scale a little bit more. There is a particular order to make things work correctly.

 

The key is realizing that all the operations take place from the perspective of the origin (0,0,0). If you translate first, and then rotate, since you’re rotating around the origin, you’ll rotate your object in a wide arc around 0.Instead, if you rotate first, and then translate, your rotation takes place at the origin, the object is still at the origin (just facing a different direction), and translate moves it correctly.

 

Similarly, if you translate first, and then scale, since you’re scaling around the origin, you’ll blow your object out further than the original translate (ie, one translated unit will become 2 translated units, or whatever). But if you scale first, since you’re scaling around the origin, your object will just be larger, and then the translate will move that larger object the correct number of units.

 

Finally, if you rotate first, and then scale, you’ll change the scale directions. For instance, if you double the size on the x axis, but first you rotate the object 90 degrees, you’ll instead double the size on the y axis. Scale first, and you’ll correctly rotate the larger object.

 

So we scale first, then rotate, then translate.

 

Except… we’re multiplying from the right. So right now, sigh, if we scale S, then rotate R, then translate T, the matrix we get is S * R * T, and when we apply it to an object, S * R * T * X, we get translation first T * X, then rotation R * T * X, then scale S * R * T * X.

 

Shut up! So that means we do all our operations in reverse order. We’re probably going to fix this later. I’m very sorry. But for now, in code, we translate, then rotate, then scale. And that centering translation? Well, that should come first (to put the object at the origin), sooooo… now it comes last.

 

I’m sorry. Now you know how weird graphics programming is. We’ll fix this in a later blog post. I’ll bring charts. And cookies.

 

Add this to graphics.cpp:

 


void Graphics::drawImage(std::string label, int x_position, int y_position, bool centered, float rotation, float scale) {
  pushModelMatrix();
  
  translate(x_position, y_position, 0);
  rotate(rotation, 0, 0, 1);
  this->scale(scale, scale, scale);

  if (centered) {
    int width = texture_widths[label];
    int height = texture_heights[label];
    translate(-width / 2.0, -height / 2.0, 0);
  }

  drawImage(label, 0, 0);

  popModelMatrix();
}

 

The important thing is it does what you think it does.

 

We’re going to test this rotation. Make some quick additions to BearsLoveHoney’s config.txt and main.cpp:

 


<section name="layout">
  <param type="int" name="screen_width">960</param>
  <param type="int" name="screen_height">600</param>
  <param type="bool" name="full_screen">false</param>
  <param type="int" name="bear_margin">340</param>
  <param type="string" name="screen_color">#F0F0F0</param>
  <param type="int" name="first_bear_x">60</param>
  <param type="int" name="first_bear_y">350</param>
  <param type="int" name="star_y">280</param>
</section>

<section name="animation">
  <param type="float" name="animation_duration">0.35</param>
  <param type="float" name="choose_duration">0.25</param>
  <param type="int" name="tween_type">1</param>
  <param type="float" name="dip_height">40</param>
  <param type="float" name="shake_width">10</param>
  <param type="float" name="starbounce_height">5</param>
  <param type="float" name="starbounce_period">2</param>
  <param type="float" name="startilt_angle">20</param>
  <param type="float" name="startilt_period">2</param>
</section>

 


float starbounce_height = hot_config->getFloat("animation", "starbounce_height");
float starbounce_period = hot_config->getFloat("animation", "starbounce_period");
effects->makeOscillation("starbounce", starbounce_height, starbounce_period);
float startilt_angle = hot_config->getFloat("animation", "startilt_angle");
float startilt_period = hot_config->getFloat("animation", "startilt_period");
effects->makeOscillation("startilt", startilt_angle, startilt_period);

 


// Draw the stars
// Translate once to change the position of all the stars
graphics->translate(0, effects->oscillation("starbounce"), 0);
for (int i = 0; i <= 2; i++) {
  float x = first_bear_x + i * bear_margin + 78;
  float y = star_y;
  graphics->drawImage("star", x, y, true, effects->oscillation("startilt"), 0.25);
}

 

We’ve added another new oscillation, startilt, and we now draw the stars shrunk down to 0.25 times their original size, and centered. Note that we changed the x value of the stars, adding 78 pixels to put them over the centers of the bears.

 

Build the library (Go to Engine and run scons) and the game (Go to BearsLoveHoney and run scons BearsLoveHoney), and then run ./BearsLoveHoney. Better! We’ve got some cute stars now. But they could be in color.

 

We’re going to pass a color to the graphics card, and use it as the global color for everything; if you set this color to green, everything will be green tinted until you set it back. Back to what? Well, to white, which is the neutral color that doesn’t modify anything. Way to play it safe, buddy.

 

First, we’ll modify graphics.cpp to add another variable for passing to the graphics card. This works just like our mvp_matrix:

 


// Variables for communicating with the graphics card
GLuint mvp_matrix_id;
GLuint color_id;
GLuint texture_sampler_id;
GLuint shader_program;

// Variables for working on the CPU size
glm::mat4 projection;
glm::mat4 model;
glm::vec4 color;
bool using_2d;
std::vector<glm::mat4> model_stack;

void initialize(Window* window);
void initializeOpenGL();
void initializeBuffersAndGeometry();
void initializeShaders();

void pushModelMatrix();
void popModelMatrix();
void translate(float x, float y, float z);
void rotate(float angle, float x, float y, float z);
void scale(float x, float y, float z);

void setColor(std::string color, float opacity);
void setColor(float r, float g, float b, float opacity);

 

We also add setColor methods so we can change this color.

 

We need to register our variable with the graphics card, and set it to the default value:

 


void Graphics::initializeBuffersAndGeometry() {
  // Here we set up some things that are going to be fed to the shader program.
  // First is the mvp (model-view-projection) matrix.
  mvp_matrix_id = glGetUniformLocation(shader_program, "mvp_matrix");
  // Second is the texture sampler.
  texture_sampler_id = glGetUniformLocation(shader_program, "texture_sampler");
  // Third is the color.
  color_id = glGetUniformLocation(shader_program, "color");

  // Remember, modern OpenGL works by feeding data (vertices, normals, textures, colors)
  // straight to the graphics card. Here we're telling OpenGL to prepare for an array of
  // vertices.
  GLuint vertex_array_id;
  glGenVertexArrays(1, &vertex_array_id);
  glBindVertexArray(vertex_array_id);

  // Here we create our rectangle texture data and pass it to the card.
  GLfloat texture_data[] = { 
    0.0f, 0.0f,
    0.0f, 1.0f,
    1.0f, 1.0f,
    1.0f, 0.0f
  };

  glGenBuffers(1, &rectangle_texture_buffer);
  glBindBuffer(GL_ARRAY_BUFFER, rectangle_texture_buffer);
  glBufferData(GL_ARRAY_BUFFER, sizeof(texture_data) * 4 * 2, texture_data, GL_STATIC_DRAW);

  // The model matrix stores the transform (rotation, translation, scale) of
  // one object. We start out with the identity matrix, which corresponds to
  // "no transform at all".
  model = glm::mat4(1.0);

  // We also start out with white as the color.
  color = glm::vec4(1.0f, 1.0f, 1.0f, 1.0f);
}

 

Now let’s fill in the setColor methods in graphics.cpp:

 


void Graphics::setColor(std::string color, float opacity) {
  // The clear screen method takes a hex-string color (eg #A4F4E3 or #FFFFFF or #003030)
  // and decomposes it into r, g, and b floats, which are each a fraction from 0 (black)
  // to 1 (fully saturated). (1,0,0) is full red, (0,1,0) is full green, (0,0,1) is full
  // blue, and (1,1,1) is full white.
  float r = std::stoi(color.substr(1,2), 0, 16) / 255.0f;
  float g = std::stoi(color.substr(3,2), 0, 16) / 255.0f;
  float b = std::stoi(color.substr(5,2), 0, 16) / 255.0f;

  setColor(r, g, b, opacity);
}

void Graphics::setColor(float r, float g, float b, float opacity) {
  color = glm::vec4(r, g, b, opacity);
  glUniform4fv(color_id, 1, glm::value_ptr(color));
}

void Graphics::clearScreen(std::string color) {
  // The clear screen method takes a hex-string color (eg #A4F4E3 or #FFFFFF or #003030)
  // and decomposes it into r, g, and b floats, which are each a fraction from 0 (black)
  // to 1 (fully saturated). (1,0,0) is full red, (0,1,0) is full green, (0,0,1) is full
  // blue, and (1,1,1) is full white.
  float r = std::stoi(color.substr(1,2), 0, 16) / 255.0f;
  float g = std::stoi(color.substr(3,2), 0, 16) / 255.0f;
  float b = std::stoi(color.substr(5,2), 0, 16) / 255.0f;

  // Tell OpenGL to clear the whole screen to our chosen color.
  glClearColor(r, g, b, 1.0f);
  glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

  // Also, for convenience, reset the model matrix
  model = glm::mat4(1.0);
  if (using_2d) {
    glUniformMatrix4fv(mvp_matrix_id, 1, GL_FALSE, glm::value_ptr(projection * model));
  }

  // and the color
  setColor(1.0f, 1.0f, 1.0f, 1.0f);
}

 

The first method does the same string trick we already did in clearColor, decomposing strings like #A4F4E3 into r,g,b values. Then it calls the second method. The second method just sends our color to the graphics card. Finally, in clearScreen, we do our users the courtesy of resetting the global color, just like we reset the model matrix. This means if you turn everything green, the effect won’t stick around in the next iteration of the game loop.

 

Using the color on the graphics card is actually pretty simple. Add this to shaders.h:

 


const std::string fragment_shader = R"(
#version 330 core

uniform sampler2D texture_sampler;
uniform vec4 color;

in vec2 fragment_texture_vector;

out vec4 final_color_vector;

void main(){
  final_color_vector = color * texture(texture_sampler, fragment_texture_vector);
} 
)";

 

We just tell the fragment shader (which, remember, operates on every pixel that gets drawn, thousands or millions of pixels) to take in a color value, and multiply whatever else we have (currently just the texture color) by the new color. This multiplication is done component by component; red * red, green * green, blue * blue, opacity * opacity. This is why white is the neutral color; white is 1, 1, 1, 1, which means it does nothing when you multiply it.

 

Now we can add colors to the stars in BearsLoveHoney! Modify config.txt and main.cpp like so:

 


<section name="layout">
  <param type="int" name="screen_width">960</param>
  <param type="int" name="screen_height">600</param>
  <param type="bool" name="full_screen">false</param>
  <param type="int" name="bear_margin">340</param>
  <param type="string" name="screen_color">#F0F0F0</param>
  <param type="int" name="first_bear_x">60</param>
  <param type="int" name="first_bear_y">350</param>
  <param type="int" name="star_y">280</param>
  <param type="string" name="star_color_0">#ff6961</param>
  <param type="string" name="star_color_1">#77dd77</param>
  <param type="string" name="star_color_2">#779ecb</param>
</section>

 


// Draw the stars
// Translate once to change the position of all the stars
graphics->translate(0, effects->oscillation("starbounce"), 0);
for (int i = 0; i <= 2; i++) {
  float x = first_bear_x + i * bear_margin + 78;
  float y = star_y;
  graphics->setColor(hot_config->getString("layout", "star_color_" + std::to_string(i), 1);
  graphics->drawImage("star", x, y, true, effects->oscillation("startilt"), 0.25);
}

 

Build the library (Go to Engine and run scons) and the game (Go to BearsLoveHoney and run scons BearsLoveHoney), and then run ./BearsLoveHoney.

 

 

Yay!

 

One last thing. When the player presses choose, I don’t just want the middle bear to shake. God damnit, I want stars. We’re going to make a little star field.

 

Add the following to BearsLoveHoney’s main.cpp:

 


// Make a star oscillation effect
int star_y = hot_config->getInt("layout", "star_y");
float starbounce_height = hot_config->getFloat("animation", "starbounce_height");
float starbounce_period = hot_config->getFloat("animation", "starbounce_period");
effects->makeOscillation("starbounce", starbounce_height, starbounce_period);
float startilt_angle = hot_config->getFloat("animation", "startilt_angle");
float startilt_period = hot_config->getFloat("animation", "startilt_period");
effects->makeOscillation("startilt", startilt_angle, startilt_period);

// Make a star field
int star_field_x[60];
int star_field_y[60];
for (int i = 0; i < 60; i++) {
  star_field_x[i] = logic->randomInt(-screen_width / 2.0, 1.5 * screen_width);
  star_field_y[i] = logic->randomInt(-screen_height / 2.0, 1.5 * screen_height);
}

...

// If the users presses the choose button, shake the middle bear.
if (input->actionDown("choose") && !logic->isTimeLocked("movement")) {
  sound->playSound("choose_" + std::to_string(logic->randomInt(1,5)), 1);
  logic->makeTimeLock("movement", lock_duration);
  effects->makeShake("shakey shakey", shake_width, choose_duration);
  effects->makeTween("star_phasing", 0, 1, animation_duration);
  animation_direction = 0;
}

...

// Clear the screen to a soft white color
graphics->clearScreen(screen_color);

// Switch to 2D drawing mode
graphics->draw2D();

// Light up the sky
if (effects->tween("star_phasing", 0) != 0) {
  float opacity = effects->tween("star_phasing", effects->SINEWAVE);
  float shooting_star = 300 * effects->tween("star_phasing", effects->LINEAR);
  graphics->setColor(hot_config->getString("layout", "star_color_1"), opacity);

  for (int i = 0; i < 60; i++) {
    graphics->drawImage("star", star_field_x[i] + shooting_star, star_field_y[i] - shooting_star, true, 0, 0.15);
  }

  graphics->setColor(1, 1, 1, 1);
}

 

First we use logic->randomInt to make a bunch of points in space. These points are all in a box that’s twice the size of the screen (ie, from -0.5 to 1.5 in both x and y directions). Then, when the player presses the choose button, we make a new tween called star_phasing.

 

Finally, at the beginning of the draw mode (first, so that it draws behind the bears), we use star_phasing to get the opacity; these stars will fade in and then fade out on a sinewave tween. Then, we use it again to set a shooting star effect; these stars will also fly off in one direction together. Then we iterate through our 60 stars, and draw each one using both tweens.

 

Finally, we set the color back to default so it doesn’t affect anything else.

 

Build the library (Go to Engine and run scons) and the game (Go to BearsLoveHoney and run scons BearsLoveHoney), and then run ./BearsLoveHoney.

 

 

It’s full of stars!

 

 

Next time we’ll add fonts and letter printing, so we can print arbitrary strings to the screen.

 

Leave a Reply

Your email address will not be published. Required fields are marked *