First working multiplayer version

This commit is contained in:
Phew
2026-02-11 23:20:46 +01:00
parent 08686e0fd6
commit a11da1b3d8
7 changed files with 284 additions and 163 deletions

View File

@@ -64,22 +64,24 @@
#define USE_GRAVITY 0 // 0/1 use gravity (LED strip going up wall)
#define BEND_POINT 550 // 0/1000 point at which the LED strip goes up the wall
// Players settings
#define MAX_PLAYERS 4
// this can become a setting in the future
#define NUM_PLAYERS 2
// GAME
long previousMillis = 0; // Time of the last redraw
int levelNumber = 0;
#define TIMEOUT 60000 // time until screen saver in milliseconds
int joystickTilt = 0; // Stores the angle of the joystick
int joystickWobble = 0; // Stores the max amount of acceleration (wobble)
int joystickTilt[NUM_PLAYERS] = {0}; // Stores the angle of the joystick
int joystickWobble[NUM_PLAYERS] = {0}; // Stores the max amount of acceleration (wobble)
// WOBBLE ATTACK
#define DEFAULT_ATTACK_WIDTH 70 // Width of the wobble attack, world is 1000 wide
int attack_width = DEFAULT_ATTACK_WIDTH;
#define ATTACK_DURATION 1000 // Duration of a wobble attack (ms)
//long attackMillis = 0; // Time the attack started
//bool attacking = 0; // Is the attack in progress?
//bool canAttackAgain = 0; // Have I finished my previous attack?
#define BOSS_WIDTH 40
// TODO all animation durations should be defined rather than literals
@@ -137,8 +139,10 @@ Conveyor conveyorPool[CONVEYOR_COUNT] = {
Boss boss = Boss();
int pcolor[3] = {0,255,0};
Player player = Player(pcolor, LIVES_PER_LEVEL, ATTACK_DURATION);
Player player[NUM_PLAYERS] = {
Player(0, LIVES_PER_LEVEL, ATTACK_DURATION),
Player(1, LIVES_PER_LEVEL, ATTACK_DURATION)
};
enum stages {
STARTUP,
@@ -151,7 +155,6 @@ enum stages {
} stage;
long stageStartTime; // Stores the time the stage changed for stages that are time based
int playerPositionModifier; // +/- adjustment to player position
long killTime;
bool lastLevel = false;
@@ -233,17 +236,29 @@ void setup() {
sound_init(DAC_AUDIO_PIN);
#ifndef USE_GYRO_JOYSTICK
pinMode(upButtonPinNumber, INPUT_PULLUP);
pinMode(downButtonPinNumber, INPUT_PULLUP);
pinMode(leftButtonPinNumber, INPUT_PULLUP);
pinMode(rightButtonPinNumber, INPUT_PULLUP);
pinMode(upButtonPinNumber1, INPUT_PULLUP);
pinMode(downButtonPinNumber1, INPUT_PULLUP);
pinMode(leftButtonPinNumber1, INPUT_PULLUP);
//pinMode(rightButtonPinNumber, INPUT_PULLUP);
pinMode(upButtonPinNumber2, INPUT_PULLUP);
pinMode(downButtonPinNumber2, INPUT_PULLUP);
pinMode(leftButtonPinNumber2, INPUT_PULLUP);
//pinMode(rightButtonPinNumber, INPUT_PULLUP);
#endif
ap_setup();
stage = STARTUP;
stageStartTime = millis();
player.Lives ( user_settings.lives_per_level );
// set players color
player[0].setColor(0,255,0);
player[1].setColor(127,2,255);
for (Player& p : player)
p.Lives ( 2 );
//p.Lives ( user_settings.lives_per_level );
}
void loop() {
@@ -254,10 +269,10 @@ void loop() {
checkSerialInput();
if(stage == PLAY){
if(player.attacking){
if(player[0].attacking){
SFXattacking();
}else{
SFXtilt(joystickTilt);
SFXtilt(joystickTilt[0]);
}
}else if(stage == DEAD){
SFXdead();
@@ -266,13 +281,18 @@ void loop() {
if (mm - previousMillis >= MIN_REDRAW_INTERVAL) {
getInput();
long frameTimer = mm;
previousMillis = mm;
if((abs(joystickTilt) > user_settings.joystick_deadzone) ||
(abs(joystickWobble) >= user_settings.attack_threshold))
// check activity of any player
bool isMoving = false;
for (int i=0; i < NUM_PLAYERS; i++) {
isMoving = isMoving &&
(abs(joystickTilt[i]) > user_settings.joystick_deadzone) ||
(abs(joystickWobble[i]) >= user_settings.attack_threshold);
}
if(isMoving)
{
lastInputTime = mm;
if(stage == SCREENSAVER){
@@ -286,8 +306,6 @@ void loop() {
}
}
if(stage == SCREENSAVER){
screenSaverTick();
}else if(stage == STARTUP){
@@ -303,53 +321,64 @@ void loop() {
}else if(stage == PLAY){
// PLAYING
if (joystickWobble >= user_settings.attack_threshold) player.startAttack(mm);
for (Player& p : player) {
if (p.captured) continue;
player.updateState(
abs(joystickTilt) > user_settings.joystick_deadzone,
abs(joystickWobble) >= user_settings.attack_threshold,
mm);
if (joystickWobble[p.Id()] >= user_settings.attack_threshold) p.startAttack(mm);
// If still not attacking, move!
player.moveby(playerPositionModifier);
if(!player.attacking){
SFXtilt(joystickTilt);
//int moveAmount = (joystickTilt/6.0); // 6.0 is ideal at 16ms interval (6.0 / (16.0 / MIN_REDRAW_INTERVAL))
int moveAmount = (joystickTilt/(6.0)); // 6.0 is ideal at 16ms interval
if(DIRECTION) moveAmount = -moveAmount;
moveAmount = constrain(moveAmount, -MAX_PLAYER_SPEED, MAX_PLAYER_SPEED);
p.updateState(
abs(joystickTilt[p.Id()]) > user_settings.joystick_deadzone,
abs(joystickWobble[p.Id()]) >= user_settings.attack_threshold,
mm);
player.moveby( -moveAmount );
// If still not attacking, move!
p.moveby(p.speed);
if(!p.attacking){
SFXtilt(joystickTilt[p.Id()]);
//int moveAmount = (joystickTilt/6.0); // 6.0 is ideal at 16ms interval (6.0 / (16.0 / MIN_REDRAW_INTERVAL))
int moveAmount = (joystickTilt[p.Id()]/(6.0)); // 6.0 is ideal at 16ms interval
if(DIRECTION) moveAmount = -moveAmount;
moveAmount = constrain(moveAmount, -MAX_PLAYER_SPEED, MAX_PLAYER_SPEED);
// stop player from leaving if boss is alive
if (boss.Alive() && player.position >= VIRTUAL_LED_COUNT) // move player back
player.moveto( 999 ); //(user_settings.led_count - 1) * (1000.0/user_settings.led_count);
p.moveby( -moveAmount );
if(player.position >= VIRTUAL_LED_COUNT && !boss.Alive()) {
// Reached exit!
levelComplete();
return;
// stop player from leaving if boss is alive
if (boss.Alive() && p.position >= VIRTUAL_LED_COUNT) // move player back
p.moveto( 999 ); //(user_settings.led_count - 1) * (1000.0/user_settings.led_count);
if(p.position >= VIRTUAL_LED_COUNT && !boss.Alive()) {
// Reached exit!
levelComplete();
return;
}
}
}
if(inLava(player.position)){
die(player);
if(inLava(p.position)){
die(p);
}
}
// Ticks and draw calls
FastLED.clear();
tickConveyors();
for (Player& p : player)
tickConveyors(p);
tickSpawners();
tickBoss();
tickLava();
tickEnemies();
drawPlayer(player);
drawAttack(player);
for (Player& p : player)
if (!p.captured) tickEnemies(p);
for (const Player& p : player) {
if (p.Alive()) {
drawPlayer(p);
drawAttack(p);
}
}
drawExit();
}else if(stage == DEAD){
// DEAD
FastLED.clear();
tickDie(player, mm);
tickDie(player[0], mm); // TODO: fix me! Add whoDied function!
if(!tickParticles()){
loadLevel();
}
@@ -369,7 +398,8 @@ void loop() {
// restart from the beginning
stage = STARTUP;
stageStartTime = millis();
player.Lives ( user_settings.lives_per_level );
for (Player& p : player)
p.Lives ( user_settings.lives_per_level );
}
}
//FastLED.show();
@@ -389,7 +419,8 @@ void loadLevel(){
/// Defaults...OK to change the following items in the levels below
attack_width = DEFAULT_ATTACK_WIDTH;
player.moveto( 0 );
for (Player& p : player)
p.Spawn( 0 + p.Id()*10 ); // slightly displace players
/* ==== Level Editing Guide ===============
Level creation is done by adding to or editing the switch statement below
@@ -442,7 +473,7 @@ void loadLevel(){
===== Other things you can adjust per level ================
Player Start position:
player.moveto( xxx );
player.Spawn( xxx );
The size of the TWANG attack
@@ -452,7 +483,8 @@ void loadLevel(){
*/
switch(levelNumber){
case 0: // basic introduction
player.moveto( 200 );
for (Player& p : player)
p.Spawn( 200 + p.Id()*10 );
spawnEnemy(1, 0, 0, 0);
break;
case 1:
@@ -499,7 +531,8 @@ void loadLevel(){
break;
case 8:
// lava moving up
player.moveto( 200 );
for (Player& p : player)
p.Spawn( 200 + p.Id()*10 );
spawnLava(10, 180, 2000, 2000, 0, Lava::OFF, 0, 0.5);
spawnEnemy(350, 0, 1, 0);
spawnPool[0].Spawn(1000, 5500, 3, 0, 0);
@@ -601,7 +634,9 @@ void spawnEnemy(int pos, int dir, int speed, int wobble){
for(int e = 0; e<ENEMY_COUNT; e++){ // look for one that is not alive for a place to add one
if(!enemyPool[e].Alive()){
enemyPool[e].Spawn(pos, dir, speed, wobble);
enemyPool[e].playerSide = pos > player.position?1:-1;
// for each player, save if it is above or below
for (const Player& p : player)
enemyPool[e].playersSide[p.Id()] = pos > p.position?1:-1;
return;
}
}
@@ -653,21 +688,26 @@ void levelComplete(){
}
if (levelNumber != 0) // no points for the first level
{
score = score + (player.Lives() * 10); //
score = score + (player[0].Lives() * 10); //
}
}
void nextLevel(){
levelNumber ++;
if(lastLevel) {
stage = STARTUP;
stageStartTime = millis();
player.Lives ( user_settings.lives_per_level );
for (Player& p : player)
p.Lives ( user_settings.lives_per_level );
}
else {
player.Lives ( user_settings.lives_per_level );
for (Player& p : player) {
if (!p.Alive()) p.Lives ( user_settings.lives_per_level );
}
loadLevel();
}
}
@@ -678,16 +718,30 @@ void gameOver(){
loadLevel();
}
void die(Player p){
void die(Player& p){
if(levelNumber > 0)
p.Kill();
//if(levelNumber > 0) // This messes up everything in 2-players mode, but can be restored...
p.Kill();
if(!player.Alive()){
// how many players are still playing the level?
int stillPlaying = 0;
// how many players have lives?
bool allDead = true;
for (const Player& pp : player) {
stillPlaying += !pp.captured;
allDead &= !pp.Alive();
}
// someone is still playing the level, continue (an animation for the dead player should be added)
if(stillPlaying>0) return;
// all players have been captured, do any of them have lives left?
// This has already been checked in allDead above.
if( allDead ){
stage = GAMEOVER;
stageStartTime = millis();
}
else
else // some still have lifes to live, go to DEAD stage and repat the level
{
for(int ip = 0; ip < PARTICLE_COUNT; ip++){
particlePool[ip].Spawn(p.position);
@@ -740,13 +794,13 @@ void tickStartup(long mm)
}
void tickEnemies(){
void tickEnemies(Player& p){
for(int i = 0; i<ENEMY_COUNT; i++){
if(enemyPool[i].Alive()){
enemyPool[i].Tick();
// Hit attack?
if(player.attacking){
if(enemyPool[i]._pos > player.position-(attack_width/2) && enemyPool[i]._pos < player.position+(attack_width/2)){
if(p.attacking){
if(enemyPool[i]._pos > p.position-(attack_width/2) && enemyPool[i]._pos < p.position+(attack_width/2)){
enemyPool[i].Kill();
SFXkill();
}
@@ -761,10 +815,12 @@ void tickEnemies(){
}
// Hit player?
if(
(enemyPool[i].playerSide == 1 && enemyPool[i]._pos <= player.position) ||
(enemyPool[i].playerSide == -1 && enemyPool[i]._pos >= player.position)
(enemyPool[i].playersSide[p.Id()] == 1 && enemyPool[i]._pos <= p.position) ||
(enemyPool[i].playersSide[p.Id()] == -1 && enemyPool[i]._pos >= p.position)
){
die(player);
Serial.println("Killing player\n");
Serial.println(p.Id());
die(p);
return;
}
}
@@ -779,30 +835,36 @@ void tickBoss(){
leds[i] = CRGB::DarkRed;
leds[i] %= 100;
}
// CHECK COLLISION
if(getLED(player.position) > getLED(boss._pos - BOSS_WIDTH/2) && getLED(player.position) < getLED(boss._pos + BOSS_WIDTH)){
die(player);
return;
}
// CHECK FOR ATTACK
if(player.attacking){
if(
(getLED(player.position+(attack_width/2)) >= getLED(boss._pos - BOSS_WIDTH/2) && getLED(player.position+(attack_width/2)) <= getLED(boss._pos + BOSS_WIDTH/2)) ||
(getLED(player.position-(attack_width/2)) <= getLED(boss._pos + BOSS_WIDTH/2) && getLED(player.position-(attack_width/2)) >= getLED(boss._pos - BOSS_WIDTH/2))
){
boss.Hit();
if(boss.Alive()){
moveBoss();
}else{
spawnPool[0].Kill();
spawnPool[1].Kill();
for (Player& p : player)
{
// CHECK COLLISION
if(getLED(p.position) > getLED(boss._pos - BOSS_WIDTH/2) && getLED(p.position) < getLED(boss._pos + BOSS_WIDTH)){
die(p);
return;
}
// CHECK FOR ATTACK
if(p.attacking){
if(
(getLED(p.position+(attack_width/2)) >= getLED(boss._pos - BOSS_WIDTH/2) && getLED(p.position+(attack_width/2)) <= getLED(boss._pos + BOSS_WIDTH/2)) ||
(getLED(p.position-(attack_width/2)) <= getLED(boss._pos + BOSS_WIDTH/2) && getLED(p.position-(attack_width/2)) >= getLED(boss._pos - BOSS_WIDTH/2))
){
boss.Hit();
if(boss.Alive()){
moveBoss();
}else{
spawnPool[0].Kill();
spawnPool[1].Kill();
}
}
}
}
}
}
void drawPlayer(Player p){
void drawPlayer(const Player& p){
if (p.captured) return;
if (!p.Alive()) return;
leds[getLED(p.position)] = CRGB(p.color[0], p.color[1], p.color[2]);
}
@@ -882,13 +944,13 @@ bool tickParticles(){
return stillActive;
}
void tickConveyors(){
void tickConveyors(Player& p){
//TODO should the visual speed be proportional to the conveyor speed?
int b, speed, n, i, ss, ee, led;
long m = 10000+millis();
playerPositionModifier = 0;
p.speed = 0;
int levels = 5; // brightness levels in conveyor
@@ -909,8 +971,8 @@ void tickConveyors(){
leds[led] = CRGB(0, 0, b);
}
if(player.position > conveyorPool[i]._startPoint && player.position < conveyorPool[i]._endPoint){
playerPositionModifier = speed;
if(p.position > conveyorPool[i]._startPoint && p.position < conveyorPool[i]._endPoint){
p.speed = speed;
}
}
}
@@ -971,7 +1033,7 @@ void tickBossKilled(long mm) // boss funeral
}
}
void tickDie(Player p, long mm) { // a short bright explosion...particles persist after it.
void tickDie(const Player& p, long mm) { // a short bright explosion...particles persist after it.
const int duration = 200; // milliseconds
const int width = 20; // half width of the explosion
@@ -1000,13 +1062,13 @@ void tickGameover(long mm) {
if(stageStartTime+GAMEOVER_SPREAD_DURATION > mm) // Spread red from player position to top and bottom
{
// fill to top
int n = _max(map(((mm-stageStartTime)), 0, GAMEOVER_SPREAD_DURATION, getLED(player.position), user_settings.led_count), 0);
for(int i = getLED(player.position); i<= n; i++){
int n = _max(map(((mm-stageStartTime)), 0, GAMEOVER_SPREAD_DURATION, getLED(player[0].position), user_settings.led_count), 0);
for(int i = getLED(player[0].position); i<= n; i++){
leds[i] = CRGB(255, 0, 0);
}
// fill to bottom
n = _max(map(((mm-stageStartTime)), 0, GAMEOVER_SPREAD_DURATION, getLED(player.position), 0), 0);
for(int i = getLED(player.position); i>= n; i--){
n = _max(map(((mm-stageStartTime)), 0, GAMEOVER_SPREAD_DURATION, getLED(player[0].position), 0), 0);
for(int i = getLED(player[0].position); i>= n; i--){
leds[i] = CRGB(255, 0, 0);
}
SFXgameover();
@@ -1053,29 +1115,32 @@ void tickWin(long mm) {
void drawLives()
{
// show how many lives are left by drawing a short line of green leds for each life
SFXcomplete(); // stop any sounds
FastLED.clear();
// show how many lives are left by drawing a short line of green leds for each life
SFXcomplete(); // stop any sounds
FastLED.clear();
int pos = 0;
for (int i = 0; i < player.Lives(); i++)
{
for (int j=0; j<4; j++)
{
leds[pos++] = CRGB(0, 255, 0);
for (const Player& p : player) {
int pos = 0;
for (int i = 0; i < p.Lives(); i++)
{
for (int j=0; j<4; j++)
{
leds[pos++] = CRGB(p.color[0], p.color[1], p.color[2]);
FastLED.show();
}
leds[pos++] = CRGB(0, 0, 0);
delay(20);
}
FastLED.show();
}
leds[pos++] = CRGB(0, 0, 0);
delay(20);
}
FastLED.show();
delay(400);
FastLED.clear();
delay(400);
FastLED.clear();
}
}
void drawAttack(Player p){
void drawAttack(const Player& p){
if(p.captured) return;
if(!p.attacking) return;
int n = map(millis() - p.last_attack, 0, ATTACK_DURATION, 100, 5);
for(int i = getLED(p.position-(attack_width/2))+1; i<=getLED(p.position+(attack_width/2))-1; i++){
@@ -1086,7 +1151,7 @@ void drawAttack(Player p){
leds[getLED(p.position)] = CRGB(255, 255, 255);
}else{
n = 0;
leds[getLED(p.position)] = CRGB(0, 255, 0);
leds[getLED(p.position)] = CRGB(p.color[0], p.color[1], p.color[2]);
}
leds[getLED(p.position-(attack_width/2))] = CRGB(n, n, 255);
leds[getLED(p.position+(attack_width/2))] = CRGB(n, n, 255);
@@ -1202,22 +1267,45 @@ void getInput() {
// and any value to joystickWobble that is greater than ATTACK_THRESHOLD (defined at start)
// For example you could use 3 momentary buttons:
bool up = digitalRead(upButtonPinNumber) == LOW;
bool down = digitalRead(downButtonPinNumber) == LOW;
bool left = digitalRead(leftButtonPinNumber) == LOW;
bool right = digitalRead(rightButtonPinNumber) == LOW;
bool up = digitalRead(upButtonPinNumber1) == LOW;
bool down = digitalRead(downButtonPinNumber1) == LOW;
bool left = digitalRead(leftButtonPinNumber1) == LOW;
//bool right = digitalRead(rightButtonPinNumber) == LOW;
bool right = left;
joystickTilt = 0;
joystickTilt[0] = 0;
if (up) {
joystickTilt = 90 ;
joystickTilt[0] = 90 ;
} else if (down) {
joystickTilt = -90;
joystickTilt[0] = -90;
}
if (left || right) {
joystickWobble = user_settings.attack_threshold;
joystickWobble[0] = user_settings.attack_threshold;
} else {
joystickWobble = 0;
joystickWobble[0] = 0;
}
up = digitalRead(upButtonPinNumber2) == LOW;
down = digitalRead(downButtonPinNumber2) == LOW;
left = digitalRead(leftButtonPinNumber2) == LOW;
//bool right = digitalRead(rightButtonPinNumber) == LOW;
right = left;
joystickTilt[1] = 0;
if (up) {
joystickTilt[1] = 90 ;
} else if (down) {
joystickTilt[1] = -90;
}
if (left || right) {
joystickWobble[1] = user_settings.attack_threshold;
} else {
joystickWobble[1] = 0;
}
}
@@ -1286,9 +1374,10 @@ void SFXtilt(int amount){
}
int f = map(abs(amount), 0, 90, 80, 900)+random8(100);
if(playerPositionModifier < 0) f -= 500;
if(playerPositionModifier > 0) f += 200;
int vol = map(abs(amount), 0, 90, user_settings.audio_volume / 2, user_settings.audio_volume * 3/4);
// TODO, how to modify for multiplayer?
//if(playerPositionModifier < 0) f -= 500;
//if(playerPositionModifier > 0) f += 200;
int vol = map(abs(amount), 0, 90, user_settings.audio_volume / 2, user_settings.audio_volume * 3/4);
sound(f,vol);
}
void SFXattacking(){