The best way to learn is to do, and sometimes what you are doing isn't quite right for what you want to learn. In these cases, personal projects are what we need, set yourself a goal to create something that will force you to learn. It keeps your dev skills fresh and can add some fun to your day.
I found making games in Power Apps a great learning project, as by its very nature I'm having to be creative as Power Apps is not made for making games.
I've already done one blog on games here flappy-app and another on making different calculators power-app-calculators if you want to see more.
So what's todays project, well I lost my internet a few days back and I was reminded of the Google Chromes Dinasaur running game, and I was inspired for a Appy Dino
What I liked was all the out the box thinking I would have to do to get a square peg (game) into a round hole (Power Apps), I ended up over coming 4 main challenges:
- Looping Animation
- Moving Animation
- Random Component Generation
- Collision
Looping Animation
There were 2 areas I wanted to animate, the Dinasaur sprite and the back ground moving.
For the sprite I loaded 6 different frames
and then I looked at 2 different approaches
Save each image in an array:
ClearCollect(colSprites,['1','2','3','4','5','6']);
Set the sprite as an image component with a variable as the image
Then on a timer 'OnTimerEnd' I updated the counter variable and the set image variable to the next image in the collection (with a reset if at the end of the collection).
If(viIndex=6,
Set(viIndex,1);
,
Set(viIndex,viIndex+1);
);
Set(voSprite,Index(colSprites,viIndex).Value);
The second approach was more basic, I add all 6 images as their own image component:
Then I created an object that holds the visibility status of each image. Then on each 'OnTimerEnd' I use a switch to update the object with next image visibility
If(viIndex=6,
Set(viIndex,1);
,
Set(viIndex,viIndex+1);
);
Switch(viIndex,
1,Set(voVis,{one:true,two:false,three:false,four:false,five:false,six:false}),
2,Set(voVis,{one:false,two:true,three:false,four:false,five:false,six:false}),
3,Set(voVis,{one:false,two:false,three:true,four:false,five:false,six:false}),
4,Set(voVis,{one:false,two:false,three:false,four:true,five:false,six:false}),
5,Set(voVis,{one:false,two:false,three:false,four:false,five:true,six:false}),
6,Set(voVis,{one:false,two:false,three:false,four:false,five:false,six:true})
);
It seemed to work perfectly, with the only issue a blank frame appearing a few times when first starting (they work almost identically but the sprite approach seemed to show more blank frames).
The sprite approach is defiantly the more scalable and simple solution.
The next was the background, and I could have repeated the same solution as the sprite, but to do something else (and easier 😎) I went with an animated GIF.
The last challenge was how to stop the gif, in the end I decided to have a image in front of the gif, and fade it out.
This solution was so much easier than the sprite, it made me think I should go with a GIF for it as well. But the stop start wasn't as perfect as the sprite, so as long as there isn't many frames I would still go with sprite approach first.
2. Moving Animation
For the Dinosaur jump we need to animate the sprite up and then down.
First I set the height of the jump as a variable called viLeap, and a second variable viJump as the same as viLeap.
Then on the jump action we set the viJump to 0.
Finally on the timer that has a condition if viJump is not equal to viLeap add 20 to viJump. We also set the variable for the sprites Y to the ground level minus viJump.
If(viJump<viLeap,
Set(viJump,viJump+20);
Set(viSpriteY,viY-viJump);
);
This solves the up animation, next is the down animation. In the timer we have a condition that checks if the sprite is above the ground and viJump equals viLeap (up has stopped). In the condition we reverse and add 20 (Y =0 is top of the screen).
If(viSpriteY<>viY && viJump=viLeap,
Set(viSpriteY,viSpriteY+20);
);
So now the sprite goes up, stops and then goes down until it reaches the ground.
3. Random Component Generation
What do I mean by Random generator, well I want the obstacles to come on at random, but be able to increase the likely hood of them over time.
In my previous game (Flappy App) I had used a convayabelt approach:
but this was good for consistent distance and patterns, so not what I needed.
Next was to use a gallery, my idea was that on a loop I could remove the first item of a collection and add a new one at the end. The gallery would use the collection as its datasource, so each time the first item was delete it would force the gallery to move.
ClearCollect(colGalleryTest,[
{id:1,visability:true},
{id:2,visability:true},
{id:3,visability:true},
{id:4,visability:true},
{id:5,visability:true},
{id:6,visability:true},
{id:7,visability:true},
{id:8,visability:true},
{id:9,visability:true},
{id:10,visability:true},
{id:11,visability:true},
{id:12,visability:true}
]);
OnTimerEnd
Remove(colGalleryTest,First(colGalleryTest));
Collect(colGalleryTest,{id:Last(colGalleryTest).id+1});
Sadly as you can see it didn't work, with the gallery just updating in one frame, so no animation.
So the next approach was inspired by my convayabelt approach, I would use the same reset action, but instead of a consistent pattern I would randomly release an obstacle.
The odds on the random release needed to be increased over time, so the approach I went with was a collection.
The collection would have 4 records with obstacles, and 10 without. A random number would select the record by its index and if it was an obstacle, it would release it. The record included following fields:
- id - to make it easy to patch
- image - so we can have different obstacles
- x - so we can update its x and move it right to left
- active - if it's been selected and moving as we don't want to select it twice
- obs - is it an obstacle or without
- comp - the component so we know its size and dimensions for impact (I didn't know before we could save components to collections)
ClearCollect(colObstacles,[
{id:1,x:800,image:pbi,active:false, obs:true, comp:imObsticle_1},
{id:2,x:800,image:pbi,active:false, obs:true, comp:imObsticle_2},
{id:3,x:800,image:pbi,active:false, obs:true, comp:imObsticle_3},
{id:4,x:800,image:pbi,active:false, obs:true, comp:imObsticle_4},
{id:5,x:800,image:pbi,active:false, obs:false, comp:imObsticle_1},
{id:6,x:800,image:pbi,active:false, obs:false, comp:imObsticle_1},
{id:7,x:800,image:pbi,active:false, obs:false, comp:imObsticle_1},
{id:8,x:800,image:pbi,active:false, obs:false, comp:imObsticle_1},
{id:9,x:800,image:pbi,active:false, obs:false, comp:imObsticle_1},
{id:10,x:800,image:pbi,active:false, obs:false, comp:imObsticle_1},
{id:11,x:800,image:pbi,active:false, obs:false, comp:imObsticle_1},
{id:12,x:800,image:pbi,active:false, obs:false, comp:imObsticle_1},
{id:13,x:800,image:pbi,active:false, obs:false, comp:imObsticle_1},
{id:14,x:800,image:pbi,active:false, obs:false, comp:imObsticle_1}
]);
For the release we check if indexed record is not active and the gap is big enough (I didn't want multiple components together as the dinosaur may not be able to jump over all of them) then we patch it to active.
///random index
Set(viRandom,RandBetween(1,CountRows(colObstacles)));
///check if record is obsticle and we are not to close to last obsticle
If(Index(colObstacles,viRandom).obs && viGapCount=0,
Patch(colObsticles, {id:viRandom},{
active:true
}
);
Set(viGapCount,viGap);
);
The last step is for all active obstacles to move and to reset any that have gone off the screen, for that we use an UpdateIf
///all active obstacles move left 10 pixels
UpdateIf(colObstacles,active,{x:x-10});
//// all off the screen reset
UpdateIf(colObstacles,x<-200,{x:800,active:false});
Now the real value of the collection approach can be found at the level up stage. We can remove one of the non-obstacle records, therefore increasing the odds of a obstacle from 4/14 to 4/13 and so on.
4. Collision
The last time I did collision detection I had to write code checking every component with the sprite, I didn't want to do that again.
Luckily my new collection approach made everything easier.
The first condition is to check the sprite is below the height of the obstacles (its Y add its height), as if it's jumping its safe.
Next I filter the collection by its x checking if it's within the sprites area. We can then count rows, and if greater then 0 we have impact.
If((imSprite.Y+imSprite.Height)>viY,
If(CountRows(Filter(colObstacles,x+comp.Width>=(viAdjustSprite+viX) && x<=(viXsprite-viAdjustSprite)))>0,
Set(vbGameOver,true);
Set(viTransparency,0);
,
If(viTransparency<100,Set(viTransparency,viTransparency+10));
)
);
The viAdjustSprite variable is used to adjust the collision for whitespace around the images. As unless its a perfect square our collisions may be over aggressive
And that's it, there is the addition of scores and high score tables but I won't go into that as sure everyone can do it.
What I enjoyed was testing the limits of Power FX, and learning (I didn't know you could put components in collections before).
Link to Export: https://github.com/wyattdave/Power-Platform
Top comments (2)
so good..
Love it mate...