├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── dist ├── aframe-super-shooter-kit.js └── aframe-super-shooter-kit.min.js ├── examples ├── basic │ └── index.html └── supercraft │ ├── components │ └── enemy.js │ ├── index.html │ └── sounds │ ├── enemy1a.ogg │ ├── enemy1b.ogg │ ├── enemy1c.ogg │ ├── enemy2.ogg │ ├── enemy3.ogg │ ├── explosion.ogg │ ├── shoot.ogg │ └── song.ogg ├── index.html ├── index.js ├── package-lock.json └── package.json /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .DS_Store 3 | *.sw[pomn] 4 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | addons: 3 | firefox: 'latest' 4 | node_js: 5 | - '6.9' 6 | 7 | install: 8 | - npm install 9 | - ./node_modules/.bin/mozilla-download ./firefox/ --product firefox --branch mozilla-aurora 10 | - export FIREFOX_NIGHTLY_BIN="./firefox/firefox/firefox-bin" 11 | 12 | before_script: 13 | - export DISPLAY=:99.0 14 | - sh -e /etc/init.d/xvfb start 15 | 16 | script: 17 | - $CI_ACTION 18 | 19 | env: 20 | global: 21 | - TEST_SUITE=unit 22 | matrix: 23 | - CI_ACTION="npm run test" 24 | - CI_ACTION="npm run dist" 25 | # - CI_ACTION="npm run lint" 26 | 27 | branches: 28 | only: 29 | - master 30 | 31 | cache: 32 | directories: 33 | - node_modules 34 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2017 Diego F. Goberna <diego@feiss.be> 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # aframe-super-shooter-kit 2 | 3 | [A-Frame](https://aframe.io) kit of components for making a simple WebVR 4 | shooter minigame. From the creators of A-Frame, Supermedium, and Supercraft. 5 | 6 | ![screenshot](https://user-images.githubusercontent.com/674727/43233052-499cf788-9074-11e8-98bb-3823e30ef13d.jpg) 7 | 8 | **[PLAY](https://supermedium.github.io/aframe-super-shooter-kit/examples/supercraft/)** 9 | 10 | The kit is set of simple and easy to use components to provide a way of 11 | building simple shooting experiences, where a "shooter" shoots "bullets" that 12 | can hit "targets". A large chunk of the game can be handled at just the 13 | declarative A-Frame layer in HTML. 14 | 15 | 1. One `bullet` entity acts as a template for the instances of shot bullets. 16 | 2. Entity with `shooter` component attached (e.g., a gun) spawns bullets on 17 | `shoot` event from its position. 18 | 3. Collisions among bullet's and `target`'s bounding boxes are checked. 19 | 4. Health and life of targets are calculated (`hit` and `die` events). 20 | 21 | So we define which entities are bullets, shooters, and targets, and then wire 22 | up the game using controls and progress the game with events. 23 | 24 | ![diagram](https://user-images.githubusercontent.com/674727/43211842-cb6de9da-9032-11e8-94ff-8c4b6b8ac176.png) 25 | 26 | [Video of Supercraft + Super Shooter Kit workflow](https://www.youtube.com/watch?v=RW3enib2X94) 27 | 28 | ## API 29 | 30 | ### `shooter` component 31 | 32 | The `shooter` component should be attached to a controller for gun. But it can 33 | also be attached to the camera to support 2D / desktop or normal smartphone if 34 | wired to mouse or touch. 35 | 36 | | Property | Description | Default Value | 37 | | -------- | ----------- | ------------- | 38 | | activeBulletType | Name of current active bullet the shooter is firing. | 'normal' | 39 | | bulletTypes | Array of possible bullet names. | ['normal'] | 40 | | cycle | A flag to tell when swapping to the `next` or `prev` bullet type, cycle to the first or last type when reaching the last or first type respectively. | false | 41 | 42 | #### Events 43 | 44 | These events can be triggered with `entity.emit(eventName)`. 45 | 46 | | Event Name | Effect | 47 | |----------------|-------------------------------------------------------------| 48 | | shoot | Shoot a bullet. | 49 | | changebullet | Swap bullet type (either `prev`, `next`, or name of bullet. | 50 | 51 | ### `bullet` component 52 | 53 | | Property | Description | Default Value | 54 | |--------------|---------------------------------------------------------------------------|---------------| 55 | | damagePoints | How many health points to remove from target when hitting target. | 1.0 | 56 | | maxTime | Life time of bullet in seconds. When elapsed, the bullet will be removed. | 1.0 | 57 | | name | Name of the bullet type. | normal | 58 | | poolSize | How many copies of this bullet can be on screen at once. | 10 | 59 | | speed | Speed of bullet in meters per second. | 1.0 | 60 | 61 | ### `target` component 62 | 63 | | Property | Description | Default Value | 64 | |--------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|---------------| 65 | | active | Whether this target is included in collision tests. | true | 66 | | healthPoints | Number of hit or health points of the target. When a bullet hits this target, its health points will decrease by the bullet's damage points. When the target reaches 0 health points, then the event 'die' is emitted on the target. | 1 | 67 | | static | Whether this object does not ever move or change shape. If set to false, then the bounding box is recalculated continuously. | true | 68 | 69 | #### Members 70 | 71 | Component members can be accessed by like `entity.components.target.lastBulletHit`: 72 | 73 | | Member | Description | 74 | |---------------|-----------------------------------------------------------------------------------------------------------------------------------------------| 75 | | lastBulletHit | Reference object to the last bullet object3D that hit the target. Useful for attaining the position of where the bullet contacted the target. | 76 | 77 | #### Events 78 | 79 | Events emitted on the target that we can listen to, to perhaps show an 80 | explosion effect on target hit or die. 81 | 82 | | Event Name | Description | 83 | |------------|--------------------------------------------------------| 84 | | hit | Target was hit by a bullet. | 85 | | die | Target ran out of healthPoints and has been destroyed. | 86 | 87 | ## With Supercraft Assets 88 | 89 | [Supercraft](https://supermedium.com/supercraft) is an A-Frame WebVR 90 | application that lets you build low-poly VR assets inside of VR with your 91 | hands, and export them to Web or JSON! With the shooter kit providing dead-easy 92 | components, A-Frame letting you do things in just HTML, and ability to create 93 | good assets without modeling experience, WebVR development is made simple. 94 | 95 | The advantage of the Supercraft JSON exports alongside 96 | [aframe-supercraft-loader](https://www.npmjs.com/package/aframe-supercraft-loader) 97 | and [aframe-supercraft-thing](https://www.npmjs.com/package/aframe-supercraft-thing) 98 | is that they are tailored for A-Frame resulting in extremely small file sizes 99 | and performant through geometry merging. 100 | 101 | All 3D assets in this scene are delivered within a single 190KB JSON file: 102 | [Supercraft Shooter](https://supermedium.github.io/supercraft-shooter). All 103 | the assets in the game were done using Supercraft in **45 minutes**, and the 104 | code is just dozens of lines of Javascript and HTML. Game created in an 105 | afternoon. 106 | 107 | An extremely cool workflow is using the `supercraft-loader` to "live-reload" 108 | assets. The Supercraft JSON is hosted on the Web via name; we just need to do 109 | `supercraft-loader="name: my-supercraft-site"`, and whenever we publish an update 110 | to `my-supercraft-site` within Supercraft, the scene will automatically have 111 | access to the fresh new assets. 112 | 113 | And `supercraft-thing-loader` can be used to pick individual objects out of a 114 | Supercraft scene of objects. You can create all your assets in one scene, make 115 | sure they have good scale relative to one another, tweak them all at once! 116 | 117 | **[VIDEO](https://www.youtube.com/watch?v=RW3enib2X94)** 118 | 119 | ## Installation 120 | 121 | ### Browser 122 | 123 | See the [Supercraft Shooter example source 124 | code](https://github.com/supermedium/aframe-super-shooter-kit/tree/master/examples/supercraft). 125 | 126 | Install and use by directly including the [browser files](dist): 127 | 128 | ```html 129 | 130 | A-Frame Super Shooter Kit - Basic 131 | 132 | 133 | 168 | 169 | 170 | 171 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | 179 | 180 | 181 | ``` 182 | 183 | ### npm 184 | 185 | Install via npm: 186 | 187 | ```bash 188 | npm install aframe-super-shooter-kit 189 | ``` 190 | 191 | Then require and use. 192 | 193 | ```js 194 | require('aframe'); 195 | require('aframe-super-shooter-kit'); 196 | ``` 197 | -------------------------------------------------------------------------------- /dist/aframe-super-shooter-kit.js: -------------------------------------------------------------------------------- 1 | !function(e){var t={};function i(n){if(t[n])return t[n].exports;var l=t[n]={i:n,l:!1,exports:{}};return e[n].call(l.exports,l,l.exports,i),l.l=!0,l.exports}i.m=e,i.c=t,i.d=function(e,t,n){i.o(e,t)||Object.defineProperty(e,t,{enumerable:!0,get:n})},i.r=function(e){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},i.t=function(e,t){if(1&t&&(e=i(e)),8&t)return e;if(4&t&&"object"==typeof e&&e&&e.__esModule)return e;var n=Object.create(null);if(i.r(n),Object.defineProperty(n,"default",{enumerable:!0,value:e}),2&t&&"string"!=typeof e)for(var l in e)i.d(n,l,function(t){return e[t]}.bind(null,l));return n},i.n=function(e){var t=e&&e.__esModule?function(){return e.default}:function(){return e};return i.d(t,"a",t),t},i.o=function(e,t){return Object.prototype.hasOwnProperty.call(e,t)},i.p="",i(i.s=0)}([function(e,t){if("undefined"==typeof AFRAME)throw new Error("Component attempted to register before AFRAME was available.");AFRAME.registerComponent("shooter",{schema:{activeBulletType:{type:"string",default:"normal"},bulletTypes:{type:"array",default:["normal"]},cycle:{default:!1}},init:function(){this.el.addEventListener("shoot",this.onShoot.bind(this)),this.el.addEventListener("changebullet",this.onChangeBullet.bind(this)),this.bulletSystem=this.el.sceneEl.systems.bullet},onShoot:function(){this.bulletSystem.shoot(this.data.activeBulletType,this.el.object3D)},onChangeBullet:function(e){var t,i=this.data,n=this.el;if("next"===e.detail){if(-1===(t=i.bulletTypes.indexOf(i.activeBulletType)))return;return t=i.cycle?(t+1)%i.bulletTypes.length:Math.min(i.bulletTypes.length-1,t+1),i.activeBulletType=i.bulletTypes[t],void n.setAttribute("shooter","activeBulletType",i.bulletTypes[t])}if("prev"===e.detail){if(-1===(t=i.bulletTypes.indexOf(i.activeBulletType)))return;return t=i.cycle?(t-1)%i.bulletTypes.length:Math.max(0,t-1),i.activeBulletType=i.bulletTypes[t],void n.setAttribute("shooter","activeBulletType",i.bulletTypes[t])}n.setAttribute("shooter","activeBulletType",e.detail)}}),AFRAME.registerComponent("bullet",{dependencies:["material"],schema:{damagePoints:{default:1,type:"float"},maxTime:{default:4,type:"float"},name:{default:"normal",type:"string"},poolSize:{default:10,type:"int",min:0},speed:{default:8,type:"float"}},init:function(){var e=this.el;e.object3D.visible=!1,e.addEventListener("object3dset",t=>{e.sceneEl.systems.bullet.registerBullet(this)})}}),AFRAME.registerSystem("bullet",{init:function(){var e;(e=document.createElement("a-entity")).id="superShooterBulletContainer",this.el.sceneEl.appendChild(e),this.container=e.object3D,this.pool={},this.targets=[]},registerBullet:function(e){var t,i,n,l;if(l=e.el.object3D)for(i=e.data,this.pool[i.name]=[],n=0;nl&&(n=i,l=o[i].time)}return this.shootBullet(o[n],t)},shootBullet:function(e,t){return e.visible=!0,e.time=0,t.getWorldPosition(e.position),t.getWorldDirection(e.direction),e.direction.multiplyScalar(-e.speed),this.container.add(e),e},tick:function(){var e=new THREE.Box3,t=new THREE.Vector3,i=new THREE.Box3;return function(n,l){var o,r,s,a,u;for(r=0;r=o.maxTime)this.killBullet(o);else for(t.copy(o.direction).multiplyScalar(l/850),o.position.add(t),e.setFromObject(o),u=0;u{e.sceneEl.systems.bullet.registerTarget(this,this.data.static)})},update:function(e){this.healthPoints=this.data.healthPoints},onBulletHit:function(e){this.data.active&&(this.lastBulletHit=e,this.healthPoints-=e.damagePoints,this.healthPoints<=0&&this.el.emit("die"))}})}]); -------------------------------------------------------------------------------- /dist/aframe-super-shooter-kit.min.js: -------------------------------------------------------------------------------- 1 | !function(e){var t={};function i(n){if(t[n])return t[n].exports;var l=t[n]={i:n,l:!1,exports:{}};return e[n].call(l.exports,l,l.exports,i),l.l=!0,l.exports}i.m=e,i.c=t,i.d=function(e,t,n){i.o(e,t)||Object.defineProperty(e,t,{enumerable:!0,get:n})},i.r=function(e){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},i.t=function(e,t){if(1&t&&(e=i(e)),8&t)return e;if(4&t&&"object"==typeof e&&e&&e.__esModule)return e;var n=Object.create(null);if(i.r(n),Object.defineProperty(n,"default",{enumerable:!0,value:e}),2&t&&"string"!=typeof e)for(var l in e)i.d(n,l,function(t){return e[t]}.bind(null,l));return n},i.n=function(e){var t=e&&e.__esModule?function(){return e.default}:function(){return e};return i.d(t,"a",t),t},i.o=function(e,t){return Object.prototype.hasOwnProperty.call(e,t)},i.p="",i(i.s=0)}([function(e,t){if("undefined"==typeof AFRAME)throw new Error("Component attempted to register before AFRAME was available.");AFRAME.registerComponent("shooter",{schema:{activeBulletType:{type:"string",default:"normal"},bulletTypes:{type:"array",default:["normal"]},cycle:{default:!1}},init:function(){this.el.addEventListener("shoot",this.onShoot.bind(this)),this.el.addEventListener("changebullet",this.onChangeBullet.bind(this)),this.bulletSystem=this.el.sceneEl.systems.bullet},onShoot:function(){this.bulletSystem.shoot(this.data.activeBulletType,this.el.object3D)},onChangeBullet:function(e){var t,i=this.data,n=this.el;if("next"===e.detail){if(-1===(t=i.bulletTypes.indexOf(i.activeBulletType)))return;return t=i.cycle?(t+1)%i.bulletTypes.length:Math.min(i.bulletTypes.length-1,t+1),i.activeBulletType=i.bulletTypes[t],void n.setAttribute("shooter","activeBulletType",i.bulletTypes[t])}if("prev"===e.detail){if(-1===(t=i.bulletTypes.indexOf(i.activeBulletType)))return;return t=i.cycle?(t-1)%i.bulletTypes.length:Math.max(0,t-1),i.activeBulletType=i.bulletTypes[t],void n.setAttribute("shooter","activeBulletType",i.bulletTypes[t])}n.setAttribute("shooter","activeBulletType",e.detail)}}),AFRAME.registerComponent("bullet",{dependencies:["material"],schema:{damagePoints:{default:1,type:"float"},maxTime:{default:4,type:"float"},name:{default:"normal",type:"string"},poolSize:{default:10,type:"int",min:0},speed:{default:8,type:"float"}},init:function(){var e=this.el;e.object3D.visible=!1,e.addEventListener("object3dset",t=>{e.sceneEl.systems.bullet.registerBullet(this)})}}),AFRAME.registerSystem("bullet",{init:function(){var e;(e=document.createElement("a-entity")).id="superShooterBulletContainer",this.el.sceneEl.appendChild(e),this.container=e.object3D,this.pool={},this.targets=[]},registerBullet:function(e){var t,i,n,l;if(l=e.el.object3D)for(i=e.data,this.pool[i.name]=[],n=0;nl&&(n=i,l=o[i].time)}return this.shootBullet(o[n],t)},shootBullet:function(e,t){return e.visible=!0,e.time=0,t.getWorldPosition(e.position),t.getWorldDirection(e.direction),e.direction.multiplyScalar(-e.speed),this.container.add(e),e},tick:function(){var e=new THREE.Box3,t=new THREE.Vector3,i=new THREE.Box3;return function(n,l){var o,r,s,a,u;for(r=0;r=o.maxTime)this.killBullet(o);else for(t.copy(o.direction).multiplyScalar(l/850),o.position.add(t),e.setFromObject(o),u=0;u{e.sceneEl.systems.bullet.registerTarget(this,this.data.static)})},update:function(e){this.healthPoints=this.data.healthPoints},onBulletHit:function(e){this.data.active&&(this.lastBulletHit=e,this.healthPoints-=e.damagePoints,this.healthPoints<=0&&this.el.emit("die"))}})}]); 2 | -------------------------------------------------------------------------------- /examples/basic/index.html: -------------------------------------------------------------------------------- 1 | 2 | A-Frame Super Shooter Kit - Basic 3 | 4 | 5 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | -------------------------------------------------------------------------------- /examples/supercraft/components/enemy.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Enemy component. 3 | * Handle enemy animation and dying behavior. 4 | * Favor using threejs `object3D` property of entities rather than `setAttribute()` 5 | * for optimization. 6 | */ 7 | AFRAME.registerComponent('enemy', { 8 | schema: { 9 | // 1: behind bushes 10 | // 2: behind trees 11 | // 3: behind clouds 12 | type: {default: 1} 13 | }, 14 | 15 | /** 16 | * Initialize listeners and component variables. 17 | * Find and link with the explosion object associated. 18 | */ 19 | init: function () { 20 | var el = this.el; 21 | var explosionScale; 22 | 23 | this.hidingPos = 0; // Save rest y position when enemy is hidden. 24 | this.timeout = null; // Timeout for waiting for the next appearance. 25 | this.tweenAppear = null; // Tween for appearing animation. 26 | this.tweenDisappear = null; // Tween for hiding animation. 27 | this.vulnerable = false; // Cannot be shoot when it's hiding. 28 | 29 | // Link with explosion object. 30 | // Hide explosion object and set scale depending on type of enemy (further == bigger). 31 | this.explosion = document.getElementById(`${this.el.id}expl`).object3D; 32 | this.explosion.visible = false; 33 | explosionScale = this.data.type * 2.2; 34 | this.explosion.scale.set(explosionScale, explosionScale, explosionScale); 35 | 36 | el.addEventListener('run', this.run.bind(this)); 37 | el.addEventListener('stop', this.stop.bind(this)); 38 | el.addEventListener('hit', this.die.bind(this)); 39 | }, 40 | 41 | /** 42 | * Game start. When start message is shot. 43 | */ 44 | run: function () { 45 | var lift; 46 | 47 | // Create tweens if not created yet. 48 | if (this.tweenAppear === null) { 49 | // Save hidingPos (default position on the Supercraft site). 50 | this.hidingPos = this.el.object3D.position.y; 51 | 52 | // Depending the type of enemy, the further it is, the higher it has to rise. 53 | lift = this.data.type * 1.2; 54 | this.tweenAppear = new TWEEN.Tween(this.el.object3D.position) 55 | .to({y: this.hidingPos + lift}, 500) 56 | .easing(TWEEN.Easing.Elastic.Out) 57 | .onComplete(this.endAppear.bind(this)); 58 | 59 | this.tweenDisappear = new TWEEN.Tween(this.el.object3D.position) 60 | .to({y: this.hidingPos}, 200) 61 | .delay(1000) 62 | .easing(TWEEN.Easing.Cubic.Out) 63 | .onComplete(this.endDisappear.bind(this)); 64 | } 65 | 66 | // Hide start message. 67 | document.getElementById('startMessage').object3D.visible = false; 68 | this.appear(); 69 | }, 70 | 71 | /** 72 | * Animate and move in. 73 | */ 74 | appear: function () { 75 | this.tweenAppear.start(); 76 | this.el.querySelector('[sound]').components.sound.playSound(); 77 | }, 78 | 79 | /** 80 | * Handle appearance animation finished. 81 | */ 82 | endAppear: function () { 83 | this.vulnerable = true; 84 | // We can start the disappear tween right away because it has a delay and it will hold a 85 | // sec. 86 | this.tweenDisappear.start(); 87 | }, 88 | 89 | /** 90 | * Handle hiding animation is finished. 91 | */ 92 | endDisappear: function () { 93 | this.vulnerable = false; 94 | // Set next appearance to a random value of seconds between 1 and 4. 95 | this.timeout = setTimeout(this.appear.bind(this), 96 | 1000 + Math.floor(Math.random() * 3000)); 97 | }, 98 | 99 | /** 100 | * Stop tweens and timeouts. 101 | */ 102 | stop: function () { 103 | this.tweenAppear.stop(); 104 | this.tweenDisappear.stop(); 105 | clearTimeout(this.timeout); 106 | this.vulnerable = false; 107 | }, 108 | 109 | /** 110 | * Enemy was killed (this is called via `die` event thrown by the bullets when collide). 111 | */ 112 | die: function (evt) { 113 | var el = this.el; 114 | 115 | if (!this.vulnerable) { return; } // I'm hidden! 116 | 117 | this.stop(); 118 | 119 | // Hide enemy, return it to hiding position. 120 | el.object3D.visible = false; 121 | el.object3D.position.y = this.hidingPos; 122 | 123 | // Play explosion sound. 124 | document.getElementById('commonExplosion').components.sound.playSound(); 125 | 126 | // Show explosion on the hit position and make it look to center of stage. 127 | this.explosion.position.copy(this.el.components.target.lastBulletHit.position); 128 | this.explosion.lookAt(0, 1.6, 0); 129 | this.explosion.visible = true; 130 | 131 | // Wait 300 ms to hide explosion. 132 | setTimeout(() => { 133 | this.explosion.visible = false; 134 | this.el.object3D.visible = true; 135 | // After a random number of secs (2-5), appear again. 136 | setTimeout(this.appear.bind(this), 137 | 2000 + Math.floor(Math.random() * 3000)); 138 | }, 300); 139 | } 140 | }); 141 | -------------------------------------------------------------------------------- /examples/supercraft/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Supercraft Shooter 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 119 | 120 | 121 | 126 | 127 | 131 | 140 | 141 | 142 | 143 | 144 | 145 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | 171 | 173 | 174 | -------------------------------------------------------------------------------- /examples/supercraft/sounds/enemy1a.ogg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/supermedium/aframe-super-shooter-kit/7b1da1b59e1adc4b182b591167456bd4a6f8caff/examples/supercraft/sounds/enemy1a.ogg -------------------------------------------------------------------------------- /examples/supercraft/sounds/enemy1b.ogg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/supermedium/aframe-super-shooter-kit/7b1da1b59e1adc4b182b591167456bd4a6f8caff/examples/supercraft/sounds/enemy1b.ogg -------------------------------------------------------------------------------- /examples/supercraft/sounds/enemy1c.ogg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/supermedium/aframe-super-shooter-kit/7b1da1b59e1adc4b182b591167456bd4a6f8caff/examples/supercraft/sounds/enemy1c.ogg -------------------------------------------------------------------------------- /examples/supercraft/sounds/enemy2.ogg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/supermedium/aframe-super-shooter-kit/7b1da1b59e1adc4b182b591167456bd4a6f8caff/examples/supercraft/sounds/enemy2.ogg -------------------------------------------------------------------------------- /examples/supercraft/sounds/enemy3.ogg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/supermedium/aframe-super-shooter-kit/7b1da1b59e1adc4b182b591167456bd4a6f8caff/examples/supercraft/sounds/enemy3.ogg -------------------------------------------------------------------------------- /examples/supercraft/sounds/explosion.ogg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/supermedium/aframe-super-shooter-kit/7b1da1b59e1adc4b182b591167456bd4a6f8caff/examples/supercraft/sounds/explosion.ogg -------------------------------------------------------------------------------- /examples/supercraft/sounds/shoot.ogg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/supermedium/aframe-super-shooter-kit/7b1da1b59e1adc4b182b591167456bd4a6f8caff/examples/supercraft/sounds/shoot.ogg -------------------------------------------------------------------------------- /examples/supercraft/sounds/song.ogg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/supermedium/aframe-super-shooter-kit/7b1da1b59e1adc4b182b591167456bd4a6f8caff/examples/supercraft/sounds/song.ogg -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | A-Frame Super Shooter Kit 6 | 7 | 48 | 49 | 50 |

