GithubHelp home page GithubHelp logo

goplus / spx Goto Github PK

View Code? Open in Web Editor NEW
99.0 4.0 28.0 2.58 MB

spx - A Scratch Compatible Go/Go+ 2D Game Engine for STEM education

Home Page: https://builder.goplus.org

License: Apache License 2.0

Go 98.88% C 0.79% Shell 0.33%
gop goplus game-engine-2d learning-gop scratch-like stem-education stem builder go golang

spx's Introduction

spx - A Scratch Compatible 2D Game Engine

Build Status Go Report Card GitHub release Language Scratch diff

How to build

How to run games powered by Go+ spx engine?

  • Download Go+ and build it. See https://github.com/goplus/gop#how-to-build.
  • Download spx and build it.
    • git clone https://github.com/goplus/spx.git
    • cd spx
    • go install -v ./...
  • Build a game and run.
    • cd game-root-dir
    • gop run .

Games powered by spx

Tutorials

tutorial/01-Weather

Screen Shot1 Screen Shot2

Through this example you can learn how to listen events and do somethings.

Here are some codes in Kai.spx:

onStart => {
	say "Where do you come from?", 2
	broadcast "1"
}

onMsg "2", => {
	say "What's the climate like in your country?", 3
	broadcast "3"
}

onMsg "4", => {
	say "Which seasons do you like best?", 3
	broadcast "5"
}

We call onStart and onMsg to listen events. onStart is called when the program is started. And onMsg is called when someone calls broadcast to broadcast a message.

When the program starts, Kai says Where do you come from?, and then broadcasts the message 1. Who will recieve this message? Let's see codes in Jaime.spx:

onMsg "1", => {
	say "I come from England.", 2
	broadcast "2"
}

onMsg "3", => {
	say "It's mild, but it's not always pleasant.", 4
	# ...
	broadcast "4"
}

Yes, Jaime recieves the message 1 and says I come from England.. Then he broadcasts the message 2. Kai recieves it and says What's the climate like in your country?.

The following procedures are very similar. In this way you can implement dialogues between multiple actors.

tutorial/02-Dragon

Screen Shot1

Through this example you can learn how to define variables and show them on the stage.

Here are all the codes of Dragon:

var (
	score int
)

onStart => {
	score = 0
	for {
		turn rand(-30, 30)
		step 5
		if touching("Shark") {
			score++
			play chomp, true
			step -100
		}
	}
}

We define a variable named score for Dragon. After the program starts, it moves randomly. And every time it touches Shark, it gains one score.

How to show the score on the stage? You don't need write code, just add a stageMonitor object into assets/index.json:

{
  "zorder": [
    {
      "type": "monitor",
      "name": "dragon",
      "size": 1,
      "target": "Dragon",
      "val": "getVar:score",
      "color": 15629590,
      "label": "score",
      "mode": 1,
      "x": 5,
      "y": 5,
      "visible": true
    }
  ]
}

tutorial/03-Clone

Screen Shot1

Through this example you can learn:

  • Clone sprites and destory them.
  • Distinguish between sprite variables and shared variables that can access by all sprites.

Here are some codes in Calf.spx:

var (
	id int
)

onClick => {
	clone
}

onCloned => {
	gid++
	...
}

When we click the sprite Calf, it receives an onClick event. Then it calls clone to clone itself. And after cloning, the new Calf sprite will receive an onCloned event.

In onCloned event, the new Calf sprite uses a variable named gid. It doesn't define in Calf.spx, but in main.spx.

Here are all the codes of main.spx:

var (
	Arrow Arrow
	Calf  Calf
	gid   int
)

run "res", {Title: "Clone and Destory (by Go+)"}

All these three variables in main.spx are shared by all sprites. Arrow and Calf are sprites that exist in this project. gid means global id. It is used to allocate id for all cloned Calf sprites.

Let's back to Calf.spx to see the full codes of onCloned:

onCloned => {
	gid++
	id = gid
	step 50
	say id, 0.5
}

It increases gid value and assigns it to sprite id. This makes all these Calf sprites have different id. Then the cloned Calf moves forward 50 steps and says id of itself.

