From 48ee4ccf64e642673364fda0405dad306287401c Mon Sep 17 00:00:00 2001 From: Evgenii Starostin Date: Mon, 13 Jul 2026 13:16:15 +0300 Subject: [PATCH] added infinite scroll animation example --- .../infinite-scroll-animation/.gitattributes | 42 ++++++++ .../infinite-scroll-animation/.gitignore | 11 +++ .../infinite-scroll-animation/example.md | 91 ++++++++++++++++++ .../infinite-scroll-animation/game.project | 24 +++++ .../input/game.input_binding | 4 + .../main/atlas.atlas | 20 ++++ .../main/images/blue_body_square.png | Bin 0 -> 741 bytes .../main/images/green_body_square.png | Bin 0 -> 771 bytes .../main/images/purple_body_square.png | Bin 0 -> 769 bytes .../main/images/red_body_square.png | Bin 0 -> 791 bytes .../main/images/tile_grey.png | Bin 0 -> 924 bytes .../main/main.collection | 48 +++++++++ .../main/scroll_manager/floor.collection | 79 +++++++++++++++ .../main/scroll_manager/floor_object.go | 26 +++++ .../main/scroll_manager/green.collection | 57 +++++++++++ .../main/scroll_manager/green_object.go | 26 +++++ .../main/scroll_manager/puprple.collection | 46 +++++++++ .../main/scroll_manager/purple_object.go | 26 +++++ .../main/scroll_manager/red.collection | 77 +++++++++++++++ .../main/scroll_manager/red_object.go | 26 +++++ .../scroll_manager/scroll_controller.script | 78 +++++++++++++++ .../main/scroll_manager/scroll_item.script | 3 + .../infinite-scroll-animation/thumbnail.webp | Bin 0 -> 10548 bytes 23 files changed, 684 insertions(+) create mode 100644 animation/infinite-scroll-animation/.gitattributes create mode 100644 animation/infinite-scroll-animation/.gitignore create mode 100644 animation/infinite-scroll-animation/example.md create mode 100644 animation/infinite-scroll-animation/game.project create mode 100644 animation/infinite-scroll-animation/input/game.input_binding create mode 100644 animation/infinite-scroll-animation/main/atlas.atlas create mode 100644 animation/infinite-scroll-animation/main/images/blue_body_square.png create mode 100644 animation/infinite-scroll-animation/main/images/green_body_square.png create mode 100644 animation/infinite-scroll-animation/main/images/purple_body_square.png create mode 100644 animation/infinite-scroll-animation/main/images/red_body_square.png create mode 100644 animation/infinite-scroll-animation/main/images/tile_grey.png create mode 100644 animation/infinite-scroll-animation/main/main.collection create mode 100644 animation/infinite-scroll-animation/main/scroll_manager/floor.collection create mode 100644 animation/infinite-scroll-animation/main/scroll_manager/floor_object.go create mode 100644 animation/infinite-scroll-animation/main/scroll_manager/green.collection create mode 100644 animation/infinite-scroll-animation/main/scroll_manager/green_object.go create mode 100644 animation/infinite-scroll-animation/main/scroll_manager/puprple.collection create mode 100644 animation/infinite-scroll-animation/main/scroll_manager/purple_object.go create mode 100644 animation/infinite-scroll-animation/main/scroll_manager/red.collection create mode 100644 animation/infinite-scroll-animation/main/scroll_manager/red_object.go create mode 100644 animation/infinite-scroll-animation/main/scroll_manager/scroll_controller.script create mode 100644 animation/infinite-scroll-animation/main/scroll_manager/scroll_item.script create mode 100644 animation/infinite-scroll-animation/thumbnail.webp diff --git a/animation/infinite-scroll-animation/.gitattributes b/animation/infinite-scroll-animation/.gitattributes new file mode 100644 index 00000000..187cdf4a --- /dev/null +++ b/animation/infinite-scroll-animation/.gitattributes @@ -0,0 +1,42 @@ +# Defold Protocol Buffer Text Files (https://github.com/github/linguist/issues/5091) +*.animationset linguist-language=JSON5 +*.atlas linguist-language=JSON5 +*.camera linguist-language=JSON5 +*.collection linguist-language=JSON5 +*.collectionfactory linguist-language=JSON5 +*.collectionproxy linguist-language=JSON5 +*.collisionobject linguist-language=JSON5 +*.cubemap linguist-language=JSON5 +*.display_profiles linguist-language=JSON5 +*.factory linguist-language=JSON5 +*.font linguist-language=JSON5 +*.gamepads linguist-language=JSON5 +*.go linguist-language=JSON5 +*.gui linguist-language=JSON5 +*.input_binding linguist-language=JSON5 +*.label linguist-language=JSON5 +*.material linguist-language=JSON5 +*.mesh linguist-language=JSON5 +*.model linguist-language=JSON5 +*.particlefx linguist-language=JSON5 +*.render linguist-language=JSON5 +*.sound linguist-language=JSON5 +*.sprite linguist-language=JSON5 +*.spinemodel linguist-language=JSON5 +*.spinescene linguist-language=JSON5 +*.texture_profiles linguist-language=JSON5 +*.tilemap linguist-language=JSON5 +*.tilesource linguist-language=JSON5 + +# Defold JSON Files +*.buffer linguist-language=JSON + +# Defold GLSL Shaders +*.fp linguist-language=GLSL +*.vp linguist-language=GLSL + +# Defold Lua Files +*.editor_script linguist-language=Lua +*.render_script linguist-language=Lua +*.script linguist-language=Lua +*.gui_script linguist-language=Lua diff --git a/animation/infinite-scroll-animation/.gitignore b/animation/infinite-scroll-animation/.gitignore new file mode 100644 index 00000000..0f4d6130 --- /dev/null +++ b/animation/infinite-scroll-animation/.gitignore @@ -0,0 +1,11 @@ +/.editor_settings +/.internal +/build +.externalToolBuilders +.DS_Store +Thumbs.db +.lock-wscript +*.pyc +.project +.cproject +builtins \ No newline at end of file diff --git a/animation/infinite-scroll-animation/example.md b/animation/infinite-scroll-animation/example.md new file mode 100644 index 00000000..1da48945 --- /dev/null +++ b/animation/infinite-scroll-animation/example.md @@ -0,0 +1,91 @@ +--- +title: Infinite Scroll Animation +brief: Learn how to make infinite scroll for all objects on your level with only two scripts +author: Evgenii Starostin +scripts: scroll_item.script, scroll_controller.script +thumbnail: thumbnail.webp +tags: animation +--- +This example demonstrates a reusable infinite scrolling system for Defold. The setup works with any type of game object and allows you to build multiple scrolling layers, making it useful for parallax backgrounds, decorations, or endlessly moving scenery. + +The project uses assets from the [Shape Characters](https://kenney.nl/assets/shape-characters) by Kenney. + + +## Project Structure +The scene is built from several independent scrolling layers. + +``` +main.collection +├── floor.collection +├── background.collection +├── middle.collection +└── foreground.collection +``` +Each layer is a collection containing: + +a root game object with `scroll_controller.script` +one or more scrolling game objects +`scroll_item.script` attached to every scrolling object + +Each layer can have its own speed, direction and scrolling bounds. + +## Registering Objects +Every scrolling object contains a small helper script. + +During initialization it registers itself with the layer controller by sending its game object id. +```lua +function init(self) + msg.post("root#scroll_controller", "prop:register", { + id = go.get_id() + }) +end +``` +The controller collects all registered objects before starting the scrolling logic. + +## Controller Properties +Each scrolling layer can be configured using script properties. +```lua +go.property("SPEED", 90) +go.property("DIRECTION", -1) +go.property("LEFT_BOUND", -1000) +go.property("RIGHT_BOUND", 1000) +go.property("COUNT_OBJECTS", 1) +``` +| Property | Description | +| -------- | ------- | +| SPEED | Scrolling speed in pixels per second. | +| DIRECTION | Scroll direction (-1 for left, 1 for right). | +| LEFT_BOUND | Position where objects wrap when scrolling left. | +| RIGHT_BOUND | Position where objects wrap when scrolling right. | +| COUNT_OBJECTS | Number of scrolling objects expected in the layer. | + +Different collection instances can override these values to create parallax effects without changing any code. + +## How It Works +Once all objects have registered, the controller: +- Reads the position of every object. +- Sorts them from left to right. +- Calculates the distance between each object and its neighbour. + +Instead of using a fixed spacing value, the controller stores the original distance between neighbouring objects. This means the objects can be placed freely in the editor while preserving their layout during scrolling. + +Every frame, all objects move by the same amount. + +When the leftmost object reaches the left boundary, it is moved to the right side of the layer using the previously calculated spacing. The same logic can also be applied when scrolling in the opposite direction. + +Because the original spacing is preserved, the scrolling loop appears seamless even when objects are unevenly distributed. + +## Customizing +The system is designed to be reusable. + +You can: +- Add or remove scrolling objects. +- Use sprites, animated game objects, labels, or any other game object. +- Create multiple scrolling layers with different speeds. +- Adjust the scrolling bounds for each layer. +- Reverse the scrolling direction. + +No changes to the scripts are required when creating additional layers. + +## Summary +By separating object registration from movement logic, the same scrolling system can be reused across many collections. Each layer manages its own objects independently, making it easy to build infinite scrolling scenes and parallax backgrounds while keeping the project structure simple \ No newline at end of file diff --git a/animation/infinite-scroll-animation/game.project b/animation/infinite-scroll-animation/game.project new file mode 100644 index 00000000..dd58be6b --- /dev/null +++ b/animation/infinite-scroll-animation/game.project @@ -0,0 +1,24 @@ +[bootstrap] +main_collection = /main/main.collectionc + +[script] +shared_state = 1 + +[display] +width = 720 +height = 720 +high_dpi = 1 + +[android] +input_method = HiddenInputField + +[physics] +gravity_y = -1000.0 +scale = 0.01 + +[html5] +scale_mode = stretch + +[project] +title = infinite-scroll-animation + diff --git a/animation/infinite-scroll-animation/input/game.input_binding b/animation/infinite-scroll-animation/input/game.input_binding new file mode 100644 index 00000000..8ed1d4e4 --- /dev/null +++ b/animation/infinite-scroll-animation/input/game.input_binding @@ -0,0 +1,4 @@ +mouse_trigger { + input: MOUSE_BUTTON_1 + action: "touch" +} diff --git a/animation/infinite-scroll-animation/main/atlas.atlas b/animation/infinite-scroll-animation/main/atlas.atlas new file mode 100644 index 00000000..b665927d --- /dev/null +++ b/animation/infinite-scroll-animation/main/atlas.atlas @@ -0,0 +1,20 @@ +images { + image: "/main/images/tile_grey.png" +} +images { + image: "/main/images/blue_body_square.png" + pivot_y: 1.0 +} +images { + image: "/main/images/green_body_square.png" + pivot_y: 1.0 +} +images { + image: "/main/images/purple_body_square.png" + pivot_y: 1.0 +} +images { + image: "/main/images/red_body_square.png" + pivot_y: 1.0 +} +extrude_borders: 2 diff --git a/animation/infinite-scroll-animation/main/images/blue_body_square.png b/animation/infinite-scroll-animation/main/images/blue_body_square.png new file mode 100644 index 0000000000000000000000000000000000000000..f0d5ba4db4d62e0913f9abe4c83bdcc7298da62c GIT binary patch literal 741 zcmW+ydq`7p7`;ALK5`cJ5F;xgLMplx_JEvdKC-U5RKhGWGcwrjUf#RiJ0DY*Q*%0< z^D%qOd}2OQGauQUQ^G$o{9{B($PBag`%QOAq%4ZM2H*D`_&A)yIpzGsI6t2t9|S@C z;x9xev&i3EFAw&P&AFRc@XU@(h(wS9o$v2V4uZIa@v+pHnsG+=lBs^hRE@gy6HJYT z(N8nAlZ=pX2ua5sg-fJx;7TB*9XJhzBoGh|0p-9+r{Ogtki%N*CfV?RSc|fu)!YtP zIqG6HHT^4^!PSZpm#WLD>UJvIVC9%gGq_UN>&)wd)#Hq+$I1E{tcMe{=0qAxpq(IFCL$^Yl1~>&|l^{n4h#E-LLRkeM3=q!+qI^I-ff5a%Oi)@1Bx)cx zLiqzo8lYGO#6>{f2qh&zssYk+pm+qyI!HD^vJnd9fNq0qG^z#WRKuJqK6FCFfj$=g+MCX~x~NoSfd12Lz|@ie99@OJ6{Prac6K zvF0nQn%U5<`J?r5JC<)f4>ih4q}ai1{h@Hq#cPJQLy^XwH9Yl3`_+iCzyDzE7(S26J+s47{?4N7nVcqDlX=3Y=`VzV@xv?06ji9_y z#1^jg@0(obE~z_Fvvp(f6m}TJr-*H~09I#m-Fr!0isrL)H%!OUxi$}$ZvAYIrI)kR zyq{nB1@xE8!R=ADX1-#En^Zhy20Ud+`FE4A6{+?0{5!ELbmYz1HF09Y`1@aOgJjXU Tu^ zi*qkth>EQE^Z8Ay(Z9sp@1JekKS)h@p|wg!E|-1B^wr$``{vHEj6LvGUVeJ$g)>Xn zH3_;VKYcfuH(^UdcD4LFWe@A?tGbS5Zn64ba-6v#?Uu~>BC((&p7Nej&ZZ0uPA3FR zPA|Tq{d@__b1w!4!S+q-7X;07JNff+w#j9+hRVvXe`9$kh%qdQYkZf<(xb|uUlTww5;Q?T+E~|pVuwj zFlh<@{3XmQjP^~RxPlo4HqAaV-ki?nIr0g%I6wEs%Ccx9+yydyU`(NmekVw!|p;y@dBXMBWg)U43Bl_ z3-{b`>5)iOHWO6tGj#jIZ}MpI9FVjg7;#8{PU znoHJ7Y!i|bw_T#kuwl#IJ?ch=xA*)PUv=aDn%pz|=5Et2FzgWg-r8Z-uyO5!>D%V( zJI5HWui|obPTu_O+xxH&>ho$V1B)GJ3!Itv|1{_F7i{Xi-}&$7%%3x9^L3rN*fqNz yuWZwJr6iNj;2@eSy5-QB1v|ecyiWMD+dk)K{_`2a$_Bv1&EV{asvQ>8dG4d1;M?#NEIy6TeV z_1;#m&8yjWi%g-+&;2c~`kO5*f1ba?ts;I<;=S3f>+BBB3riU`yPq~?%z8ZU|9y=O z{eQR_gc`TVZYV9@`|IPA%|^c-zMcGzcm1!BhF>$oBb~UmBv0HL&A;k>d$zsQj_ns? z1FwcDPMzA*Vep3ou&*QN|2_-0M;7@WDh^A9605A5y8CCV{M>TxPWfhM6-Zrt2MJa5~JmB7LU2 ze&Xl4aQ%-R$`71WS6PQ%nRLY2a;vbXP=9v#;#t-n%xpaOkFd#EcreGzVrzJ5J=KJ_ zAX3a?-|{J`T`%^`d3&zudqeWCoJEVcHLTSxbnu!K*&MoI@$GyYr%4i1@0=oW!-pKw zg)O}^9F*mj^jaKplSx$W+aOp!H}wdM)JNHjCsSliASx;*m;rVC;4^vTc&h`bn;E3~ z&*Djf^;3+25-|=MyV`=1!76}Upn1zr`5tKkE8Nj3Vc6)VXV9qTvq12EqZ^RqzXT%7 z6P1CE+Py(gIO9m{4Tz}_#Wo(&maZNQGyi^F{KeQe=E4O*kG(H~PI)YpT|R;D6ZiH^ zC$5(kEAq8d*1kwOrMZQbA?*FTIsr@h>wErduDWr*W~;&TK0EW!O2O|bOE`OCSoc?@ zr@OMQU35xQIMBpAUSBSr)xPBaw{M%6bOk>|7`JH&G0r-FPiL>px|1i{-|6O^&sgKj uA@yfHpC#+9fSk_h*FMi(dhf;eeas~hrq7JS!jyqYo59o7&t;ucLK6Vs#8EW> literal 0 HcmV?d00001 diff --git a/animation/infinite-scroll-animation/main/images/red_body_square.png b/animation/infinite-scroll-animation/main/images/red_body_square.png new file mode 100644 index 0000000000000000000000000000000000000000..9b19f3f3945dcd980a8a92314966bf058d4c3116 GIT binary patch literal 791 zcmV+y1L*vTP)1gh5C`!8+^wBYk~Gvfq9hWJfQv{;s`w4!k|IKM2t`yVq6L(K(!4>al1M4c8yG2+ zh)`2tgX53$?GA;VV4^h1h+K~Rf08Bpe8p%yv$uP;?*td_KSB9gND}-l&>Su+)>mciJs2BeEjKj z(^&$4ZtZNSpq5EsIU|bd-1g>sV3fdL&)+`sf_kP=>GL5g1M z1U^Z#i2KHiiw=|XGl&arxIv}jfV2V?B%MCGt&?mcyy4PhfcEz$rsii*oSp!n6{kVV zX_BDZYG)l^Zgg91NCdcIq3l$()NOVE%{r`n={7rR=Ff%|Xv72>Ic5G#Sb;`NppgT6 z*d)q2fV^|c7B=A+XvCF>5i8J$2{hs~#>g>!Hf&)dCeVmupb?jHMr>gtr)*&pR-h5b zKqFS55vMUmJfUjDG0=!DY{Y|z5nI@Z6==kzoDnO~h-07;r!husVIwBch+k_OaSSwK z3mY+kM*Ml35i8J$XQYj|lr!Qq#)xB}5ff;{KN}jc0*#nJBd$b@Sb;`Npb?jHMjQi; zoI;xdEMy%>9x2dtfHnYL_V|h#4nZ3bDQ6u?E~z#m5v>o8#C%-#A)<8wfTOjwuMjC+ z_5--pRF9YT_7)ug#6ZlufXD=|<7#;(47w7R+hky=&(<%()`X@CFErQlC}URhZW z5o$8Jg9H6dq0K0u`rzQ;!|66TztflH<@rt=S4B|aT^&Xgbye(6dtbYMc-R_$>o>Db VWfj&K?0*0N002ovPDHLkV1f|SW%B?4 literal 0 HcmV?d00001 diff --git a/animation/infinite-scroll-animation/main/images/tile_grey.png b/animation/infinite-scroll-animation/main/images/tile_grey.png new file mode 100644 index 0000000000000000000000000000000000000000..2addcca62c8b18109cdbf4550ff7251c9aeaf707 GIT binary patch literal 924 zcmV;N17rM&P)(x|)A)ZXu>y3(e)(x$x8rMl6j zy3wY((yF`NrMuGB-tW@i?$O`x(ckXU-|hea0L$6w&fM$G+w08Q>c-XP#nk1{-R#TQ z>BrUP#M0!hztgb6)up)4rntqi%HF(Tw~diiIdE-f?Q+EHWC#hTPk9W zV#3gv?~e@?m82R)GW;Ur4}K!UIT?TO8yWV<@R%sXKfqCu8QjKlfLiCWku&pB2*tE>W zkKuEh3UZAN%Tk;yZ79e!PA#QZnNtP1<`1q~N*A%JAlF#249Cgvih^9DU>l8!(SnLt zCkG4TQ@%enUQm*19FpOZj6YcUimDP-J5*Jueoa-4n%`74aO;?Rb-{P+dgfq#`02jk yB~3&0b0O9ElcF*8@VCNkX=;A{Tys-Pxb+t{HdswWdVxj&0000 self.RIGHT_BOUND then + local leftmost = self.OBJECTS[1] + rightmost.pos.x = leftmost.pos.x -- - self.SPACING + table.sort(self.OBJECTS, function (a, b) + return a.pos.x < b.pos.x + end) + end + return + end +end + + +function on_message(self, message_id, message) + if message_id == hash("prop:register") then + table.insert(self.OBJECTS, { id = message.id }) + if #self.OBJECTS == self.COUNT_OBJECTS then + register_objects(self) + end + end +end \ No newline at end of file diff --git a/animation/infinite-scroll-animation/main/scroll_manager/scroll_item.script b/animation/infinite-scroll-animation/main/scroll_manager/scroll_item.script new file mode 100644 index 00000000..bafffe98 --- /dev/null +++ b/animation/infinite-scroll-animation/main/scroll_manager/scroll_item.script @@ -0,0 +1,3 @@ +function init(self) + msg.post("root#scroll_controller", "prop:register", { id = go.get_id() }) +end \ No newline at end of file diff --git a/animation/infinite-scroll-animation/thumbnail.webp b/animation/infinite-scroll-animation/thumbnail.webp new file mode 100644 index 0000000000000000000000000000000000000000..93e65315fc56475d062d42ca1a3cdec5cb0c9f27 GIT binary patch literal 10548 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