typescript工程_使用TypeScript构建游戏。 工程图网格2/5

本文档介绍如何使用TypeScript构建游戏,特别是关注工程的绘图网格部分。内容来源于对Medium上的一篇文章的翻译,探讨了游戏开发中TypeScript的应用。
摘要由CSDN通过智能技术生成

typescript工程

Chapter III in the series of tutorials on how to build a game from scratch with TypeScript and native browser APIs

系列教程的第三章,介绍如何使用TypeScript和本机浏览器API从头开始构建游戏

Hello there, and welcome back! This is the series of articles where we discuss how to build a simple turn-based game with TypeScript and native browser APIs! Chapter III is dedicated to building a grid for this game, other Chapters are available here:

您好,欢迎回来! 这是系列文章,我们讨论如何使用TypeScript和本机浏览器API构建简单的回合制游戏! 第三章致力于为该游戏构建网格,其他章节也可以在此处找到:

  • Introduction

    介绍

  • Chapter I. Entity Component System

    第一章实体组件系统

  • Chapter II. Game loop (Part 1, Part 2)

    第二章 游戏循环( 第1 部分第2部分 )

  • Chapter III. Drawing Grid (Part 1, Part 2, Part 3, Part 4, Part 5)

    第三章 工程图网格( 第1部分 ,第2部分,第3部分,第4部分,第5部分)

  • Chapter IV. Drawing ships

    第四章 绘图船
  • Chapter V. Interaction System

    第五章互动系统
  • Chapter VI. Pathfinding

    第六章 寻找路径
  • Chapter VII. Moving ship

    第七章 搬船
  • Chapter VIII. State Machina

    第八章 国家机械工业
  • Chapter IX. Attack System: Health and Damage

    第九章 攻击系统:生命与伤害
  • Chapter X. Winning and Losing the Game

    第十章。输赢
  • Chapter XI. Enemy AI

    第十一章 敌人AI

In the first part of this chapter, we successfully drew the grid. Canvas API was in great help for us then. However, the solution was rather dirty and not flexible. We simply put all the code into the single place: Game Entity. If we continue going this path, soon enough, our game script becomes enormously large and hard to maintain. Moreover, we wrote no tests last time, leaving ourselves without any insurance. In this post, we are going to improve our code and make it more maintainable and extendable.

在本章的第一部分中,我们成功绘制了网格。 那时,Canvas API对我们有很大帮助。 但是,该解决方案相当脏,而且不灵活。 我们只需将所有代码放在一个位置: Game实体。 如果我们继续走这条路,很快,我们的游戏脚本将变得非常庞大且难以维护。 此外,我们上次没有编写测试,因此没有任何保险。 在本文中,我们将改进代码,使其更易于维护和扩展。

There is something else we have to think about, aside from the code quality. At this point, all we did is drew a static image of the Grid. It has no functionality what’s so ever. In fact, the only dynamic part of the grid now is its size and color. But as we’ll see in future Chapters, Grid is much more than just an image. It is a vital part of the gameplay, and we need it to be ready to fulfill our growing needs. The Grid should become an Entity.

除了代码质量,我们还有其他需要考虑的问题。 至此,我们所做的只是绘制了 Grid 的静态图像 。 它没有任何功能。 实际上,网格的唯一动态部分就是它的大小和颜色。 但是,正如我们将在以后的章节中看到的那样,Grid不仅仅是一个图像。 这是游戏玩法中至关重要的一部分,我们需要它做好准备以满足不断增长的需求。 网格应成为实体。

Feel free to switch to the drawing-grid-1 branch of the repository. It contains the working result of the previous posts and is a great starting point for this one.

随时切换到存储库drawing-grid-1分支。 它包含了以前的帖子的工作结果,是此帖子的一个很好的起点。

目录 (Table of Contents)

  1. The Grid Entity

    网格实体
  2. Testing Game Entity with the Grid

    用网格测试游戏实体
  3. Introducing Node Entity

    节点实体介绍
  4. Writing our First Component

    编写我们的第一个组件
  5. Testing Node Entity

    测试节点实体
  6. Conclusion

    结论

网格实体 (The Grid Entity)

Image for post
Background vector created by pikisuperstar 由pikisuperstar创建的背景矢量

We coupled the grid code with the game entity. While the grid is indeed important for the game, it doesn’t mean they have to live together. It is time for us to define our very first child entity:

我们将网格代码与游戏实体结合在一起。 尽管网格对于游戏确实很重要,但这并不意味着它们必须生活在一起。 现在是时候定义我们的第一个子实体了:

// src/grid/grid.ts
import { Entity } from '@/utils'


