Modding and Adding Characters to Pressing Competition 2

From XGN Blog. Read it here


Pressing Competition 2 or Typing Competition 2 is a Godot game which allows you to choose characters and fight against each other.

It's open-sourced under MIT here

You can find the showcase here

It's developed with Godot Mono 3.2.3



What you need:

  • Godot Mono 3.2.3 or later
  • Source code from Github

In the tutorial, you want to get ZJS from Hell Hole Studios into the game.

0. Note

This tutorial is updated on 2021/1/30 and is aimed for version 1.5

This tutorial is updated again on 2021/3/6 to match 1.10

This tutorial is updated agin on 2021/3/13 to match 1.11

1. Import

Download source code and import it as Godot project. You are done!

Due to some Godot bugs, please ignore all errors.

2. Adding a Character Group

Create a new folder under res://script/characters/Groups called HHS and add a new script extending CharGroup:

extends CharGroup
class_name HHSGroup #Group Class Name

func _init().("Hell Hole Studios",[ # We will define ZJS later

Provide two parameters to super._init():

  • name:String, the name of the group
  • chars:Array, the CharacterBases in this group

Register your group at res://script/characters/

extends Node
# Contains all available characters in groups
var chars=[,,,,, #Add here

3. Adding a character

Create file res://script/characters/Groups/HHS/ extending CharacterBase:

Let's say ZJS's skill is to deal 1000 damage to himself

extends CharacterBase

class_name ZJS

# Only after 1.10
func onSkillA(me,sub,enemy,enemySub,scene,isSub):

# Before 1.10:
# func onSkill(me,enemy,scene):
#   scene.emit_signal("damage_given",me,1000)

func _init():
    self.version=1 # Only after 1.10
Skill: Crazy Dance
Dance crazily and hurt yourself by 1000 HP

3.1 The init function

Define the following properties here:

  • maxhp:Int(1-1000)
  • atk:Int(1-5)
  • name:String
  • skillCost:Int(1-2000). Don't set it to 0 unless it's a passive skill.
  • image:String Image path
  • heal:Int(1-5)
  • desc:String Description
  • skillType:Int(0-1). Optional. Set this to 1 to set the skill to be a passive type.
  • version:Int(0-1). Optional. Set this to 1 for a more advanced skill overwrite. 1 is recommended for >=1.10 users
  • nosub:Bool Optional. Setting this to true will make the character unavailable for minions

3.2 The onSkillA function

onSkillA function comes with 6 parameters. It's called when player uses a skill. It's also called every frame if it's a passive skill. This method won't be called unless version is set to 1.

  • me: Character the skill user side's master
  • sub: Character the skill user side's minion
  • enemy: Character the skill enemy side's master
  • enemySub: Character the skill enemy side's minion
  • scene: GameSubview current scene
  • isSub: Bool whether the skill was called by a minion

3.3 The onSkill function

onSkill is an easier version of onSkillA. This method won't be called unless version is set to 0(default).

  • me:Character the skill user
  • enemy:Character skill user's enemy
  • scene:GameSubview current GameSubview scene

3.4 Character Properties

Let's take a look at what's in the class Character so you know what you can do:

extends Object

class_name Character

var hp: int # Current HP.
var base: CharacterBase # It's character base. READ-ONLY
var skillGauge: int # Current Skill Gauge
var status: Array # Current Buffs
var maxhp: int # Current Max HP
var skillCost: int # Current Skill Cost
var atk: int # Current Attack
var heal: int # Current Heal
var role: int #Role: 0 = player 1, 1 = player 2. MODIFY IT MAY CAUSE UNEXPECTED BEHAVIOUR
var vars: Dictionary = {} #Custom Variables you can store
var ai: int #AI Level 0=player control

CharacterBase uses Singleton design principle so:

base is read-only. Writing it will cause the next round to be corrupted. Write to corresponding variables instead

Do not use self or internal variables in onSkill. Writing it will cause the next round to be corrupted. Write to me or me.vars instead

Use scene.emit_signal("damage_given",...,...) to deal damage or the HP will not be displayed correctly

Some common scenario

  • Increase attack by 1: me.atk+=1
  • Double skill cost: enemy.skillCost*=2
  • Adding Status: enemy.status.append("test",600,1,2))
  • Deal 60 damage: scene.emit_signal("damage_given",enemy,60)
  • Heal 60 HP: scene.emit_signal("damage_given",me,-60)
  • Writing to user variable: me.vars["123"]+=1
  • Writing to scene objects: Lorelei:6
  • Initalizing a variable fast: Koishi:6
  • Checking minion: Satono:6
  • Calling others' skill: Rin:8

3.5 Key controls(>=1.5)

  • Use Character.checkAction("attack/defense/skill/gauge") to check if a character is attacking/healing/gauge-filling/using skill at the same frame this is called.
  • Character.key holds the bitmask for the actions at this frame
  • Use Character.checkA1("attack/defense/skill/gauge") to check if a key is just pressed
  • Use Character.checkA2("attack/defense/skill/gauge") to check if a key is pressed/hold
  • Use Character.checkB1("attack/defense/skill/gauge",default) to check if a key is just pressed or return default if it's AI
  • Use Character.checkB2("attack/defense/skill/gauge",default) to check if a key is pressed/hold or return default if it's AI

3.6 Adding pictures

The last step is to put your picture in the path you set to. Make sure it's facing right. It's recommended to put it under res://pic

4. Status

Use status to create lengthy events!

4.1 Built-in Status

System comes with built-in status:

  •,duration,damage,interval) will inflict a status called name and is duration frames long. It will cause damage damage every interval frames

  •,duration,damage,interval) will inflict a status called name and is duration frames long. It will lose damage skill gauge every interval frames

  •,duration,da,db,interval) will inflict a status called name and is duration frames long. It will cause da to db damage randomly every interval frames

4.2 Custom Status

Custom Status should extends Status:

extends Status

class_name TimestopBuff #Class name

func onCast(me, scene): #Required. Called every frame. me is a Character and scene is a GameSubview

func _init():"Timestop" #Required
    self.time=300 #Required. In frames

This is another example:

extends Status

class_name YuyukoBuff

func onCast(me, scene):
    if me.hp<=me.maxhp*0.2:
func _init():"Barrier"

Note that using self and internal variables are okay:

extends Status

class_name GDBuff

var val=0
var interval=0
var damage=0

func onCast(me, scene):
    if val%interval==0:
func _init(name,time,damage,interval):

5. Global & Config

scene.Global contains the Global variables, use them if you need:

extends Node
var p1: #Player 1
var p2: #Minion 1
var s1: #Player 2
var s2: #Minion 2

var incMana = 2 #gauge increase speed (per frame)
var decMana = 1 #gauge decrease speed (per frame)
var death = 0 #Sudden Death damage

# Config Element
var phaseLength = 60 # Phase Length in seconds
var musicVol = 0 # Music Volume in dB. Read
var HPMul = 1 # HP Multiplier.

6. Minions (>=1.10)

Minions are a new concept and game mechanics in 1.10. The minion is stored in Global.s1 and Global.s2 as a character. It's basically a character with their HP,heal,atk unused, so don't deal damage or cast status on them. You can set nosub to true to prevent a character being selected as minion.

The minion gauge is stored in minion's skillGauge variable and the threshold is in skillCost.

When master attacks, gauge will increase by 10. When master gets hurt, gauge will increase by damage.

7. Refer to source code for 100+ examples!

8. Build and release!


You should release your own version of PC2 with credits and link to the original version. You can also have your characters included in the main version with a pull request.

So, with that being said, enjoy modding and creating!

来源:Hell Hole Studios Blog

< <上一篇