How to write more flexible game code

The Shaggy Dev
14 Sept 202208:36

Summary

TLDR本视频提供了编写更清晰代码的实用技巧,强调了应用层次结构的重要性,以及如何通过遵循这一结构来保持代码的灵活性和易于变更。视频中讨论了依赖关系的正确流向、关注点分离、数据与逻辑的结合、使用组件化和依赖注入来提高代码的可重用性和可维护性。此外,还介绍了设计模式的价值,特别是状态机和观察者模式,并鼓励开发者逐步学习和实践,以避免过度理论化。最后,视频强调了迭代改进代码风格的重要性,并提倡在实践中学习和成长。

Takeaways

  • 📈 保持代码灵活性和易于变更的关键在于尊重应用层级结构,确保依赖关系仅向下流动。
  • 🔄 避免在代码中假设结构,以保持对象的独立性和游戏的灵活性。
  • 🤖 保持关注点分离,例如,玩家对象不应直接操作动画的帧或位图数据。
  • 🔍 将数据和操作数据的逻辑保持在一起,而不是分离它们。
  • 🧱 使用组件来封装功能,而不是创建大型单一对象或过度使用继承。
  • 📌 依赖注入可以提高代码的可重用性和灵活性,通过在运行时提供对象引用。
  • 🔄 学习设计模式可以避免重复发明轮子,特别是状态机和观察者模式。
  • 🔄 迭代改进代码风格,不要一开始就试图掌握所有设计模式。
  • 🚀 游戏开发中,即使代码质量一般,能够发布的游戏也比完美但未发布的游戏要好。
  • 📚 在遇到痛点时再进行研究和学习,实践中学到的经验比理论更有价值。
  • 🔄 通过小项目实践,了解最终产品的样子和学到的经验,为将来做得更好打下基础。

Q & A

  • 什么是应用层次结构?

    -应用层次结构是指每个应用无论简单或复杂,都有一个层次结构,这个结构需要被尊重。依赖关系应该只向下流动,而不是向上或横向,这样可以保持代码的解耦和灵活性。

  • 为什么我们应该避免在对象树中假设结构?

    -假设结构会导致代码不够灵活,因为一旦改变或破坏了依赖关系,可能会影响到其他部分的代码。例如,如果玩家对象假设总是有一个名为物品的同级对象,那么在所有地方都需要这个物品对象,这限制了游戏的灵活性。

  • 分离关注点是什么意思?

    -分离关注点是指保持代码的不同部分专注于它们自己的职责。例如,玩家对象应该只关心高层次的操作,而不是动画的具体实现细节,这样可以在不破坏玩家代码的情况下,后期更改动画的实现方式。

  • 为什么建议将数据和操作数据的逻辑保持在一起?

    -这样做可以提高代码的可重用性和灵活性。例如,玩家对象应该告诉动画容器改变动画,而不是直接访问和修改动画容器中的数据。这样可以确保动画相关的代码逻辑集中,便于管理和调试。

  • 组件化有什么好处?

    -组件化可以将复杂的对象分解为更小、自给自足的代码块,这样可以提高代码的组织性和可重用性。例如,游戏中需要健康条的多个对象可以使用同一个健康组件,而不是在每个对象上管理单独的变量和方法。

  • 依赖注入是什么?

    -依赖注入是一种在运行时而不是编译时提供对象引用的技术。这可以使代码更加可重用和灵活,因为它允许在不硬编码具体对象的情况下,动态地将对象引用传递给需要它们的部分。

  • 为什么学习设计模式很重要?

    -设计模式是解决软件开发中常见问题的成熟解决方案。学习设计模式可以避免重复发明轮子,提高开发效率,并帮助开发者更好地理解和解决复杂问题。

  • 状态机和观察者模式为什么很重要?

    -状态机和观察者模式是两种非常重要的设计模式。状态机可以帮助管理对象的状态变化,而观察者模式可以使得对象之间的通信更加灵活。它们在游戏开发和软件工程中有着广泛的应用。

  • 如何提高代码质量?

    -提高代码质量可以通过迭代改进代码风格、学习设计模式、使用组件化和依赖注入等技术。同时,实践中遇到问题时进行研究和改进,以及在小项目中尝试不同的方法,都是提高代码质量的有效途径。

  • 为什么说发布一个中等质量的游戏比完美代码的游戏更好?

    -因为游戏开发的目标是创造出可以被玩家体验的产品。一个即使代码质量不是最优,但能够发布并被玩家接受的游戏,比一个从未完成的完美代码游戏更有价值。实践中的经验和反馈对于长期提高开发技能至关重要。

