🔍

µTHREE – the original minimalist local multiplayer game console

µTHREE – VERSION II

The second version of the µONE is a slight update for three players – each one button and a LED ring as playground. The builtin game can be described as a kind of battle PONG.

trxy_btn.h

// -------------------------------
// TXY'S button helper class
// --------------------------------
// serves you wonderful functionality right on the plate :)
 
    // is_pressed = momentary state of button
    // is_holded = momentary state if button is down for at least 1s
     
    // on_pressed = triggered once if button is down
    // on_pressed = triggered once if button is up
    // on_holded = triggered once if button is down for at least 1s
 
 
class aBTN {
  private:
    int bpin;
    long ts = 0;
    bool previous_state = false;
    bool prev_is_holded = false;
    bool flipped_phase = false;
 
  public:
    bool is_pressed = false;
    bool is_holded = false;
 
    bool on_pressed = false;
    bool on_released = false;
    bool on_holded = false;
 
    aBTN(int bpin, bool _flipped_phase) {
      this->bpin = bpin;
      this->flipped_phase = _flipped_phase;
      init();
    }
    void init() {
      pinMode(bpin, INPUT);
 
    }
 
 
    void operateBUTTON() {
 
      this->is_pressed = digitalRead(bpin);
 
        // flip phase if button phase is flipped
       if(this->flipped_phase){this->is_pressed = !this->is_pressed;}
 
        // reset press states
      this->on_pressed = false;
      this->on_released = false;
 
      // if there is any change of state :)
      if (this->previous_state != this->is_pressed) {
 
 
            if (this->is_pressed) {
             
            //button is pressed down -------
            this->ts = millis(); // set timestamp
            this->on_pressed = true;
 
            } else {
            //button is released -------
            this->on_released = true;
            this->is_holded = false;
            this->on_holded = false;
            this->prev_is_holded = false;
            }
 
            // buffer prev state to avoid repeat
            this->previous_state = this->is_pressed;
 
      }
 
        // reset on hold state first
        this->on_holded = false;
 
        // if button is holded for more than 1s and is still pressed!
      if (this->ts + 1000 < millis() && this->is_pressed ) {
 
        this->is_holded = true; // set is holded state each frame here
 
        if (this->prev_is_holded != this->is_holded && this->is_pressed ) {
 
          this->on_holded = true;
          this->prev_is_holded = this->is_holded;
           
        }
      }
    }
};

main.cpp

// using the Microduino 328P
 
#include <Arduino.h>
#include <Adafruit_NeoPixel.h>
#include "trxy_btn.h"
  
#define PIN  10 //LED RING PIN
 
#define buzzpin  3 // buzzer_pin
  
// How many NeoPixels are attached to the Arduino?
#define NUMPIXELS 12
 
int red_score = 10;
int blue_score = 10;
int green_score = 10;
 
  
 
Adafruit_NeoPixel pixels(NUMPIXELS, PIN, NEO_GRB + NEO_KHZ400);
  
#define DELAYVAL 10 // Time (in milliseconds) to pause between pixels
   
  
aBTN btn1(2,false); // init button 1 on digipin 2
aBTN btn2(11,false);  // init button 2 on digipin 4
aBTN btn3(12,false);
 
// hardware layout button to led ring 
// btn1 >>> LED = 7
// btn2 >>> LED = 3
// btn3 >>> LED = 11
 
int btn1_pos = 11;
int btn2_pos = 3;
int btn3_pos = 7;
 
// 0 1r 2g 3b
int target_posis_arr[] = {0,11,3,7};
  
  
void setup() {
 
   Serial.begin(9600);
  // put your setup code here, to run once:
  pixels.begin(); // INITIALIZE NeoPixel strip object (REQUIRED)
}
  
float cp = 0;
float pulse = 0;
 
int target_id = 1; // 1r 2g 3b
int next_target_id = 1;
 
float acc = .1;
float btn_range_threshold = 1.1; // how forgiving the button reacts / the higher the more forgiving
 
    int r = 0;
    int g = 0;
    int b = 0;
  
  int ccol[] = {11,11,11};
 

 void waiting_for_player(){


    

     for (int i=0;i<12;i++){

           ccol[0] = int(random(50));
     ccol[1] = int(random(50));
     ccol[2] = int(random(50));
 
        // draw the basic color set + mini pulsing
         pixels.setPixelColor(i, pixels.Color( ccol[0],ccol[1],ccol[2]));
 
    }

     pixels.show();

     tone(buzzpin,900+int(random(400)),60);


 }
 
 void idle_anim(){


     ccol[0] = (sin(millis()*.001)+1)*20;
     ccol[1] = (sin(millis()*.0003)+1)*20;
     ccol[2] = (sin(millis()*.0002)+1)*20;

     for (int i=0;i<12;i++){
 
        // draw the basic color set + mini pulsing
         pixels.setPixelColor(i, pixels.Color( ccol[0],ccol[1],ccol[2]));
 
    }

     pixels.show();


 }


