Quantcast
Channel: Planet Python
Viewing all articles
Browse latest Browse all 22462

Vladimir Iakolev: Writing breakout clone with micropython

$
0
0

I have pyboard, OLED display (SSD1306) and joystick (Keyes_SJoys), so I decided to try to make breakout clone. First of all I decided to create something like a little framework, that will be a bit similar to React, and all game can be formalized just in two functions:

  • (state) → new-state– controller that updates state;
  • (state) → primitives– view that converts state to generator of primitives.

For example, code that draws chess cells and invert it on click, will be like:

fromlib.ssd1306importDisplayfromlib.keyesimportJoystickfromlib.engineimportGame,rectangle,textdefis_filled(x,y,inverted):fill=(x+y)%40==0ifinverted:returnnotfillelse:returnfilldefview(state):# Display data available in state['display']forxinrange(0,state['display']['width'],20):foryinrange(0,state['display']['height'],20):# rectangle is bundled view that yields pointsyieldfromrectangle(x=x,y=y,w=20,h=20,
fill=is_filled(x,y,state['inverted']))


defcontroller(state):
#Joystickdataavailableinstate['display']
ifstate['joystick']['clicked']:
returndict(state,inverted=notstate['inverted'])
else:
returnstateinitial_state={'inverted':False}
chess_deck=Game(display=Display(pinout={'sda':'Y10',
'scl':'Y9'},
height=64,
external_vcc=False),
joystick=Joystick('X1','X2','X3'),
initial_state=initial_state,
view=view,
controller=controller)

if__name__=='__main__':
chess_deck.run()

In action:

Chess photo

From the code you can see, that views can be easily nested with yield from. So if we want to move cell to separate view:

defcell(x,y,inverted):yieldfromrectangle(x=x,y=y,w=20,h=20,
fill=is_filled(x,y,inverted))


defview(state):
forxinrange(0,state['display']['width'],20):
foryinrange(0,state['display']['height'],20):
yieldfromcell(x,y,state['inverted'])

And another nice thing about this approach, is that because of generators we consume not a lot of memory, if we’ll make it eager, we’ll fail with MemoryError: memory allocation failed soon.

Back to breakout, let’s start with views, first of all implement splash screen:

defsplash(w,h):forninrange(0,w,20):yieldfromrectangle(x=n,y=0,w=10,h=h,fill=True)

yieldfromrectangle(x=0,y=17,w=w,h=30,fill=False)
#textisbundledviewyieldfromtext(x=0,y=20,string='BREAKOUT',size=3)


defview(state):
yieldfromsplash(state['display']['width'],
state['display']['height'])

It will draw a nice splash screen:

Splash screen

On splash screen game should be started when user press joystick, so we should update code a bit:

GAME_NOT_STARTED=0GAME_ACTIVE=1GAME_OVER=2defcontroller(state):ifstate['status']==GAME_NOT_STARTEDandstate['joystick']['clicked']:state['status']=GAME_ACTIVEreturnstateinitial_state={'status':GAME_NOT_STARTED}

Now when joystick is pressed, game changes status to GAME_ACTIVE. And now it’s time to create view for game screen:

BRICK_W=8BRICK_H=4BRICK_BORDER=1BRICK_ROWS=4PADDLE_W=16PADDLE_H=4BALL_W=3BALL_H=3defbrick(data):yieldfromrectangle(x=data['x']+BRICK_BORDER,
y=data['y']+BRICK_BORDER,
w=BRICK_W-BRICK_BORDER,
h=BRICK_H-BRICK_BORDER,
fill=True)


defpaddle(data):
yieldfromrectangle(x=data['x'],y=data['y'],
w=PADDLE_W,h=PADDLE_H,
fill=True)


defball(data):
yieldfromrectangle(x=data['x'],y=data['y'],
w=BALL_W,h=BALL_H,fill=True)


defdeck(state):
forbrick_datainstate['bricks']:
yieldfrombrick(brick_data)

yieldfrompaddle(state['paddle'])
yieldfromball(state['ball'])


defview(state):
ifstate['status']==GAME_NOT_STARTED:
yieldfromsplash(state['display']['width'],
state['display']['height'])
else:
yieldfromdeck(state)


defget_initial_game_state(state):
state['status']=GAME_ACTIVEstate['bricks']=[{'x':x,'y':yn*BRICK_H}
forxinrange(0,state['display']['width'],BRICK_W)
foryninrange(BRICK_ROWS)]
state['paddle']={'x':(state['display']['width']-PADDLE_W)/2,
'y':state['display']['height']-PADDLE_H}
state['ball']={'x':(state['display']['width']-BALL_W)/2,
'y':state['display']['height']-PADDLE_H*2-BALL_W}
returnstatedefcontroller(state):
ifstate['status']==GAME_NOT_STARTEDandstate['joystick']['clicked']:
state=get_initial_game_state(state)
returnstate

Game screen

And last part of views – game over screen:

defgame_over():yieldfromtext(x=0,y=20,string='GAMEOVER',size=3)


defview(state):
ifstate['status']==GAME_NOT_STARTED:
yieldfromsplash(state['display']['width'],
state['display']['height'])
else:
yieldfromdeck(state)
ifstate['status']==GAME_OVER:
yieldfromgame_over()