A-Frame super-shooter kit

51 | 52 |
    53 |
  • 54 | 55 |

    Supercraft Shooter

    56 |

    Full example of using aframe-super-shooter-kit with assets made in VR with Supercraft.

    57 |
  • 58 | 59 |
  • 60 | 61 |

    Basic

    62 |

    This is a barebones example.

    63 |
  • 64 |
65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 74 | 75 | 76 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | /* global AFRAME */ 2 | 3 | if (typeof AFRAME === 'undefined') { 4 | throw new Error('Component attempted to register before AFRAME was available.'); 5 | } 6 | 7 | /** 8 | * Shooter. Entity that spawns bullets and handles bullet types. 9 | */ 10 | AFRAME.registerComponent('shooter', { 11 | schema: { 12 | activeBulletType: {type: 'string', default: 'normal'}, 13 | bulletTypes: {type: 'array', default: ['normal']}, 14 | cycle: {default: false} 15 | }, 16 | 17 | init: function () { 18 | this.el.addEventListener('shoot', this.onShoot.bind(this)); 19 | this.el.addEventListener('changebullet', this.onChangeBullet.bind(this)); 20 | this.bulletSystem = this.el.sceneEl.systems.bullet; 21 | }, 22 | 23 | /** 24 | * Listent to `shoot` action / event to tell bullet system to fire a bullet. 25 | */ 26 | onShoot: function () { 27 | this.bulletSystem.shoot(this.data.activeBulletType, this.el.object3D); 28 | }, 29 | 30 | /** 31 | * Listen to `changebullet` action / event telling the shooter to change bullet type. 32 | */ 33 | onChangeBullet: function (evt) { 34 | var data = this.data; 35 | var el = this.el; 36 | var idx; 37 | 38 | // Cycle to next bullet type. 39 | if (evt.detail === 'next') { 40 | idx = data.bulletTypes.indexOf(data.activeBulletType); 41 | if (idx === -1) { return; } 42 | idx = data.cycle 43 | ? (idx + 1) % data.bulletTypes.length 44 | : Math.min(data.bulletTypes.length - 1, idx + 1); 45 | data.activeBulletType = data.bulletTypes[idx]; 46 | el.setAttribute('shooter', 'activeBulletType', data.bulletTypes[idx]); 47 | return; 48 | } 49 | 50 | // Cycle to previous bullet type. 51 | if (evt.detail === 'prev') { 52 | idx = data.bulletTypes.indexOf(data.activeBulletType); 53 | if (idx === -1) { return; } 54 | idx = data.cycle 55 | ? (idx - 1) % data.bulletTypes.length 56 | : Math.max(0, idx - 1); 57 | data.activeBulletType = data.bulletTypes[idx]; 58 | el.setAttribute('shooter', 'activeBulletType', data.bulletTypes[idx]); 59 | return; 60 | } 61 | 62 | // Direct set bullet type. 63 | el.setAttribute('shooter', 'activeBulletType', evt.detail); 64 | } 65 | }); 66 | 67 | /** 68 | * Bullet template component 69 | */ 70 | AFRAME.registerComponent('bullet', { 71 | dependencies: ['material'], 72 | 73 | schema: { 74 | damagePoints: {default: 1.0, type: 'float'}, 75 | maxTime: {default: 4.0, type: 'float'}, // seconds. 76 | name: {default: 'normal', type: 'string'}, 77 | poolSize: {default: 10, type: 'int', min: 0}, 78 | speed: {default: 8.0, type: 'float'} // meters / sec. 79 | }, 80 | 81 | init: function () { 82 | var el = this.el; 83 | el.object3D.visible = false; 84 | el.addEventListener('object3dset', evt => { 85 | el.sceneEl.systems.bullet.registerBullet(this); 86 | }); 87 | } 88 | }); 89 | 90 | /** 91 | * Bullet system for collision detection. 92 | */ 93 | AFRAME.registerSystem('bullet', { 94 | init: function () { 95 | var bulletContainer; 96 | bulletContainer = document.createElement('a-entity'); 97 | bulletContainer.id = 'superShooterBulletContainer'; 98 | this.el.sceneEl.appendChild(bulletContainer); 99 | 100 | this.container = bulletContainer.object3D; 101 | this.pool = {}; 102 | this.targets = []; 103 | }, 104 | 105 | /** 106 | * Register and initialize bullet type. 107 | */ 108 | registerBullet: function (bulletComponent) { 109 | var bullet; 110 | var bulletData; 111 | var i; 112 | var model; 113 | 114 | model = bulletComponent.el.object3D; 115 | if (!model) { return; } 116 | bulletData = bulletComponent.data; 117 | 118 | // Initialize pool and bullets. 119 | this.pool[bulletData.name] = []; 120 | for (i = 0; i < bulletData.poolSize; i++) { 121 | bullet = model.clone(); 122 | bullet.damagePoints = bulletData.damagePoints; 123 | bullet.direction = new THREE.Vector3(0, 0, -1); 124 | bullet.maxTime = bulletData.maxTime * 1000; 125 | bullet.name = bulletData.name + i; 126 | bullet.speed = bulletData.speed; 127 | bullet.time = 0; 128 | bullet.visible = false; 129 | this.pool[bulletData.name].push(bullet); 130 | } 131 | }, 132 | 133 | /** 134 | * Register single target. 135 | */ 136 | registerTarget: function (targetComponent, isStatic) { 137 | var targetObj; 138 | this.targets.push(targetComponent.el); 139 | if (!isStatic) { return; } 140 | 141 | // Precalculate bounding box of bullet. 142 | targetObj = targetComponent.el.object3D; 143 | targetObj.boundingBox = new THREE.Box3().setFromObject(targetObj); 144 | }, 145 | 146 | shoot: function (bulletName, gun) { 147 | var i; 148 | var oldest = 0; 149 | var oldestTime = 0; 150 | var pool = this.pool[bulletName]; 151 | 152 | if (pool === undefined) { return null; } 153 | 154 | // Find available bullet and initialize it. 155 | for (i = 0; i < pool.length; i++) { 156 | if (pool[i].visible === false) { 157 | return this.shootBullet(pool[i], gun); 158 | } else if (pool[i].time > oldestTime){ 159 | oldest = i; 160 | oldestTime = pool[i].time; 161 | } 162 | } 163 | 164 | // All bullets are active, pool is full, grab oldest bullet. 165 | return this.shootBullet(pool[oldest], gun); 166 | }, 167 | 168 | shootBullet: function (bullet, gun) { 169 | bullet.visible = true; 170 | bullet.time = 0; 171 | gun.getWorldPosition(bullet.position); 172 | gun.getWorldDirection(bullet.direction); 173 | bullet.direction.multiplyScalar(-bullet.speed); 174 | this.container.add(bullet); 175 | return bullet; 176 | }, 177 | 178 | tick: (function () { 179 | var bulletBox = new THREE.Box3(); 180 | var bulletTranslation = new THREE.Vector3(); 181 | var targetBox = new THREE.Box3(); 182 | 183 | return function (time, delta) { 184 | var bullet; 185 | var i; 186 | var isHit; 187 | var targetObj; 188 | var t; 189 | 190 | for (i = 0; i < this.container.children.length; i++) { 191 | bullet = this.container.children[i]; 192 | if (!bullet.visible) { continue; } 193 | bullet.time += delta; 194 | if (bullet.time >= bullet.maxTime) { 195 | this.killBullet(bullet); 196 | continue; 197 | } 198 | bulletTranslation.copy(bullet.direction).multiplyScalar(delta / 850); 199 | bullet.position.add(bulletTranslation); 200 | 201 | // Check collisions. 202 | bulletBox.setFromObject(bullet); 203 | for (t = 0; t < this.targets.length; t++) { 204 | let target = this.targets[t]; 205 | if (!target.getAttribute('target').active) { continue; } 206 | targetObj = target.object3D; 207 | if (!targetObj.visible) { continue; } 208 | isHit = false; 209 | if (targetObj.boundingBox) { 210 | isHit = targetObj.boundingBox.intersectsBox(bulletBox); 211 | } else { 212 | targetBox.setFromObject(targetObj); 213 | isHit = targetBox.intersectsBox(bulletBox); 214 | } 215 | if (isHit) { 216 | this.killBullet(bullet); 217 | target.components.target.onBulletHit(bullet); 218 | target.emit('hit', null); 219 | break; 220 | } 221 | } 222 | } 223 | }; 224 | })(), 225 | 226 | killBullet: function (bullet) { 227 | bullet.visible = false; 228 | } 229 | }); 230 | 231 | /** 232 | * target component 233 | */ 234 | AFRAME.registerComponent('target', { 235 | schema: { 236 | active: {default: true}, 237 | healthPoints: {default: 1, type: 'float'}, 238 | static: {default: true}, 239 | }, 240 | 241 | init: function () { 242 | var el = this.el; 243 | el.addEventListener('object3dset', evt => { 244 | el.sceneEl.systems.bullet.registerTarget(this, this.data.static); 245 | }); 246 | }, 247 | 248 | update: function (oldData) { 249 | // `this.healthPoints` is current hit points with taken damage. 250 | // `this.data.healthPoints` is total hit points. 251 | this.healthPoints = this.data.healthPoints; 252 | }, 253 | 254 | /** 255 | * Take damage. 256 | */ 257 | onBulletHit: function (bullet) { 258 | if (!this.data.active) { return; } 259 | this.lastBulletHit = bullet; 260 | this.healthPoints -= bullet.damagePoints; 261 | if (this.healthPoints <= 0) { this.el.emit('die'); } 262 | } 263 | }); 264 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "aframe-super-shooter-kit", 3 | "version": "1.1.7", 4 | "description": "A-Frame kit of components for making a simple WebVR shooter minigame.", 5 | "main": "index.js", 6 | "unpkg": "dist/aframe-super-shooter-kit.min.js", 7 | "scripts": { 8 | "build": "webpack index.js -o dist/aframe-super-shooter-kit.js", 9 | "dev": "budo index.js:dist/aframe-super-shooter-kit.min.js --port 7000 --live --open", 10 | "dist": "npm run build && uglifyjs dist/aframe-super-shooter-kit.js > dist/aframe-super-shooter-kit.min.js", 11 | "lint": "semistandard -v | snazzy", 12 | "prepublish": "npm run dist", 13 | "ghpages": "ghpages", 14 | "start": "npm run dev" 15 | }, 16 | "repository": { 17 | "type": "git", 18 | "url": "git+https://github.com/supermedium/aframe-super-shooter-kit.git" 19 | }, 20 | "keywords": [ 21 | "aframe", 22 | "aframe-component", 23 | "aframe-vr", 24 | "vr", 25 | "webvr", 26 | "shooter" 27 | ], 28 | "author": "Supermedium ", 29 | "license": "MIT", 30 | "bugs": { 31 | "url": "https://github.com/supermedium/aframe-super-shooter-kit/issues" 32 | }, 33 | "homepage": "https://github.com/supermedium/aframe-super-shooter-kit#readme", 34 | "devDependencies": { 35 | "aframe": "*", 36 | "browserify": "^13.0.0", 37 | "budo": "^8.2.2", 38 | "ghpages": "^0.0.8", 39 | "randomcolor": "^0.4.4", 40 | "semistandard": "^8.0.0", 41 | "shelljs": "^0.7.0", 42 | "shx": "^0.1.1", 43 | "snazzy": "^4.0.0", 44 | "uglify-es": "github:mishoo/UglifyJS2#harmony" 45 | }, 46 | "semistandard": { 47 | "globals": [ 48 | "AFRAME", 49 | "THREE" 50 | ], 51 | "ignore": [ 52 | "examples/build.js", 53 | "dist/**" 54 | ] 55 | } 56 | } 57 | --------------------------------------------------------------------------------