int winnercol[4] ;

void calcWinnerStats(int looser_player_id){
 
   // pixels.clear();
     winnercol[0]=0;
     winnercol[1]=0;
     winnercol[2]=0;


    int largestValue = max(max(red_score,green_score),blue_score);
 
    if(red_score==largestValue){
      //red wins!
      //pixels.setPixelColor(btn1_pos, pixels.Color(255,0,0)); 
      winnercol[0]=155;
       winnercol[3] = btn1_pos;
    }
 
     if(green_score==largestValue){
      //green wins!
      //pixels.setPixelColor(btn2_pos, pixels.Color(0,255,0)); 
      winnercol[1]=155;
      winnercol[3] = btn2_pos;
    }
 
 
     if(blue_score==largestValue){
      //blue wins!
     // pixels.setPixelColor(btn3_pos, pixels.Color(0,0,255)); 
     winnercol[2]=155;
     winnercol[3] = btn3_pos;
    }
 
    //pixels.show();
    //Serial.println("SHOW STATS");
 
}
 
 
 
bool game_running = false;
bool intro_running = false;
 
void hitsnd(){
 
  tone(buzzpin,1100+int(random(200)),80);
 
}
 
float general_speed = .04;
 
void hitTheBallBack(){
 
    //float backhit_acc = (random(3) + 7)*.03; // change ball speed
    // continously get faster!!
    general_speed += .01;
     
    if(acc>0){
 
        acc = -general_speed;
 
    }else{
        acc = general_speed;
    }
 
 
     
     
    //iterate as long as there is not the same player selected!
    while( next_target_id == target_id){
 
          next_target_id = int(random(3)+1);
    }
 
    target_id = next_target_id;
 
}
 
 
int pcp = -1;
int intro_count = 0;
bool show_stats = false;
int  stats_show_count = 0;
 
bool does_cursor_pass_target_pos(){
 
   int tp = target_posis_arr[target_id]; // get current target pos
 
    if(tp == int(cp) && pcp != int(cp)){ pcp = int(cp); return true;}
 
    if(pcp != int(cp)){ pcp = int(cp); }
 
      return false;
}
 
 
 
void restart_game(){
 
  // todo >>> random start pos + dir
  acc = .1;
  general_speed = .04;
  red_score = 10;
  blue_score = 10;
  green_score = 10;
  game_running = true;
 intro_count = 0;
 intro_running = false;
 show_stats = false;
 stats_show_count = 0;
}
 
 
 void showWinnerStats(){


     float pulse = abs(sin(millis()*.02));

     for (int i=0;i<12;i++){
 
          if(i!= winnercol[3]){
        // draw the basic color set + mini pulsing
               pixels.setPixelColor(i, pixels.Color( winnercol[0]*pulse,winnercol[1]*pulse,winnercol[2]*pulse));
          }else{
               pixels.setPixelColor(i, pixels.Color( 255,255,255));
          }
 
    }

     pixels.show();
     stats_show_count++;

     if(stats_show_count%10){
          tone(buzzpin,300+random(800),20);
     }

     if( stats_show_count>400){
          game_running = false;
     }

 }

 