Outlines

00:00

📚 代码架构与应用层次

本段落讨论了如何保持代码的灵活性和易于变更。首先强调了应用层次的重要性,指出依赖关系应该仅在层次结构中向下流动,避免向上或横向依赖。通过一个小游戏的层次结构示例,说明了保持代码解耦的重要性。接着,讨论了分离关注点的概念,即对象应该专注于其职责范围内的操作,避免涉及超出其范围的实现细节。此外,提出了将数据和操作数据的逻辑保持在一起的原则,以及使用组件来组织代码的建议。最后,介绍了依赖注入的概念,这是一种在运行时提供对象引用的技术,有助于提高代码的可重用性和灵活性。

05:01

🔍 设计模式与代码迭代

这一段落继续深入探讨了设计模式的重要性,特别是状态机和观察者模式,它们对于提高代码的灵活性和可维护性至关重要。作者建议开发者学习这些模式,以避免重复造轮子。同时,提到了中介者模式作为另一个有用的设计模式。作者还强调了实践的重要性,建议开发者在遇到具体问题时再进行研究和学习,而不是一开始就试图掌握所有设计模式。最后,作者提醒开发者,一个能够发布的平庸代码的游戏比一个从未发布的完美代码的游戏要好,鼓励开发者在实践中学习和改进。

Mindmap

Keywords

💡应用层次结构

应用层次结构是指在软件开发中,各个组件或模块之间存在的一种层级关系。在这个视频中,作者强调了尊重这种层次结构的重要性,即依赖关系应该向下流动,而不是向上或横向。这有助于保持代码的解耦和灵活性。例如,如果玩家对象不需要依赖于游戏中的其他对象,如物品对象,那么它可以在游戏的任何地方使用,这增加了游戏的灵活性。

💡解耦

解耦是指在软件设计中减少或消除组件之间的依赖关系,使得每个组件能够独立于其他组件运行。在视频中,作者提到通过尊重应用层次结构和分离关注点,可以保持代码的解耦。例如,玩家对象不应该直接操作动画的帧,而应该通过动画容器来进行操作,这样可以在不破坏玩家代码的情况下更改动画的实现方式。

💡组件

组件是一种软件设计模式,它允许开发者将功能分解成可重用的、自给自足的代码块。在视频中,作者建议使用组件来组织代码,例如创建一个健康组件来管理游戏中所有需要健康条的对象。这种方法可以提高代码的组织性和可重用性。

💡依赖注入

依赖注入是一种设计模式,它允许在运行时而不是编译时将对象的依赖关系注入到对象中。这使得代码更加灵活和可重用。在视频中,作者提到依赖注入可以用来解决状态机需要引用其父对象的问题,而不需要为每个对象复制状态机。

💡设计模式

设计模式是软件工程中的一种最佳实践,用于解决特定问题的通用解决方案。在视频中,作者鼓励学习设计模式,特别是状态机和观察者模式,因为它们对于提高代码的灵活性和可维护性非常有帮助。

💡迭代改进

迭代改进是指在软件开发过程中,不断地评估和优化代码质量。在视频中,作者建议不要一开始就试图学习所有的设计模式,而是应该在遇到问题时进行研究和改进。这种方法有助于避免过度理论化,同时确保开发者能够在实践中学习和成长。

💡状态机

状态机是一种设计模式,用于管理对象在其生命周期内可能处于的不同状态以及状态之间的转换。在视频中,作者强调了状态机的重要性,并建议开发者熟悉这种模式,因为它可以帮助处理游戏中的复杂逻辑。

💡观察者模式

观察者模式是一种设计模式,它定义了对象之间的一对多依赖关系,当一个对象的状态发生变化时,所有依赖于它的对象都会得到通知并自动更新。在视频中,作者提到观察者模式对于提高代码的灵活性非常有用,并且大多数引擎和编程语言都支持这种模式。