export class Grid extends Entity { }

And add a barrel file for it:

并为其添加一个桶文件:

// src/grid/index.ts
export * from './grid'

We can now add this new entity as a child to the Game. Also, let’s ensure only game has write access to its children by making entities private field with a public getter:

现在,我们可以将此新实体添加为游戏的子代。 另外,我们通过使用公共获取程序将entities设为私有字段,确保只有游戏对其子级具有写权限:

// game.ts
import { Entity } from '@/utils'
import { Settings } from '@/settings'
import { Grid } from '@/grid' // <--- ADD


export class Game extends Entity {
  // ... //


  public Entities: Entity[] = [] // <--- REMOVE


  // --- ADD --- //
  private _entities: Entity[] = []


  public get Entities(): Entity[] {
    return this._entities
  }
  // --- ADD --- //


  public Awake(): void {
	super.Awake()
	
    // --- ADD --- //
    // instantiate and Grid to the list of children
    this._entities.push(new Grid())
    // --- ADD --- //


    // ... //
  }
  // ... //
}

And that’s all we have to do! The game will awake and update the Grid as we set up in previous posts.

这就是我们要做的! 游戏将按照我们先前文章中的设置唤醒并更新Grid。

用网格测试游戏 (Testing Game with Grid)

Image for post
Background vector created by freepik Freepik创建的背景矢量

Before we go any further, let’s update the Game’s test a bit. When we setup game.spec.ts, we also created a bunch of fake Entities:

在继续进行之前,让我们先更新一下游戏的测试。 设置game.spec.ts ,我们还创建了一堆假实体:

// game.spec.ts
// ... //
class E1 extends Entity { }		
class E2 extends Entity { }		
class E3 extends Entity { }		


describe('>>> Game', () => {
 // ... //
})

They served us well, but now we can make a step further. We know exactly what entity is a child of the Game: it’s the Grid. We don’t need a fake children anymore. This approach allows us to verify both:

他们为我们服务很好,但现在我们可以再进一步。 我们确切地知道哪个实体是Game的子Game :它是Grid 。 我们不再需要孩子了。 这种方法使我们可以验证两个:

  • The game works properly with its entities

    游戏与其实体正常工作
  • It works appropriately specifically with the Grid

    特别适用于Grid

I start by removing all fake entities from the spec:

首先,从规范中删除所有假实体:

// game.spec.ts
import { Game } from '@/game'
import { IComponent } from '@/utils' // <--- CHANGE
// ... //
// --- REMOVE --- //
class E1 extends Entity { }		
class E2 extends Entity { }		
class E3 extends Entity { }		
// --- REMOVE --- //


describe('>>> Game', () => {
  // ... //
  // --- REMOVE --- //
  const e1 = new E1()
  const e2 = new E2()
  const e3 = new E3()
  // --- REMOVE --- //


  beforeEach(() => {
    // ... //
    game.Entities.push(e1, e2, e3) // <--- REMOVE
    // ... //
  })


  // ... //
  
  // --- REMOVE --- //
  it('should awake all children', () => {
    const spy1 = jest.spyOn(e1, 'Awake')
    const spy2 = jest.spyOn(e2, 'Awake')
    const spy3 = jest.spyOn(e3, 'Awake')


    expect(spy1).not.toBeCalled()
    expect(spy2).not.toBeCalled()
    expect(spy3).not.toBeCalled()


    game.Awake()


    expect(spy1).toBeCalled()
    expect(spy2).toBeCalled()
    expect(spy3).toBeCalled()
  })


  it('should update all children', () => {
    const spy1 = jest.spyOn(e1, 'Update')
    const spy2 = jest.spyOn(e2, 'Update')
    const spy3 = jest.spyOn(e3, 'Update')


    expect(spy1).not.toBeCalled()
    expect(spy2).not.toBeCalled()
    expect(spy3).not.toBeCalled()


    game.Update()


    expect(spy1).toBeCalled()
    expect(spy2).toBeCalled()
    expect(spy3).toBeCalled()
  })


  // --- REMOVE --- //
})

Then, I add a new test to check all children are awakened and updated. Even though now we have only one child, the Grid, in future we can update the test when we introduce more children:

然后,我添加了一个新测试以检查所有孩子是否都被唤醒和更新。 即使现在只有一个孩子,即Grid,将来我们可以在引入更多孩子时更新测试:

// game.spec.ts
// ... //
describe('>>> Game', () => {
  // ... //
 
  it('should awake and update all children', () => { }) // <--- ADD
})