void player_lost( int _playerid){
 
  //tone( buzzpin,1111,200);
  calcWinnerStats(_playerid);
  //game_running = false;
  show_stats = true;
  //delay(1000); 
}
 
 
void loop() {
  
    
     
     // operate all buttons
     btn1.operateBUTTON();
    btn2.operateBUTTON();
     btn3.operateBUTTON();
  
 
 
      if(!game_running){

               // any button is pressed down for 2s
          if( btn1.is_holded ||  btn2.is_holded || btn3.is_holded  ){
               
               
               intro_running = true;

          }  

          if(intro_running){
               intro_count++;
          }

         

          if(intro_count>120){


               restart_game();
          }

          // in idle mode!
          if(intro_count==0){

               idle_anim();
             
          }else{
             
             // waiting for players mode!
             waiting_for_player();
                       }
          delay(10);
          return;
     }else{

          if(show_stats){

               showWinnerStats();
               delay(10);
               return;
          }

     }


   // if(!game_running){  showWinnerStats(); return;}
 
       // pulse = (sin(millis()*.002)+1)*.5; // wave from 0 to 1
 
 
    cp += acc;
 
 
    // ------------------------------------
 
// ---- OBSERVE THE SCORES -----------------
    // ----------------------------------
    if(red_score<1){ player_lost(1);}
    if(green_score<1){player_lost(2);}
    if(blue_score<1){player_lost(3);}
 
 
    if(ccol[0]>red_score/10){ ccol[0] *= .9; }else{ ccol[0] = red_score/10; }
    if(ccol[1]>green_score/10){ccol[1] *= .9; }else{ ccol[1] = green_score/10; }
    if(ccol[2]>blue_score/10){ ccol[2] *= .9;}else{ ccol[2] = blue_score/10; }
      
      
 
     // --------------------------------------------
    // ----  OBESERVE THE CURSOR -----------------------
    // -----------------------------------------
  
      if( does_cursor_pass_target_pos() ){
 
            // CURSOR PASSES the TARGET AREA!
 
            tone(buzzpin,40,16);
            if( target_id==1 ){  if(red_score>0){red_score--;}   }
            if( target_id==2 ){  if(green_score>0){green_score--;   }   }
            if( target_id==3 ){  if(blue_score>0){blue_score--;}   }
 
      }
 
     
 
 
    if(btn1.on_pressed){
  
       
       if( abs(btn1_pos - cp+1 ) < btn_range_threshold){
           
           if(target_id == 1){
 
              hitsnd();
              hitTheBallBack();
 
              
              ccol[0] = 111;
              ccol[1] = green_score;
              ccol[2] = blue_score;
  
              red_score+=2;
 
           }else{
 
             // pushed button, but not on target!
             red_score--;
 
           }
 
          // Serial.println("BUTTON 1");
       }
 
   }
 
    if(btn2.on_pressed){
  
        
      if( abs(btn2_pos - cp+1 ) < btn_range_threshold){
           
 
          if(target_id == 2){
 
             
              hitsnd();
              hitTheBallBack();
 
              
              ccol[0] = red_score;
              ccol[1] = 111;
              ccol[2] = blue_score;
  
              green_score+=2;
 
          }else{
 
             // pushed button, but not on target!
             green_score--;
 
           }
 
           
          
       }
 
       
   }
 
    if(btn3.on_pressed){
  
       if( abs(btn3_pos-cp+1) < btn_range_threshold){
 
          if(target_id == 3){
 
              hitsnd();
              hitTheBallBack();
 
              
              ccol[0] = red_score;
              ccol[1] = green_score;
              ccol[2] = 111;
       
              blue_score+=2;
 
          }else{
 
             // pushed button, but not on target!
             blue_score--;
 
           }
 
         // Serial.println("BUTTON 3");
       }
   }
   
      
  
    // --------------------------------------------
    //set the BACKGROUND -----------------------
    // -----------------------------------------
 
    for (int i=0;i<12;i++){
 
        // draw the basic color set + mini pulsing
         pixels.setPixelColor(i, pixels.Color( ccol[0],ccol[1],ccol[2]));
 
    }
     
      
    //------------------------------------------
    // draw CURSOR IN TARGET COLOR -----------------------
    //------------------------------------------
 
 
      if(target_id == 0){
          pixels.setPixelColor(int(cp), pixels.Color(2,2,2)); 
      }
 
      if(target_id == 1){
          pixels.setPixelColor(int(cp), pixels.Color(122,0,0));  
      }
 
       if(target_id == 2){
          pixels.setPixelColor(int(cp), pixels.Color(0,122,0));  
      }
 
      if(target_id == 3){
          pixels.setPixelColor(int(cp), pixels.Color(0,0,122));  
      }
     
  
 
 
    // ------------------------------------------------------
    // simply light up the aligned LED in the current color when button is down
     // ------------------------------------------------------
 
 
    if(btn3.is_pressed){
 
          pixels.setPixelColor(btn3_pos, pixels.Color( 0 ,0,255)); 
    }
     
    if(btn2.is_pressed){
          pixels.setPixelColor(btn2_pos, pixels.Color( 0 ,255,0)); 
 
    }
 
    if(btn1.is_pressed){
        pixels.setPixelColor(btn1_pos, pixels.Color(255,0,0)); 
    } 
 
      
 
    pixels.show();   // Send the updated pixel colors to the hardware.
  
    delay(DELAYVAL); // Pause before next pass through loop
  
    // make sure, the cursor runs in circles :)
    if(cp>12){cp=0;}
    if(cp<0){cp=12;}
    
  
}