💡分离关注点

分离关注点是指在软件设计中,将不同的功能或责任分配给不同的模块或组件,以提高代码的可维护性和可扩展性。在视频中,作者提到了分离关注点的重要性,并用动画容器和玩家对象的关系来说明这一点。

💡代码重用

代码重用是指在不同的程序或项目中重复使用相同的代码片段。在视频中,作者提到通过使用组件和依赖注入,可以提高代码的重用性,因为相同的功能可以被不同的对象共享。

💡干代码原则

干代码原则(Don't Repeat Yourself, DRY)是一种软件工程原则,旨在避免在代码中重复相同的逻辑。在视频中,作者提到通过使用组件和依赖注入,可以避免重复代码,从而保持代码的清晰和维护性。

Highlights

讨论了编写更清晰代码的技巧

分享了一些结构化游戏的个人技术

讨论如何保持代码的灵活性和易于改变

应用层次结构的重要性

依赖关系应该只向下流动,而不是向上或横向

每个对象都应该能够在隔离状态下工作

举例说明了玩家对象的灵活性

避免假设结构和关注点分离

保持事物分离意味着可以后期更改实现而不破坏代码

数据和操作数据的逻辑应该保持在一起

使用组件来划分代码,而不是创建大型单一对象

依赖注入的概念和应用

设计模式的重要性和学习优先级

状态机和观察者模式的重要性

迭代改进代码风格,不要一开始就追求完美

实践中学习,遇到问题时再研究解决方案

游戏开发和软件开发是长期学习的过程

Transcripts

play00:00

we've looked at some tips for writing

play00:01

cleaner code and I've even shown some of

play00:03

my own techniques for structuring games

play00:05

so now let's take a proper slightly

play00:07

higher level view of our application and

play00:09

discuss how we can keep our code

play00:10

flexible and more receptable to change

play00:12

this is not intended as an authoritative

play00:14

or exhaustive list by any means but

play00:17

rather a collection of small actionable

play00:19

tips that you can use to immediately

play00:20

improve your code base so let's first

play00:23

discuss the foundational idea behind

play00:25

several of the tips we'll discuss today

play00:26

the application hierarchy every

play00:28

application no matter how simple or how

play00:31

complex has some sort of a hierarchy and

play00:33

this hierarchy needs to be respected

play00:35

this means that dependencies should only

play00:37

flow down the hierarchy never up or

play00:39

laterally additionally we generally want

play00:42

to go down only one level for any

play00:43

dependencies this keeps our code

play00:45

properly decoupled and makes it easy to

play00:47

use various objects around the

play00:48

application wherever we need them since

play00:50

each object is able to work in isolation

play00:52

and has no dependencies on the state or

play00:54

structure of the application Beyond its

play00:56

realm of focus as an example of this

play00:58

principle let's consider the simple

play01:00

hierarchy of a small game if our player

play01:02

is completely isolated and only

play01:04

concerned with what it contains then we

play01:06

can put our player wherever we want

play01:07

other levels bonus game modes where the

play01:10

game structure is completely different

play01:11

or even as an interactive way of

play01:13

selecting levels from a menu but if the

play01:15

player always assumes that it will have

play01:17

a sibling in the tree named items that

play01:19

immediately breaks this flexibility as

play01:21

we now have to have an items object in

play01:22

the same level of the player in all

play01:24

locations whether or not we need it not

play01:26

only that but we've also locked

play01:28

ourselves into the implementation of our

play01:30

sibling and parent dependencies how much

play01:31

flexibility does our game really have if

play01:33

we always assume that there is an item

play01:35

sibling with coin objects as direct

play01:37

children and each one has a function

play01:39

called play sound effect this has

play01:40

nothing to do with the player

play01:41

specifically but if we change or break

play01:43

this dependency our player is now broken

play01:45

as well so that establishes why we don't

play01:48

want to assume structure above or around

play01:50

our object in the tree but why is going

play01:52

too far down a problem well really for

play01:54

the same reason Assumption of structure

play01:56

and separation of concerns if our player

play01:58

needs to change animations it can tell

play02:00

the animation container to do so but it

play02:02

shouldn't try to manipulate the

play02:04

individual frames of a specific

play02:05

animation and it definitely shouldn't

play02:07

try to alter the bitmap data itself as

play02:09

that is Way Beyond its scope keeping

play02:11

things separated means we can change our

play02:13

implementation later without breaking

play02:15

the player code maybe we originally used

play02:17

individual images to stitch into an

play02:19

animation and later switch over to a

play02:21

Sprite sheet the player which should

play02:22

really only be concerned with the

play02:23

overall high level operation of our

play02:25

character really doesn't need to know or

play02:27

care how the animation is played only

play02:29

that it has a way to request animation

play02:31

changes which really leads me right into

play02:33

my next Point generally speaking we want

play02:36

to keep data and the logic operating on

play02:38

those data together rather than asking

play02:40

an object for its data so we can operate

play02:42

on them separately there are exceptions

play02:44

to this rule of course such as maybe

play02:45

with some very generic utility functions

play02:48

but I'm going to keep it more specific

play02:49

to the player for instance so revisiting

play02:52

the previous example about the player's

play02:54

relationship to the animation container

play02:55

the player should tell the animation

play02:57

container the change animations but not

play02:59

manual annually access the data within

play03:01

the container and make any required

play03:03

changes as that moves the logic of

play03:05

changing animations outside of the

play03:06

animation oriented code and into an

play03:08

object that really shouldn't concern

play03:10

itself with how this transition occurs

play03:12

creating a function on the animation

play03:14

container the player can call keeps our

play03:15

animation related code where it belongs

play03:17

and ensures we can reason about the

play03:19

state of the application at any time as

play03:21

there are no external effects to be

play03:23

worried about for example if an

play03:25

animation isn't playing properly we know

play03:27

that the issue must be contained within

play03:28

the animation container not sitting in a

play03:30

random function somewhere else in our

play03:32

application this also keeps our code

play03:34

reusable and flexible since all the

play03:35

functionality we may need is contained

play03:37

in our animation object enemies NPCs and

play03:40

the player can all use one consistent

play03:42

interface to manage animations this rule

play03:44

may seem a bit obvious but these issues

play03:46

can easily sneak their way into the

play03:48

application when you really just need to

play03:50

make one small change this one time

play03:52

the previous rules really lead right

play03:55

into this next one rather than creating

play03:57

large monolithic objects to get all the

play03:59

functionality you need or going

play04:00

overboard with inheritance use

play04:02

components to compartmentalize smaller

play04:04

self-sufficient bits of code and compose

play04:06

complex objects out of those components

play04:08

for instance there's probably a lot of

play04:10

objects in your game that may have a

play04:12

health bar the player various enemies

play04:14

breakable crates and so on rather than

play04:16

having to manage separate variables and

play04:18

methods on each object to manage Health

play04:20

you could create a health component or

play04:22

even an entire stats component that

play04:24

houses and standards the data and

play04:26

functions you need now every item in

play04:28

your game that needs a health bar can

play04:29

use this component components can

play04:31

massively improve the organization and

play04:32

reusability of your code and also remove

play04:34

dead code by not having a lot of

play04:36

inheritance you don't need now let's

play04:39

talk dependency injection which is the

play04:41

technique of supplying references to

play04:43

objects at runtime rather than compile

play04:44

time it's a great way to make your code

play04:46

more reusable and flexible because sure

play04:48

it's easy to say that there's a

play04:50

hierarchy to follow and that certain

play04:52

design patterns can help you maintain

play04:54

that hierarchy but there are times when

play04:56

you do still need a reference to

play04:57

something that is outside of your direct

play04:59

descendants State machines come to mind

play05:01

for instance since they're generally

play05:02

owned by an object higher in the

play05:04

hierarchy than them but they need a

play05:06

reference to that parent object so that

play05:07

they can control it if we hard code this

play05:09

reference we'll have to duplicate our

play05:11

state machine for every object which not

play05:13

only is a pain in the butt but it also

play05:15

breaks the principles of keeping our

play05:16

code dry with dependency injection we

play05:19

can instead say that our state will

play05:20

control an object of Type X such as a

play05:22

rigid body and write our code around

play05:24

that type but without any specific

play05:27

references to the objects that will hold

play05:29

this state at runtime a parent object

play05:31

can pass a reference of itself to the

play05:33

state and that state will now control

play05:34

that specific object as if it knew about

play05:36

it all along that's dependency injection

play05:39

in a nutshell hopefully you can see just

play05:41

how useful it can be in a lot of

play05:42

different situations including the

play05:44

previously mentioned technique about

play05:46

compartmentalizing functionality in two

play05:48

components with components and

play05:49

dependency injection we can write

play05:51

generic code to control movement Health

play05:53

Ai and more that can flexibly operate on

play05:56

a variety of different objects as needed

play05:58

so now let's go even higher level and

play06:00

talk about design patterns and look I

play06:02

get it design patterns aren't the

play06:04

sexiest thing in the world to study but

play06:06

why bother Reinventing the wheel and

play06:08

probably doing a worse job of it when a

play06:10

lot of common issues software developers

play06:12

run into have already been solved it's

play06:14

worth it to take some time to at least

play06:16

learn the major ones but I'm not just

play06:18

going to leave it at telling you to go

play06:19

study software architecture although

play06:21

that's not the worst idea in the world

play06:22

if you've got Ambitions for systems

play06:24

driven games so here's what I would

play06:25

prioritize first off State machines I've

play06:28

talked about State machines multiple

play06:30

times already and I could probably

play06:31

squeeze out at least one more video on

play06:33

them if I felt compelled to so let that

play06:35

be a signal of just how strongly I feel

play06:37

you should be familiar with this pattern

play06:38

whether you prefer to use enumerators or

play06:41

more object-oriented code learn State

play06:43

machines even the simplest of games can

play06:45

often benefit from them next up I would

play06:47

learn about the Observer pattern State

play06:49

machines may be my number one pick but

play06:51

the Observer pattern is a very close

play06:53

second that has huge implications for

play06:55

making your code more flexible with

play06:56

Native support in most engines and

play06:58

programming languages which means you

play07:00

don't even have to put in a lot of time

play07:01

or effort into implementing it this is

play07:03

arguably the biggest bang for your buck

play07:05

when studying design patterns after

play07:07

those two patterns it kind of starts

play07:09

getting into a realm of what do you

play07:10

really need to study to learn about but

play07:12

the mediator pattern could be a good way

play07:14

to go after the Observer pattern I've

play07:16

not covered this one yet but it is

play07:18

coming so I'll just leave it at this if

play07:20

the Observer pattern is too one way for

play07:21

you this is the pattern you want to

play07:23

study and as I just mentioned once

play07:25

you're comfortable with a couple of

play07:26

these patterns you can start diving into

play07:28

specific issues you're running into or

play07:30

even just doing a full read of a book

play07:31

like game programming patterns or maybe

play07:33

a more generic software engineering book

play07:35

and seeing what sticks out to you and

play07:37

that's largely it for today but I do

play07:39

want to leave you with one more

play07:40

important tip it's okay to be iterative

play07:42

with improving your code style don't try

play07:44

to learn every design pattern right away

play07:46

and spend weeks at the start of your

play07:47

next project trying to make your code as

play07:49

perfect as possible before you even

play07:51

fully know what the requirements are or

play07:53

even have the smallest vertical slice

play07:55

possible too much Theory and not enough

play07:57

practice can just burn you out out or

play07:59

even lead to not really understanding

play08:01

the why and how because you've not put

play08:02

it into practice plus let's not forget

play08:04

that a game with mediocre code that

play08:06

ships is better than a perfectly coded

play08:08

game that never does it is okay to work

play08:10

until you hit a pain point and then

play08:12

research how to fix it and it can even

play08:13

be worth doing it wrong on a small

play08:15

project seeing what the final product

play08:17

looked like and what your Lessons

play08:18

Learned were and then researching how to

play08:20

do it better in the future since you'll

play08:22

have a better idea of the issues you're

play08:23

going to run into remember both game

play08:25

development and software development as

play08:27

a whole are entire career paths it takes

play08:30

time to learn how to tackle these

play08:31

problems

play08:32

[Music]

Rate This

5.0 / 5 (0 votes)

Related Tags
游戏开发代码优化应用层次依赖注入组件化状态机观察者模式迭代改进软件架构设计模式
Do you need a summary in English?