The test works similarly to the way fake entities test worked. First, I spy on Awake and Update method of the Grid. And then, I ensure they are called only after game.Awake and game.Update are executed respectively:

该测试的工作方式类似于假实体测试的工作方式。 首先,我监视Grid Awake and Update方法。 然后,确保它们仅在game.Awakegame.Update之后game.Awake调用:

// game.spec.ts
// ... //
import { Grid } from '@/grid' // <--- ADD


describe('>>> Game', () => {
  // ... //
  
  
  it('should awake and update all children', () => {
	// --- ADD --- //
    const spyGridAwake = jest.spyOn(Grid.prototype, 'Awake')
    const spyGridUpdate = jest.spyOn(Grid.prototype, 'Update')


    expect(spyGridAwake).not.toBeCalled()
    expect(spyGridUpdate).not.toBeCalled()


    game.Awake()
    expect(spyGridAwake).toBeCalled()


    game.Update()
    expect(spyGridUpdate).toBeCalled()
    // --- ADD --- //
  })
})

Note, I don’t have access to Grid instance since it’s created and encapsulated by the Game. But I can rely on Grid.prototype to access Grid methods without an actual instance.

注意,由于它是由游戏创建和封装的,因此我无权访问Grid实例。 但是我可以依靠Grid.prototype来访问Grid方法,而无需实际实例。

The code now should compile without errors if you run npm start. All test should pass if you run npm t

如果您运行npm start那么代码现在应该可以正确编译。 如果您运行npm t则所有测试均应通过

节点介绍 (Introducing Node)

We created a dedicated entity for the Grid.

我们为网格创建了一个专用实体。

Rendering the entire grid as a whole monolithic thing was a simple endeavor. Unfortunately, it’s not sufficient for us. We have to keep track of the individual rectangles of the Grid for different purposes: to highlight them and indicate where players can move their ships, to store information about the nearby nodes to make pathfinding possible, and so on.

将整个网格渲染为整体是一件简单的事情。 不幸的是,这对我们来说还不够。 为了不同的目的,我们必须跟踪网格的各个矩形: 突出显示它们并指示玩家可以其船移动到哪里,存储有关附近节点的信息以使寻路成为可能,等等。

Image for post
Business vector created by fullvector 商业矢量由fullvector创建

We made the first step to make this tracking possible: we created an entity for the Grid. Now we should create an entity for every Node of the Grid:

我们迈出了第一步,以使这种跟踪成为可能:我们为Grid创建了一个实体。 现在,我们应该为Grid每个Node创建一个实体:

// src/node/node.ts
import { Entity } from '@/utils'


export class Node extends Entity { }

And let’s not forget about the barrel file:

并且不要忘了桶文件:

// src/node/index.ts
export * from './node'

Like any other entity, it should take its place in the hierarchy of the game objects. It feels natural to make it a child of the Grid. We then can perform a bulk operations on all nodes within the Grid.

像任何其他实体一样,它应在游戏对象的层次结构中占据一席之地。 使它成为Grid的子代是很自然的。 然后,我们可以在Grid内的所有节点上执行批量操作。

The process of adding children to the Grid is the same we did for the Game. I define the private field and a public getter:

将子级添加到Grid的过程与我们在Game所做的相同。 我定义了私有字段和公共获取者:

// src/grid/grid.ts
import { Node } from '@/node' // <--- ADD
// ... //
export class Grid extends Entity {
  // --- ADD --- //
  private _nodes: Node[] = []


  public get Nodes(): Node[] {
    return this._nodes
  }
  // --- ADD --- //
}

The grid should awake and update all nodes:

网格应苏醒并更新所有节点:

// src/grid/grid.ts
// ... //
export class Grid extends Entity {
  // ... //


  // --- ADD --- //
  public Awake(): void {
    // awake components
    super.Awake()


    // awake children
    for (const node of this._nodes) {
      node.Awake()
    }
  }


  public Update(deltaTime: number): void {
    // update components
    super.Update(deltaTime)


    // update children
    for (const node of this._nodes) {
      node.Update(deltaTime)
    }
  }
  // --- ADD --- //
}

Note, we should call super.Awake() and super.Update() to allow abstract Entity to do its default work and awake and update components. We have none yet, but we will add some in future chapters.

注意,我们应该调用super.Awake()和super.Update()以允许抽象实体执行其默认工作以及唤醒和更新组件。 我们还没有,但是我们将在以后的章节中添加一些内容。

Great, but how would we draw the Node? Should we add drawing functionality to the Node Entity’s Awake method? We could, yet it wouldn’t be a flexible solution.

很好,但是我们将如何绘制节点? 我们是否应该向Node实体的Awake方法添加绘图功能? 我们可以 ,但它不是一个灵活的解决方案。

