切方块游戏HTML5+jQuery【附源码】
博主介绍: 🚀自媒体 JavaPub 独立维护人,全网粉丝15w+,csdn博客专家、java领域优质创作者,51ctoTOP10博主,知乎/掘金/华为云/阿里云/InfoQ等平台优质作者、专注于Java技术领域和副业。🚀
公众号:JavaPub ⭐ ⭐简历模板、学习资料、面试题库等都给你💪
🍅
文末获取源码
🍅 无套路,免费领取
点赞再看,养成习惯
适合人群:初级学习者和爱好者,下面有展示图。计算机毕业设计
1 前言
🚀获取源码,文末公众号回复【切方块游戏】,即可。
⭐欢迎点赞留言
2 正文
公众号:JavaPub
2.1 展示预览
3MB GIF可以欣赏:
https://tva4.sinaimg.cn/large/007F3CC8ly1h3fkju6bgfg30ug0lxb2b.gif
2.2 项目结构
2.2 主要代码展示
function setupCanvases() {
const ctx = canvas.getContext('2d');
// devicePixelRatio alias
const dpr = window.devicePixelRatio || 1;
// View will be scaled so objects appear sized similarly on all screen sizes.
let viewScale;
// Dimensions (taking viewScale into account!)
let width, height;
function handleResize() {
const w = window.innerWidth;
const h = window.innerHeight;
viewScale = h / 1000;
width = w / viewScale;
height = h / viewScale;
canvas.width = w * dpr;
canvas.height = h * dpr;
canvas.style.width = w + 'px';
canvas.style.height = h + 'px';
}
// Set initial size
handleResize();
// resize fullscreen canvas
window.addEventListener('resize', handleResize);
// Run game loop
let lastTimestamp = 0;
function frameHandler(timestamp) {
let frameTime = timestamp - lastTimestamp;
lastTimestamp = timestamp;
// always queue another frame
raf();
// If game is paused, we'll still track frameTime (above) but all other
// game logic and drawing can be avoided.
if (isPaused()) return;
// make sure negative time isn't reported (first frame can be whacky)
if (frameTime < 0) {
frameTime = 17;
}
// - cap minimum framerate to 15fps[~68ms] (assuming 60fps[~17ms] as 'normal')
else if (frameTime > 68) {
frameTime = 68;
}
const halfW = width / 2;
const halfH = height / 2;
// Convert pointer position from screen to scene coords.
pointerScene.x = pointerScreen.x / viewScale - halfW;
pointerScene.y = pointerScreen.y / viewScale - halfH;
const lag = frameTime / 16.6667;
const simTime = gameSpeed * frameTime;
const simSpeed = gameSpeed * lag;
tick(width, height, simTime, simSpeed, lag);
// Auto clear canvas
ctx.clearRect(0, 0, canvas.width, canvas.height);
// Auto scale drawing for high res displays, and incorporate `viewScale`.
// Also shift canvas so (0, 0) is the middle of the screen.
// This just works with 3D perspective projection.
const drawScale = dpr * viewScale;
ctx.scale(drawScale, drawScale);
ctx.translate(halfW, halfH);
draw(ctx, width, height, viewScale);
ctx.setTransform(1, 0, 0, 1, 0, 0);
}
const raf = () => requestAnimationFrame(frameHandler);
// Start loop
raf();
}
// interaction.js
// ============================================================================
// ============================================================================
// Interaction
// -----------------------------
function handleCanvasPointerDown(x, y) {
if (!pointerIsDown) {
pointerIsDown = true;
pointerScreen.x = x;
pointerScreen.y = y;
// On when menus are open, point down/up toggles an interactive mode.
// We just need to rerender the menu system for it to respond.
if (isMenuVisible()) renderMenus();
}
}
function handleCanvasPointerUp() {
if (pointerIsDown) {
pointerIsDown = false;
touchPoints.push({
touchBreak: true,
life: touchPointLife
});
// On when menus are open, point down/up toggles an interactive mode.
// We just need to rerender the menu system for it to respond.
if (isMenuVisible()) renderMenus();
}
}
function handleCanvasPointerMove(x, y) {
if (pointerIsDown) {
pointerScreen.x = x;
pointerScreen.y = y;
}
}
// Use pointer events if available, otherwise fallback to touch events (for iOS).
if ('PointerEvent' in window) {
canvas.addEventListener('pointerdown', event => {
event.isPrimary && handleCanvasPointerDown(event.clientX, event.clientY);
});
canvas.addEventListener('pointerup', event => {
event.isPrimary && handleCanvasPointerUp();
});
canvas.addEventListener('pointermove', event => {
event.isPrimary && handleCanvasPointerMove(event.clientX, event.clientY);
});
// We also need to know if the mouse leaves the page. For this game, it's best if that
// cancels a swipe, so essentially acts as a "mouseup" event.
document.body.addEventListener('mouseleave', handleCanvasPointerUp);
} else {
let activeTouchId = null;
canvas.addEventListener('touchstart', event => {
if (!pointerIsDown) {
const touch = event.changedTouches[0];
activeTouchId = touch.identifier;
handleCanvasPointerDown(touch.clientX, touch.clientY);
}
});
canvas.addEventListener('touchend', event => {
for (let touch of event.changedTouches) {
if (touch.identifier === activeTouchId) {
handleCanvasPointerUp();
break;
}
}
});
canvas.addEventListener('touchmove', event => {
for (let touch of event.changedTouches) {
if (touch.identifier === activeTouchId) {
handleCanvasPointerMove(touch.clientX, touch.clientY);
event.preventDefault();
break;
}
}
}, {
passive: false });
}
<!DOCTYPE html>
<html lang="en">
<head>
<title>Backbone Game Engine</title>
<meta name="description" content="Elementary HTML5 Canvas Game Engine based on Backbone. Specialized for 2D platformers, and optimized for mobile.">
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<link href="favicon.ico" rel="shortcut icon" type="image/x-icon" />
<link href="apple_touch_icon.png" rel="apple-touch-icon" />
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no">
<script src="docs/jquery.min.js" type="text/javascript"></script>
<script src="docs/bootstrap.min.js" type="text/javascript"></script>
<link href="docs/bootstrap.min.css" rel="stylesheet" type="text/css" charset="utf-8">
<link href="docs/docs.css" rel="stylesheet" type="text/css" charset="utf-8">
</head>
<body data-spy="scroll" data-target="#sidebar" data-offset="100">
<header class="navbar navbar-inverse" role="banner">
<div class="navbar-header">
<button type="button" class="navbar-toggle pull-left" data-toggle="collapse" data-target="#navbar-collapse">
<span class="sr-only">Toggle navigation</span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
</button>
<a class="navbar-brand" href="index.html" title="Elementary HTML5 Canvas Game Engine based on Backbone."><img src="apple_touch_icon.png" /> Backbone Game Engine</a>
</div>
<div id="navbar-collapse" class="collapse navbar-collapse" role="navigation">
<ul class="nav navbar-nav">
<li><a href="index.html">Documentation</a></li>
<li><a href="examples.html">Examples</a></li>
</ul>
<ul class="nav navbar-nav navbar-right">
<li class="github-icon"><a href="https://github.com/martindrapeau/backbone-game-engine" title="Fork me on Github"><img src="docs/github.png" />Github</a></li>
</ul>
</div>
</header>
<div class="container">
<div class="row">
<div class="col-md-9">
<div id="introduction" class="row">
<div class="col-md-12">
<h1>HTML5 Canvas & Backbone</h1>
</div>
</div>
<div class="row">
<div class="col-md-6 col-xs-6">
<p>
An elementary HTML5 Canvas game engine built on Backbone. Specialized for 2D platformers, and optimized for mobile.
</p>
<h4>Examples</h4>
<ul>
<li>Real game: <a href="http://www.ludosquest.com" target="_blank">Ludo's Quest</a> - <a href="https://itunes.apple.com/ca/app/ludos-quest/id1047863228" target="_blank">launched on iOS</a> using <a href="https://www.cocoon.io" target="_blank">Cocoon.io</a></li>
<li>Test game: <a href="http://www.mariocraft.club" target="_blank">Mariocraft</a></li>
<li>Advanced: <a href="super-mario-bros/index.html" target="_blank">Super Mario Bros, level 1-1</a></li>
<li>Elementary: <a href="ball/index.html" target="_blank">Bouncing ball</a></li>
<li>Basic: <a href="mario/index.html" target="_blank">Mario in an empty world</a></li>
<li>Basic: <a href="gui/index.html" target="_blank">GUI</a></li>
<li>Basic: <a href="frog/index.html" target="_blank">Hoppy frog</a></li>
</ul>
<p>
These examples are on <a href="https://github.com/martindrapeau/backbone-game-engine">Github</a>.
</p>
</div>
<div class="col-md-3 col-xs-6">
<p>
<div class="carousel slide" data-ride="carousel">
<div class="carousel-inner">
<a class="item active" href="super-mario-bros/index.html" target="_blank">
<img src="docs/super-mario-bros-level-1-1.png" alt="" class="img-responsive" alt="Super Mario Bros level 1-1" title="Super Mario Bros level 1-1" />
<span class="carousel-caption">Click to play</span>
</a>
</div>
</div>
</p>
</div>
</div>
<div class="row">
<div class="col-md-12">
<h4>CocoonJS Example</h4>
<p>
Backbone Game Engine was written to run inside of CocoonJS Canvas+, so you can turn your HTML5 game into a native application on iOS or Android. If you have the CocoonJS launcher loaded on your iOS or Android device, you can load Super Mario Bros level 1-1 via this URL:
</p>
<pre>
http://martindrapeau.github.io/cocoon-mario/cocoon-mario.zip
</pre>
</div>
</div>
<div id="features" class="row">
<div class="col-md-12">
<h3>Features:</h3>
<ul>
<li><strong>Built on Backbone</strong>. Events, models, collections, inheritance and RESTful persistence. Why reinvent the wheel?</li>
<li><strong>HTML5 canvas only</strong>. No jQuery, as little DOM manipulations as possible.</li>
<li><strong>Mobile optimized</strong>. Build to run on mobile devices with transparent touch and viewport support. Everything is optimized for maxium frames per seconds (FPS).</li>
<li><strong>Go Native with CocoonJS.</strong>. Built to run in Ludei's <a href="http://support.ludei.com/hc/en-us/articles/200767118-Canvas-development-Guide" target="_blank">CocoonJS canvas+</a>. Deploy native on iOS and Android.</li>
animations: {
idle: {
sequences: [0, 1],
delay: 200
}
}
</pre>
<p>
This defines an animation of two frames, alternating at an interval of 200ms. Values 0 and 1 in array <code>sequences</code> are frame indices defined in the sprite sheet. Sprite attributes <code>state</code> and <code>sequenceIndex</code> control which animation and sequence are currently used. The <code>sequenceIndex</code> is automatically incremented (and reset to 0) by the sprite's draw function. Attribute <code>state</code> determines the current animation. It must be set to <code>idle</code> in the above example (as there is only one).
</p>
<p>
Extra animation options are available. Here is a complete list:
</p>
<ul>
<li><code>sequences</code>: Array of frame indices, or squence objects. A sequence object looks like this: <code>{frame: 52, x: 0, y: -32, scaleX: 1.00, scaleY: 1}</code>. It allows you to specify an offset to apply when the sprite is drawn, and a scaling factor.</li>
<li><code>scaleX, scaleY</code>: Optional. Scaling factors. Set <code>scaleX</code> to -1 to flip horizontally. Defaults to 1 if omitted.</li>
<li><code>delay</code>: Optional. The time to change to the next sequence. No need to specify if there is only one frame (as there is no animation). You can also define a sprite method <code>sequenceDelay</code> to programmatically return the delay. It will be passed the current animation.</li>
</ul>
<p>
For detailed examples of animations, look at file <code>artifacts.js</code> in the <code>super-mario-bros</code> folder. Class <code>Backbone.Pennie</code> implements a basic animation sequence using frame indices, while <code>Backbone.FlyingPennie</code> implements a more complex animation with sequence objects.
</p>
<pre>
Backbone.Pennie = Backbone.AnimatedTile.extend({
...
animations: {
idle: {
sequences: [52, 52, 53, 54, 53, 52],
delay: 50
}
},
...
Backbone.FlyingPennie = Backbone.Sprite.extend({
...
animations: {
anim: {
sequences: [
{frame: 52, x: 0, y: -32, scaleX: 1.00, scaleY: 1},
{frame: 52, x: 0, y: -64, scaleX: 0.50, scaleY: 1},
{frame: 53, x: 0, y: -90, scaleX: 0.50, scaleY: 1},
{frame: 53, x: 0, y: -128, scaleX: 1.00, scaleY: 1},
{frame: 53, x: 0, y: -128, scaleX: 0.50, scaleY: 1},
{frame: 52, x: 0, y: -112, scaleX: 0.50, scaleY: 1},
{frame: 52, x: 0, y: -90, scaleX: 1.00, scaleY: 1},
{frame: 52, x: 0, y: -80, scaleX: 0.50, scaleY: 1},
{frame: 53, x: 0, y: -80, scaleX: 0.50, scaleY: 1}
],
delay: 50
}
},
...
</pre>
</div>
</div>
<div id="documentation-Input" class="row">
<div class="col-md-12">
<h3>Backbone.Input</h3>
<pre>new Backbone.Input([attributes], [options]);</pre>
<p>
<code>Backbone.Input</code> class is a model which captures user input events and stores them as model attributes. For example pressing the left arrow, sets the <code>left</code> attribute to <code>true</code>. Depressing sets it to <code>false</code>. Bind to on the attribute change event to be notified.
</p>
<p>
<code>Backbone.Input</code> supports keyboard, mouse and touch events. It can draw a touchpad on screen with left and right arrow keys, an A (red) button and a B (blue) button.
</p>
<p>
<img src="docs/input.png" class="img-responsive" alt="Backbone.Input" />
</p>
<div class="alert alert-info">
Note: The Backbone.Input model only captures input when attached to a Backbone.Engine.
</div>
<h4>Attributes</h4>
<p>This attribute can be passed when creating the model, to configure the input.</p>
<ul>
<li><code>drawTouchpad</code>: Optional. Boolean or string "auto" to indicate whether to draw the touchpad. When "auto", the touchpad will be drawn only on touch devies. Defaults to "auto".</li>
</ul>
<p>The following model attributes are set by the model. They should not be set externally.</p>
<ul>
<li><code>touchEnabled</code>: Boolean set to true if the device is touch enabled.</li>
<li><code>left</code>: Boolean set to true when the left touchpad arrow or left keyboard arrow key is pressed.</li>
<li><code>right</code>: Boolean set to true when the right touchpad arrow or right keyboard arrow key is pressed.</li>
<li><code>buttonA</code>: Boolean set to true when the A touchpad button is pressed, or when the z keyboard key is pressed.</li>
<li><code>buttonB</code>: Boolean set to true when the B touchpad button is pressed, or when the x keyboard key is pressed.</li>
</ul>
<h4>Methods</h4>
<ul>
<li><code>hasTouchpad()</code>: Returns true if the touchpad is drawn.</li>
<li><code>leftPressed()</code>: Returns true if the left button is pressed.</li>
<li><code>rightPressed()</code>: Returns true if the right button is pressed.</li>
<li><code>buttonAPressed()</code>: Returns true if button A is pressed.</li>
<li><code>buttonBPressed()</code>: Returns true if button B is pressed.</li>
</ul>
<h4>Events</h4>
<ul>
<li><code>attach</code>: Triggered when the input is attached to the engine. Will start listening to user input.</li>
<li><code>detach</code>: Triggered when the input is detached to the engine. Will stop listening to user input.</li>
</ul>
<p>
You can add or remove a <code>Backbone.Input</code> model from the engine on the fly. In the Super Mario Bros example, the <code>Backbone.Input</code> and the <code>Backbone.WorldEditor</code> are swapped when moving from play to edit modes.
</p>
<h4>Usage</h4>
<pre>
var input = new Backbone.Input();
var engine = new Backbone.Engine();
engine.add(input);
input.bind("change:left", function(input) {
if (input.leftPressed())
console.log("left pressed:)");
else
console.log("left depressed:(");
});
</pre>
</div>
</div>
<div id="documentation-World" class="row">
<div class="col-md-12">
<h3>Backbone.World</h3>
<pre>new Backbone.World([attributes], [options])</pre>
<p>
<code>Backbone.World</code> is model which contains a collection of sprites that interact with each other. A world is an environment composed of tiles and characters. The world extends beyond the canvas however the viewport, the visible portion, is constrained within its prescribed limits. <code>Backbone.World</code> is similar to a sprite; it implements the <code>update</code> and <code>draw</code> methods required by the <code>Backbone.Engine</code> collection.
</p>
<h4>Attributes</h4>
<ul>
<li><code>x, y</code>: Origin of top-left corner in pixels.</li>
<li><code>width, height</code>: Size of world in tiles.</li>
<li><code>tileWidth, tileHeight</code>: Size of a tile in pixels.</li>
<li><code>viewportLeft, viewportRight, viewportTop, viewportBottom</code>: Defines an area in the canvas the world is constrained to. Each value provides gutter regions in pixels. Anything drawn by the world will be clipped in the area. Useful for drawing a menu bar or buttons on the same canvas. In provided demos for example, <code>viewportBottom</code> is set to 156 pixels to make room the the touchpad.</li>
<li><code>sprites</code>: Array of sprite models for persistence.</li>
<li><code>backgroundColor</code>: Background color of the world.</li>
<li><code>backgroundImage</code>: Id attribute of an image element in the DOM to show as background.</li>
<li><code>state</code>: Persisted state either play or pause.</li>
</ul>
<div class="alert alert-info">
Note: Setting a viewport different than the canvas size uses canvas clipping. Avoid using if you can as clipping the is expensive and can introduce performance issues.
</div>
<h4>Options</h4>
<ul>
<li><code>backgroundImage</code>: Optional. Pass to use a background image instead of a background color. Anchored to the origin.</li>
<li><code>input</code>: Input instance to control the hero.</li>
<li><code>camera</code>: Camera instance to keep the hero in the viewport.</li>
<li><code>debugPanel</code>: Optional.</li>
</ul>
<h4>Methods</h4>
<p>
A <code>Backbone.World</code> is a model that wraps a collection stored in property <code>sprites</code>. To prevent outside direct access to this collection, it provides these two methods:
</p>
<ul>
<li><code>add(models, [options])</code>: Add one or many models. Adds <code>world</code> to options and delegates to the <code>sprites</code> collection's <code>add</code> method. Then sets the <code>world</code> property as back-reference on the new model(s). Returns the new model(s).</li>
<li><code>remove(models, [options])</code>: Removes one or many models. Delegates to the <code>sprites</code> collection's <code>remove</code> method. Deletes the <code>world</code> back-reference and returns the model(s).</li>
</ul>
<p>
In addition to standard <code>Backbone.Model</code> methods, it also exposes these:
</p>
<ul>
<li><code>spawnSprites()</code>: Resets the sprites collection by retrieving the <code>sprites</code> attribute from the model. This is called after the world is loaded.</li>
<li><code>update(dt)</code>: Update function called by the engine. Will in turn call the update method of all sprites.</li>
<li><code>draw(context)</code>: Draw function called by the engine. Will in turn call the draw method of all sprites.</li>
<li><code>cloneAtPosition(sprite, x, y)</code>: Clones the sprite model and places the new instance at the specified coordinates. Will pass <code>world</code> to the <code>options</code> payload when created. Will also set property <code>world</code> as back-reference. If the sprite name matches that of the world attribute <code>hero</code> it will also pass option <code>input</code>, and if a <code>Backbone.Camera</code> exists, it will be tied to it. This function also acts as a toggle when placing a sprite over a tile where another exists. The existing one is removed. This ensures only one tile (static sprite) exists at one location.</li>
<li><code>width(), height()</code>: Return the size of the world in pixels.</li>
<li><code>getWorldIndex(object)</code>: Calculates the index position of a tile based on coordinates. Argument <code>object</code> must contain <code>x</code> and <code>y</code>, or be a model with those attributes. The index is calculated with formula <code>height * x/tileWidth + y/tileHeight</code>. This method is used to set the <code>id</code> of tile sprites.</li>
<li><code>getWorldCol(x), getWorldRow(y)</code>: Returns the tile position in columns or rows of a coordinate.</li>
<li><code>findAt(x, y, [type], [exclude], [collision])</code>: Finds the first sprite at the specified coordinate. Use for collision detection. Optional arguments allow you to filter what to look for. Set <code>type</code> to <code>character</code> to find moving sprites, or <code>tile</code> to find tiles. Set <code>exclude</code> to the id of the sprite you want to exclude from the search. In a collision detection scheme, this is usually the id of the sprite you are checking against. Set <code>collision</code> to true to find only tiles that have their <code>collision</code> flag set.</li>
<li><code>filterAt(x, y, [type], [exclude], [collision])</code>: Same as <code>findAt</code> but returns the list of all matching sprites instead of the first.</li>
<li><code>findCollidingAt(x, y)</code>: Finds a colliding tile. Just like calling <code>findAt(x, y, "tile", null, true)</code>.</li>
<li><code>findCollisions()</code>: Finds collisions for a given set of collision map.</li>
</ul>
<h4>Events</h4>
<ul>
<li><code>attach</code>: Triggered when the sprite is attached to the engine. Will trigger the <code>attach</code> method of all sprites in the world.</li>
<li><code>detach</code>: Triggered when the sprite is detached to the engine. Will trigger the <code>detach</code> event of all sprites in the world.</li>
<li><code>tap</code>:
Trigerred when the user taps or clicks on the canvas. The callback will be passed the DOM event extended with these properties: <code>canvas</code>, <code>canvasX</code>, <code>canvasY</code>, <code>world</code>, <code>worldX</code> and <code>worldY</code>.
</li>
<li><code>key</code>: Trigerred on a keyup event. The event callback will be passed the DOM event as argument.</li>
</ul>
<h4>How it works</h4>
<p>
Sprites can be added and removed via methods <code>add</code> and <code>remove</code>. Sprites are automatically attached to the <a href="#documentation-Engine" target="_blank">Backbone.Engine</a> the world is attached to. Sprites then have properties <code>engine</code> and <code>world</code> set as back-reference. In each request frame, the same mechanics apply as for sprites attached directly to an engine; methods <code>update</code> and <code>draw</code> are called for each sprite. The exception is for static sprites which are only updated/redrawn when required (see below).
</p>
<p>
Internally, the world keeps sprites into a collection stored in property <code>sprites</code>. It further splits sprites into 2 collections for faster lookup:
</p>
<ul>
<li><code>staticSprites</code>: Background sprites that have no animation. These are usually same-sized tiles. Sprites that have their <code>static</code> attribute set to <code>true</code> will be put in this collection. Sprites are given an <code>id</code> determined by their position (column and row). The collection is ordered and indexed on <code>id</code> allowing for fast lookup on a pair of x/y coordinates. In addition, these sprites are drawn on a background canvas only drawn when required (i.e. world is panned).</li>
<li><code>dynamicSprites</code>: Animated tiles and characters. Their <code>static</code> attribute must be set to <code>false</code> to fall in this collection. These sprites are given unique <code>id</code> attributes based on their name (i.e. <code>mario.1</code>). They are not indexed therefore lookup has an order of N. Keep the number of sprites here to a minimum.</li>
</ul>
<p>
A world is measured in tiles via attributes <code>width</code>, <code>height</code>, <code>tileWidth</code> and <code>tileHeight</code>. Call methods <code>width</code> and <code>height</code> to get the size in pixels. Attributes <code>x</code> and <code>y</code> determine the origin in pixels (top-left corner) and allow the world to be panned consequently changing the viewport.
</p>
<div class="alert alert-info">
Note: If you define an animated tile, make sure its <code>static</code> attribute is set to false to prevent redraws every animation frame. Also ensure the width of tile sprites match that of world attributes <code>tileWidth</code> and <code>tileHeight</code>.
</div>
<h4>Sprites</h4>
<p>
When the world is created, sprites are instantiated in method <code>spawnSprites</code>. Each sprite instance is attached to the engine. Sprites then have properties <code>engine</code> and <code>world</code> set pointing to those respective objects.
</p>
<p>
Sprites can be categorized with attribute <code>type</code> as to identify one another when they interact with each other. Sprites of type <code>tile</code> are usually static and obstacles. Sprites of type <code>character</code> are usually dynamic and moving sprites.
</p>
<p>
You can add sprites with method <code>add</code> which delegates to the <code>sprites</code> collection's <code>add</code> method. It takes care of passing the <code>world</code> as option.
</p>
<p>
You can also use method <code>cloneAtPosition(sprite, x, y)</code>. It takes as argument an existing sprite, and coordinates. It will clone the sprite and place it at the specified coordinates passing <code>world</code> and <code>input</code> as options. If the sprite has its <code>hero</code> attribute set to <code>ture</code>, and if a <code>Backbone.Camera</code> exists, it will be tied to it. Backbone.WorldEditor</code> uses this function for instance.
</p>
<p>
Methods <code>getWorldIndex</code>, <code>getWorldCol</code> and <code>getWorldRow</code> can be used to find the position of a sprite. A sprite's <code>x</code> and <code>y</code> attributes determine their position relative to the world origin.
</p>
<h4>Background and Tiles</h4>
<p>
The background of a world is composed of same-size tiles defined by attributes <code>tileWidth</code> and <code>tileHeight</code>. A tile is usually a non-animated sprite with its <code>static</code> attribute set to true. The model <code>id</code> is the position of the sprite on screen (column and row). As such, there can only be one tile per location.
</p>
<h4>Character Sprites</h4>
<p>
Characters are sprites that interact with their environment. <a href="#documentation-Character">Backbone.Character</a> and <a href="#documentation-Hero">Backbone.Hero</a> are character sprites. Character sprite models usually have teir <code>type</code> attribute set to <code>character</code>. You are free to use attribute <code>type</code> to classify your sprites.
</p>
<h4>Collision detection</h4>
<p>Internally sprite positions are stored in a <a href="https://github.com/asaarinen/qtree" target="_blank">QuadTree</a>. As a sprite moves, its position in the QuadTree is updated.
</p>
<p>Three methods exist to detect collisions.</p>
<ul>
<li><code>findAt(x, y, [type], [exclude], [collision])</code>:
Find the first sprite touching the given point or <code>null</code> if none are found. Optional arguments can be passed for limiting the lookup:
<ul>
<li><code>type</code>: Optional. If set, will only lookup sprites matching that type. Typical values are <code>character</code> or <code>tile</code>.</li>
<li><code>exlcude</code>: Optional. The sprite model's <code>id</code> to exclude in lookup.</li>
<li><code>collision</code>: Optional. Boolean indicating whether to only include sprites that have the <code>collision</code> attribute explicitly set to <code>true</code>.
</ul>
</li>
</pre>
<p>
When an obstacle is hit, the character stops moving. Its <code>x</code> is anchored, and <code>velocity</code> set to 0.
</p>
<pre>
if (velocity >= 0) {
// Stop if obstacle to the right
var rightX = this.world.width();
for (i = 0; i < this.collisionMap.right.sprites.length; i++)
if (heroTopY > 0 )
rightX = Math.min(rightX, this.collisionMap.right.sprites[i].getLeft(true));
if (heroRightX >= rightX) {
attrs.velocity = velocity = 0;
attrs.x = x = rightX - heroWidth - paddingLeft;
for (i = 0; i < this.collisionMap.right.sprites.length; i++)
this.collisionMap.right.sprites[i].trigger("hit", this, "left", cur.mov2);
if (this.cancelUpdate) return true;
}
}
</pre>
<p>
A collision with other sprites triggers an <code>hit</code> event on that sprite. The event hanlder will receive the colliding sprite (hero), and its relative position where the hit occured (left of the enemie sprite in this case).
</p>
<h4>Usage</h4>
<p>
This excerpt is taken from the Mario example.
</p>
<pre>
Backbone.Mario = Backbone.Hero.extend({
defaults: _.extend({
}, Backbone.Hero.prototype.defaults, {
name: "mario",
spriteSheet: "mario"
})
});
var canvas = document.getElementById("foreground");
var spriteSheets = new Backbone.SpriteSheetCollection([{
id: "mario",
img: "#mario",
tileWidth: 32,
tileHeight: 64,
tileColumns: 21,
tileRows: 6
}]).attachToSpriteClasses();
var input = new Backbone.Input({
drawTouchpad: true,
drawPause: true
});
var mario = new Backbone.Mario({
x: 400, y: 400
}, {
input: input
});
var world = new Backbone.World({
width: 30, height: 18,
tileWidth: 32, tileHeight: 32,
backgroundColor: "rgba(66, 66, 255, 1)"
});
world.add(mario);
var engine = new Backbone.Engine({
}, {
canvas: canvas,
input: input
});
engine.add([
world,
input
]);
</pre>
</div>
</div>
<div id="documentation-Camera" class="row">
<div class="col-md-12">
<h3>Backbone.Camera</h3>
<pre>new Backbone.Camera([attributes], [options])</pre>
<p>
<code>Backbone.Camera</code> is a model which ensures a sprite, the subject, is always in the viewport (the canvas). It pans the <code>Backbone.World</code> when the character steps out of a window in the viewport. <code>Backbone.Camera</code> must be added to the engine (not the world).
</p>
<h4>Attributes</h4>
<ul>
<li><code>left, right, top, bottom</code>: Coordinates of the window with respect to the drawn canvas.</li>
</ul>
<h4>Options</h4>
<ul>
<li><code>subject</code>: The sprite to keep in the viewport.</li>
<li><code>world</code>: The Backbone.World.</li>
</ul>
<h4>Methods</h4>
<ul>
<li><code>update(dt)</code>: No-op. Simply returns false for no redraw.</li>
<li><code>draw(context)</code>: No-op.</li>
<li><code>maybePan()</code>: Called when the <code>x</code> or <code>y</code> position of the subject changes. Ensures the subject is within the specified window, otherwise pans the world.</li>
</ul>
<h4>Events</h4>
<ul>
<li><code>attach</code>: Triggered when the sprite is attached to the engine. Starts monitoring the subject and pans the <code>Backbone.World</code> when necessary.</li>
<li><code>detach</code>: Triggered when the sprite is detached to the engine. Stops monitoring the subject.</li>
</ul>
<h4>Usage</h4>
<pre>
var spriteSheets = new Backbone.SpriteSheetCollection({
id: "mario",
img: "#mario",
tileWidth: 32,
tileHeight: 64,
tileColumns: 21,
tileRows: 6
});
var mario = new Backbone.Mario();
var world = new Backbone.World();
var camera = new Backbone.Camera(
{
left: 200, right: 600, top: 100, bottom: 50},
{
subject: mario, world: world}
);
world.add(mario);
var engine = new Backbone.Engine();
engine.add([world, camera]);
</pre>
</div>
</div>
<div id="documentation-Clock" class="row">
<div class="col-md-12">
<h3>Backbone.Clock</h3>
<p>
<code>Backbone.Clock</code> is a model which ticks at a set time interval. Attribute <code>ticks</code> stores an integer tick value that gets incremented every interval. Useful for synchronizing sprite animations.
</p>
<h4>Attributes</h4>
<ul>
<li><code>ticks</code>: Initial tick value. An integer that gets incremented every interval.</li>
<li><code>delay</code>: Interval between ticks.</li>
</ul>
<h4>Methods</h4>
<ul>
<li><code>update(dt)</code>: Updates the ticks.</li>
<li><code>draw(context)</code>: No-op. Simply returns the model.</li>
</ul>
<h4>Events</h4>
<ul>
<li><code>attach</code>: Triggered when the sprite is attached to the engine. Starts ticking.</li>
<li><code>detach</code>: Triggered when the sprite is detached to the engine. Stops ticking.</li>
</ul>
<h4>Usage</h4>
<pre>
var clock = new Backbone.Clock({
delay: 2000});
clock.on("change:ticks", function() {
console.log("tick", clock.get("ticks");
});
</pre>
<p>
As an example, look at the Super Mario Bros example, question blocks and pennies are all synced on the same clock. These sprites are sub-classes of <code>Backbone.AnimatedTile</code>. Instances create a Backbone.Clock if one does not exist. Otherwise, they find the existing one by that name and reuse it.
</p>
<pre>
Backbone.AnimatedTile = Backbone.Tile.extend({
initialize: function(attributes, options) {
Backbone.Tile.prototype.initialize.apply(this, arguments);
this.on("attach", this.onAttach, this);
this.on("detach", this.onDetach, this);
},
onAttach: function() {
if (!this.engine) return;
this.onDetach();
this.clock = this.engine.findWhere({
name: "animatedTileClock"});
if (!this.clock)
this.clock = this.engine.add(new Backbone.Clock({
name: "animatedTileClock", delay: 200}));
this.listenTo(this.clock, "change:ticks", this.updateAnimationIndex);
},
onDetach: function() {
if (this.clock) this.stopListening(this.clock);
this.clock = undefined;
},
update: function(dt) {
return true;
},
updateAnimationIndex: function() {
var animation = this.getAnimation(),
sequenceIndex = this.get("sequenceIndex") || 0;
if (!animation) return;
this.set("sequenceIndex", sequenceIndex < animation.sequences.length-1 ? sequenceIndex + 1 : 0);
}
});
</pre>
</div>
</div>
<div id="documentation-Element" class="row">
<div class="col-md-12">
<h3>Backbone.Element</h3>
<p>
<code>Backbone.Element</code> is a model that mimics a rudimentary DOM element.
Currently, it supports these features:
</p>
<ul>
<li>Fixed position specified by top-left <code>x</code> and <code>y</code>.</li>
<li>Optional background color and rounded corners.</li>
<li>Optional image.</li>
<li>Optional text.</li>
<li>Can be animated with easing functions. Supported animations are: translation, fade in and fade out.</li>
</ul>
<h4>Attributes</h4>
<ul>
<li><code>x, y</code>: Top left corner of button.</li>
<li><code>width, height</code>: Size of button.</li>
<li><code>backgroundColor</code>: Fill style of the button. Set to <code>transparent</code> to see thru.</li>
<li><code>borderRadius</code>: Border radius in pixels for rounded corners.</li>
<li><code>img</code>: The <code>Image</code> object or element id selector of the image to find in the DOM (i.e. <code>#icons</code>). A pointer to the Image object is then stored in property <code>img</code></li>
<li><code>imgX, imgY, imgWidth, imgHeight</code>: Bounding box of image location in source image.</li>
<li><code>imgMargin</code>: Offset in pixels to apply when drawing the image inside the button.</li>
<li><code>text</code>: Text to draw. If empty, not text is drawn.</li>
<li><code>textPadding, textLineHeight</code>: Padding around the text and line height in pixels.</li>
<li><code>textContextAttributes</code>: Canvas text styling properties. Will be set on the canvas context before calling <code>fillText</code>. Consult <a href="http://www.html5canvastutorials.com/tutorials/html5-canvas-text-font-size/" target="_blank">HTML5 fillText documentation</a> for details.</li>
<li><code>easing</code>: Easing function to use as defined in <code>Backbone.EasingFunctions</code> (see below).</li>
<li><code>easingTime</code>: Duration of the animation in ms. Defaults to 1000ms.</li>
<li><code>opacity</code>: Opacity of the element. 1 for fully visible, 0 for invisible and in-between for translucent. This value is changed by <code>fadeIn</code> and <code>fadeOut</code> animations.</li>
<li><code>scale</code>: Zoom-in, zoom-out scale to apply to the element. Default is 1. Can be used to defined your own animation (see Backbone.Button's <code>pressed</code> animation below).</li>
</ul>
<h4>Methods</h4>
<ul>
<li><code>update(dt)</code>: Draws the element and handles animations.</li>
<li><code>onUpdate(dt)</code>: Not defined by default. If deinfed, called at the end of <code>update</code>. It must return <code>true</code> or <code>false</code> to determine whether to draw the element or not.</li>
<li><code>draw(context, options)</code>: Draws the element.</li>
<li><code>onDraw(context, options)</code>: Not defined by default. If deinfed, called at the end of <code>draw</code>.</li>
<li><code>drawText(b, context, options)</code>: Called by <code>draw</code> to draw text. Argument <code>b</code> is the JSONized model (<code>this.toJSON()</code>) defining the context. You can call this to draw extra text if you implemented method <code>onDraw</code>.</li>
<li><code>overlaps(x, y):</code>: Checks to see if the sprite overlaps with the passed coordinates. Returns a Boolean.</li>
<li><code>moveTo(x, y, callback)</code>: Translation animation. Will move the element from the current position to the specified <code>x</code> and <code>y</code> position using the <code>easing</code> function and <code>easingTime</code> duration.</li>
<li><code>fadeIn(callback)</code>: Animates a fade in. Starts with an opacity of 0 going up to 1.</li>
<li><code>fadeOut(callback)</code>: Animates a fade out. Starts with an opacity of 1 going down to 0.</li>
</ul>
<h4>Events</h4>
<ul>
<li><code>attach</code>: Triggered when the sprite is attached to the engine. Starts listening to user input.</li>
<li><code>detach</code>: Triggered when the sprite is detached to the engine. Stops listening to user input.</li>
</ul>
<h4>Animations</h4>
<p>
Animations are driven by attributes <code>easing</code> and <code>easingTime</code>. The first defines the easing function to use and the second the duration in ms. These functions are found in <code>Backbone.EasingFunctions</code> and are:
<ul>
<li><code>linear</code></li>
<li><code>easeInQuad</code></li>
<li><code>easeOutQuad</code></li>
<li><code>easeInOutQuad</code></li>
<li><code>easeInCubic</code></li>
<li><code>easeOutCubic</code></li>
<li><code>easeInOutCubic</code></li>
<li><code>easeInQuart</code></li>
<li><code>easeOutQuart</code></li>
<li><code>easeInOutQuart</code></li>
<li><code>easeInQuint</code></li>
<li><code>easeOutQuint</code></li>
<li><code>easeInOutQuint</code></li>
</ul>
Easing functions return a value between 0 and 1.
</p>
<p>
You can create your own animations pretty easily. Look at the code for examples.
</p>
<h4>Usage</h4>
<pre>
TO DO...
</pre>
</div>
</div>
<div id="documentation-Button" class="row">
<div class="col-md-12">
<h3>Backbone.Button</h3>
<p>
<code>Backbone.Button</code> is a <code>Backbone.Element</code> which listens to tap/click events and triggers a <code>tap</code> event when pressed. When pressed there is a grow-shrink animation to give the user feedback.
</p>
<h4>Usage</h4>
<pre>
var button = new Backbone.Button({
x: 4, y: 4, width: 52, height: 52, borderRadius: 5,
img: "#icons", imgX: 0, imgY: 0, imgWidth: 32, imgHeight: 32, imgMargin: 10
});
button.on("tap", function() {
console.log("button tapped!");
});
</pre>
</div>
</div>
<div id="documentation-DebugPanel" class="row">
<div class="col-md-12">
<h3>Backbone.DebugPanel</h3>
<p>
<code>Backbone.DebugPanel</code> is a Backbone model on which you set attributes to be dumped on screen. Upon draw, it will <code>JSON.stringify</code> attributes.
</p>
<h4>Events</h4>
<ul>
<li><code>update(dt)</code>: No-op. Simply returns true.</li>
<li><code>draw(context)</code>: Draws the debug information on screen.</li>
<li><code>attach</code>: Triggered when the sprite is attached to the engine.</li>
<li><code>detach</code>: Triggered when the sprite is detached to the engine.</li>
</ul>
<h4>Usage</h4>
<pre>
var debugPanel = new Backbone.DebugPanel();
var engine = new Backbone.Engine({}, {
debugPanel: debugPanel
});
engine.add(debugPanel);
debugPanel.set({hello: "Word"});
// Draws this on screen
// {"fps": 58, "ct": 7, "hello": "World"}
debugPanel.set({hello: "Dolly"});
// {"fps": 58, "ct": 7, "hello": "Dolly"}
debugPanel.unset("hello");
// {"fps": 58, "ct": 7}
</pre>
<p>
In the above example, the debug panel is created. It is added to the engine as a model to draw. It is also passed as an option to the engine so it can output <code>fps</code> and <code>ct</code> (cycle time).
</p>
<p>
We manually add attribute <code>hello</code> to be tracked. Whenever it changes, so does the print out on screen. Use <code>unset</code> to remove a tracked attribute.
</p>
<h4>Conditional Usage</h4>
<p>
It is recommended that you support the non-existence of the debug panel with an <code>if (this.debugPanel)</code> statement before setting. For example, when you extend a class, pass in the debug panel as an option. Then, in your code, check to see if it exists. For example, this is done in the <code>Backbone.Engine.draw</code> method:
</p>
<pre>if (this.debugPanel) this.debugPanel.set({fps: this.fps, ct: this.cycleTime});</pre>
<p>
This supports the case where the debug panel is never created (<code>debugPanel</code> = <code>undefined</code>), such as in production.
</p>
</div>
</div>
<div id="persistence" class="row">
<div class="col-md-12">
<h1>Persistence</h1>
<p>
Backbone offers RESTful persistence via <a href="http://backbonejs.org/#Sync" target="_blank">Backbone.sync</a>. Models have methods <code>fetch</code> and <code>save</code> to retrieve/send the model/collection JSONified data to/from the server. As such, you can easily implement server-side persistence using well established RESTful standards.
</p>
<p>
In our Super Mario Bros example, we use <a href="https://developer.apple.com/library/safari/documentation/iPhone/Conceptual/SafariJSDatabaseGuide/Name-ValueStorage/Name-ValueStorage.html" target="_blank">local storage</a> instead. This is done by overriding <code>Backbone.World</code> methods <code>save</code> and <code>fetch</code>. See file <code>src/local-storage.js</code> for details.
</p>
</div>
</div>
<div id="performance" class="row">
<div class="col-md-12">
<h1>Performance and Debugging</h1>
<p>
HTML5 canvas has come a long way in terms on performance. Browser implementations of canvas now offer impressive performance. On mobile, Apple leads the pack being able to sustain a 60fps for the Super Mario Bros example. However on Android, frame rates drop fast to the 30s when the background needs to be redrawn. On Surface performance seems good on newer models, however on first generation RT models, as slow as on Android tablets.
</p>
<p>
This being said, there are things you can do to ensure the best performance.
</p>
<h4>Keep cycle time below 16ms</h4>
<p>
That is the time you have between redraws, 60 times a second. The <code>Backbone.Engine</code> will report the frame rate (fps), and cycle time (ct) if you add and attach a <code>Backbone.DebugPanel</code>. Make sure to use it. If you see <code>fps</code> go down while <code>ct</code> goes up, then your <code>update</code> and <code>draw</code> times must be too long. You can time the <code>update</code> time to pinpoint the issue.
</p>
<h4>Play well with Javascript Garbage Collection</h4>
<p>
You can't avoid it. You will leak memory. Every call made by <code>requestAnimationFrame</code> creates a function scope. It does so 60 times a second and it will need to be garbage collected. The browser will pause to collect garbage.
</p>
<p>
You can however control the leakage rate. Try to create objects upfront, and pool resources as much as possible. That's why sprite sheets are shared among sprites. If you ever see that your game jerks, at an even interval (i.e. every 30s), then you are probably being hit by the garbage collector recuperating large amounts of memory (>10MB).
</p>
<p>
You can use the Timeline tool in Chrome/Safari Developer Tools to identify this. Record a session and once done, you can apply a filter <code>gc</code> to filter on garbage collection events. You will notice they are at evenly spaced intervals. On my machine, for Super Mario Bros, 3.5MB is collected every 4s on average. There is no jerk. No jerk on a tablet means healthy memory management.
</p>
<p>
Some further references and good resources on performance:
</p>
<ul>
<li><a href="http://blog.artillery.com/2012/10/browser-garbage-collection-and-framerate.html" target="_blank">Browser Garbage Collection and Frame Rate</a></li>
<li><a href="https://www.scirra.com/blog/76/how-to-write-low-garbage-real-time-javascript" target="_blank">How to write low garbage real-time Javascript</a></li>
<li><a href="http://www.html5rocks.com/en/tutorials/canvas/performance/" target="_blank">Improving HTML5 Canvas Performance</a></li>
</ul>
</div>
</div>
<div id="publishing" class="row">
<div class="col-md-12">
<h1>Publishing your Game</h1>
<h2>On the Web</h2>
<p>
If you forked this repo, your game is already published on the web on your Github page under <code>[username].github.io/backbone-game-engine</code>.
</p>
<p>
If you own an iPad or iPhone, you can add it to the home screen as a Web app. It will open in full-screen and if you've implemented an Application Cache, it will work offline too.
</p>
<h2>On iOS and Android</h2>
<p>
Backbone Game Engine was built to run in <a href="https://www.ludei.com/cocoonjs/" target="_blank">CocoonJS</a> canvas+. You can try out Super Mario Bros level 1-1 in the <a href="http://support.ludei.com/hc/en-us/articles/201048463-CocoonJS-launcher-user-guide" target="_blank">CocoonJS launcher</a> by pointing to the zip file at this URL: http://martindrapeau.github.io/cocoon-mario/cocoon-mario.zip.
</p>
<pre>
http://martindrapeau.github.io/cocoon-mario/cocoon-mario.zip
</pre>
<p>
Checkout the Github repo <a href="" target="_blank">cocoon-mario</a>.
It can be used as the basis for your own native game on iOS or Android.
</p>
</div>
</div>
<div id="change-log" class="row">
<div class="col-md-12">
<h1>Change Log</h1>
<h4>0.40 - TBD</h4>
<ul>
<li>Upcoming release to include bug fixes, improvements and new features to come following the release to iOS of Ludo's Quest.</li>
</ul>
<h4>0.30 - 2015-03-22</h4>
<ul>
<li>Backbone.Element - a rudimentary DOM element with image, text and animations.</li>
<li>Backbone.World now uses a QuadTree for collision detection.</li>
<li>Removed dependence on hammer.js. Backbone.Engine now triggers tap and key events.</li>
<li>Complete rewrite of Backbone.Input. Removed its pause button.</li>
<li>Complete rewrite of Backbone.Character.</li>
<li>Complete rewrite of Backbone.Hero.</li>
<li>Backbone.Editor now resizes sprites to fit in the specified tileWidth and tileHeight.</li>
<li>Rewrite of adjustViewport global function to work cross-device.</li>
<li>Official support of CocoonJS canvas+.</li>
</ul>
<h4>0.21 - 2015-02-06</h4>
<ul>
<li>Sprite padding</li>
<li>More efficient gamepad drawing</li>
<li>Editor: paging, shrink large sprites, highlight tiles</li>
<li>World: z-index, tap event,key event, fixed background image, improved sprite lookup, bug fixes</li>
</ul>
<h4>0.20 - 2014-12-31</h4>
<p>
Major improvements including:
<ul>
<li>Performance improvements.</li>
<li>Fast sprite lookup.</li>
<li>Faster dynamic and static drawing.</li>
<li>Efficient collision detection.</li>
<li>Character and hero knockout and dying.</li>
<li>Bug fixes.</li>
</ul>
</p>
<h4>0.11 - 2014-11-12</h4>
<p>
Adjust viewport on orientation change, and center canvas.
</p>
<h4>0.10 - 2014-05-19</h4>
<p>
Initial release.
</p>
</div>
</div>
</div>
<div class="col-md-3">
<div id="sidebar" class="bs-sidebar affix">
<ul class="nav bs-sidenav">
<li class="active"><a href="#introduction">Introduction</a></li>
<li><a href="#getting-started">Getting Started</a></li>
<li><a href="#documentation">Reference</a></li>
<li><a href="#documentation-Engine"> Backbone.Engine</a></li>
<li><a href="#documentation-SpriteSheet"> Backbone.SpriteSheet</a></li>
<li><a href="#documentation-SpriteSheetCollection"> Backbone.SpriteSheetCollection</a></li>
<li><a href="#documentation-Sprite"> Backbone.Sprite</a></li>
<li><a href="#documentation-Input"> Backbone.Input</a></li>
<li><a href="#documentation-World"> Backbone.World</a></li>
<li><a href="#documentation-WorldEditor"> Backbone.WorldEditor</a></li>
<li><a href="#documentation-Character"> Backbone.Character</a></li>
<li><a href="#documentation-Hero"> Backbone.Hero</a></li>
<li><a href="#documentation-Camera"> Backbone.Camera</a></li>
<li><a href="#documentation-Clock"> Backbone.Clock</a></li>
<li><a href="#documentation-Element"> Backbone.Element</a></li>
<li><a href="#documentation-Button"> Backbone.Button</a></li>
<li><a href="#documentation-DebugPanel"> Backbone.DebugPanel</a></li>
<li><a href="#documentation-Shapes"> Shape functions</a></li>
<li><a href="#mobile-devices">Mobile Devices</a></li>
<li><a href="#going-offline">Going Offline</a></li>
<li><a href="#persistence">Persistence</a></li>
<li><a href="#performance">Performance</a></li>
<li><a href="#publishing">Publishing</a></li>
<li><a href="#change-log">Change Log</a></li>
</ul>
</div>
</div>
</div>
</div>
<br/>
<footer class="navbar navbar-default">
<p class="navbar-text navbar-left">
© 2014 <a href="http://martindrapeau.tumblr.com/">Martin Drapeau.</a>
<a href="https://github.com/martindrapeau/backbone-game-engine/blob/gh-pages/LICENSE">Licensed under MIT.</a>
</p>
<p class="navbar-text navbar-right">Written in Montréal, Canada.</p>
<p class="navbar-text navbar-right"> </p>
</footer>
</body>
</html>
源码下载
获取源码,公众号回复【切方块游戏】,即可。好了,可以点关注+三连了 ~
不会还有人没 点赞 + 关注 + 收藏 吧!
系列推荐:
查看更多博主首页更多实战项目 >>>
项目源码获取方法
点赞本文,然后私信我,我免费分享给你哈~
源码获取:
大家
点赞、收藏、关注、评论
啦 、查看👇🏻👇🏻👇🏻微信
公众号获取联系方式👇🏻👇🏻👇🏻
精彩专栏推荐订阅:在下方专栏👇🏻👇🏻👇🏻👇🏻
转载:https://blog.csdn.net/qq_40374604/article/details/125384148
查看评论