SenPie Posted February 1, 2022 Share Posted February 1, 2022 What I'm trying to accomplish is to basically write all my game logic in world units and use camera to see part of it rendered on the screen. Let's say my world is an infinite plane and I want to add a sprite, which will have dimensions of one unit in x axis and one unit in y axis. Then, I define a camera with dimensions of 4 units on x axis and 2 units on y axis. Visualization below. To get close to my desired result I followed the example in this blog post and used RenderTextureSystem to actually define source canvas dimensions equal to my world dimensions and target dimensions to the browser window size. Here is the code class WorldContainer extends DisplayObject { sortDirty: boolean; public content: Container; public camWidth = 4; public camHeight = 2; public camOffsetX = 0; public camOffsetY = 0; constructor() { super(); this.content = new Container(); } calculateBounds(): void { return this.content._calculateCachedBounds(); } removeChild(child: DisplayObject): void { this.content.removeChild(child); } removeChildren(): DisplayObject[] { return this.content.removeChildren(); } addChild(child: DisplayObject): DisplayObject { return this.content.addChild(child); } render(renderer: Renderer) { const targetWidth = renderer.view.width; const targetHeight = renderer.view.height; const targetRatio = targetWidth / targetHeight; const sourceWidth = this.camWidth; const sourceHeight = this.camHeight; const sourceRatio = sourceWidth / sourceHeight; let requiredWidth = 0; let requiredHeight = 0; if (sourceRatio >= targetRatio) { // source is wider than target in proportion requiredWidth = targetWidth; requiredHeight = requiredWidth / sourceRatio; } else { // source is higher than target in proportion requiredHeight = targetHeight; requiredWidth = requiredHeight * sourceRatio; } renderer.renderTexture.bind( null, new Rectangle(this.camOffsetX - this.camWidth / 2, -this.camOffsetY + this.camHeight / 2, this.camWidth, this.camHeight), new Rectangle((targetWidth - requiredWidth) / 2, (targetHeight - requiredHeight) / 2, requiredWidth, requiredHeight), ); this.content.render(renderer); renderer.batch.flush(); renderer.renderTexture.bind(null); } updateTransform() { super.updateTransform(); const { _tempDisplayObjectParent: tempDisplayObjectParent } = this.content; this.content.parent = tempDisplayObjectParent as Container; this.content.updateTransform(); this.content.parent = null; } } I have done some extra work to preserve aspect ratio of camera, when drawing to canvas, so it won't get stretched and will just fit the screen. However, real issue comes when I try to make sprites, because when I give it a texture and let's say dimensions of my texture are 64x64 pixels, then my sprite is huge filling the whole screen. That's when I thought just setting width and height of the sprite should be enough. For example to recreate the example in the picture above, I would make the sprite like this and add it to the content of world container. const sprite: Sprite = Sprite.from('sadge.png'); sprite.anchor.set(0.5); sprite.x = 1.5; sprite.y = 1.5; sprite.width = 1; sprite.height = 1; worldContainer.content.addChild(sprite); Now, what I don't like about this solution is, that when I add child sprite to that previous sprite and give it's x to be equal to 1, y to be equal to 0, I expect it to appear on second row and third column. However, not only the child sprite is not in my expected position, it's also not visible. After, hours of debugging I found out, that when setting width and height for parent sprite, under the hood pixi modifies scale and it ruins everything for child sprite. If, I set sprite's width height to be 1x1, it sets scale for sprite to be ( 4 / canvas_w, 2 / canvas_h), which is very tiny and because that scale is also applied to its children, they get so small that they are not visible. I know that I can manually multiply by inverse of the scale ratio for every child and cancel that effect, but to be frank it is very ugly solution. I was wondering if you could help me to fix this issue, or give me a directing advice on how to approach it. If feels like I am solving this whole world unit issue in a very wrong way and there is far simpler and neater solution. I've been struggling with this thing for weeks now and would really appreciate any help. Quote Link to comment Share on other sites More sharing options...
Exca Posted February 2, 2022 Share Posted February 2, 2022 I dont think you need to use separate render texture there. You could just have your sprites be already in woorld coordinates and then scale the container to desired zoom level and set it's pivot & coordinate to use as your camera. And if you have a lot of sprites then you should add culling into the mix by just going through all sprites and checking if their bounds are outside of your view and set renderable false/true according to that. Here's a very simple (untested) example that assumes that 1 unit in your world coordinate is 256px. function addSprite( worldX, worldY, asset, world ){ const s = Sprite.from(asset); s.x = worldX; s.y = worldY; s.scale.set(1/256); world.addChild(s); } const world = new Container(); // Put the world into center of screen. world.x = renderer.screen.width/2; world.y = renderer.screen.height/2; // Show 10 world units on screen. world.scale.set(10); // Create a camera data object that has camera position in world units. const camera = {x:5, y:10}; // Use pivot point to control camera. world.pivot.set( camera.x, camera.y ); addSprite(2,4, "monster1.png"); addSprite(8,5, "monster2.png"); addSprite(10,0, "monster3.png"); addSprite(22,14, "monster4.png"); ... in some logic update camera position & other game things ... in render loop render the world Quote Link to comment Share on other sites More sharing options...
SenPie Posted February 3, 2022 Author Share Posted February 3, 2022 Hi @Exca, thank you for reply. I have followed your ideas and code example and made this codepen. However, I'm still not getting quite the desired result. I've added debug graphics rectangle to show area where camera image will be rendered, however all of my contents are not fitting it correctly. I want to make it responsive, so canvas always takes the whole document space, however camera rectangle should fit canvas without changing the aspect ratio. Like in this case, camera cannot fill the whole canvas, but it fills in horizontal direction. And with that, I correspondingly want to scale the sprites that no matter how you change the window size, you will see the same image from camera with same dimensions. Quote Link to comment Share on other sites More sharing options...
Exca Posted February 3, 2022 Share Posted February 3, 2022 If you want to keep the distances inside your world the same you shouldnt change the size of things inside the world. So keep their scale in relation to world and change the viewport (camera) scale. Also if you want to have only a certain area that is visible (while still allowing canvas to go over the red area) add a mask to the viewport. If on the other hand you dont need canvas on the light blue area then you could just limit the canvas element size and have only the red area be rendered saving small bit of performance. Here's an example based on your codepen. Don't have a codepen account so couldnt create a fork of it: import { Application, Container, Sprite, Renderer, DisplayObject, Graphics, Ticker } from "https://cdn.skypack.dev/[email protected]"; function resize(renderer: Renderer, world: Container) { // Resize the canvas. renderer.resize( window.visualViewport.width, window.visualViewport.height, ); // Center world. world.x = app.renderer.screen.width / 2; world.y = app.renderer.screen.height / 2; } const app = new Application({ width: window.visualViewport.width, height: window.visualViewport.height, backgroundColor: 0xabcdef, }); document.body.appendChild(app.view); const world = new Container(); // Put the world into the center of screen. world.x = app.renderer.screen.width / 2; world.y = app.renderer.screen.height / 2; // Create a camera data object that has camera position in world units. const camera = { x: 0, y: 0 }; // Use pivot point to control camera. world.pivot.set(camera.x, camera.y); const displayObjs: DisplayObject = []; const viewportMask = new Graphics(); // Use a 1x1 mask and scale it to fit. viewportMask.beginFill(0xffffff,1); viewportMask.drawRect(0,0,1,1); viewportMask.endFill(); // Should maybe add a a separate container here instead to allow other stuff on canvas than world & it's viewport. app.stage.mask = viewportMask; const utilities = (function Utilities(world: Container) { function addSprite(worldX: number, worldY: number, asset: string) { const sprite: Sprite = Sprite.from(asset); sprite.x = worldX; sprite.y = worldY; sprite.anchor.set(0.5); world.addChild(sprite); sprite.scale.set(1/960); displayObjs.push(sprite); updateScale(); } function addDebug() { // Use this as a ground for world const graphics: Graphics = new Graphics(); graphics.beginFill(0xFF0000); graphics.drawRect(-10, -10, 20, 20); graphics.endFill(); graphics.pivot.set(2, 1); world.addChild(graphics); } function updateScale() { // Scale the viewport & world mask so that it always has 4 world units of width & 2 of height. if(window.innerWidth/4 < window.innerHeight/2){ // Width is the limiting factor to fit 4x2 square. const worldScale = window.innerWidth/4; viewportMask.height = window.innerWidth/4*2; viewportMask.width = window.innerWidth; world.scale.set(worldScale); } else{ // Use height as limiting const worldScale = window.innerHeight/2; viewportMask.height = window.innerHeight; viewportMask.width = window.innerHeight/2*4; world.scale.set(worldScale); } // Keep the 4x2 area in the center of screen. viewportMask.x = (app.renderer.screen.width-viewportMask.width)/2; viewportMask.y = (app.renderer.screen.height-viewportMask.height)/2; } return { addDebug: addDebug, addSprite: addSprite, updateScale: updateScale, } })(world); utilities.updateScale(); utilities.addDebug(); utilities.addSprite(0, 0, "https://cdn.pixabay.com/photo/2021/08/16/22/04/nature-6551466_960_720.jpg"); utilities.addSprite(1, 1, "https://cdn.pixabay.com/photo/2021/08/16/22/04/nature-6551466_960_720.jpg"); utilities.addSprite(2, 0, "https://cdn.pixabay.com/photo/2021/08/16/22/04/nature-6551466_960_720.jpg"); utilities.addSprite(3, -1, "https://cdn.pixabay.com/photo/2021/08/16/22/04/nature-6551466_960_720.jpg"); utilities.addSprite(4, 0, "https://cdn.pixabay.com/photo/2021/08/16/22/04/nature-6551466_960_720.jpg"); utilities.addSprite(5, 1, "https://cdn.pixabay.com/photo/2021/08/16/22/04/nature-6551466_960_720.jpg"); utilities.addSprite(-1, 1, "https://cdn.pixabay.com/photo/2021/08/16/22/04/nature-6551466_960_720.jpg"); utilities.addSprite(-2, 0, "https://cdn.pixabay.com/photo/2021/08/16/22/04/nature-6551466_960_720.jpg"); utilities.addSprite(-3, -1, "https://cdn.pixabay.com/photo/2021/08/16/22/04/nature-6551466_960_720.jpg"); utilities.addSprite(-4, 0, "https://cdn.pixabay.com/photo/2021/08/16/22/04/nature-6551466_960_720.jpg"); utilities.addSprite(-5, 1, "https://cdn.pixabay.com/photo/2021/08/16/22/04/nature-6551466_960_720.jpg"); app.stage.addChild(world); // Update initial scale utilities.updateScale(); window.onresize = function() { resize(app.renderer, world); utilities.updateScale(); } // Add camera movement over time Ticker.shared.add( ()=>{ const now = Date.now()/1000; camera.x = Math.cos(now*0.1)*2; camera.y = Math.sin(now*0.2)*1.5; world.pivot.set( camera.x, camera.y ); }); SenPie 1 Quote Link to comment Share on other sites More sharing options...
SenPie Posted February 4, 2022 Author Share Posted February 4, 2022 @ExcaThank you for your time. it works, however my main issue is still present. When I add child sprite to the parent sprite, because of the paren't small scale, 1/960 to be precise, it gets applied to the child element, which also has local scale of 1/960, which results child's scale to be 1/921600, and therefore is not visible. Desired behaviour is that I have something like two types of scales for DisplayO bjects, worldScale and scale. By default although their scale is 1/960, their virtual scale would be 1 and 1. So, only virtual scale would be applied to each other and I would have something like this when I add child sprite with coords x: 0.5 y: 0.5 Also, for example if parent has virtual scale of (0.5, 1) then and only then it would get applied to child and they both would look stretched. Also, because of parent's scale affecting child, to move child with 1 virtual scale to left by one world unit, I have set its x value to be 960. Basically, this scaling won't let me make use of graph scene hierarchy. Quote Link to comment Share on other sites More sharing options...
SenPie Posted February 4, 2022 Author Share Posted February 4, 2022 p. s. I edited the codepen example just a bit. look at line 106 const parent: Sprite = utilities.addSprite(0, 0, "https://cdn.pixabay.com/photo/2021/08/16/22/04/nature-6551466_960_720.jpg"); const child: Sprite = utilities.addSprite(0.5, 0.5, "https://cdn.pixabay.com/photo/2021/08/16/22/04/nature-6551466_960_720.jpg"); parent.addChild(child); // comment this to see the pic Quote Link to comment Share on other sites More sharing options...
Exca Posted February 4, 2022 Share Posted February 4, 2022 Ah true. That solution will have issues if you want to have a scene graph inside the world. One way to fix that could be to use empty containers and have sprites always as leaf nodes. Though that adds a bit of unnecessary complexity to scene graph. Cant immediatly come up with a solution to that. Need to test few things. Quote Link to comment Share on other sites More sharing options...
SenPie Posted February 4, 2022 Author Share Posted February 4, 2022 How so? If sprites would always be leaf nodes, then again one sprite cannot have a child node. You mean I should have something like this? With sprites just being texture holders and all the "virtual" transformations would be done through containers. // make parent const parentContainer: Container = new Container(); const parentSprite: Sprite = new Sprite(); parentContainer.addChild(parentSprite); // apply all transformations to parent container parentContainer.scale.set(0.5, 0.5); // make child const childContainer: Container new Container(); const childCSprite: Sprite = new Sprite(); childContainer.addChild(childSprite); // add child and apply tranform to child parentContainer.addChild(childContainer); childContainer.position.set(1, 1); Yeah, I have been breaking my head into this for a long time now, and still couldn't find a good solution(( pixi.js's scene graph is very coupled with pixels ( if I can formulate that way ). Quote Link to comment Share on other sites More sharing options...
Exca Posted February 5, 2022 Share Posted February 5, 2022 Having it like this (example assumes somekind of character): world - Character 1 (container) - Torso (sprite, scaled) - Belt (sprite, scaled) - Head (container) - Face ( sprite, scaled) - Hat (sprite,scaled) - Eyes (container) - Eye 1 (sprite, scaled) - Eye 2 (sprite, scaled) - Mouth (sprite,scaled) - Legs (container) - Leg 1 (sprite, scaled) - Leg 2 (sprite, scaled) SenPie 1 Quote Link to comment Share on other sites More sharing options...
Recommended Posts
Join the conversation
You can post now and register later. If you have an account, sign in now to post with your account.
Note: Your post will require moderator approval before it will be visible.