What if there are a few ways to draw a Node? Let’s say, some nodes would be circles instead of rectangles. Or maybe we do not even want to draw some nodes at all, make them invisible? To achieve that flexibility we could use conditions:

如果有几种绘制Node怎么办? 假设某些节点是圆形而不是矩形。 也许我们甚至根本不想画一些节点,使它们不可见? 为了实现这种灵活性,我们可以使用以下条件:

// pseudo code
if(this.isRect){
  this.drawRectNode()
} else if(this.isCircle){
  this.drawCircleNode()
} else { 
  // do not draw at all?
}

That way, we have all drawing logic within Node: drawRectNode, drawCircleNode.

这样,我们在Node拥有所有绘制逻辑: drawRectNodedrawCircleNode

But we have a much more powerful tool under our belt: Components. We can define different components: RectangleDraw, CircleDraw, and attach only necessary ones. Or do not even assign any if we want to skip drawing. Moreover, we can do that in realtime! Following this approach, we decouple drawing logic from the core logic of the Node.

但是,我们拥有一个强大得多的工具: Components 。 我们可以定义不同的组件: RectangleDrawCircleDraw和仅附加必要的组件。 或者,如果我们要跳过绘图,甚至不分配任何内容。 而且,我们可以实时做到这一点! 按照这种方法,我们将绘制逻辑与Node的核心逻辑解耦。

编写我们的第一个组件 (Writing our First Component)

Image for post
Business photo created by d3images d3images创建的商业照片

We will start small and add oneDraw Component to the node that will handle the drawing logic for us. Yet, we keep options open. If we need different drawing components, we can quickly create them without affecting the existing code.

我们将从小处开始,并将一个Draw Component添加到将为我们处理绘制逻辑的节点。 但是,我们保持选择开放。 如果我们需要不同的图形组件,则可以快速创建它们而不会影响现有代码。

Note how nicely this approach plays with SOLID ‘open-close’ principle: we keep our code open for extension.

请注意,这种方法与SOLID“打开-关闭”原理的配合效果非常好:我们保持代码开放以进行扩展。

Component is any class that conforms to IComponent:

Component是符合IComponent任何类:

// src/node/components/draw/draw.ts
import { IComponent } from '@/utils'
import { Node } from '@/node'


export class NodeDrawComponent implements IComponent {
  public Entity: Node


  public Awake(): void {
    // to implement
  }


  public Update(deltaTime: number): void {
    // to implement
  }
}

First, I defined a class that implements IComponent. The interface requires specifying which entity the component can be attached to. In this case, it’s the Node entity. We also have to implement Awake and Update per IComponent requirements.

首先,我定义了一个实现IComponent的类。 该接口要求指定组件可以附加到哪个实体。 在这种情况下,它是Node实体。 我们还必须根据IComponent要求实现AwakeUpdate

Don’t forget to add and update necessary barrel files:

不要忘记添加和更新必要的桶文件:

// src/node/components/draw/index.ts
export * from './draw'


// src/node/components/index.ts
export * from './draw'


// src/node/index.ts
export * from './components' // <--- ADD
export * from './node'

Note, I defined a dedicated folder for components and even a separate folder for draw component. This folder structure will keep going on throughout the series.

注意,我为组件定义了一个专用文件夹,甚至为绘图组件定义了一个单独的文件夹。 此文件夹结构将在整个系列中继续进行。

All is left for us is to add NodeDrawComponent to the Node:

我们剩下NodeDrawComponent就是将NodeDrawComponent添加到Node

// src/node/node.ts
import { Entity } from '@/utils'
import { NodeDrawComponent } from './components' // <--- ADD


export class Node extends Entity {
  // --- ADD --- //
  public Awake(): void {
    this.AddComponent(new NodeDrawComponent())


    super.Awake()
  }
  // --- ADD --- //
}

测试节点 (Testing Node)

Image for post
The watercolor vector created by milano83 milano83创建的水彩矢量

Before we wrap up this post, let me add some tests. NodeDrawComponent is empty now, so there is nothing yet to test. But we can test Node.

在结束这篇文章之前,让我添加一些测试。 NodeDrawComponent现在为空,因此尚无要测试的内容。 但是我们可以测试Node

// src/node/node.spec.ts
import { Node } from './node'


describe('>>> Node', () => {
  let node: Node


  beforeEach(() => {
    node = new Node()
  })
})

Node is rather modest in its functionality now. All it does is add NodeDrawComponent. Allow me to be a bit more generic and create a test suite for all components, even though we have only one now:

