守望者--AIR技术交流

 找回密码
 立即注册

QQ登录

只需一步,快速开始

扫一扫,访问微社区

搜索
热搜: ANE FlasCC 炼金术
查看: 293|回复: 0

[H5周边技术] 用 TypeScript 写一个贪食蛇小游戏

[复制链接]
  • TA的每日心情
    擦汗
    2018-4-10 15:18
  • 签到天数: 447 天

    [LV.9]以坛为家II

    1742

    主题

    2094

    帖子

    13万

    积分

    超级版主

    Rank: 18Rank: 18Rank: 18Rank: 18Rank: 18

    威望
    562
    贡献
    29
    金币
    51756
    钢镚
    1422

    开源英雄守望者

    发表于 2016-8-3 11:44:10 | 显示全部楼层 |阅读模式

    TypeScript & VS code

    已经忘了是如何缘结 TypeScript 的了,应该是偶然使用过 Visual Studio Code 这款优秀的 IDE 才认识 TS 的吧。目前还是 Preview 版本的 VS code,集众多优秀卓越的功能特性,已经深深地征服了我,在它身上看不到一点以往对微软的那种繁冗晦涩质感,取而代之的是轻灵优美且功能上不乏先进之处。有关 VS code 这是后话,下面先说说 TypeScript。

    TypeScript & CoffeeScript

    提到它很多人会拿 CoffeeScript 与之比对,我作为两者都使用过的过来人,简单介绍下两者的异同:

    在我看来相同点只有一点:都是 JavaScript Compiler 的定位,有点类似于 Sass, Less 之于 CSS。

    不同点很多:

    1. TypeScript 是 JavaScript 的超集,这使得它能够与普通的 JavaScript 混用,而 CoffeeScript 使用自己的那一套类 Ruby 的语法使得这是不可能滴;
    2. TypeScript 玩的概念比较多,比如 Module, Interface 等,而 CoffeeScript 自己实现的语法糖则比较多;
    3. TypeScript 已经开始支持 ES6,CoffeeScript 会不会支持和什么时候支持,还都是未知数;
    4. 最大的一点不同:TypeScript 在编译过程中可对类型进行检查,将 JavaScript 这个灵活的动态型语言变成了静态类型的语言。算是有利有弊吧:好处是相当于将部分的“测试”工作提前了,问题的定位也更加精准;坏处是丧失了一点灵活性与增加了一些代码量。

    TypeScript & Go

    不管怎样,我对 TypeScript 初识就有好感。这种好感源自之前看到过的关于 Go 语言的介绍系列:build web application with golang

    虽然我不是后端工程师,但冲着它优雅的语法,我在当下是很有冲动来学习 Go 的。

    TypeScript 在某些方面与 Go 很像,是我喜欢的部分(也许是我接触的静态型语言实在太少)。比如定义一个取两数较大值的函数:

    Go:

    func max(a, b int) int {
        if a > b {
            return a
        }
        return b
    }

    TypeScript:

    function max(a: number, b: number): number {
        if (a > b) {
            return a
        }
        return b
    }

    Go 的自定义类型:

    type Human struct {
        name string
        age int
        phone string
    }
    
    var person Human

    TypeScript 的 Type Interface:

    interface Human {
        name: string
        age: number
        phone: string
    }
    
    var person: Human

    以上。我认为静态类型的好处,不止在于它在编译时提前找出错误,还在于它可以在你程序设计初期,帮助你去理清思路。

    贪食蛇

    下面进入正题,看看如何用 TypeScript 实现一个简单的贪食蛇小游戏。

    Snake Game

    首先,我们看构成这个游戏的要素,主要由这三个部分组成:

    1. 地板: Floor
    2. 蛇: Snake
    3. 食物: Food

    可以看到,它们的组成单位,其实是一样,都是由单个“块”组成的:Floor 是由纵横两个维度的块组成,Snake 是由一列块组成,而 Food 是单个块。块是这个游戏世界构建的基本单位,我们改如何来实现这个块呢?这至关重要。

    简单来分析下,从图中可看出,块有三种颜色,分别是:

    1. Floor: 白色
    2. Snake: 黑色
    3. Food: 红色

    可以对全体的块进行三种分类,每类有相应的样式:

    const FLOOR = {
        SPACE: 'space',
        BODY: 'body',
        FOOD: 'food'
    }
    .space {
        background-color: white;
    }
    
    .body {
        background-color: black;
    }
    
    .food {
        background-color: red;
    }

    其次,我们要让块动起来,使蛇移动,还要记录块的位置信息,即为横向和纵向上的 unique 坐标。

    最后,要用 JS 操作的介质,无非是承载着这些“块数据”的 DOM,块自身的颜色的变化,需要通过改变其对应的 DOM 元素的样式来实现。

    这样我们其实对块实现已经有了基本的想法:

    interface Block {
        pos: Pos
        type: string
        node: HTMLElement
    }

    这里由于块坐标我们后面用的很多,所以就定义了一个 Pos 类型:

    interface Pos {
        x: number
        y: number
    }

    你会发现,在 TypeScript 的世界中,创建一个类型是多么地随心所欲和不费力气。

    好了,我们已经有了构建世界的基本粒子了,可以开始创建 Floor 和 Snake 了(先不管 Food,还没到这一步)。创建完后,再让 Snake 能够“动起来”,并且通过键盘的“上”、“下”、“左”、“右”键可控制其方向,就算完成大半了。

    创建 Floor 类:

    class Floor {
        private table: HTMLTableElement
        private parent: HTMLElement
        private row: number
        private col: number
        public blocks: Block[] // 提供给 Snake 使用的 block 集合
    
        constructor(options?) {
            options = options || {};
            this.table = document.createElement('table')
            this.parent = options.parent || document.body
            this.row = options.row || 20
            this.col = options.col || 20
            this.blocks = []
        }
    
        initialize() {
            let x: number
            let y: number
    
            for (y = 0; y < this.row; y++) {
                let tr = <HTMLTableRowElement>this.table.insertRow(-1)
                for (x = 0; x < this.col; x++) {
                    let td = <HTMLTableCellElement>tr.insertCell(-1)
                    td.className = FLOOR.SPACE
                    this.blocks.push({
                        node: td,
                        type: FLOOR.SPACE,
                        pos: {x: x, y: y}
                    })
                }
            }
    
            this.parent.appendChild(this.table)
        }
    }

    创建 Snake 类:

    class Snake {
        private initLength: number
        private bodies: Block[]
        private speed: number
    
        constructor(options?) {
            options = options || {}
            this.initLength = options.initLength || 3
            this.speed = options.speed || 300
            this.bodies = []
        }
    
        born() {
            for (let i = this.initLength - 1; i >= 0; i--) {
                this.bodies.push(floor.blocks[i]) // floor 是 Floor 的一个实例
            }
    
            this.bodies.forEach(body => {
                body.type = FLOOR.BODY
                body.node.className = body.type // 着色
            })
        }
    }

    好了,new 一个 Snake 试一下,是否有一只“三节蛇”已赫然印入眼帘。

    let snake = new Snake()
    snake.born()

    加上一个 move 方法让它动起来:

    class Snake {
        ...
        move() {
            let head: Block = this.bodies[0]
            let tail: Block = this.bodies[this.bodies.length - 1]
            let next: Block = this.sbling(head) // 获取 head 右侧的 block
    
            // body move
            for (let i = this.bodies.length - 1; i > 0; i--) {
                this.bodies[i] = this.bodies[i - 1]
            }
    
            next.type = FLOOR.BODY
            this.bodies[0] = next
    
            // clear original tail
            tail.type = FLOOR.SPACE
            tail.node.className = tail.type
    
            // change color of blocks
            this.blocks.forEach(block => {
                block.node.className = block.type
            })
        }
    
        sbling(source: Block): Block {
            return this.blocks.filter((block) => {
                if (source.pos.x + 1 === block.pos.x
                && source.pos.y === block.pos.y) {
                    return true
                }
            })[0]
        }
    }

    以上几句代简单码完成后,只要在 born 方法中加定计时任务,就可以使我们的小蛇向右跑起来了:

    born() {
        ...
        // keep moving
        setInterval(function() { this.move(); }.bind(this), this.speed)
    }

    接下来加上键盘控制事件之前,要先对 sbling 方法进行改造,因为移动过程中的下一个块 next: Block,要根据其移动方向来获得了。

    const enum Direction {
        left, up, right, down
    }
    
    class Snake {
        ...
        private direction: Direction
        private offsets: Array<number[]>
    
        constructor(options?) {
            ...
            this.direction = Direction.right
            this.offsets = [[-1, 0], [0, -1], [+1, 0], [0, +1]]
        }
        ...
    
        move() {
            let head: Block = this.bodies[0]
            let tail: Block = this.bodies[this.bodies.length - 1]
            let next: Block = this.sbling(head, this.direction)
            ...
        }
    
        sbling(source: Block, direction: Direction): Block {
            return this.blocks.filter((block) => {
                if (source.pos.x + this.offsets[direction][0] === block.pos.x
                && source.pos.y + this.offsets[direction][1] === block.pos.y) {
                    return true
                }
            })[0]
        }
    }

    正式加上键盘事件,齐活了:

    born() {
        ...
        let keyHandler = (e: KeyboardEvent): void  => {
            const keyCode: number = e.keyCode || e.which || e.charCode
    
            switch (keyCode) {
                case KeyCode.left:
                    if (this.direction !== Direction.right) {
                        this.direction = Direction.left
                    }
                    break
                case KeyCode.up:
                    if (this.direction !== Direction.down) {
                        this.direction = Direction.up
                    }
                    break
                case KeyCode.right:
                    if (this.direction !== Direction.left) {
                        this.direction = Direction.right
                    }
                    break
                case KeyCode.down:
                    if (this.direction !== Direction.up) {
                        this.direction = Direction.down
                    }
                    break
            }
        }
    
        document.addEventListener('keydown', keyHandler, false)
    }

    这下你可以操纵这条三节蛇满地跑了,有点意思。缺点意思的是:一没食物二到处碰壁死不了。别急,首先解决食物是怎么生成的:

    class Floor() {
        ...
        genFood() {
            // 在地板内的随机位置
            let pos: Pos = {
                x: Math.floor(Math.random() * this.col),
                y: Math.floor(Math.random() * this.row)
            }
    
            // 根据位置获取食物 block
            let food = this.blocks.filter((block) => {
                if (block.pos.x === pos.x && block.pos.y === pos.y) {
                    return true
                }
            })[0]
    
            food.type = FLOOR.FOOD
            food.node.className = food.type
        }
    }

    生成食物在蛇一出生就执行一次,随后,在蛇移动的过程中,每吃到一次食物,就重新再生成一次食物:

    class Snake {
        ...
        born() {
            ...
    
            // generate food
            floor.genFood()
    
            // keep moving
            setInterval(function() { this.move(); }.bind(this), this.speed)
        }
    
        move() {
            ...
    
            if (next.type === FLOOR.FOOD) {
                this.eat(next)
            }
    
            // body move
            for (let i = this.bodies.length - 1; i > 0; i--) {
                this.bodies[i] = this.bodies[i - 1]
            }
    
            ...
        }
    
        eat(block: Block) {
            this.bodies.push(block)
            floor.genFood()
        }
    }

    好,最后我们让这条长生不死的神蛇落入生死轮回的凡界。仔细思考下,它的死因有两种:一、碰壁(下一个块不存在);二、吃到自己的身体(贪食而亡),那代码实现的方式就很简单咯:

    move() {
        let head: Block = this.bodies[0]
        let tail: Block = this.bodies[this.bodies.length - 1]
        let next: Block = this.sbling(head, this.direction)
    
        if (!next || next.type === FLOOR.BODY) {
            this.die()
            return
        }
    
        if (next.type === FLOOR.FOOD) {
            this.eat(next)
        }
    
        ...
    }

    至此,简单的贪食蛇小游戏就基本完成了。当然,后续还需要许多优化:比如生成的食物块刚好是蛇的身体怎么办?比如在蛇的一次 move 中频繁多次触发键盘事件,direction 到底取哪一次?在这里就不展开了,感兴趣的可直接看项目源码

    总结

    最后提一下,为了使逻辑更加清晰以及日后方便扩展维护,我又抽离了一个 Model 类,专门用来做 Floor 和 Snake 的纽带,专门负责操作管理 block 集合。因为用 TypeScript 来定制实现模块实在是太方便了,提供了众多方法还有继承 (extends) 和实现 (implements) 等概念,感觉就像摆在你的面前一大堆各式各样的工具,都不知道该挑一把锤子或是一只镊子。

    照这么说,用 TypeScript 写一些小规模的项目确实有一种打蚊子用大炮的赶脚,而且,有时候灵活度也会下降:比如贪食蛇中对 block 集合关系管理,由一个 block 去获取到它相邻位置的 block,在这一点上要是用上一点点 JavaScript 动态语言的黑魔法的话实现起来会简单的多:

    this.blocks[[x, y]] = {
        node: td,
        type: FLOOR.SPACE,
        pos: [x, y]
    }

    这样你会发现 this.blocks 可通过两个维度来获取到相应的 block:

    this.blocks[0]
    this.blocks[this.blocks.length - 1]
    
    this.blocks[[1, 2]]
    this.blocks[this.blocks[1].pos]

    也只有在动态的弱语言里能做到这一点。但简单方便之余带来的副作用是不太好理解,所以如果是一些团队协作的项目,我个人的建议是宁可放弃掉一些实现效率,为了今后的可维护性和扩展性,尽量写的清晰一些,不要爽了自己却坑了队友。TypeScript 在这一点上是符合我的理念的,此处如果真的使用了这项黑魔法它会报错:An index expression argument must be of type 'string', 'number', 'symbol', or 'any',不会让你走捷径胡来了。

    随着项目的复杂程度递增,其优势也愈发明显。光光静态类型检测就可以将多少潜在问题杀死在编译阶段了。

    而回头看我们的互联网世界,恰巧碰上富应用的时代,而且随着这几年计算机硬件性能提升,加上 JavaScript 自身的不断优化,我相信我们的应用还会对交互的要求越来越高,对代码的复杂程度也会越来越高。应用自身的规模也越来越大,特别是一些跨平台的应用。这让 TypeScript 的优势愈加明显。

    查看原文:http://roshanca.com/2015/write-a-simple-snake-game-with-typescript

    守望者AIR技术交流社区(www.airmyth.com)
    回复

    使用道具 举报

    您需要登录后才可以回帖 登录 | 立即注册

    本版积分规则

    
    关闭

    站长推荐上一条 /4 下一条

    QQ|手机版|Archiver|网站地图|小黑屋|守望者 ( 京ICP备14061876号

    GMT+8, 2019-10-23 01:10 , Processed in 0.048423 second(s), 36 queries .

    守望者AIR

    守望者AIR技术交流社区

    本站成立于 2014年12月31日

    快速回复 返回顶部 返回列表