Why these Calf sprites need different id? Because we want destory one of them by its id.

Here are all the codes in Arrow.spx:

onClick => {
	broadcast "undo", true
	gid--
}

When we click Arrow, it broadcasts an "undo" message (NOTE: We pass the second parameter true to broadcast to indicate we wait all sprites to finish processing this message).

All Calf sprites receive this message, but only the last cloned sprite finds its id is equal to gid then destroys itself. Here are the related codes in Calf.spx:

onMsg "undo", => {
	if id == gid {
		destroy
	}
}

tutorial/04-Bullet

Screen Shot1

Through this example you can learn:

  • How to keep a sprite following mouse position.
  • How to fire bullets.

It's simple to keep a sprite following mouse position. Here are some related codes in MyAircraft.spx:

onStart => {
	for {
		# ...
		setXYpos mouseX, mouseY
	}
}

Yes, we just need to call setXYpos mouseX, mouseY to follow mouse position.

But how to fire bullets? Let's see all codes of MyAircraft.spx:

onStart => {
	for {
		wait 0.1
		Bullet.clone
		setXYpos mouseX, mouseY
	}
}

In this example, MyAircraft fires bullets every 0.1 seconds. It just calls Bullet.clone to create a new bullet. All the rest things are the responsibility of Bullet.

Here are all the codes in Bullet.spx:

onCloned => {
	setXYpos MyAircraft.xpos, MyAircraft.ypos+5
	show
	for {
		wait 0.04
		changeYpos 10
		if touching(Edge) {
			destroy
		}
	}
}

When a Bullet is cloned, it calls setXYpos MyAircraft.xpos, MyAircraft.ypos+5 to follow MyAircraft's position and shows itself (the default state of a Bullet is hidden). Then the Bullet moves forward every 0.04 seconds and this is why we see the Bullet is flying.