Node功能相当适中。 它所做的只是添加NodeDrawComponent 。 让我更通用一些,为所有组件创建一个测试套件,即使现在只有一个:

// src/node/node.spec.ts
// ... //
describe('>>> Node', () => {
  // ... //
  // --- ADD --- //
  it('should awake and update all Components', () => { })
  // --- ADD --- //
})

Testing communication with NodeDrawComponent should sound familiar. We don’t have access to the instance of a component, but we can spy on its prototype. We then use the spy to verify component indeed is awakened and updated when the entity is awakened and updated:

NodeDrawComponent测试通信NodeDrawComponent应该很熟悉。 我们无权访问组件的实例,但是我们可以监视其原型。 然后,当实体被唤醒和更新时,我们使用间谍来验证组件是否确实被唤醒和更新:

// src/node/node.spec.ts
// ... //
import { NodeDrawComponent } from './components' // <--- ADD


describe('>>> Node', () => {
  // ... //
  it('should awake and update all Components', () => {
    // --- ADD --- //
    const spyDrawCompAwake = jest.spyOn(NodeDrawComponent.prototype, 'Awake')
    const spyDrawCompUpdate = jest.spyOn(NodeDrawComponent.prototype, 'Update')


    expect(spyDrawCompAwake).not.toBeCalled()
    expect(spyDrawCompUpdate).not.toBeCalled()


    node.Awake()
    expect(spyDrawCompAwake).toBeCalled()


    node.Update(0)
    expect(spyDrawCompUpdate).toBeCalled()
    // --- ADD --- //
  })
  // ... //
})

If you start your code with npm start, it should compile without errors. If you run tests with npm t, they should pass as well.

如果您使用npm start启动代码,那么它应该编译没有错误。 如果使用npm t运行测试,它们也应该通过。

You can find the complete source code of this post in the drawing-grid-2 branch of the repository.

您可以在存储库drawing-grid-2分支中找到此文章的完整源代码。

结论 (Conclusion)

Awesome! We did a lot, but nothing changed on the screen: we still can only see the old “dirty” drawing of the Grid. But we accomplished a lot: we learned how to test the functionality of the class without creating the instance, we introduced two new entities: Grid and Node, decoupled them from the Game and even set up our very first component!

太棒了! 我们做了很多事情,但屏幕上没有任何变化:我们仍然只能看到Grid的旧“脏”图。 但是我们完成了很多工作:我们学会了如何在不创建实例的情况下测试类的功能,引入了两个新实体: GridNode ,将它们与Game分离,甚至建立了我们的第一个组件!

In Part 3, we will work closely with NodeRawComponent and get rid of the dirty draw once and for all. And we’ll meet a new friend who will help us with this (and keep helping a lot in the future!): Vector2D.

在第3部分中,我们将与NodeRawComponent紧密合作,并NodeRawComponent地摆脱脏污 。 我们将遇到一个新朋友,它将为我们提供帮助(并在将来继续提供帮助!): Vector2D

If you have any comments, suggestions, questions, or any other feedback, don’t hesitate to send me a private message or leave a comment below! Thank you for reading, and I’ll see you next time!

如果您有任何意见,建议,问题或其他反馈,请随时给我发送私人消息或在下面发表评论! 感谢您的阅读,下次再见!

This is Chapter III in the series of tutorials “Building a game with TypeScript”. Other Chapters are available here:

这是系列教程“ 使用TypeScript构建游戏 ”的第三章 其他章节可在此处找到:

  • Introduction

    介绍

  • Chapter I. Entity Component System

    第一章实体组件系统

  • Chapter II. Game loop (Part 1, Part 2)

    第二章 游戏循环( 第1 部分第2部分 )

  • Chapter III. Drawing Grid (Part 1, Part 2, Part 3, Part 4, Part 5)

    第三章 工程图网格( 第1部分 ,第2部分,第3部分,第4部分,第5部分)

  • Chapter IV. Drawing ships

    第四章 绘图船
  • Chapter V. Interaction System

    第五章互动系统
  • Chapter VI. Pathfinding

    第六章 寻找路径
  • Chapter VII. Moving ship

    第七章 搬船
  • Chapter VIII. State Machina

    第八章 国家机械工业
  • Chapter IX. Attack System: Health and Damage

    第九章 攻击系统:生命与伤害
  • Chapter X. Winning and Losing the Game

    第十章。输赢
  • Chapter XI. Enemy AI

    第十一章。 敌人AI

翻译自: https://medium.com/javascript-in-plain-english/building-a-game-with-typescript-drawing-grid-2-5-206555719490

typescript工程

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值