FIT CTU
Adam Vesecký
vesecky.adam@gmail.com
Lecture 5
Patterns
If game programmers ever cracked open Design Patterns at all, never got past Singleton. Gang of Four’s is inapplicable to games in its original version.Robert Nystrom
Single Responsibility Principle
Open-Closed Principle
Liskov Substitution Principle
Dependency-Inversion Principle
Interface-Segregation Principle
Design patterns in applications
Design patterns in games
Unreal Blueprints
Unity FlowCanvas
What we need
implementation
1 | |
2 | public async Task EnterDoorAction(Door door) { |
3 | this.Context.Player.BlockInput(); |
4 | await new DoorAnimation(door).Open(); |
5 | await new WalkAnimation(this.Context.Player).Walk(this.Context.Player.direction); |
6 | this.Context.Player.Hide(); // hide the sprite once it approaches the house |
7 | await new DoorAnimation(door).Close(); |
8 | await Delay(500); // wait for 500 ms |
9 | } |
10 | |
11 | ....... |
12 | |
13 | public async Task OnPlayerDoorApproached(Door door) { |
14 | await new EnterDoorAction(door); |
15 | await new SceneLoader(door.TargetScene); |
16 | } |
1 | IEnumerator Spawn () { |
2 | // Create a random wait time before the prop is instantiated. |
3 | float waitTime = Random.Range(minTimeBetweenSpawns, maxTimeBetweenSpawns); |
4 | // Wait for the designated period. |
5 | yield return new WaitForSeconds(waitTime); |
6 | |
7 | // Instantiate the prop at the desired position. |
8 | Rigidbody2D propInstance = Instantiate(backgroundProp, spawnPos, Quaternion.identity) as Rigidbody2D; |
9 | // Restart the coroutine to spawn another prop. |
10 | StartCoroutine(Spawn()); |
11 | } |
1 | if(asteroid.position.distance(rocket.position) <= MIN_PROXIMITY) { // detect proximity |
2 | rocket.runAnimation(ANIM_EXPLOSION); // react instantly and handle everything |
3 | asteroid.runAnimation(ANIM_EXPLOSION); |
4 | playSound(SOUND_EXPLOSION); |
5 | asteroid.destroy(); |
6 | rocket.destroy(); |
7 | } |
1 | // collision-system.ts |
2 | let collisions = this.collisionSystem.checkProximity(allGameObjects); |
3 | collisions.forEach(colliding => this.sendEvent(COLLISION_TRIGGERED, colliding)); |
4 | // rocket-handler.ts |
5 | onCollisionTriggered(colliding) { |
6 | this.destroy(); |
7 | this.sendEvent(ROCKET_DESTROYED); |
8 | } |
9 | // sound-component.ts |
10 | onGameObjectDestroyed() { |
11 | this.playSound(SOUND_EXPLOSION); |
12 | } |
1 | void() PlayerDie = { |
2 | DropBackpack(); |
3 | |
4 | self.weaponmodel=""; |
5 | self.view_ofs = '0 0 -8'; |
6 | self.deadflag = DEAD_DYING; |
7 | self.solid = SOLID_NOT; |
8 | self.flags = self.flags - (self.flags & FL_ONGROUND); |
9 | self.movetype = MOVETYPE_TOSS; |
10 | |
11 | if (self.velocity_z < 10) |
12 | self.velocity_z = self.velocity_z + random()*300; |
13 | |
14 | DeathSound(); |
15 | |
16 | if (self.weapon == IT_AXE) { |
17 | player_die_ax1 (); |
18 | return; |
19 | } |
20 | |
21 | i = 1 + floor(random()*6); |
22 | if (i == 1) |
23 | player_diea1(); |
24 | else if (i == 2) |
25 | player_dieb1(); |
26 | else player_diec1(); |
27 | }; |
Individual units
Battle formation
Memory caching issues
Avoiding cache miss
Avoiding branch misprediction
1 | // iterating inside out -> SLOW |
2 | for (i = 0 to size) |
3 | for (j = 0 to size) |
4 | do something with array[j][i] |
1 | // iterating outside in -> FAST |
2 | for (i = 0 to size) |
3 | for (j = 0 to size) |
4 | do something with array[i][j] |
1 | // assume only 50% active/visible objects |
2 | gameLoop(delta, absolute) { |
3 | for(let entity in this.entities) { |
4 | // 50% mispredictions |
5 | if(entity.ACTIVE) { |
6 | entity.update(delta, absolute); |
7 | } |
8 | if(entity.VISIBLE) { |
9 | entity.draw(); |
10 | } |
11 | } |
12 | } |
Randomly
Sequentially
1 | // hashing function |
2 | inline unsigned SDBMHash(unsigned hash, unsigned char c) { |
3 | return c + (hash << 6) + (hash << 16) - hash; |
4 | } |
5 | |
6 | unsigned calc(const char* str, unsigned hash = 0) { |
7 | while (*str) { |
8 | // Perform the current hashing as case-insensitive |
9 | char c = *str; |
10 | hash = SDBMHash(hash, (unsigned char)tolower(c)); |
11 | ++str; |
12 | } |
13 | return hash; |
14 | } |
1 | #define GODOT_VARIANT_SIZE (16 + sizeof(int64_t)) |
2 | |
3 | typedef struct { |
4 | uint8_t _dont_touch_that[GODOT_VARIANT_SIZE]; |
5 | } godot_variant; |
6 | |
7 | typedef enum godot_variant_type { |
8 | // atomic types |
9 | GODOT_VARIANT_TYPE_NIL, |
10 | GODOT_VARIANT_TYPE_BOOL, |
11 | GODOT_VARIANT_TYPE_INT, |
12 | ... |
1 | class Brainbot extends Unit { |
2 | |
3 | private damage: number; |
4 | private currentWeapon: WeaponType; |
5 | |
6 | constructor() { |
7 | super(UnitType.BRAIN_BOT); |
8 | } |
9 | |
10 | init(damage: number, currentWeapons: WeaponType) { |
11 | this.damage = damage; |
12 | this.currentWeapon = currentWeapons; |
13 | } |
14 | } |
Locator
Blackboard (Context)
1 | public void OnTriggerEvent(Event evt, Context ctx) { |
2 | |
3 | if(evt.Key == "LIFE_LOST") { |
4 | ctx.Lives--; // access the context |
5 | if(ctx.Lives <= 0) { |
6 | this.FireEvent("GAME_OVER"); |
7 | } |
8 | } |
9 | } |
1 | const getPlayer(scene: Scene) => scene.findObjectByName('player'); |
2 | |
3 | const getAllUnits(scene: Scene) => scene.findObjectsByTag('unit_basic'); |
4 | |
5 | const getAllUnitsWithinRadius(scene: Scene, pos: Vector, radius: number) => { |
6 | return getAllUnits(scene).filter(unit => unit.pos.distance(pos) <= radius); |
7 | } |
8 | |
9 | const getAllExits(scene: Scene) => { |
10 | const doors = scene.findObjectsByTag('door'); |
11 | return doors.filter(door => !door.locked); |
12 | } |
Cleaning
1 | void AnimationCache::_clear_cache() { |
2 | while (connected_nodes.size()) { |
3 | connected_nodes.front()->get() |
4 | ->disconnect("tree_exiting", callable_mp(this, &AnimationCache::_node_exit_tree)); |
5 | connected_nodes.erase(connected_nodes.front()); |
6 | } |
7 | path_cache.clear(); |
8 | cache_valid = false; |
9 | cache_dirty = true; |
10 | } |
11 | |
12 | void AnimationCache::_update_cache() { |
13 | cache_valid = false; |
14 | |
15 | for (int i = 0; i < animation->get_track_count(); i++) { |
16 | // ... 100 lines of code |
17 | } |
18 | |
19 | cache_dirty = false; |
20 | cache_valid = true; |
21 | } |
22 |
1 | public class Generator { |
2 | private final static AtomicInteger counter = new AtomicInteger(); |
3 | |
4 | public static int getId() { |
5 | return counter.incrementAndGet(); |
6 | } |
7 | } |
1 | function* generateId() { |
2 | let id = 0; |
3 | while(true) { |
4 | yield id; |
5 | id++; |
6 | } |
7 | } |
8 | |
9 | let newId = generateId().next(); |
1 | class Generator { |
2 | static idCounter = 0; |
3 | |
4 | getId(): number { |
5 | return Generator.idCounter++; |
6 | } |
7 | } |
1 | // stateless, the creature will jump each frame |
2 | updateCreature() { |
3 | if(eventSystem.isPressed(KeyCode.UP)) { |
4 | this.creature.jump(); |
5 | } |
6 | } |
7 | |
8 | // introduction of a state |
9 | updateCreature() { |
10 | if(eventSystem.isPressed(KeyCode.UP) && this.creature.state !== STATE_JUMPING) { |
11 | this.creature.changeState(STATE_JUMPING); |
12 | this.creature.jump(); |
13 | } |
14 | } |
1 | class Builder { |
2 | private _position: Vector; |
3 | private _scale: Vector; |
4 | |
5 | position(pos: Vector) { |
6 | this.position = pos; |
7 | return this; |
8 | } |
9 | |
10 | scale(scale: Vector) { |
11 | this.scale = scale; |
12 | return this; |
13 | } |
14 | |
15 | build() { |
16 | return new GameObject(this._position, this._scale); |
17 | } |
18 | } |
19 | |
20 | new Builder().position(new Vector(12, 54)).scale(new Vector(2, 1)).build(); |
Prefabs in Unity
1 | const superBallTransmuter = (entity: GameObject) => { |
2 | entity.removeComponent<BallBehavior>(); |
3 | entity.addComponent(new SuperBallBehavior()); |
4 | entity.state.speed = SUPER_BALL_SPEED; |
5 | entity.state.size = SUPER_BALL_SIZE; |
6 | return entity; |
7 | } |
1 | class UnitFactory { |
2 | |
3 | private pikemanBuilder: Builder; // preconfigured to build pikemans |
4 | private musketeerBuilder: Builder; // preconfigured to build musketeers |
5 | private archerBuilder: Builder; // preconfigured to build archers |
6 | |
7 | public spawnPikeman(position: Vector, faction: FactionType): GameObject { |
8 | return this.pikeman.position(position).faction(faction).build(); |
9 | } |
10 | |
11 | public spawnMusketeer(position: Vector, faction: FactionType): GameObject { |
12 | return this.musketeerBuilder.position(position).faction(faction).build(); |
13 | } |
14 | |
15 | public spawnArcher(position: Vector, faction: FactionType): GameObject { |
16 | return this.archerBuilder.position(position).faction(faction).build(); |
17 | } |
18 | } |
Machines aren't capable of evil. Humans make them that wayChrono Trigger