At last, when the Bullet touches screen Edge or any enemy (in this example we don't have enemies), it destroys itself.

These are all things about firing bullets.

spx's People

Contributors

damonchen avatar dependabot[bot] avatar foreversmart avatar jessonchan avatar jiepengtan avatar kevwan avatar loadgame avatar sunqirui1987 avatar visualfc avatar xumingyu07 avatar xushiwei avatar zrcoder avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar

spx's Issues

Unify `Die` behavior of initial / cloned sprite

In SPX, there are different implementations of the Die method for initial and cloned sprites:

spx/sprite.go

Lines 483 to 494 in 3df776b

func (p *Sprite) Die() { // prototype sprite can't be destroyed, but can die
aniName := p.getStateAnimName(StateDie)
p.SetDying()
if ani, ok := p.animations[aniName]; ok {
p.goAnimate(aniName, ani)
}
if p.isCloned_ {
p.doDestroy()
} else {
p.Hide()
}
}

Ideally, there should be no noticeable differences for game developers, but this is not the case. For example, consider this sprite code:

onStart => {
	for {
		wait 0.1
		println "im alive"
	}
}

onStart => {
	wait 1
	die
}

After calling die, the loop continues, so "im alive" keeps being printed.

In contrast, for a cloned sprite:

onCloned => {
	for {
		wait 0.1
		println "im alive"
	}
}

onCloned => {
	wait 1
	die
}

onStart => {
	clone
}

After calling die, the loop stops, and "im alive" is no longer printed.

Game developers may perform more complex actions than just println "im alive", potentially affecting overall game behavior. Therefore, it is crucial to unify the behavior of die for both sprite types.

Please check this out, @xushiwei @JiepengTan.

(Costume-group) Animation issues

Project: animation.zip

For sprite Fighter:

Config:

{
  "fAnimations": {
    "fight": {
      "from": "__animation_fight_attack_1-1",
      "to": "__animation_fight_attack_1-4",
      "duration": 0.4,
      "anitype": 0
    },
    "dying": {
      "from": "__animation_dying_dead-1",
      "to": "__animation_dying_dead-3",
      "duration": 0.6,
      "anitype": 0
    },
    "walk": {
      "from": "__animation_walk_walk-1",
      "to": "__animation_walk_walk-8",
      "duration": 0.8,
      "anitype": 0
    },
    "default": {
      "from": "__animation_default_idle-1",
      "to": "__animation_default_idle-6",
      "duration": 0.6,
      "anitype": 0
    }
  },
  "defaultAnimation": "default",
  "animBindings": {
    "die": "dying",
    "step": "walk"
  }
}

Code:

onKey KeyUp, => {
    say "animate fight", 0.3
    animate "fight"
}

onKey KeyDown, => {
    say "die", 0.3
    die
}

onKey KeyLeft, => {
    say "step left"
    setRotationStyle LeftRight
    turnTo Left
    step 50
}

onKey KeyRight, => {
    say "step right"
    setRotationStyle LeftRight
    turnTo Right
    step 50
}

There are some issues:

  1. The animation default (which is bound as defaultAnimation) is played only once while sprite in default state. It is expecetd to be played repeatedly (see details in goplus/builder#603 (comment))

  2. After animation fight (played by calling animate "fight") finished playing, the sprite does not return to the default state, while it keeps the last csotume of animation fight

  3. If we call step 50, it is expected to play animation walk (which is bound to state step). However, the sprite plays fight, and then die, and then walk, and then panic:

    panic: invalid costume index [recovered]
        panic: invalid costume index
    

Strange behavior of `OnTouched`

For sprite A with code:

// sprite A
onTouched => {
    println "onTouched"
}

The callback will not be called when A collides with another sprite (referred as B).

The callback is called when A collides with B, and method Touching of B is called with A, for example:

// sprite B
onMoving => {
    if touching("A") {
        // do whatever here
    }
}

It's strange, but may be intended. @xushiwei can you offer any hint?

Related PR: #68

P.S. There is no similar API in Scratch

Type definition for sprite

Consider a modified version of the game AircraftWar where instead of firing bullets, the aircraft uses lasers to destroy enemies. The laser immediately eliminates the nearest enemy at the same xpos when fired.

Implementing this using the touching and onTouched API is not feasible because:

  • There is no "touching" event triggered.
  • Even if a laser sprite is created and the "touching" event is utilized between the laser and enemies, it becomes challenging to identify "the nearest enemy" since each enemy cannot determine its proximity.

In this scenario, game developers may need to manage a global sprite array, which simplifies the logic:

// main.spx

var (
	enemies []Sprite
)

func onPlayerAttack() {
	// Locate the nearest enemy with the same xpos as the player
	// Iterate through `enemies` and use `enemy.xpos()` / `enemy.ypos()`
}
// Enemy.spx

onCloned => {
	enemies = append(enemies, this)
}

However, there’s a complication: spx lacks an appropriate type definition for Sprite.

The Sprite type currently refers to a struct, while any enemy in the game is an instance of Enemy:

type Enemy struct {
	spx.Sprite
	*Game
}

So it is inappropriate to use []Sprite to store enemy instances.

Using []Enemy for the enemies array is inappropriate either because:

  • We need []*Enemy to store the correct data, while we want to avoid pointer usage by game developers.
  • If multiple enemy types exist (with different appearances and behaviors), representing them could lead to ambiguity—should we use []*SmallEnemy or []*BigEnemy?

I propose renaming the existing type Sprite struct{} to SpriteImpl and defining a new type Sprite:

type Sprite interface {
	Visible() bool
	Xpos() float64
	Ypos() float64
	// Any methods intended for game developers will be defined in this interface
}

type SpriteImpl struct {
	// Additionally, `*SpriteImpl` will implement `Sprite`, as will sprite classes like `*Enemy`
}

With this structure, the sprite array logic will function as intended.

Please share your ideas here, @xushiwei @JiepengTan.

composing sprite

Not a scratch feature, but very important for write a big game.

camara

Not a scratch feature, but very important to support large scene.

think

think V
think V, N

changeEffect/setEffect/clearGraphEffects

changeEffect G, N    # G = ColorEffect | FishEye | Whirl | Pixelate | Mosaic | Brightness | Ghost
setEffect G, N
clearGraphEffects

G = ColorEffect, Brightness is important.

Click-capture issue with sprite pivot

There is issue with click on a sprite with pivot.

For example, in this project, we configured the sprite with:

{
  "pivot": {
    "x": 45,
    "y": -70
  },
}

Which uses the sprite's center point as its pivot.

The click event is not triggered if we click at most of the area of the sprite. It is triggered only when we click at the right-bottom part of the sprite.

Screen.Recording.2024-07-03.at.15.06.14.mov

`Die` behavior with animation

As mentioned in #314 , if we call die of cloned sprite, its scripts / coroutines will be stopped.

For sprite with code below:

onCloned => {
	for {
		wait 0.1
		println "im alive"
	}
}

onCloned => {
	show
	wait 1
	println "call die"
	die
}

onStart => {
	clone
}

We'll get output like this:

✗ gop run .
im alive
im alive
im alive
im alive
im alive
im alive
im alive
im alive
im alive
call die

But if there's a dying animation, which requires some time to play, we'll get output like this:

✗ gop run .
im alive
im alive
im alive
im alive
im alive
im alive
im alive
im alive
im alive
call die
im alive
im alive
im alive
im alive
im alive
im alive
im alive
im alive

Game developers may perform more complex actions than just println "im alive", potentially affecting overall game behavior. They will have to maintain their own isDead state to avoid mistakes:

var isDead bool

onCloned => {
	for {
		wait 0.1
		if isDead {
			break
		}
		println "im alive"
	}
}

onCloned => {
	show
	wait 1
	println "call die"
	isDead = true
	die
}

onStart => {
	clone
}

It is advisable to adjust SPX behavior to lessen the burden on game developers. We could stop scripts / coroutines when die is called, rather than waiting for the animation to finish.

Please share your ideas here, @xushiwei @JiepengTan.

this.Clone undefined on gop v1.1.2

https://github.com/goplus/AircraftWar
gop run .

# command-line-arguments
HugeEnemy.spx:8: this.Clone undefined (type *HugeEnemy has no field or method Clone)
Bomb.spx:21: this.Clone undefined (type *Bomb has no field or method Clone)
MyAircraft.spx:11: this.Bullet.Clone undefined (type Bullet has no field or method Clone)
SmallEnemy.spx:8: this.Clone undefined (type *SmallEnemy has no field or method Clone)
MiddleEnemy.spx:8: this.Clone undefined (type *MiddleEnemy has no field or method Clone)

gop run panic in 09-AircraftWar

2021/10/01 07:25:29 Member Initialize false // *index
panic: -: undefined (type *index has no field or method Initialize)

goroutine 1 [running]:
github.com/goplus/gox.(*CodeBuilder).MemberVal(...)
github.com/goplus/[email protected]/codebuild.go:1376
github.com/goplus/gop/cl.gmxMainFunc(0xc0029b7d40, 0xc002a0b200)
github.com/goplus/gop/cl/class_file.go:160 +0x345

The problem is that calling Initialize function not exist

Unexpected behavior of `RotationStyle`

In Scratch, for a sprite with rotationStyle: left-right:

  • heading: [0, 180] is considered facing right
  • heading: (-180, 0) is considered facing left

While in spx, the logic is strange:

spx/spgdi.go

Lines 72 to 79 in 72b5b4f

} else if p.sprite.rotationStyle == LeftRight {
if math.Abs(p.sprite.direction) > 155 && math.Abs(p.sprite.direction) < 205 {
geo.Scale(-1, 1)
}
if math.Abs(p.sprite.direction) > 0 && math.Abs(p.sprite.direction) < 25 {
geo.Scale(-1, 1)
}
}

I don't understand. But it causes some result that is IMO not expected, for example:

setRotationStyle LeftRight

setHeading float64(Left)  // spx will render the sprite facing right
setHeading float64(Down)  // spx will render the sprite facing left
setHeading float64(Right) // spx will render the sprite facing right
setHeading float64(Up)    // spx will render the sprite facing right

I think it is reasonable to adopt the same logic as Scratch.

As discussed, we should adopt the same logic as Scratch.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.