bastd.actor.controlsguide
Defines Actors related to controls guides.
1# Released under the MIT License. See LICENSE for details. 2# 3"""Defines Actors related to controls guides.""" 4 5from __future__ import annotations 6 7from typing import TYPE_CHECKING 8 9import ba 10import ba.internal 11 12if TYPE_CHECKING: 13 from typing import Any, Sequence 14 15 16class ControlsGuide(ba.Actor): 17 """A screen overlay of game controls. 18 19 category: Gameplay Classes 20 21 Shows button mappings based on what controllers are connected. 22 Handy to show at the start of a series or whenever there might 23 be newbies watching. 24 """ 25 26 def __init__( 27 self, 28 position: tuple[float, float] = (390.0, 120.0), 29 scale: float = 1.0, 30 delay: float = 0.0, 31 lifespan: float | None = None, 32 bright: bool = False, 33 ): 34 """Instantiate an overlay. 35 36 delay: is the time in seconds before the overlay fades in. 37 38 lifespan: if not None, the overlay will fade back out and die after 39 that long (in milliseconds). 40 41 bright: if True, brighter colors will be used; handy when showing 42 over gameplay but may be too bright for join-screens, etc. 43 """ 44 # pylint: disable=too-many-statements 45 # pylint: disable=too-many-locals 46 super().__init__() 47 show_title = True 48 scale *= 0.75 49 image_size = 90.0 * scale 50 offs = 74.0 * scale 51 offs5 = 43.0 * scale 52 ouya = False 53 maxw = 50 54 self._lifespan = lifespan 55 self._dead = False 56 self._bright = bright 57 self._cancel_timer: ba.Timer | None = None 58 self._fade_in_timer: ba.Timer | None = None 59 self._update_timer: ba.Timer | None = None 60 self._title_text: ba.Node | None 61 clr: Sequence[float] 62 extra_pos_1: tuple[float, float] | None 63 extra_pos_2: tuple[float, float] | None 64 if ba.app.iircade_mode: 65 xtweak = 0.2 66 ytweak = 0.2 67 jump_pos = ( 68 position[0] + offs * (-1.2 + xtweak), 69 position[1] + offs * (0.1 + ytweak), 70 ) 71 bomb_pos = ( 72 position[0] + offs * (0.0 + xtweak), 73 position[1] + offs * (0.5 + ytweak), 74 ) 75 punch_pos = ( 76 position[0] + offs * (1.2 + xtweak), 77 position[1] + offs * (0.5 + ytweak), 78 ) 79 80 pickup_pos = ( 81 position[0] + offs * (-1.4 + xtweak), 82 position[1] + offs * (-1.2 + ytweak), 83 ) 84 extra_pos_1 = ( 85 position[0] + offs * (-0.2 + xtweak), 86 position[1] + offs * (-0.8 + ytweak), 87 ) 88 extra_pos_2 = ( 89 position[0] + offs * (1.0 + xtweak), 90 position[1] + offs * (-0.8 + ytweak), 91 ) 92 self._force_hide_button_names = True 93 else: 94 punch_pos = (position[0] - offs * 1.1, position[1]) 95 jump_pos = (position[0], position[1] - offs) 96 bomb_pos = (position[0] + offs * 1.1, position[1]) 97 pickup_pos = (position[0], position[1] + offs) 98 extra_pos_1 = None 99 extra_pos_2 = None 100 self._force_hide_button_names = False 101 102 if show_title: 103 self._title_text_pos_top = ( 104 position[0], 105 position[1] + 139.0 * scale, 106 ) 107 self._title_text_pos_bottom = ( 108 position[0], 109 position[1] + 139.0 * scale, 110 ) 111 clr = (1, 1, 1) if bright else (0.7, 0.7, 0.7) 112 tval = ba.Lstr( 113 value='${A}:', subs=[('${A}', ba.Lstr(resource='controlsText'))] 114 ) 115 self._title_text = ba.newnode( 116 'text', 117 attrs={ 118 'text': tval, 119 'host_only': True, 120 'scale': 1.1 * scale, 121 'shadow': 0.5, 122 'flatness': 1.0, 123 'maxwidth': 480, 124 'v_align': 'center', 125 'h_align': 'center', 126 'color': clr, 127 }, 128 ) 129 else: 130 self._title_text = None 131 pos = jump_pos 132 clr = (0.4, 1, 0.4) 133 self._jump_image = ba.newnode( 134 'image', 135 attrs={ 136 'texture': ba.gettexture('buttonJump'), 137 'absolute_scale': True, 138 'host_only': True, 139 'vr_depth': 10, 140 'position': pos, 141 'scale': (image_size, image_size), 142 'color': clr, 143 }, 144 ) 145 self._jump_text = ba.newnode( 146 'text', 147 attrs={ 148 'v_align': 'top', 149 'h_align': 'center', 150 'scale': 1.5 * scale, 151 'flatness': 1.0, 152 'host_only': True, 153 'shadow': 1.0, 154 'maxwidth': maxw, 155 'position': (pos[0], pos[1] - offs5), 156 'color': clr, 157 }, 158 ) 159 clr = (0.2, 0.6, 1) if ouya else (1, 0.7, 0.3) 160 pos = punch_pos 161 self._punch_image = ba.newnode( 162 'image', 163 attrs={ 164 'texture': ba.gettexture('buttonPunch'), 165 'absolute_scale': True, 166 'host_only': True, 167 'vr_depth': 10, 168 'position': pos, 169 'scale': (image_size, image_size), 170 'color': clr, 171 }, 172 ) 173 self._punch_text = ba.newnode( 174 'text', 175 attrs={ 176 'v_align': 'top', 177 'h_align': 'center', 178 'scale': 1.5 * scale, 179 'flatness': 1.0, 180 'host_only': True, 181 'shadow': 1.0, 182 'maxwidth': maxw, 183 'position': (pos[0], pos[1] - offs5), 184 'color': clr, 185 }, 186 ) 187 pos = bomb_pos 188 clr = (1, 0.3, 0.3) 189 self._bomb_image = ba.newnode( 190 'image', 191 attrs={ 192 'texture': ba.gettexture('buttonBomb'), 193 'absolute_scale': True, 194 'host_only': True, 195 'vr_depth': 10, 196 'position': pos, 197 'scale': (image_size, image_size), 198 'color': clr, 199 }, 200 ) 201 self._bomb_text = ba.newnode( 202 'text', 203 attrs={ 204 'h_align': 'center', 205 'v_align': 'top', 206 'scale': 1.5 * scale, 207 'flatness': 1.0, 208 'host_only': True, 209 'shadow': 1.0, 210 'maxwidth': maxw, 211 'position': (pos[0], pos[1] - offs5), 212 'color': clr, 213 }, 214 ) 215 pos = pickup_pos 216 clr = (1, 0.8, 0.3) if ouya else (0.8, 0.5, 1) 217 self._pickup_image = ba.newnode( 218 'image', 219 attrs={ 220 'texture': ba.gettexture('buttonPickUp'), 221 'absolute_scale': True, 222 'host_only': True, 223 'vr_depth': 10, 224 'position': pos, 225 'scale': (image_size, image_size), 226 'color': clr, 227 }, 228 ) 229 self._pick_up_text = ba.newnode( 230 'text', 231 attrs={ 232 'v_align': 'top', 233 'h_align': 'center', 234 'scale': 1.5 * scale, 235 'flatness': 1.0, 236 'host_only': True, 237 'shadow': 1.0, 238 'maxwidth': maxw, 239 'position': (pos[0], pos[1] - offs5), 240 'color': clr, 241 }, 242 ) 243 clr = (0.9, 0.9, 2.0, 1.0) if bright else (0.8, 0.8, 2.0, 1.0) 244 self._run_text_pos_top = (position[0], position[1] - 135.0 * scale) 245 self._run_text_pos_bottom = (position[0], position[1] - 172.0 * scale) 246 sval = 1.0 * scale if ba.app.vr_mode else 0.8 * scale 247 self._run_text = ba.newnode( 248 'text', 249 attrs={ 250 'scale': sval, 251 'host_only': True, 252 'shadow': 1.0 if ba.app.vr_mode else 0.5, 253 'flatness': 1.0, 254 'maxwidth': 380, 255 'v_align': 'top', 256 'h_align': 'center', 257 'color': clr, 258 }, 259 ) 260 clr = (1, 1, 1) if bright else (0.7, 0.7, 0.7) 261 self._extra_text = ba.newnode( 262 'text', 263 attrs={ 264 'scale': 0.8 * scale, 265 'host_only': True, 266 'shadow': 0.5, 267 'flatness': 1.0, 268 'maxwidth': 380, 269 'v_align': 'top', 270 'h_align': 'center', 271 'color': clr, 272 }, 273 ) 274 275 if extra_pos_1 is not None: 276 self._extra_image_1: ba.Node | None = ba.newnode( 277 'image', 278 attrs={ 279 'texture': ba.gettexture('nub'), 280 'absolute_scale': True, 281 'host_only': True, 282 'vr_depth': 10, 283 'position': extra_pos_1, 284 'scale': (image_size, image_size), 285 'color': (0.5, 0.5, 0.5), 286 }, 287 ) 288 else: 289 self._extra_image_1 = None 290 if extra_pos_2 is not None: 291 self._extra_image_2: ba.Node | None = ba.newnode( 292 'image', 293 attrs={ 294 'texture': ba.gettexture('nub'), 295 'absolute_scale': True, 296 'host_only': True, 297 'vr_depth': 10, 298 'position': extra_pos_2, 299 'scale': (image_size, image_size), 300 'color': (0.5, 0.5, 0.5), 301 }, 302 ) 303 else: 304 self._extra_image_2 = None 305 306 self._nodes = [ 307 self._bomb_image, 308 self._bomb_text, 309 self._punch_image, 310 self._punch_text, 311 self._jump_image, 312 self._jump_text, 313 self._pickup_image, 314 self._pick_up_text, 315 self._run_text, 316 self._extra_text, 317 ] 318 if show_title: 319 assert self._title_text 320 self._nodes.append(self._title_text) 321 if self._extra_image_1 is not None: 322 self._nodes.append(self._extra_image_1) 323 if self._extra_image_2 is not None: 324 self._nodes.append(self._extra_image_2) 325 326 # Start everything invisible. 327 for node in self._nodes: 328 node.opacity = 0.0 329 330 # Don't do anything until our delay has passed. 331 ba.timer(delay, ba.WeakCall(self._start_updating)) 332 333 @staticmethod 334 def _meaningful_button_name(device: ba.InputDevice, button: int) -> str: 335 """Return a flattened string button name; empty for non-meaningful.""" 336 if not device.has_meaningful_button_names: 337 return '' 338 return device.get_button_name(button).evaluate() 339 340 def _start_updating(self) -> None: 341 342 # Ok, our delay has passed. Now lets periodically see if we can fade 343 # in (if a touch-screen is present we only want to show up if gamepads 344 # are connected, etc). 345 # Also set up a timer so if we haven't faded in by the end of our 346 # duration, abort. 347 if self._lifespan is not None: 348 self._cancel_timer = ba.Timer( 349 self._lifespan, 350 ba.WeakCall(self.handlemessage, ba.DieMessage(immediate=True)), 351 ) 352 self._fade_in_timer = ba.Timer( 353 1.0, ba.WeakCall(self._check_fade_in), repeat=True 354 ) 355 self._check_fade_in() # Do one check immediately. 356 357 def _check_fade_in(self) -> None: 358 from ba.internal import get_device_value 359 360 # If we have a touchscreen, we only fade in if we have a player with 361 # an input device that is *not* the touchscreen. 362 # (otherwise it is confusing to see the touchscreen buttons right 363 # next to our display buttons) 364 touchscreen: ba.InputDevice | None = ba.internal.getinputdevice( 365 'TouchScreen', '#1', doraise=False 366 ) 367 368 if touchscreen is not None: 369 # We look at the session's players; not the activity's. 370 # We want to get ones who are still in the process of 371 # selecting a character, etc. 372 input_devices = [ 373 p.inputdevice for p in ba.getsession().sessionplayers 374 ] 375 input_devices = [ 376 i for i in input_devices if i and i is not touchscreen 377 ] 378 fade_in = False 379 if input_devices: 380 # Only count this one if it has non-empty button names 381 # (filters out wiimotes, the remote-app, etc). 382 for device in input_devices: 383 for name in ( 384 'buttonPunch', 385 'buttonJump', 386 'buttonBomb', 387 'buttonPickUp', 388 ): 389 if ( 390 self._meaningful_button_name( 391 device, get_device_value(device, name) 392 ) 393 != '' 394 ): 395 fade_in = True 396 break 397 if fade_in: 398 break # No need to keep looking. 399 else: 400 # No touch-screen; fade in immediately. 401 fade_in = True 402 if fade_in: 403 self._cancel_timer = None # Didn't need this. 404 self._fade_in_timer = None # Done with this. 405 self._fade_in() 406 407 def _fade_in(self) -> None: 408 for node in self._nodes: 409 ba.animate(node, 'opacity', {0: 0.0, 2.0: 1.0}) 410 411 # If we were given a lifespan, transition out after it. 412 if self._lifespan is not None: 413 ba.timer( 414 self._lifespan, ba.WeakCall(self.handlemessage, ba.DieMessage()) 415 ) 416 self._update() 417 self._update_timer = ba.Timer( 418 1.0, ba.WeakCall(self._update), repeat=True 419 ) 420 421 def _update(self) -> None: 422 # pylint: disable=too-many-statements 423 # pylint: disable=too-many-branches 424 # pylint: disable=too-many-locals 425 from ba.internal import get_device_value, get_remote_app_name 426 427 if self._dead: 428 return 429 punch_button_names = set() 430 jump_button_names = set() 431 pickup_button_names = set() 432 bomb_button_names = set() 433 434 # We look at the session's players; not the activity's - we want to 435 # get ones who are still in the process of selecting a character, etc. 436 input_devices = [p.inputdevice for p in ba.getsession().sessionplayers] 437 input_devices = [i for i in input_devices if i] 438 439 # If there's no players with input devices yet, try to default to 440 # showing keyboard controls. 441 if not input_devices: 442 kbd = ba.internal.getinputdevice('Keyboard', '#1', doraise=False) 443 if kbd is not None: 444 input_devices.append(kbd) 445 446 # We word things specially if we have nothing but keyboards. 447 all_keyboards = input_devices and all( 448 i.name == 'Keyboard' for i in input_devices 449 ) 450 only_remote = len(input_devices) == 1 and all( 451 i.name == 'Amazon Fire TV Remote' for i in input_devices 452 ) 453 454 right_button_names = set() 455 left_button_names = set() 456 up_button_names = set() 457 down_button_names = set() 458 459 # For each player in the game with an input device, 460 # get the name of the button for each of these 4 actions. 461 # If any of them are uniform across all devices, display the name. 462 for device in input_devices: 463 # We only care about movement buttons in the case of keyboards. 464 if all_keyboards: 465 right_button_names.add( 466 device.get_button_name( 467 get_device_value(device, 'buttonRight') 468 ) 469 ) 470 left_button_names.add( 471 device.get_button_name( 472 get_device_value(device, 'buttonLeft') 473 ) 474 ) 475 down_button_names.add( 476 device.get_button_name( 477 get_device_value(device, 'buttonDown') 478 ) 479 ) 480 up_button_names.add( 481 device.get_button_name(get_device_value(device, 'buttonUp')) 482 ) 483 484 # Ignore empty values; things like the remote app or 485 # wiimotes can return these. 486 bname = self._meaningful_button_name( 487 device, get_device_value(device, 'buttonPunch') 488 ) 489 if bname != '': 490 punch_button_names.add(bname) 491 bname = self._meaningful_button_name( 492 device, get_device_value(device, 'buttonJump') 493 ) 494 if bname != '': 495 jump_button_names.add(bname) 496 bname = self._meaningful_button_name( 497 device, get_device_value(device, 'buttonBomb') 498 ) 499 if bname != '': 500 bomb_button_names.add(bname) 501 bname = self._meaningful_button_name( 502 device, get_device_value(device, 'buttonPickUp') 503 ) 504 if bname != '': 505 pickup_button_names.add(bname) 506 507 # If we have no values yet, we may want to throw out some sane 508 # defaults. 509 if all( 510 not lst 511 for lst in ( 512 punch_button_names, 513 jump_button_names, 514 bomb_button_names, 515 pickup_button_names, 516 ) 517 ): 518 # Otherwise on android show standard buttons. 519 if ba.app.platform == 'android': 520 punch_button_names.add('X') 521 jump_button_names.add('A') 522 bomb_button_names.add('B') 523 pickup_button_names.add('Y') 524 525 run_text = ba.Lstr( 526 value='${R}: ${B}', 527 subs=[ 528 ('${R}', ba.Lstr(resource='runText')), 529 ( 530 '${B}', 531 ba.Lstr( 532 resource='holdAnyKeyText' 533 if all_keyboards 534 else 'holdAnyButtonText' 535 ), 536 ), 537 ], 538 ) 539 540 # If we're all keyboards, lets show move keys too. 541 if ( 542 all_keyboards 543 and len(up_button_names) == 1 544 and len(down_button_names) == 1 545 and len(left_button_names) == 1 546 and len(right_button_names) == 1 547 ): 548 up_text = list(up_button_names)[0] 549 down_text = list(down_button_names)[0] 550 left_text = list(left_button_names)[0] 551 right_text = list(right_button_names)[0] 552 run_text = ba.Lstr( 553 value='${M}: ${U}, ${L}, ${D}, ${R}\n${RUN}', 554 subs=[ 555 ('${M}', ba.Lstr(resource='moveText')), 556 ('${U}', up_text), 557 ('${L}', left_text), 558 ('${D}', down_text), 559 ('${R}', right_text), 560 ('${RUN}', run_text), 561 ], 562 ) 563 564 if self._force_hide_button_names: 565 jump_button_names.clear() 566 punch_button_names.clear() 567 bomb_button_names.clear() 568 pickup_button_names.clear() 569 570 self._run_text.text = run_text 571 w_text: ba.Lstr | str 572 if only_remote and self._lifespan is None: 573 w_text = ba.Lstr( 574 resource='fireTVRemoteWarningText', 575 subs=[('${REMOTE_APP_NAME}', get_remote_app_name())], 576 ) 577 else: 578 w_text = '' 579 self._extra_text.text = w_text 580 if len(punch_button_names) == 1: 581 self._punch_text.text = list(punch_button_names)[0] 582 else: 583 self._punch_text.text = '' 584 585 if len(jump_button_names) == 1: 586 tval = list(jump_button_names)[0] 587 else: 588 tval = '' 589 self._jump_text.text = tval 590 if tval == '': 591 self._run_text.position = self._run_text_pos_top 592 self._extra_text.position = ( 593 self._run_text_pos_top[0], 594 self._run_text_pos_top[1] - 50, 595 ) 596 else: 597 self._run_text.position = self._run_text_pos_bottom 598 self._extra_text.position = ( 599 self._run_text_pos_bottom[0], 600 self._run_text_pos_bottom[1] - 50, 601 ) 602 if len(bomb_button_names) == 1: 603 self._bomb_text.text = list(bomb_button_names)[0] 604 else: 605 self._bomb_text.text = '' 606 607 # Also move our title up/down depending on if this is shown. 608 if len(pickup_button_names) == 1: 609 self._pick_up_text.text = list(pickup_button_names)[0] 610 if self._title_text is not None: 611 self._title_text.position = self._title_text_pos_top 612 else: 613 self._pick_up_text.text = '' 614 if self._title_text is not None: 615 self._title_text.position = self._title_text_pos_bottom 616 617 def _die(self) -> None: 618 for node in self._nodes: 619 node.delete() 620 self._nodes = [] 621 self._update_timer = None 622 self._dead = True 623 624 def exists(self) -> bool: 625 return not self._dead 626 627 def handlemessage(self, msg: Any) -> Any: 628 assert not self.expired 629 if isinstance(msg, ba.DieMessage): 630 if msg.immediate: 631 self._die() 632 else: 633 # If they don't need immediate, 634 # fade out our nodes and die later. 635 for node in self._nodes: 636 ba.animate(node, 'opacity', {0: node.opacity, 3.0: 0.0}) 637 ba.timer(3.1, ba.WeakCall(self._die)) 638 return None 639 return super().handlemessage(msg)
17class ControlsGuide(ba.Actor): 18 """A screen overlay of game controls. 19 20 category: Gameplay Classes 21 22 Shows button mappings based on what controllers are connected. 23 Handy to show at the start of a series or whenever there might 24 be newbies watching. 25 """ 26 27 def __init__( 28 self, 29 position: tuple[float, float] = (390.0, 120.0), 30 scale: float = 1.0, 31 delay: float = 0.0, 32 lifespan: float | None = None, 33 bright: bool = False, 34 ): 35 """Instantiate an overlay. 36 37 delay: is the time in seconds before the overlay fades in. 38 39 lifespan: if not None, the overlay will fade back out and die after 40 that long (in milliseconds). 41 42 bright: if True, brighter colors will be used; handy when showing 43 over gameplay but may be too bright for join-screens, etc. 44 """ 45 # pylint: disable=too-many-statements 46 # pylint: disable=too-many-locals 47 super().__init__() 48 show_title = True 49 scale *= 0.75 50 image_size = 90.0 * scale 51 offs = 74.0 * scale 52 offs5 = 43.0 * scale 53 ouya = False 54 maxw = 50 55 self._lifespan = lifespan 56 self._dead = False 57 self._bright = bright 58 self._cancel_timer: ba.Timer | None = None 59 self._fade_in_timer: ba.Timer | None = None 60 self._update_timer: ba.Timer | None = None 61 self._title_text: ba.Node | None 62 clr: Sequence[float] 63 extra_pos_1: tuple[float, float] | None 64 extra_pos_2: tuple[float, float] | None 65 if ba.app.iircade_mode: 66 xtweak = 0.2 67 ytweak = 0.2 68 jump_pos = ( 69 position[0] + offs * (-1.2 + xtweak), 70 position[1] + offs * (0.1 + ytweak), 71 ) 72 bomb_pos = ( 73 position[0] + offs * (0.0 + xtweak), 74 position[1] + offs * (0.5 + ytweak), 75 ) 76 punch_pos = ( 77 position[0] + offs * (1.2 + xtweak), 78 position[1] + offs * (0.5 + ytweak), 79 ) 80 81 pickup_pos = ( 82 position[0] + offs * (-1.4 + xtweak), 83 position[1] + offs * (-1.2 + ytweak), 84 ) 85 extra_pos_1 = ( 86 position[0] + offs * (-0.2 + xtweak), 87 position[1] + offs * (-0.8 + ytweak), 88 ) 89 extra_pos_2 = ( 90 position[0] + offs * (1.0 + xtweak), 91 position[1] + offs * (-0.8 + ytweak), 92 ) 93 self._force_hide_button_names = True 94 else: 95 punch_pos = (position[0] - offs * 1.1, position[1]) 96 jump_pos = (position[0], position[1] - offs) 97 bomb_pos = (position[0] + offs * 1.1, position[1]) 98 pickup_pos = (position[0], position[1] + offs) 99 extra_pos_1 = None 100 extra_pos_2 = None 101 self._force_hide_button_names = False 102 103 if show_title: 104 self._title_text_pos_top = ( 105 position[0], 106 position[1] + 139.0 * scale, 107 ) 108 self._title_text_pos_bottom = ( 109 position[0], 110 position[1] + 139.0 * scale, 111 ) 112 clr = (1, 1, 1) if bright else (0.7, 0.7, 0.7) 113 tval = ba.Lstr( 114 value='${A}:', subs=[('${A}', ba.Lstr(resource='controlsText'))] 115 ) 116 self._title_text = ba.newnode( 117 'text', 118 attrs={ 119 'text': tval, 120 'host_only': True, 121 'scale': 1.1 * scale, 122 'shadow': 0.5, 123 'flatness': 1.0, 124 'maxwidth': 480, 125 'v_align': 'center', 126 'h_align': 'center', 127 'color': clr, 128 }, 129 ) 130 else: 131 self._title_text = None 132 pos = jump_pos 133 clr = (0.4, 1, 0.4) 134 self._jump_image = ba.newnode( 135 'image', 136 attrs={ 137 'texture': ba.gettexture('buttonJump'), 138 'absolute_scale': True, 139 'host_only': True, 140 'vr_depth': 10, 141 'position': pos, 142 'scale': (image_size, image_size), 143 'color': clr, 144 }, 145 ) 146 self._jump_text = ba.newnode( 147 'text', 148 attrs={ 149 'v_align': 'top', 150 'h_align': 'center', 151 'scale': 1.5 * scale, 152 'flatness': 1.0, 153 'host_only': True, 154 'shadow': 1.0, 155 'maxwidth': maxw, 156 'position': (pos[0], pos[1] - offs5), 157 'color': clr, 158 }, 159 ) 160 clr = (0.2, 0.6, 1) if ouya else (1, 0.7, 0.3) 161 pos = punch_pos 162 self._punch_image = ba.newnode( 163 'image', 164 attrs={ 165 'texture': ba.gettexture('buttonPunch'), 166 'absolute_scale': True, 167 'host_only': True, 168 'vr_depth': 10, 169 'position': pos, 170 'scale': (image_size, image_size), 171 'color': clr, 172 }, 173 ) 174 self._punch_text = ba.newnode( 175 'text', 176 attrs={ 177 'v_align': 'top', 178 'h_align': 'center', 179 'scale': 1.5 * scale, 180 'flatness': 1.0, 181 'host_only': True, 182 'shadow': 1.0, 183 'maxwidth': maxw, 184 'position': (pos[0], pos[1] - offs5), 185 'color': clr, 186 }, 187 ) 188 pos = bomb_pos 189 clr = (1, 0.3, 0.3) 190 self._bomb_image = ba.newnode( 191 'image', 192 attrs={ 193 'texture': ba.gettexture('buttonBomb'), 194 'absolute_scale': True, 195 'host_only': True, 196 'vr_depth': 10, 197 'position': pos, 198 'scale': (image_size, image_size), 199 'color': clr, 200 }, 201 ) 202 self._bomb_text = ba.newnode( 203 'text', 204 attrs={ 205 'h_align': 'center', 206 'v_align': 'top', 207 'scale': 1.5 * scale, 208 'flatness': 1.0, 209 'host_only': True, 210 'shadow': 1.0, 211 'maxwidth': maxw, 212 'position': (pos[0], pos[1] - offs5), 213 'color': clr, 214 }, 215 ) 216 pos = pickup_pos 217 clr = (1, 0.8, 0.3) if ouya else (0.8, 0.5, 1) 218 self._pickup_image = ba.newnode( 219 'image', 220 attrs={ 221 'texture': ba.gettexture('buttonPickUp'), 222 'absolute_scale': True, 223 'host_only': True, 224 'vr_depth': 10, 225 'position': pos, 226 'scale': (image_size, image_size), 227 'color': clr, 228 }, 229 ) 230 self._pick_up_text = ba.newnode( 231 'text', 232 attrs={ 233 'v_align': 'top', 234 'h_align': 'center', 235 'scale': 1.5 * scale, 236 'flatness': 1.0, 237 'host_only': True, 238 'shadow': 1.0, 239 'maxwidth': maxw, 240 'position': (pos[0], pos[1] - offs5), 241 'color': clr, 242 }, 243 ) 244 clr = (0.9, 0.9, 2.0, 1.0) if bright else (0.8, 0.8, 2.0, 1.0) 245 self._run_text_pos_top = (position[0], position[1] - 135.0 * scale) 246 self._run_text_pos_bottom = (position[0], position[1] - 172.0 * scale) 247 sval = 1.0 * scale if ba.app.vr_mode else 0.8 * scale 248 self._run_text = ba.newnode( 249 'text', 250 attrs={ 251 'scale': sval, 252 'host_only': True, 253 'shadow': 1.0 if ba.app.vr_mode else 0.5, 254 'flatness': 1.0, 255 'maxwidth': 380, 256 'v_align': 'top', 257 'h_align': 'center', 258 'color': clr, 259 }, 260 ) 261 clr = (1, 1, 1) if bright else (0.7, 0.7, 0.7) 262 self._extra_text = ba.newnode( 263 'text', 264 attrs={ 265 'scale': 0.8 * scale, 266 'host_only': True, 267 'shadow': 0.5, 268 'flatness': 1.0, 269 'maxwidth': 380, 270 'v_align': 'top', 271 'h_align': 'center', 272 'color': clr, 273 }, 274 ) 275 276 if extra_pos_1 is not None: 277 self._extra_image_1: ba.Node | None = ba.newnode( 278 'image', 279 attrs={ 280 'texture': ba.gettexture('nub'), 281 'absolute_scale': True, 282 'host_only': True, 283 'vr_depth': 10, 284 'position': extra_pos_1, 285 'scale': (image_size, image_size), 286 'color': (0.5, 0.5, 0.5), 287 }, 288 ) 289 else: 290 self._extra_image_1 = None 291 if extra_pos_2 is not None: 292 self._extra_image_2: ba.Node | None = ba.newnode( 293 'image', 294 attrs={ 295 'texture': ba.gettexture('nub'), 296 'absolute_scale': True, 297 'host_only': True, 298 'vr_depth': 10, 299 'position': extra_pos_2, 300 'scale': (image_size, image_size), 301 'color': (0.5, 0.5, 0.5), 302 }, 303 ) 304 else: 305 self._extra_image_2 = None 306 307 self._nodes = [ 308 self._bomb_image, 309 self._bomb_text, 310 self._punch_image, 311 self._punch_text, 312 self._jump_image, 313 self._jump_text, 314 self._pickup_image, 315 self._pick_up_text, 316 self._run_text, 317 self._extra_text, 318 ] 319 if show_title: 320 assert self._title_text 321 self._nodes.append(self._title_text) 322 if self._extra_image_1 is not None: 323 self._nodes.append(self._extra_image_1) 324 if self._extra_image_2 is not None: 325 self._nodes.append(self._extra_image_2) 326 327 # Start everything invisible. 328 for node in self._nodes: 329 node.opacity = 0.0 330 331 # Don't do anything until our delay has passed. 332 ba.timer(delay, ba.WeakCall(self._start_updating)) 333 334 @staticmethod 335 def _meaningful_button_name(device: ba.InputDevice, button: int) -> str: 336 """Return a flattened string button name; empty for non-meaningful.""" 337 if not device.has_meaningful_button_names: 338 return '' 339 return device.get_button_name(button).evaluate() 340 341 def _start_updating(self) -> None: 342 343 # Ok, our delay has passed. Now lets periodically see if we can fade 344 # in (if a touch-screen is present we only want to show up if gamepads 345 # are connected, etc). 346 # Also set up a timer so if we haven't faded in by the end of our 347 # duration, abort. 348 if self._lifespan is not None: 349 self._cancel_timer = ba.Timer( 350 self._lifespan, 351 ba.WeakCall(self.handlemessage, ba.DieMessage(immediate=True)), 352 ) 353 self._fade_in_timer = ba.Timer( 354 1.0, ba.WeakCall(self._check_fade_in), repeat=True 355 ) 356 self._check_fade_in() # Do one check immediately. 357 358 def _check_fade_in(self) -> None: 359 from ba.internal import get_device_value 360 361 # If we have a touchscreen, we only fade in if we have a player with 362 # an input device that is *not* the touchscreen. 363 # (otherwise it is confusing to see the touchscreen buttons right 364 # next to our display buttons) 365 touchscreen: ba.InputDevice | None = ba.internal.getinputdevice( 366 'TouchScreen', '#1', doraise=False 367 ) 368 369 if touchscreen is not None: 370 # We look at the session's players; not the activity's. 371 # We want to get ones who are still in the process of 372 # selecting a character, etc. 373 input_devices = [ 374 p.inputdevice for p in ba.getsession().sessionplayers 375 ] 376 input_devices = [ 377 i for i in input_devices if i and i is not touchscreen 378 ] 379 fade_in = False 380 if input_devices: 381 # Only count this one if it has non-empty button names 382 # (filters out wiimotes, the remote-app, etc). 383 for device in input_devices: 384 for name in ( 385 'buttonPunch', 386 'buttonJump', 387 'buttonBomb', 388 'buttonPickUp', 389 ): 390 if ( 391 self._meaningful_button_name( 392 device, get_device_value(device, name) 393 ) 394 != '' 395 ): 396 fade_in = True 397 break 398 if fade_in: 399 break # No need to keep looking. 400 else: 401 # No touch-screen; fade in immediately. 402 fade_in = True 403 if fade_in: 404 self._cancel_timer = None # Didn't need this. 405 self._fade_in_timer = None # Done with this. 406 self._fade_in() 407 408 def _fade_in(self) -> None: 409 for node in self._nodes: 410 ba.animate(node, 'opacity', {0: 0.0, 2.0: 1.0}) 411 412 # If we were given a lifespan, transition out after it. 413 if self._lifespan is not None: 414 ba.timer( 415 self._lifespan, ba.WeakCall(self.handlemessage, ba.DieMessage()) 416 ) 417 self._update() 418 self._update_timer = ba.Timer( 419 1.0, ba.WeakCall(self._update), repeat=True 420 ) 421 422 def _update(self) -> None: 423 # pylint: disable=too-many-statements 424 # pylint: disable=too-many-branches 425 # pylint: disable=too-many-locals 426 from ba.internal import get_device_value, get_remote_app_name 427 428 if self._dead: 429 return 430 punch_button_names = set() 431 jump_button_names = set() 432 pickup_button_names = set() 433 bomb_button_names = set() 434 435 # We look at the session's players; not the activity's - we want to 436 # get ones who are still in the process of selecting a character, etc. 437 input_devices = [p.inputdevice for p in ba.getsession().sessionplayers] 438 input_devices = [i for i in input_devices if i] 439 440 # If there's no players with input devices yet, try to default to 441 # showing keyboard controls. 442 if not input_devices: 443 kbd = ba.internal.getinputdevice('Keyboard', '#1', doraise=False) 444 if kbd is not None: 445 input_devices.append(kbd) 446 447 # We word things specially if we have nothing but keyboards. 448 all_keyboards = input_devices and all( 449 i.name == 'Keyboard' for i in input_devices 450 ) 451 only_remote = len(input_devices) == 1 and all( 452 i.name == 'Amazon Fire TV Remote' for i in input_devices 453 ) 454 455 right_button_names = set() 456 left_button_names = set() 457 up_button_names = set() 458 down_button_names = set() 459 460 # For each player in the game with an input device, 461 # get the name of the button for each of these 4 actions. 462 # If any of them are uniform across all devices, display the name. 463 for device in input_devices: 464 # We only care about movement buttons in the case of keyboards. 465 if all_keyboards: 466 right_button_names.add( 467 device.get_button_name( 468 get_device_value(device, 'buttonRight') 469 ) 470 ) 471 left_button_names.add( 472 device.get_button_name( 473 get_device_value(device, 'buttonLeft') 474 ) 475 ) 476 down_button_names.add( 477 device.get_button_name( 478 get_device_value(device, 'buttonDown') 479 ) 480 ) 481 up_button_names.add( 482 device.get_button_name(get_device_value(device, 'buttonUp')) 483 ) 484 485 # Ignore empty values; things like the remote app or 486 # wiimotes can return these. 487 bname = self._meaningful_button_name( 488 device, get_device_value(device, 'buttonPunch') 489 ) 490 if bname != '': 491 punch_button_names.add(bname) 492 bname = self._meaningful_button_name( 493 device, get_device_value(device, 'buttonJump') 494 ) 495 if bname != '': 496 jump_button_names.add(bname) 497 bname = self._meaningful_button_name( 498 device, get_device_value(device, 'buttonBomb') 499 ) 500 if bname != '': 501 bomb_button_names.add(bname) 502 bname = self._meaningful_button_name( 503 device, get_device_value(device, 'buttonPickUp') 504 ) 505 if bname != '': 506 pickup_button_names.add(bname) 507 508 # If we have no values yet, we may want to throw out some sane 509 # defaults. 510 if all( 511 not lst 512 for lst in ( 513 punch_button_names, 514 jump_button_names, 515 bomb_button_names, 516 pickup_button_names, 517 ) 518 ): 519 # Otherwise on android show standard buttons. 520 if ba.app.platform == 'android': 521 punch_button_names.add('X') 522 jump_button_names.add('A') 523 bomb_button_names.add('B') 524 pickup_button_names.add('Y') 525 526 run_text = ba.Lstr( 527 value='${R}: ${B}', 528 subs=[ 529 ('${R}', ba.Lstr(resource='runText')), 530 ( 531 '${B}', 532 ba.Lstr( 533 resource='holdAnyKeyText' 534 if all_keyboards 535 else 'holdAnyButtonText' 536 ), 537 ), 538 ], 539 ) 540 541 # If we're all keyboards, lets show move keys too. 542 if ( 543 all_keyboards 544 and len(up_button_names) == 1 545 and len(down_button_names) == 1 546 and len(left_button_names) == 1 547 and len(right_button_names) == 1 548 ): 549 up_text = list(up_button_names)[0] 550 down_text = list(down_button_names)[0] 551 left_text = list(left_button_names)[0] 552 right_text = list(right_button_names)[0] 553 run_text = ba.Lstr( 554 value='${M}: ${U}, ${L}, ${D}, ${R}\n${RUN}', 555 subs=[ 556 ('${M}', ba.Lstr(resource='moveText')), 557 ('${U}', up_text), 558 ('${L}', left_text), 559 ('${D}', down_text), 560 ('${R}', right_text), 561 ('${RUN}', run_text), 562 ], 563 ) 564 565 if self._force_hide_button_names: 566 jump_button_names.clear() 567 punch_button_names.clear() 568 bomb_button_names.clear() 569 pickup_button_names.clear() 570 571 self._run_text.text = run_text 572 w_text: ba.Lstr | str 573 if only_remote and self._lifespan is None: 574 w_text = ba.Lstr( 575 resource='fireTVRemoteWarningText', 576 subs=[('${REMOTE_APP_NAME}', get_remote_app_name())], 577 ) 578 else: 579 w_text = '' 580 self._extra_text.text = w_text 581 if len(punch_button_names) == 1: 582 self._punch_text.text = list(punch_button_names)[0] 583 else: 584 self._punch_text.text = '' 585 586 if len(jump_button_names) == 1: 587 tval = list(jump_button_names)[0] 588 else: 589 tval = '' 590 self._jump_text.text = tval 591 if tval == '': 592 self._run_text.position = self._run_text_pos_top 593 self._extra_text.position = ( 594 self._run_text_pos_top[0], 595 self._run_text_pos_top[1] - 50, 596 ) 597 else: 598 self._run_text.position = self._run_text_pos_bottom 599 self._extra_text.position = ( 600 self._run_text_pos_bottom[0], 601 self._run_text_pos_bottom[1] - 50, 602 ) 603 if len(bomb_button_names) == 1: 604 self._bomb_text.text = list(bomb_button_names)[0] 605 else: 606 self._bomb_text.text = '' 607 608 # Also move our title up/down depending on if this is shown. 609 if len(pickup_button_names) == 1: 610 self._pick_up_text.text = list(pickup_button_names)[0] 611 if self._title_text is not None: 612 self._title_text.position = self._title_text_pos_top 613 else: 614 self._pick_up_text.text = '' 615 if self._title_text is not None: 616 self._title_text.position = self._title_text_pos_bottom 617 618 def _die(self) -> None: 619 for node in self._nodes: 620 node.delete() 621 self._nodes = [] 622 self._update_timer = None 623 self._dead = True 624 625 def exists(self) -> bool: 626 return not self._dead 627 628 def handlemessage(self, msg: Any) -> Any: 629 assert not self.expired 630 if isinstance(msg, ba.DieMessage): 631 if msg.immediate: 632 self._die() 633 else: 634 # If they don't need immediate, 635 # fade out our nodes and die later. 636 for node in self._nodes: 637 ba.animate(node, 'opacity', {0: node.opacity, 3.0: 0.0}) 638 ba.timer(3.1, ba.WeakCall(self._die)) 639 return None 640 return super().handlemessage(msg)
A screen overlay of game controls.
category: Gameplay Classes
Shows button mappings based on what controllers are connected. Handy to show at the start of a series or whenever there might be newbies watching.
27 def __init__( 28 self, 29 position: tuple[float, float] = (390.0, 120.0), 30 scale: float = 1.0, 31 delay: float = 0.0, 32 lifespan: float | None = None, 33 bright: bool = False, 34 ): 35 """Instantiate an overlay. 36 37 delay: is the time in seconds before the overlay fades in. 38 39 lifespan: if not None, the overlay will fade back out and die after 40 that long (in milliseconds). 41 42 bright: if True, brighter colors will be used; handy when showing 43 over gameplay but may be too bright for join-screens, etc. 44 """ 45 # pylint: disable=too-many-statements 46 # pylint: disable=too-many-locals 47 super().__init__() 48 show_title = True 49 scale *= 0.75 50 image_size = 90.0 * scale 51 offs = 74.0 * scale 52 offs5 = 43.0 * scale 53 ouya = False 54 maxw = 50 55 self._lifespan = lifespan 56 self._dead = False 57 self._bright = bright 58 self._cancel_timer: ba.Timer | None = None 59 self._fade_in_timer: ba.Timer | None = None 60 self._update_timer: ba.Timer | None = None 61 self._title_text: ba.Node | None 62 clr: Sequence[float] 63 extra_pos_1: tuple[float, float] | None 64 extra_pos_2: tuple[float, float] | None 65 if ba.app.iircade_mode: 66 xtweak = 0.2 67 ytweak = 0.2 68 jump_pos = ( 69 position[0] + offs * (-1.2 + xtweak), 70 position[1] + offs * (0.1 + ytweak), 71 ) 72 bomb_pos = ( 73 position[0] + offs * (0.0 + xtweak), 74 position[1] + offs * (0.5 + ytweak), 75 ) 76 punch_pos = ( 77 position[0] + offs * (1.2 + xtweak), 78 position[1] + offs * (0.5 + ytweak), 79 ) 80 81 pickup_pos = ( 82 position[0] + offs * (-1.4 + xtweak), 83 position[1] + offs * (-1.2 + ytweak), 84 ) 85 extra_pos_1 = ( 86 position[0] + offs * (-0.2 + xtweak), 87 position[1] + offs * (-0.8 + ytweak), 88 ) 89 extra_pos_2 = ( 90 position[0] + offs * (1.0 + xtweak), 91 position[1] + offs * (-0.8 + ytweak), 92 ) 93 self._force_hide_button_names = True 94 else: 95 punch_pos = (position[0] - offs * 1.1, position[1]) 96 jump_pos = (position[0], position[1] - offs) 97 bomb_pos = (position[0] + offs * 1.1, position[1]) 98 pickup_pos = (position[0], position[1] + offs) 99 extra_pos_1 = None 100 extra_pos_2 = None 101 self._force_hide_button_names = False 102 103 if show_title: 104 self._title_text_pos_top = ( 105 position[0], 106 position[1] + 139.0 * scale, 107 ) 108 self._title_text_pos_bottom = ( 109 position[0], 110 position[1] + 139.0 * scale, 111 ) 112 clr = (1, 1, 1) if bright else (0.7, 0.7, 0.7) 113 tval = ba.Lstr( 114 value='${A}:', subs=[('${A}', ba.Lstr(resource='controlsText'))] 115 ) 116 self._title_text = ba.newnode( 117 'text', 118 attrs={ 119 'text': tval, 120 'host_only': True, 121 'scale': 1.1 * scale, 122 'shadow': 0.5, 123 'flatness': 1.0, 124 'maxwidth': 480, 125 'v_align': 'center', 126 'h_align': 'center', 127 'color': clr, 128 }, 129 ) 130 else: 131 self._title_text = None 132 pos = jump_pos 133 clr = (0.4, 1, 0.4) 134 self._jump_image = ba.newnode( 135 'image', 136 attrs={ 137 'texture': ba.gettexture('buttonJump'), 138 'absolute_scale': True, 139 'host_only': True, 140 'vr_depth': 10, 141 'position': pos, 142 'scale': (image_size, image_size), 143 'color': clr, 144 }, 145 ) 146 self._jump_text = ba.newnode( 147 'text', 148 attrs={ 149 'v_align': 'top', 150 'h_align': 'center', 151 'scale': 1.5 * scale, 152 'flatness': 1.0, 153 'host_only': True, 154 'shadow': 1.0, 155 'maxwidth': maxw, 156 'position': (pos[0], pos[1] - offs5), 157 'color': clr, 158 }, 159 ) 160 clr = (0.2, 0.6, 1) if ouya else (1, 0.7, 0.3) 161 pos = punch_pos 162 self._punch_image = ba.newnode( 163 'image', 164 attrs={ 165 'texture': ba.gettexture('buttonPunch'), 166 'absolute_scale': True, 167 'host_only': True, 168 'vr_depth': 10, 169 'position': pos, 170 'scale': (image_size, image_size), 171 'color': clr, 172 }, 173 ) 174 self._punch_text = ba.newnode( 175 'text', 176 attrs={ 177 'v_align': 'top', 178 'h_align': 'center', 179 'scale': 1.5 * scale, 180 'flatness': 1.0, 181 'host_only': True, 182 'shadow': 1.0, 183 'maxwidth': maxw, 184 'position': (pos[0], pos[1] - offs5), 185 'color': clr, 186 }, 187 ) 188 pos = bomb_pos 189 clr = (1, 0.3, 0.3) 190 self._bomb_image = ba.newnode( 191 'image', 192 attrs={ 193 'texture': ba.gettexture('buttonBomb'), 194 'absolute_scale': True, 195 'host_only': True, 196 'vr_depth': 10, 197 'position': pos, 198 'scale': (image_size, image_size), 199 'color': clr, 200 }, 201 ) 202 self._bomb_text = ba.newnode( 203 'text', 204 attrs={ 205 'h_align': 'center', 206 'v_align': 'top', 207 'scale': 1.5 * scale, 208 'flatness': 1.0, 209 'host_only': True, 210 'shadow': 1.0, 211 'maxwidth': maxw, 212 'position': (pos[0], pos[1] - offs5), 213 'color': clr, 214 }, 215 ) 216 pos = pickup_pos 217 clr = (1, 0.8, 0.3) if ouya else (0.8, 0.5, 1) 218 self._pickup_image = ba.newnode( 219 'image', 220 attrs={ 221 'texture': ba.gettexture('buttonPickUp'), 222 'absolute_scale': True, 223 'host_only': True, 224 'vr_depth': 10, 225 'position': pos, 226 'scale': (image_size, image_size), 227 'color': clr, 228 }, 229 ) 230 self._pick_up_text = ba.newnode( 231 'text', 232 attrs={ 233 'v_align': 'top', 234 'h_align': 'center', 235 'scale': 1.5 * scale, 236 'flatness': 1.0, 237 'host_only': True, 238 'shadow': 1.0, 239 'maxwidth': maxw, 240 'position': (pos[0], pos[1] - offs5), 241 'color': clr, 242 }, 243 ) 244 clr = (0.9, 0.9, 2.0, 1.0) if bright else (0.8, 0.8, 2.0, 1.0) 245 self._run_text_pos_top = (position[0], position[1] - 135.0 * scale) 246 self._run_text_pos_bottom = (position[0], position[1] - 172.0 * scale) 247 sval = 1.0 * scale if ba.app.vr_mode else 0.8 * scale 248 self._run_text = ba.newnode( 249 'text', 250 attrs={ 251 'scale': sval, 252 'host_only': True, 253 'shadow': 1.0 if ba.app.vr_mode else 0.5, 254 'flatness': 1.0, 255 'maxwidth': 380, 256 'v_align': 'top', 257 'h_align': 'center', 258 'color': clr, 259 }, 260 ) 261 clr = (1, 1, 1) if bright else (0.7, 0.7, 0.7) 262 self._extra_text = ba.newnode( 263 'text', 264 attrs={ 265 'scale': 0.8 * scale, 266 'host_only': True, 267 'shadow': 0.5, 268 'flatness': 1.0, 269 'maxwidth': 380, 270 'v_align': 'top', 271 'h_align': 'center', 272 'color': clr, 273 }, 274 ) 275 276 if extra_pos_1 is not None: 277 self._extra_image_1: ba.Node | None = ba.newnode( 278 'image', 279 attrs={ 280 'texture': ba.gettexture('nub'), 281 'absolute_scale': True, 282 'host_only': True, 283 'vr_depth': 10, 284 'position': extra_pos_1, 285 'scale': (image_size, image_size), 286 'color': (0.5, 0.5, 0.5), 287 }, 288 ) 289 else: 290 self._extra_image_1 = None 291 if extra_pos_2 is not None: 292 self._extra_image_2: ba.Node | None = ba.newnode( 293 'image', 294 attrs={ 295 'texture': ba.gettexture('nub'), 296 'absolute_scale': True, 297 'host_only': True, 298 'vr_depth': 10, 299 'position': extra_pos_2, 300 'scale': (image_size, image_size), 301 'color': (0.5, 0.5, 0.5), 302 }, 303 ) 304 else: 305 self._extra_image_2 = None 306 307 self._nodes = [ 308 self._bomb_image, 309 self._bomb_text, 310 self._punch_image, 311 self._punch_text, 312 self._jump_image, 313 self._jump_text, 314 self._pickup_image, 315 self._pick_up_text, 316 self._run_text, 317 self._extra_text, 318 ] 319 if show_title: 320 assert self._title_text 321 self._nodes.append(self._title_text) 322 if self._extra_image_1 is not None: 323 self._nodes.append(self._extra_image_1) 324 if self._extra_image_2 is not None: 325 self._nodes.append(self._extra_image_2) 326 327 # Start everything invisible. 328 for node in self._nodes: 329 node.opacity = 0.0 330 331 # Don't do anything until our delay has passed. 332 ba.timer(delay, ba.WeakCall(self._start_updating))
Instantiate an overlay.
delay: is the time in seconds before the overlay fades in.
lifespan: if not None, the overlay will fade back out and die after that long (in milliseconds).
bright: if True, brighter colors will be used; handy when showing over gameplay but may be too bright for join-screens, etc.
Returns whether the Actor is still present in a meaningful way.
Note that a dying character should still return True here as long as their corpse is visible; this is about presence, not being 'alive' (see ba.Actor.is_alive() for that).
If this returns False, it is assumed the Actor can be completely deleted without affecting the game; this call is often used when pruning lists of Actors, such as with ba.Actor.autoretain()
The default implementation of this method always return True.
Note that the boolean operator for the Actor class calls this method, so a simple "if myactor" test will conveniently do the right thing even if myactor is set to None.
628 def handlemessage(self, msg: Any) -> Any: 629 assert not self.expired 630 if isinstance(msg, ba.DieMessage): 631 if msg.immediate: 632 self._die() 633 else: 634 # If they don't need immediate, 635 # fade out our nodes and die later. 636 for node in self._nodes: 637 ba.animate(node, 'opacity', {0: node.opacity, 3.0: 0.0}) 638 ba.timer(3.1, ba.WeakCall(self._die)) 639 return None 640 return super().handlemessage(msg)
General message handling; can be passed any message object.
Inherited Members
- ba._actor.Actor
- autoretain
- on_expire
- expired
- is_alive
- activity
- getactivity