Game screen

So we ended up with views, now we should add ability to move paddle with joystick:

defupdate_paddle(paddle,joystick,w):paddle['x']+=int(joystick['x']/10)ifpaddle['x']<0:paddle['x']=0elifpaddle['x']>(w-PADDLE_W):paddle['x']=w-PADDLE_Wreturnpaddledefcontroller(state):ifstate['status']==GAME_NOT_STARTEDandstate['joystick']['clicked']:state=get_initial_game_state(state)elifstate['status']==GAME_ACTIVE:state['paddle']=update_paddle(state['paddle'],state['joystick'],state['display']['width'])returnstate

Never mind performance, it’ll be fixed in the end of the article:

Now it’s time for teh hardest thing – moving and bouncing ball, so there’s no real physics, for simplification ball movements will be represented as vx and vy, so when ball:

  • initialised: vx = rand(SPEED), vy = √SPEED^2 - vx^2;
  • hits the top wall: vy = -vy;
  • hits the left or right wall: vx = -vx:
  • hits the brick or paddle: vx = vx + (0.5 - intersection) * SPEED, where intersection is between 0 and 1; vy = √SPEED^2 - vx^2.

And I implemented something like this with a few hacks:

BALL_SPEED=6BALL_SPEED_BORDER=0.5defget_initial_game_state(state):state['status']=GAME_ACTIVEstate['bricks']=[{'x':x,'y':yn*BRICK_H}forxinrange(0,state['display']['width'],BRICK_W)foryninrange(BRICK_ROWS)]state['paddle']={'x':(state['display']['width']-PADDLE_W)/2,'y':state['display']['height']-PADDLE_H}# Initial velocity for ball:ball_vx=BALL_SPEED_BORDER+pyb.rng()%(BALL_SPEED-BALL_SPEED_BORDER)ball_vy=-math.sqrt(BALL_SPEED**2-ball_vx**2)state['ball']={'x':(state['display']['width']-BALL_W)/2,'y':state['display']['height']-PADDLE_H*2-BALL_W,'vx':ball_vx,'vy':ball_vy}returnstatedefcalculate_velocity(ball,item_x,item_w):"""Calculates velocity for collision."""intersection=(item_x+item_w-ball['x'])/item_wvx=ball['vx']+BALL_SPEED*(0.5-intersection)ifvx>BALL_SPEED-BALL_SPEED_BORDER:vx=BALL_SPEED-BALL_SPEED_BORDERelifvx<BALL_SPEED_BORDER-BALL_SPEED:vx=BALL_SPEED_BORDER-BALL_SPEEDvy=math.sqrt(BALL_SPEED**2-vx**2)ifball['vy']>0:vy=-vyreturnvx,vydefcollide(ball,item,item_w,item_h):returnitem['x']-BALL_W<ball['x']<item['x']+item_w \
           anditem['y']-BALL_H<ball['y']<item['y']+item_hdefupdate_ball(state):state['ball']['x']+=state['ball']['vx']state['ball']['y']+=state['ball']['vy']# Collide with left/right wallifstate['ball']['x']<=0orstate['ball']['x']>=state['display']['width']:state['ball']['vx']=-state['ball']['vx']# Collide with top wallifstate['ball']['y']<=0:state['ball']['vy']=-state['ball']['vy']# Collide with paddleifcollide(state['ball'],state['paddle'],PADDLE_W,PADDLE_H):vx,vy=calculate_velocity(state['ball'],state['paddle']['x'],PADDLE_W)state['ball'].update(vx=vx,vy=vy)# Collide with brickforn,brickinenumerate(state['bricks']):ifcollide(state['ball'],brick,BRICK_W,BRICK_H):vx,vy=calculate_velocity(state['ball'],brick['x'],BRICK_W)state['ball'].update(vx=vx,vy=vy)state['bricks'].pop(n)returnstatedefcontroller(state):ifstate['status']==GAME_NOT_STARTEDandstate['joystick']['clicked']:state=get_initial_game_state(state)elifstate['status']==GAME_ACTIVE:state['paddle']=update_paddle(state['paddle'],state['joystick'],state['display']['width'])state=update_ball(state)returnstate

And it seems to be wroking:

So now the last part, we should show “Game Over” when ball hits the bottom wall or when all bricks destroyed, and then start game again if user clicks joystick:

defis_game_over(state):returnnotstate['bricks']orstate['ball']['y']>state['display']['height']defcontroller(state):ifstate['status']in(GAME_NOT_STARTED,GAME_OVER)\
            andstate['joystick']['clicked']:state=get_initial_game_state(state)elifstate['status']==GAME_ACTIVE:state['paddle']=update_paddle(state['paddle'],state['joystick'],state['display']['width'])state=update_ball(state)ifis_game_over(state):state['status']=GAME_OVERreturnstate

And it works too:

Performance so bad because drawing pixel on the screen is relatively time consuming operation, and we can easily fix performance by just decreasing count of bricks:

BRICK_W=12BRICK_H=6BRICK_BORDER=4BRICK_ROWS=3

And now it’s smooth:

Source code.


Viewing all articles
Browse latest Browse all 22462

Trending Articles



<script src="https://jsc.adskeeper.com/r/s/rssing.com.1596347.js" async> </script>