Write in the beginning

These years of work, I have been writing too many routine business, most of which are not background system or small program, and then H5 etc. Recently, the company is using maps of various manufacturers to do the development of relevant maps, but they are also related to routine business. About half a year ago, I wanted to write something related to the game, sometimes I also pay attention to this aspect of things, just happened to have a holiday on New Year’s Day, I rubbed a snake to play, incidentally write an article (I have not written before, mainly I do not want to write things).

Don’t say anything. Let’s start with the renderings, okay

Ps :UI map I was found on the Internet, and their OWN PS processing, if which design greatly said I stole the map, here to explain the sorry ha, I do not make commercial use, is pure writing articles.

Before looking at the code, take a look at the basics of the game

instructions

Snake is a classic game, played many times when I was a child, of course, now there are many complex snake all kinds of effects, I here is to achieve the basic things and simple effects.

Relationship between

classDiagram Canvas <|-- Scene Canvas <|-- Events Scene <|-- SnakeGame Scene <|-- Food Scene <|-- Snake Scene <|-- Canvas class Canvas{drawText() drawImage() on() remove() get layer() get events()... Scene: Scene class Scene{add() join() loadSource() update() clear()... } SnakeGame: game class SnakeGame{initGame() start() _registerAction() _loadImgSource()... } class listeners {-listeners on() remove() findListenerByKey() } class Food{ setScene() draw() collideCheck() } class Snake{ setScene() draw() collideCheck() } class BackGround{ setScene() draw() collideCheck() }

Class introduction

  • The Canvas class provides basic drawing capabilities
  • The Events class provides event registration and message correlation
  • Scene class registers the Scene, inherits the basic canvas capabilities, and provides the ability to load resources, redraw scenes, add members, enter scenes and so on
  • The main process of the SnakeGame class, which inherits from Scene, contains game-related event registration, behavior registration, and execution logic related to various game initializations
  • Food class, BackGround class, Snake class contains DRAW drawing, collision detection and so on to achieve the SnakeService.common interface

Okay, that’s a lot of talk, so it’s time for code

Ps: If the big guy and leisure refined see, there is a bad place to write, please help correct, thank you!

Based on the canvas

class Canvas {
  constructor(config? : CanvasTypes.constructorParam) {
    CreateElement ('canvas')
    / /...
    this._canvas = canvas;
    this._ctx = canvas? .getContext('2d');
    this._events = new Events();
  }
  get layer() {
    return this._canvas;
  }
  get events() {
    return this._events;
  }
  getLayerWH(): CanvasTypes.rect {
    return {
      width: this._canvas.width,
      height: this._canvas.height,
    };
  }
  /** * Register events/add events to canvas *@param key
   * @param listener
   * @param IsAddLayer whether to add events to canvas. Default is false */
  on(key: string, listener: EventListenerOrEventListenerObject, isAddLayer: boolean = false){}remove(key: string) {}
  setGlobalCompositeOperation(key? :string) {}
  setAttributes(obj: CanvasTypes.attributes) {}
  drawRect(color: string, rectPram: CanvasTypes.rectPram) {}
  clearRect(rectPram: CanvasTypes.rectPram) {}
  drawText(option: CanvasTypes.text) {}
  rotate(num: number) {}
  translate(x: number, y: number) {}
  /** * Adds the image to the canvas *@param Img adds the object *@param S1 The object clipping rule * to add@param P2 to add object placement rules *@param Gco adds the object compositeOperation rule, referring to canvas's GlobalCompositeOperation property */
  drawImage(obj: CanvasTypes.drawImage){}}Copy the code

This class has nothing to say about it, except that it provides some of the capabilities of the Canvas, but it is encapsulated

Scene Scene class


class Scene extends Canvas {
   // Remove the attribute and constructor code and place it at.......

  /** * Add things to the scene *@param member* /
  add(member: any) {
    if (!Reflect.has(member, 'setScene')) {
      throw Error('Members joining the scene must have the setScene function');
    }
    if (!Reflect.has(member, 'draw')) {
      throw Error('Members joining the scene must have the draw function');
    }
    member.setScene(this);
    member.draw();
    this._memberContainer.add(member);
  }

  /** * Add other scenarios *@param scene* /
  join(scene: ScenePo) {
    scene.drawImage({
      // The argument to be passed in...
      });
  }
  /** * Add the loaded resource *@param sourceData* /
  addSourceMap(sourceData: Map<any.any>) {
    this.sourceMap = sourceData;
  }
  /** * Simple image loading resource *@param The URL can be a web resource or an image file named xxx.png * in images@param callback* /
  loadSource(url: string, callback: callback) {
   // Scenarios load resources with...
  }
  // Update the scene by calling the member drawing method draw within the scene
  update() {
    this.clear();
    forEach(this._memberContainer, item= > {
      item.draw();
    });
  }

  / / clean up
  clear() {
    this.clearRect({
     // ...}); }}Copy the code

A scene class acts like a container that you can add members to and that you can add to another container. At the same time, there are all the resources that members need to present in the scene, as well as the way to update and redraw the scene and members, no scene mind their own business

The Food Food class


class Food implements SnakeService.common {
  // remove the attribute code, occupy the location.......

  /** * Set the scene *@param scene* /
  setScene(scene: ScenePo) {
    this.scene = scene;
  }

  _updateCollideTag(status: boolean) {
    this._isCollided = status;
  }

  collideCheck(snake: SnakePo) {
      // Calculate collision with snake...
  }
 
  _calFoodPosition(): CanvasTypes.position {
      // Calculate the position to ensure that the position appears within the scope of the placed scene....
  }
  
  _getFoodImg(position: CanvasTypes.position, img? :any) {
    // Get the food map...
  }
  reload() {
    this._calFoodPosition();
  }
  /** * Draw food without collision */
  _drawOld() {
    this.scene.drawImage(this._getFoodImg({ ... this.position })); }draw() {
    // There is no collision and the position remains unchanged
    if (this.position? .x && !this._isCollided) {
      this._drawOld();
      return;
    }
    const _position = this._calFoodPosition();
    this.scene.drawImage(this._getFoodImg({ ..._position }));
  }
}

Copy the code

Food, snakes and the background for implementation of the interface is the same, there are methods of the draw, setScene, just realize logic is not the same, but to explain the background of the instance and the snake and the instance of food is not in the same scene, but in the end to synthesize into a picture again, because of the dynamic variation of the background is not, I’m just going to keep it in one state, so I’m not going to paste it, but I’m just going to paste the snake.

snakes

class Snake implements SnakeService.common {
   // Remove the attribute code and part of the logic code, occupied location.......
  
  /** * set the forward direction *@param direction* /
  setDirection(direction: SnakeService.direction) {
    this.direction = direction; } _getBodyItem(data? : position): position {// Get body block...
  }
  /** * Set the scene *@param scene* /
  setScene(scene: ScenePo) {
    this.scene = scene;
  }
  _getSnakeLastBody(): position {}
  _addHeader(body: position) {
    body.direction = this.direction;
    this.body.unshift(body);
  }
  // Here is the behavior...
  up() {}
  down() {}
  left() {}
  right() {}

  _getBodyImg(position: CanvasTypes.position, img? :any) {}
  
  _rotateRange(name: string, data: position) {
    // Get the orientation of img for body, head, and tail
  }
  draw() {
     / / draw...
  }
  / * * * note: the body is composed of each had to * originally want to deal with mobile way thinking is the overall move, after the movement when a before into a position, but doing so will cycle processing data * eventually made the move on the vision, is each operation will be the last piece to the front, the visual effect of mobile * /
  _changeBody() {
    const _header = utils.findFirstItem(this.body);
    const _body = this._getSnakeLastBody();
    if (_body) {
      switch (this.direction) {
       // Set the body..... based on the movement direction}}this.body.unshift(_body);
  }
  /** * boundary detection *@returns* /
  _checkOverSide(): boolean {
    // Determine the result of hitting a wall
    // Scope of the scenario
    const { width, height } = this.scene;
    const { x, y } = utils.findFirstItem(this.body);
    return x <= 0 || x >= width || y <= 0 || y >= height;
  }

  /** * Detect eating snake */
  _checkEatSelf(): boolean {}
  // Collision detection
  collideCheck(food: FoodPo) {
    const _isOverSide = this._checkOverSide();
    const _isEatSelf = this._checkEatSelf();
    if (_isOverSide || _isEatSelf) return true;
    // Detect food collisions and redraw food
    if (food.collideCheck(this)) this.body.push(this.lastBody);
    return false; }}Copy the code

It’s time for the main progression

// Remove dependent classes
// Here is the image resource to initialize
const headerImg = ['headerUp'.'headerDown'.'headerLeft'.'headerRight'];
const tailImg = ['tailUp'.'tailDown'.'tailLeft'.'tailRight'];
// The game itself is a Scene
class SnakeGame extends Scene { snake! : SnakePo; food! : FoodPo; backGround! : BackGroundPo; SF_scene! : ScenePo; fps =1000 / 3;
  timer: any;
  isPause: boolean = true;
  source = ['bg'.'body'.'gameover'. headerImg,'start'. tailImg,'food'];
  // Resource load counter
  loadSourceNum = 0;
  score = 0;
  gameTimeTotal = 0;
  timeTimer: any;
  // Built-in event callbackhandleLoadSource! : callback; handlePause! : callback; handleEnd! : callback; handleScoreChange! : callback;constructor(data: CanvasTypes.initData) {
    // Remove code to save space...
  }
   // Game initialization entry
  async initGame(handleCallback? : SnakeService.handleCallback) {
    if (handleCallback...) {
    // Remove the built-in event assignment code...
    }
    // Initialize additional scene objects needed to inject resources after they are loaded for subsequent direct drawing
    this._addNewScene();
    // Load the resources needed to start the game
    const _res = await this._loadImgSource();
    if (_res) {
      // Add resources to the food and snake scene
      this.SF_scene.addSourceMap(this.sourceMap);
      this._initGame();
    } else {
      console.log('Resource loading error... '); }}_addNewScene() {
    const_sceneData = {... };const _scene = new Scene(_sceneData);
    // Layer of the food and snake scene
    this.SF_scene = _scene;
  }
   // Initialize the objects and events that the game needs
  _initGame() {
    this.backGround = new BackGround();
    this.food = new Food();
    this.snake = new Snake();
    this.add(this.backGround);
    this.SF_scene.add(this.snake);
    this.SF_scene.add(this.food);
    this.SF_scene.join(this);
    this._drawTimeAndLengthText();
    // Register events
    this._initAction();
  }
  /** * resource loader */
  _loadImgSource() {
     /** * Use Promise only to call initGame when the resources are all loaded and not in the following message, to call each scene resource injection and * initialization game needed objects and events */
    return new Promise((resolve, reject) = > {
      try {
        console.log('Resource loading... ');
        this._registerAction('sourceLoading'.() = > {
          this.loadSourceNum += 1;
          if (this.loadSourceNum === this.source.length) {
            console.log('Resource load completed');
            resolve(true);
          }
          // Percentage of resource loading progress bars
          if (this.handleLoadSource)
            this.handleLoadSource((this.loadSourceNum / this.source.length) * 100);
        });
        forEach(this.source, item= > {
          this.loadSource(item, () = > {
            const _load = this._getAction('sourceLoading');
            _load();
          });
        });
      } catch (error) {
        resolve(false); }}); }reloadGame(){... }_start() {
    if (this.isPause) return;
    // Trigger behavior
    const fn = this._getAction(this.snake.direction + ' ');
    if (fn) fn();
    // Collision detection
    const isTrue = this.snake.collideCheck(this.food);
    // Score change notification
    this.score += 1;
    if (this.handleScoreChange) this.handleScoreChange(this.score);

    if (isTrue) {
      this.end();
      return;
    }
    this._updateView();
  }
  
  pause() {
    if (this.isPause) return;
    this.isPause = true;
    clearInterval(this.timer);
    clearInterval(this.timeTimer);
  }
  
  start() {
    if (!this.isPause) return;
    if (this.handlePause) this.handlePause(false);
    this._startSumTime();
    this.isPause = false;
    this.timer = setInterval(this._start.bind(this), this.fps);
  }
  // Game over
  end(){... }// Each scene updates its own content and draws
  _updateView() {
    this.update();
    this.SF_scene.update();
    this.SF_scene.join(this);
    this._drawTimeAndLengthText();
  }

  _startSumTime() {
    this.timeTimer = setInterval(() = > {
      this.gameTimeTotal += 1;
    }, 1000);
  }
  _drawTimeAndLengthText(){... }_drawTime() {
    this._drawCommonText(...) ; }_drawSnakeLength() {
    this._drawCommonText(...) ; }_drawCommonText(text: string, x? :number) {
  // Draw text, time, length, score, etc. }// Get the behavior
  _getAction(action: string): callback {
    return this.events.findListenerByKey(action);
  }
  // Register behavior
  _registerAction(action: string, callback: callback) {
    this.on(action, callback);
  }
  // Initialize the behavior
  _initAction() {
    // Register snake behavior so the keyboard triggers
    this._registerAction(direction.up + ' '.this.snake.up.bind(this.snake)); . .this._addGlobalEvents();
  }

  _addGlobalEvents() {
    window.onkeydown = e= > {
      // Keyboard triggers, omits a bunch of code.// Trigger behavior
      this._start(); }; }}Copy the code

The _updateView function uses the scene’s update() function to facilitate the draw() of the scene’s members to update the view.

Add a screenshot of the UI’s start, end, and React components

import React, { useRef, useEffect, useState } from 'react';
import Snake from '... ';
import Loading from '... ';
import Start from '... ';
import GameOver from '... ';
let snakeGame: any;
export default() = > {const _canvas = useRef(null);
  const [process, setProcess] = useState(0);
  const [isPause, setIsPause] = useState(false);
  const [isOver, setIsOver] = useState(null);
  useEffect(() = > {
    if (process >= 100) setIsPause(true);
  }, [process]);

  useEffect(() = > {
    const ele = ele1.current;
    if (ele) {
      const _scene = {
        width: 800.height: 760,
        ele,
      };
      const _s = new Snake(_scene);
      snakeGame = _s;
      _s.initGame({
        handleLoadSource: val= > {
          setProcess(val);
        },
        handlePause: val= > {
          setIsPause(val);
          if(! val) setIsOver(null);
        },
        handleEnd: val= >{ setIsOver(val); }}); }} []);return (
    <>
      <div className="bgc-white border-dash">
        {isOver ? (
          <GameOver score={isOver.score} len={isOver.length} start={()= > snakeGame.start()} />
        ) : null}
        {process >= 100 && isPause ? <Start start={()= > snakeGame.start()} /> : null}
        <Loading process={process} />{/* Main screen */}<canvas className="border-base" ref={_canvas} />
      </div>
    </>
  );
};


Copy the code

I wish you all a New Year!!

Is actually the beginning to play, just want to finish, to complete a basic little move again, and had larger, didn’t think about what the UI, to add various layers, may not be a programmer to write after their own things, are wanting to try to do my best, just behind the various details perfect, then to find the UI in figure, p figure, however, after the completion of Feel good, to finish the mood is happy Don’t know everyone, especially the front page, or regular business, especially the interface CSS to get much, will not feel feel like doing nothing, is to write these things every day, if you have this in mind, I think can change kind of content to play their own, after all front a lot of things, It’s okay. I thought I’d play Node. Sometimes a different way to relax is…