previous prototypes

first prototype attempt

This first prototype kept simplicity in mind – with just one button each player and one shared LED as playground. Although this is quiet handy to work with, the possibilities are a lot of limited – and leads to boring games. So this needed an update 🙂

schematics MICRO:ONE

parts list

  • 2 x ARCADE pushbuttons
  • 2 x 10kOhm resistors for buttons
  • 2 x white LED’s 5mm
  • 1 x LED’s 10mm ( or any other led )
  • 3 x 220 Ohm resistors for LED protection
  • 1 x piezzo buzzer
  • in real life

    This is still a „UN“proper way to design electronic devices, but anyway this works as ultra rapid prototype in the best manner. With proper usage of freehand soldering, lasercut and a lot of hotglue.

    The CODE

    As for the setup is very simplistic, we need to distill the most out of what we have from the interface elements. So, here we have some basic helper classes for you to use to get started with your interactions instead of getting stuck with rudimentaries 🙂 – There is three classes, that need to be included to your project – one for the buttons, one for all your led’s one for the incredible sound effects 🙂 – There is no need to change anything in those classes. You can acess everything in your main script. Please read the heads of those scripts to get an idea of their functionality 🙂

    trx_btn.h

    // -------------------------------
    // TXY'S button helper class
    // --------------------------------
    // serves you wonderful functionality right on the plate :)
    
        // is_pressed = momentary state of button
        // is_holded = momentary state if button is down for at least 1s
        
        // on_pressed = triggered once if button is down
        // on_pressed = triggered once if button is up
        // on_holded = triggered once if button is down for at least 1s
    
    
    class aBTN {
      private:
        int bpin;
        long ts = 0;
        bool previous_state = false;
        bool prev_is_holded = false;
        bool flipped_phase = false;
    
      public:
        bool is_pressed = false;
        bool is_holded = false;
    
        bool on_pressed = false;
        bool on_released = false;
        bool on_holded = false;
    
        aBTN(int bpin, bool _flipped_phase) {
          this->bpin = bpin;
          this->flipped_phase = _flipped_phase;
          init();
        }
        void init() {
          pinMode(bpin, INPUT);
    
        }
    
    
        void operateBUTTON() {
    
          this->is_pressed = digitalRead(bpin);
    
            // flip phase if button phase is flipped
           if(this->flipped_phase){this->is_pressed = !this->is_pressed;}
    
            // reset press states
          this->on_pressed = false;
          this->on_released = false;
    
          // if there is any change of state :)
          if (this->previous_state != this->is_pressed) {
    
    
                if (this->is_pressed) {
                
                //button is pressed down -------
                this->ts = millis(); // set timestamp
                this->on_pressed = true;
    
                } else {
                //button is released -------
                this->on_released = true;
                this->is_holded = false;
                this->on_holded = false;
                this->prev_is_holded = false;
                }
    
                // buffer prev state to avoid repeat
                this->previous_state = this->is_pressed;
    
          }
    
            // reset on hold state first
            this->on_holded = false;
    
            // if button is holded for more than 1s and is still pressed!
          if (this->ts + 1000 < millis() && this->is_pressed ) {
    
            this->is_holded = true; // set is holded state each frame here
    
            if (this->prev_is_holded != this->is_holded && this->is_pressed ) {
    
              this->on_holded = true;
              this->prev_is_holded = this->is_holded;
              
            }
          }
        }
    };
    

    trx_led.h

    
    // -------------------------------
    // TXY'S LED helper class
    // --------------------------------
    // serves you wonderful functionality right on the plate :)
    
    // each LED has its own display mode you can set >>>  //0 manual / 1:sinpulse / 2:fading
    // have a look at the public vars some lines below to see some functionality :)
    
     
    // basic lerp function for fading effects
    float return_lerp(float _s, int _target, int _time) {
      _s = _s + (( float(_target) - _s) / float(_time));
      return _s;
     
    }
     
    // --------------
    // LED Object
    // -------------
     
    class aLED {
      private:
        int pin;
        long ts = 0;
       
      public:
    
        int led_mode = 0; //0 manual / 1:sinpulse / 2:fading
        int brightness = 0; // current LED brightness ( can be set manually in manual mode) 
        
        // pulse mode variables mode(1)
         float pulse_speed = .004; // speed of pulse when in pulsemode the higher, the faster
         int pulse_amplitude = 125; // how strong is the pulse difference
         int pulse_offset = 0; // offset the pulse centerpoint
    
        // fade mode variables mode(2)
        float fade_target = 125; // where to fade? 0-254
        int fade_speed = 10; // the higher, the slower!
        
        aLED(int pin) {
          this->pin = pin;
          init();
        }
     
        void init() {
          pinMode(pin, OUTPUT);
        }
     
        void operateLED() {
          if (led_mode == 0) {
     
            // if in manual mode
            // do nothing for now :)
     
          } else if (led_mode == 1) {
     
            // -----------  PULSE MODE
            brightness =  int((sin(millis() * pulse_speed) + 1) * pulse_amplitude * .5) + pulse_offset;
     
          } else if (led_mode == 2) {
     
            // -----------  FADING MODE
            brightness = int(return_lerp(brightness, fade_target, fade_speed));
     
          }
     
          // apply to LED
          analogWrite(pin, brightness);
     
        }
     
        void setMode(int _mode) {
          led_mode = _mode;
        }
     
    };
    
    

    trx_sound.cpp

    int buzzer_pin = 9; // setup the buzzer pin
    float audiotick  = 0;
    int buffer[12] = {0,0,0,0,0,0,0};
    
    // ------------------------------
    // --- some fun sounds ------------- 
    // ------------------------------
    
    int blip[] = {1200,600};
    int blep[] = {400,1100};
    int mel_upupup[] = {800,500,450,400,300};
    int mel_downdowndown[] = {300,400,550,600,700};
    
     
    // this function needs to be executed each step to make the engine work
    void operate_sfx(){
     
      if(audiotick < 0){ 
        noTone(buzzer_pin); return;
        }else{
        tone(buzzer_pin, buffer[ int(audiotick )  ] );
         audiotick  -= 0.006;
      }
     
    }
    
    // call this function to add tones to the playlist
    // like: play_sfx(tone_array,number_of_tones_in_array);
    
    void play_sfx(int _narr[],int _cnt = 1){
       
        for (int i = 0; i < _cnt ; i++) {
             buffer[i] = _narr[i];
        }
        audiotick = _cnt; 
    }
    

    Here we have a pretty basic demo script to show off all functions of the pico_one console 🙂
    main.cpp

    // include libs to make it work!
    #include <Arduino.h>
    #include <trx_btn.h>
    #include <trx_led.h>
    #include <trx_sound.h>
     
    // setup the hardware and helper libs
    aBTN btn1(8,true); // init button 1 on digipin 2
    aBTN btn2(12,true);  // init button 2 on digipin 4
    aLED led_main(3); // init main LED on PWM pin 6
    aLED led_one(5); // init player_one LED on PWM pin 10
    aLED led_two(6); // init player_two LED on PWM pin 5 >> do not use pin 11! > conflict with buzzer!
     
     
    void setup() {
      
     //Serial.begin(9600);
     //Serial.println("demo startup");
      
      // first init and set all leds to manual mode with brightness of 50
     led_main.setMode(0);
     led_main.brightness = 50;
     
     led_one.setMode(0);
     led_one.brightness = 50;
     
     led_two.setMode(0);
     led_two.brightness = 50;
     
    }
     
    void loop() {
       
       
      // each button obj needs to be operated each tick to work
       btn1.operateBUTTON();
       btn2.operateBUTTON();
     
    
      if(btn1.is_pressed){
         led_one.brightness = 200;
      }else{
         led_one.brightness = 0;
      }
    
    
      if(btn2.is_pressed){
         led_two.brightness = 200;
      }else{
         led_two.brightness = 0;
      }
    
      if(btn1.on_pressed){
        
        play_sfx(blep,2);
      }
      if(btn2.on_pressed){
         
         play_sfx(blip,2);
     
      }
     
       if(btn2.on_holded){
     
          // set led to pulse mode at random pulse fequency
          led_main.setMode(1);
          led_main.pulse_speed = random(100)*.001+.004;
           
           play_sfx(mel_downdowndown,5);
        }
     
      if(btn1.on_holded){
     
          // simply fade the led to a random value and stay there!
          led_main.setMode(2);
          led_main.fade_target = int(random(222)+30);
          led_main.fade_speed = 64; // the higher, the slower!
            
           play_sfx(mel_upupup,5);
     
        }
     
         // each LED obj needs to be operated each tick to work
        led_main.operateLED(); 
        led_one.operateLED();
        led_two.operateLED();
       
        operate_sfx(); // operate the sound effects
     
    }